SimpleHttpTunnelHandler::open()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 18
nc 4
nop 0
dl 0
loc 23
ccs 18
cts 18
cp 1
crap 3
rs 9.6666
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\Driver\Pjb62\Exception\BrokenConnectionException;
41
use Soluble\Japha\Bridge\Exception\ConnectionException;
42
use Soluble\Japha\Bridge\Http\Cookie;
43
use Soluble\Japha\Bridge\Socket\StreamSocket;
44
45
class SimpleHttpTunnelHandler extends SimpleHttpHandler
46
{
47
    /**
48
     * @var resource
49
     */
50
    public $socket;
51
52
    /**
53
     * @var bool
54
     */
55
    protected $hasContentLength = false;
56
57
    /**
58
     * @var bool
59
     */
60
    protected $isRedirect;
61
62
    /**
63
     * @var string
64
     */
65
    protected $httpHeadersPayload;
66
67
    /**
68
     * @param Protocol $protocol
69
     * @param string   $ssl
70
     * @param string   $host
71
     * @param int      $port
72
     * @param string   $java_servlet
73
     * @param int      $java_recv_size
74
     * @param int      $java_send_size
75
     *
76
     * @throws ConnectionException
77
     */
78 30
    public function __construct(Protocol $protocol, $ssl, $host, $port, $java_servlet, $java_recv_size, $java_send_size)
79
    {
80 30
        parent::__construct($protocol, $ssl, $host, $port, $java_servlet, $java_recv_size, $java_send_size);
81 30
        $this->open();
82 28
        $this->httpHeadersPayload = $this->getHttpHeadersPayload();
83 28
    }
84
85 30
    public function createSimpleChannel()
86
    {
87 30
        $this->channel = new EmptyChannel($this, $this->java_recv_size, $this->java_send_size);
88 30
    }
89
90 30
    public function createChannel()
91
    {
92 30
        $this->createSimpleChannel();
93 30
    }
94
95 17
    public function shutdownBrokenConnection(string $msg = '', int $code = null): void
96
    {
97 17
        if (is_resource($this->socket)) {
98 17
            fflush($this->socket);
99 17
            fclose($this->socket);
100
        }
101 17
        PjbProxyClient::unregisterAndThrowBrokenConnectionException($msg, $code);
102
    }
103
104
    /**
105
     * @throws ConnectionException
106
     */
107 30
    protected function open()
108
    {
109
        try {
110 30
            $persistent = $this->protocol->client->getParam(Client::PARAM_USE_PERSISTENT_CONNECTION);
111 30
            $streamSocket = new StreamSocket(
112 30
                $this->ssl === 'ssl://' ? StreamSocket::TRANSPORT_SSL : StreamSocket::TRANSPORT_TCP,
113 30
                $this->host.':'.$this->port,
114 30
                null,
115 30
                StreamSocket::DEFAULT_CONTEXT,
116 30
                $persistent
117
            );
118 28
            $socket = $streamSocket->getSocket();
119 2
        } catch (\Throwable $e) {
120 2
            $logger = $this->protocol->getClient()->getLogger();
121 2
            $logger->critical(sprintf(
122 2
                '[soluble-japha] %s (%s)',
123 2
                $e->getMessage(),
124 2
                __METHOD__
125
            ));
126 2
            throw new ConnectionException($e->getMessage(), $e->getCode());
127
        }
128 28
        stream_set_timeout($socket, -1);
129 28
        $this->socket = $socket;
130 28
    }
131
132
    /**
133
     * @throws BrokenConnectionException
134
     */
135
    public function fread(int $size): ?string
136
    {
137
        $line = fgets($this->socket, $this->java_recv_size);
138
        if ($line === false) {
139
            throw new BrokenConnectionException(
140
                'Cannot read from socket'
141
            );
142
        }
143
144
        $length = (int) hexdec($line);
145
        $data = '';
146
        while ($length > 0) {
147
            $str = fread($this->socket, $length);
148
            if (feof($this->socket) || $str === false) {
149
                return null;
150
            }
151
            $length -= strlen($str);
152
            $data .= $str;
153
        }
154
        fgets($this->socket, 3);
155
156
        return $data;
157
    }
158
159
    public function fwrite(string $data): ?int
160
    {
161
        $len = dechex(strlen($data));
162
        $written = fwrite($this->socket, "${len}\r\n${data}\r\n");
163
        if ($written === false) {
164
            return null;
165
        }
166
167
        return $written;
168
    }
169
170
    protected function close(): void
171
    {
172
        fwrite($this->socket, "0\r\n\r\n");
173
        fgets($this->socket, $this->java_recv_size);
174
        fgets($this->socket, 3);
175
        fclose($this->socket);
176
    }
177
178 25
    public function read(int $size): string
179
    {
180 25
        if (null === $this->headers) {
181 25
            $this->parseHeaders();
182
        }
183
184 25
        $http_error = $this->headers['http_error'] ?? null;
185
186 25
        if ($http_error !== null) {
187 17
            $str = null;
188 17
            if (isset($this->headers['transfer_chunked'])) {
189
                $str = $this->fread($this->java_recv_size);
190 17
            } elseif (isset($this->headers['content_length'])) {
191 17
                $len = $this->headers['content_length'];
192 17
                for ($str = fread($this->socket, $len); strlen($str) < $len; $str .= fread($this->socket, $len - strlen($str))) {
193
                    if (feof($this->socket)) {
194
                        break;
195
                    }
196
                }
197
            } else {
198
                $str = fread($this->socket, $this->java_recv_size);
199
            }
200 17
            $str = ($str === false || $str === null) ? '' : $str;
0 ignored issues
show
introduced by
The condition $str === null is always false.
Loading history...
201
202 17
            if ($http_error === 401) {
203
                $this->shutdownBrokenConnection('Authentication exception', 401);
204
            } else {
205 17
                $this->shutdownBrokenConnection($str);
206
            }
207
        }
208
209 9
        $response = $this->fread($this->java_recv_size);
210 9
        if ($response === null) {
211
            $this->shutdownBrokenConnection('Cannot socket read response from SimpleHttpTunnelHandler');
212
        }
213
214 9
        return (string) $response;
215
    }
216
217 25
    protected function getBodyFor($compat, $data): string
218
    {
219 25
        $length = dechex(2 + strlen($data));
220
221 25
        return "\r\n${length}\r\n\177${compat}${data}\r\n";
222
    }
223
224 28
    protected function getHttpHeadersPayload(): string
225
    {
226
        $headers = [
227 28
            "PUT {$this->getWebApp()} HTTP/1.1",
228 28
            "Host: {$this->host}:{$this->port}",
229 28
            'Cache-Control: no-cache',
230 28
            'Pragma: no-cache',
231 28
            'Transfer-Encoding: chunked',
232
        ];
233
234 28
        if (($cookieHeaderLine = Cookie::getCookiesHeaderLine()) !== null) {
235
            $headers[] = $cookieHeaderLine;
236
        }
237
238 28
        if (($context = trim($this->getContext())) !== '') {
239
            $headers[] = $context;
240
        }
241
242 28
        $client = $this->protocol->getClient();
243 28
        if (($user = $client->getParam(Client::PARAM_JAVA_AUTH_USER)) !== null) {
244 25
            $password = $client->getParam(Client::PARAM_JAVA_AUTH_PASSWORD);
245 25
            $encoded_credentials = base64_encode("{$user}:{$password}");
246 25
            $headers[] = "Authorization: Basic {$encoded_credentials}";
247
        }
248
249 28
        return implode("\r\n", $headers);
250
    }
251
252 25
    public function write(string $data): ?int
253
    {
254 25
        $compat = PjbProxyClient::getInstance()->getCompatibilityOption($this->protocol->client);
255 25
        $this->headers = null; // reset headers
256
257 25
        $request = $this->httpHeadersPayload."\r\n".$this->getBodyFor($compat, $data);
258
259 25
        $count = @fwrite($this->socket, $request);
260 25
        if ($count === false) {
261 1
            $this->shutdownBrokenConnection(
262 1
                sprintf(
263 1
                    'Cannot write to socket, broken connection handle: %s',
264 1
                    json_encode(error_get_last())
265
                )
266
            );
267
        }
268 25
        $flushed = @fflush($this->socket);
269 25
        if ($flushed === false) {
270
            $this->shutdownBrokenConnection(
271
                sprintf(
272
                    'Cannot flush to socket, broken connection handle: %s',
273
                    json_encode(error_get_last())
274
                )
275
            );
276
        }
277
278 25
        return (int) $count;
279
    }
280
281 25
    protected function parseHeaders(): void
282
    {
283 25
        $this->headers = [];
284
285 25
        $res = @fgets($this->socket, $this->java_recv_size);
286 25
        if ($res === false) {
287 1
            $this->shutdownBrokenConnection('Cannot parse headers, socket cannot be read.');
288
        }
289 25
        $line = trim($res);
290 25
        $ar = explode(' ', $line);
291 25
        $code = ((int) $ar[1]);
292 25
        if ($code !== 200) {
293 17
            $this->headers['http_error'] = $code;
294
        }
295 25
        while ($str = trim(fgets($this->socket, $this->java_recv_size))) {
296 25
            if ($str[0] === 'X') {
297 24
                if (!strncasecmp('X_JAVABRIDGE_REDIRECT', $str, 21)) {
298 24
                    $this->headers['redirect'] = trim(substr($str, 22));
299 24
                } elseif (!strncasecmp('X_JAVABRIDGE_CONTEXT', $str, 20)) {
300 24
                    $this->headers['context'] = trim(substr($str, 21));
301
                }
302 25
            } elseif ($str[0] === 'S') {
303
                if (!strncasecmp('SET-COOKIE', $str, 10)) {
304
                    $str = substr($str, 12);
305
                    $this->cookies[] = $str;
306
                    $ar = explode(';', $str);
307
                    $cookie = explode('=', $ar[0]);
308
                    $path = '';
309
                    if (isset($ar[1])) {
310
                        $p = explode('=', $ar[1]);
311
                    }
312
                    if (isset($p)) {
313
                        $path = $p[1];
314
                    }
315
                    $this->doSetCookie($cookie[0], $cookie[1], $path);
316
                }
317 25
            } elseif ($str[0] === 'C') {
318 25
                if (!strncasecmp('CONTENT-LENGTH', $str, 14)) {
319 25
                    $this->headers['content_length'] = trim(substr($str, 15));
320 25
                    $this->hasContentLength = true;
321 25
                } elseif (!strncasecmp('CONNECTION', $str, 10) && !strncasecmp('close', trim(substr($str, 11)), 5)) {
322 25
                    $this->headers['connection_close'] = true;
323
                }
324 25
            } elseif ($str[0] === 'T') {
325
                if (!strncasecmp('TRANSFER-ENCODING', $str, 17) && !strncasecmp('chunked', trim(substr($str, 18)), 7)) {
326
                    $this->headers['transfer_chunked'] = true;
327
                }
328
            }
329
        }
330 25
    }
331
332
    /**
333
     * @return ChunkedSocketChannel
334
     */
335
    protected function getSimpleChannel()
336
    {
337
        return new ChunkedSocketChannel($this->socket, $this->host, $this->java_recv_size, $this->java_send_size);
338
    }
339
340 9
    public function redirect(): void
341
    {
342 9
        $this->isRedirect = isset($this->headers['redirect']);
343 9
        if ($this->isRedirect) {
344 9
            $channelName = $this->headers['redirect'];
345
        } else {
346
            $channelName = null;
347
        }
348 9
        $context = $this->headers['context'];
349 9
        $len = strlen($context);
350 9
        $len0 = chr(0xFF);
351 9
        $len1 = chr($len & 0xFF);
352 9
        $len >>= 8;
353 9
        $len2 = chr($len & 0xFF);
354 9
        if ($this->isRedirect) {
355 9
            $this->protocol->setSocketHandler(new SocketHandler($this->protocol, $this->getChannel($channelName)));
356 9
            $this->protocol->write("\177${len0}${len1}${len2}${context}");
357 9
            $this->context = sprintf("X_JAVABRIDGE_CONTEXT: %s\r\n", $context);
358 9
            $this->close();
359 9
            $this->protocol->handler = $this->protocol->getSocketHandler();
360 9
            if ($this->protocol->client->sendBuffer !== null) {
361 9
                $written = $this->protocol->handler->write($this->protocol->client->sendBuffer);
362 9
                if ($written === null) {
363
                    $this->protocol->handler->shutdownBrokenConnection('Broken local connection handle');
364
                }
365 9
                $this->protocol->client->sendBuffer = null;
366 9
                $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...
367
            }
368
        } else {
369
            $this->protocol->setSocketHandler(new SocketHandler($this->protocol, $this->getSimpleChannel()));
370
            $this->protocol->handler = $this->protocol->getSocketHandler();
371
        }
372 9
    }
373
}
374