Passed
Push — master ( 23b5cc...9353aa )
by Sébastien
04:46
created

SimpleHttpTunnelHandler::write()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 3.0017

Importance

Changes 0
Metric Value
cc 3
eloc 16
nc 4
nop 1
dl 0
loc 27
ccs 16
cts 17
cp 0.9412
crap 3.0017
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * soluble-japha / PHPJavaBridge driver client.
4
 *
5
 * Refactored version of phpjababridge's Java.inc file compatible
6
 * with php java bridge 6.2
7
 *
8
 *
9
 * @credits   http://php-java-bridge.sourceforge.net/pjb/
10
 *
11
 * @see      http://github.com/belgattitude/soluble-japha
12
 *
13
 * @author Jost Boekemeier
14
 * @author Vanvelthem Sébastien (refactoring and fixes from original implementation)
15
 * @license   MIT
16
 *
17
 * The MIT License (MIT)
18
 * Copyright (c) 2014-2017 Jost Boekemeier
19
 * Permission is hereby granted, free of charge, to any person obtaining a copy
20
 * of this software and associated documentation files (the "Software"), to deal
21
 * in the Software without restriction, including without limitation the rights
22
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
 * copies of the Software, and to permit persons to whom the Software is
24
 * furnished to do so, subject to the following conditions:
25
 *
26
 * The above copyright notice and this permission notice shall be included in
27
 * all copies or substantial portions of the Software.
28
 *
29
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35
 * THE SOFTWARE.
36
 */
37
38
namespace Soluble\Japha\Bridge\Driver\Pjb62;
39
40
use Soluble\Japha\Bridge\Exception\ConnectionException;
41
use Soluble\Japha\Bridge\Http\Cookie;
42
use Soluble\Japha\Bridge\Socket\StreamSocket;
43
44
class SimpleHttpTunnelHandler extends SimpleHttpHandler
45
{
46
    /**
47
     * @var resource
48
     */
49
    public $socket;
50
51
    /**
52
     * @var bool
53
     */
54
    protected $hasContentLength = false;
55
56
    /**
57
     * @var bool
58
     */
59
    protected $isRedirect;
60
61
    /**
62
     * @var string
63
     */
64
    protected $httpHeadersPayload;
65
66
    /**
67
     * @param Protocol $protocol
68
     * @param string   $ssl
69
     * @param string   $host
70
     * @param int      $port
71
     * @param string   $java_servlet
72
     * @param int      $java_recv_size
73
     * @param int      $java_send_size
74 26
     *
75
     * @throws ConnectionException
76 26
     */
77 26
    public function __construct(Protocol $protocol, $ssl, $host, $port, $java_servlet, $java_recv_size, $java_send_size)
78 24
    {
79 24
        parent::__construct($protocol, $ssl, $host, $port, $java_servlet, $java_recv_size, $java_send_size);
80
        $this->open();
81 26
        $this->httpHeadersPayload = $this->getHttpHeadersPayload();
82
    }
83 26
84 26
    public function createSimpleChannel()
85
    {
86 26
        $this->channel = new EmptyChannel($this, $this->java_recv_size, $this->java_send_size);
87
    }
88 26
89 26
    public function createChannel()
90
    {
91 14
        $this->createSimpleChannel();
92
    }
93 14
94 14
    public function shutdownBrokenConnection(string $msg = '', int $code = null): void
95
    {
96 14
        if (is_resource($this->socket)) {
97
            fflush($this->socket);
98
            fclose($this->socket);
99
        }
100
        PjbProxyClient::unregisterAndThrowBrokenConnectionException($msg, $code);
101
    }
102 26
103
    /**
104
     * @throws ConnectionException
105 26
     */
106 26
    protected function open()
107 26
    {
108 26
        try {
109
            $persistent = $this->protocol->client->getParam(Client::PARAM_USE_PERSISTENT_CONNECTION);
110 24
            $streamSocket = new StreamSocket(
111 2
                $this->ssl === 'ssl://' ? StreamSocket::TRANSPORT_SSL : StreamSocket::TRANSPORT_TCP,
112 2
                $this->host.':'.$this->port,
113 2
                null,
114 2
                StreamSocket::DEFAULT_CONTEXT,
115 2
                $persistent
116 2
            );
117
            $socket = $streamSocket->getSocket();
118 2
        } catch (\Throwable $e) {
119
            $logger = $this->protocol->getClient()->getLogger();
120 24
            $logger->critical(sprintf(
121 24
                '[soluble-japha] %s (%s)',
122 24
                $e->getMessage(),
123
                __METHOD__
124
            ));
125
            throw new ConnectionException($e->getMessage(), $e->getCode());
126
        }
127
        stream_set_timeout($socket, -1);
128
        $this->socket = $socket;
129
    }
130
131
    public function fread(int $size): ?string
132
    {
133
        $length = hexdec(fgets($this->socket, $this->java_recv_size));
134
        $data = '';
135
        while ($length > 0) {
136
            $str = fread($this->socket, $length);
0 ignored issues
show
Bug introduced by
It seems like $length can also be of type double; however, parameter $length of fread() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

136
            $str = fread($this->socket, /** @scrutinizer ignore-type */ $length);
Loading history...
137
            if (feof($this->socket) || $str === false) {
138
                return null;
139
            }
140
            $length -= strlen($str);
141
            $data .= $str;
142
        }
143
        fgets($this->socket, 3);
144
145
        return $data;
146
    }
147
148
    public function fwrite(string $data): ?int
149
    {
150
        $len = dechex(strlen($data));
151
        $written = fwrite($this->socket, "${len}\r\n${data}\r\n");
152
        if ($written === false) {
0 ignored issues
show
introduced by
The condition $written === false can never be true.
Loading history...
153
            return null;
154
        }
155
156
        return $written;
157
    }
158
159
    protected function close(): void
160
    {
161
        fwrite($this->socket, "0\r\n\r\n");
162 21
        fgets($this->socket, $this->java_recv_size);
163 21
        fgets($this->socket, 3);
164
        fclose($this->socket);
165
    }
166 21
167
    public function read(int $size): string
168 21
    {
169 14
        if (null === $this->headers) {
170 14
            $this->parseHeaders();
171
        }
172 14
173 14
        $http_error = $this->headers['http_error'] ?? null;
174 14
175
        if ($http_error !== null) {
176
            $str = null;
177
            if (isset($this->headers['transfer_chunked'])) {
178
                $str = $this->fread($this->java_recv_size);
179
            } elseif (isset($this->headers['content_length'])) {
180
                $len = $this->headers['content_length'];
181
                for ($str = fread($this->socket, $len); strlen($str) < $len; $str .= fread($this->socket, $len - strlen($str))) {
182 14
                    if (feof($this->socket)) {
183
                        break;
184 14
                    }
185
                }
186
            } else {
187 14
                $str = fread($this->socket, $this->java_recv_size);
188
            }
189
            $str = ($str === false || $str === null) ? '' : $str;
0 ignored issues
show
introduced by
The condition $str === false || $str === null can never be true.
Loading history...
190
191 8
            if ($http_error === 401) {
192 8
                $this->shutdownBrokenConnection('Authentication exception', 401);
193
            } else {
194
                $this->shutdownBrokenConnection($str);
195
            }
196 8
        }
197
198
        $response = $this->fread($this->java_recv_size);
199
        if ($response === null) {
200
            $this->shutdownBrokenConnection('Cannot socket read response from SimpleHttpTunnelHandler');
201 21
        }
202
203 21
        return (string) $response;
204
    }
205
206
    protected function getBodyFor($compat, $data): string
207
    {
208
        $length = dechex(2 + strlen($data));
209 24
210 24
        return "\r\n${length}\r\n\177${compat}${data}\r\n";
211 24
    }
212 24
213 24
    protected function getHttpHeadersPayload(): string
214
    {
215
        $headers = [
216 24
            "PUT {$this->getWebApp()} HTTP/1.1",
217
            "Host: {$this->host}:{$this->port}",
218
            'Cache-Control: no-cache',
219
            'Pragma: no-cache',
220 24
            'Transfer-Encoding: chunked',
221
        ];
222
223
        if (($cookieHeaderLine = Cookie::getCookiesHeaderLine()) !== null) {
224 24
            $headers[] = $cookieHeaderLine;
225 24
        }
226 21
227 21
        if (($context = trim($this->getContext())) !== '') {
228 21
            $headers[] = $context;
229
        }
230
231 24
        $client = $this->protocol->getClient();
232
        if (($user = $client->getParam(Client::PARAM_JAVA_AUTH_USER)) !== null) {
233
            $password = $client->getParam(Client::PARAM_JAVA_AUTH_PASSWORD);
234
            $encoded_credentials = base64_encode("{$user}:{$password}");
235
            $headers[] = "Authorization: Basic {$encoded_credentials}";
236 21
        }
237 21
238
        return implode("\r\n", $headers);
239 21
    }
240
241 21
    public function write(string $data): ?int
242 21
    {
243 1
        $compat = PjbProxyClient::getInstance()->getCompatibilityOption($this->protocol->client);
244
        $this->headers = null; // reset headers
245 21
246 21
        $request = $this->httpHeadersPayload."\r\n".$this->getBodyFor($compat, $data);
247
248
        $count = @fwrite($this->socket, $request);
249
        if ($count === false) {
0 ignored issues
show
introduced by
The condition $count === false can never be true.
Loading history...
250 21
            $this->shutdownBrokenConnection(
251
                sprintf(
252
                    'Cannot write to socket, broken connection handle: %s',
253
                error_get_last()
254
            )
255 21
            );
256
        }
257 21
        $flushed = @fflush($this->socket);
258 21
        if ($flushed === false) {
259 1
            $this->shutdownBrokenConnection(
260
                sprintf(
261 21
                    'Cannot flush to socket, broken connection handle: %s',
262 21
                error_get_last()
0 ignored issues
show
Bug introduced by
error_get_last() of type array is incompatible with the type string expected by parameter $args of sprintf(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

262
                /** @scrutinizer ignore-type */ error_get_last()
Loading history...
263 21
            )
264 21
            );
265 14
        }
266
267 21
        return (int) $count;
268 21
    }
269 20
270 20
    protected function parseHeaders(): void
271 20
    {
272 20
        $this->headers = [];
273
274 21
        $res = @fgets($this->socket, $this->java_recv_size);
275
        if ($res === false) {
0 ignored issues
show
introduced by
The condition $res === false can never be true.
Loading history...
276
            $this->shutdownBrokenConnection('Cannot parse headers, socket cannot be read.');
277
        }
278
        $line = trim($res);
279
        $ar = explode(' ', $line);
280
        $code = ((int) $ar[1]);
281
        if ($code !== 200) {
282
            $this->headers['http_error'] = $code;
283
        }
284
        while ($str = trim(fgets($this->socket, $this->java_recv_size))) {
285
            if ($str[0] === 'X') {
286
                if (!strncasecmp('X_JAVABRIDGE_REDIRECT', $str, 21)) {
287
                    $this->headers['redirect'] = trim(substr($str, 22));
288
                } elseif (!strncasecmp('X_JAVABRIDGE_CONTEXT', $str, 20)) {
289 21
                    $this->headers['context'] = trim(substr($str, 21));
290 21
                }
291 21
            } elseif ($str[0] === 'S') {
292 21
                if (!strncasecmp('SET-COOKIE', $str, 10)) {
293 21
                    $str = substr($str, 12);
294 21
                    $this->cookies[] = $str;
295
                    $ar = explode(';', $str);
296 21
                    $cookie = explode('=', $ar[0]);
297
                    $path = '';
298
                    if (isset($ar[1])) {
299
                        $p = explode('=', $ar[1]);
300
                    }
301
                    if (isset($p)) {
302 21
                        $path = $p[1];
303
                    }
304
                    $this->doSetCookie($cookie[0], $cookie[1], $path);
305
                }
306
            } elseif ($str[0] === 'C') {
307
                if (!strncasecmp('CONTENT-LENGTH', $str, 14)) {
308
                    $this->headers['content_length'] = trim(substr($str, 15));
309
                    $this->hasContentLength = true;
310
                } elseif (!strncasecmp('CONNECTION', $str, 10) && !strncasecmp('close', trim(substr($str, 11)), 5)) {
311
                    $this->headers['connection_close'] = true;
312
                }
313
            } elseif ($str[0] === 'T') {
314 8
                if (!strncasecmp('TRANSFER-ENCODING', $str, 17) && !strncasecmp('chunked', trim(substr($str, 18)), 7)) {
315 8
                    $this->headers['transfer_chunked'] = true;
316 8
                }
317
            }
318
        }
319
    }
320 8
321 8
    /**
322 8
     * @return ChunkedSocketChannel
323 8
     */
324 8
    protected function getSimpleChannel()
325 8
    {
326 8
        return new ChunkedSocketChannel($this->socket, $this->host, $this->java_recv_size, $this->java_send_size);
327 8
    }
328 8
329 8
    public function redirect(): void
330 8
    {
331 8
        $this->isRedirect = isset($this->headers['redirect']);
332 8
        if ($this->isRedirect) {
333 8
            $channelName = $this->headers['redirect'];
334 8
        } else {
335
            $channelName = null;
336
        }
337 8
        $context = $this->headers['context'];
338 8
        $len = strlen($context);
339
        $len0 = chr(0xFF);
340
        $len1 = chr($len & 0xFF);
341
        $len >>= 8;
342
        $len2 = chr($len & 0xFF);
343
        if ($this->isRedirect) {
344 8
            $this->protocol->setSocketHandler(new SocketHandler($this->protocol, $this->getChannel($channelName)));
345
            $this->protocol->write("\177${len0}${len1}${len2}${context}");
346
            $this->context = sprintf("X_JAVABRIDGE_CONTEXT: %s\r\n", $context);
347
            $this->close();
348
            $this->protocol->handler = $this->protocol->getSocketHandler();
349
            if ($this->protocol->client->sendBuffer !== null) {
0 ignored issues
show
introduced by
The condition $this->protocol->client->sendBuffer !== null can never be false.
Loading history...
350
                $written = $this->protocol->handler->write($this->protocol->client->sendBuffer);
351
                if ($written === null) {
352
                    $this->protocol->handler->shutdownBrokenConnection('Broken local connection handle');
353
                }
354
                $this->protocol->client->sendBuffer = null;
355
                $read = $this->protocol->handler->read(1);
0 ignored issues
show
Unused Code introduced by
The assignment to $read is dead and can be removed.
Loading history...
356
            }
357
        } else {
358
            $this->protocol->setSocketHandler(new SocketHandler($this->protocol, $this->getSimpleChannel()));
359
            $this->protocol->handler = $this->protocol->getSocketHandler();
360
        }
361
    }
362
}
363