Import upstream version 0.3
Debian Janitor
2 years ago
0 | 0 | language: php |
1 | 1 | |
2 | services: docker | |
3 | ||
2 | 4 | php: |
3 | - 5.4 | |
4 | - 5.5 | |
5 | 5 | - 5.6 |
6 | 6 | - 7.0 |
7 | 7 | - 7.1 |
8 | 8 | - 7.2 |
9 | - hhvm | |
9 | - 7.3 | |
10 | - 7.4 | |
11 | - nightly | |
12 | ||
13 | env: | |
14 | - ABTEST=client | |
15 | - ABTEST=server | |
16 | ||
17 | matrix: | |
18 | allow_failures: | |
19 | - php: nightly | |
10 | 20 | |
11 | 21 | before_install: |
12 | - export PATH=$HOME/.local/bin:$PATH | |
13 | - pip install --user autobahntestsuite | |
14 | - pip list --user autobahntestsuite | |
22 | - docker pull crossbario/autobahn-testsuite | |
15 | 23 | |
16 | 24 | before_script: |
17 | 25 | - composer install |
0 | 0 | # RFC6455 - The WebSocket Protocol |
1 | 1 | |
2 | 2 | [![Build Status](https://travis-ci.org/ratchetphp/RFC6455.svg?branch=master)](https://travis-ci.org/ratchetphp/RFC6455) |
3 | ![Autobahn Testsuite](https://img.shields.io/badge/Autobahn-passing-brightgreen.svg) | |
3 | [![Autobahn Testsuite](https://img.shields.io/badge/Autobahn-passing-brightgreen.svg)](http://socketo.me/reports/rfc-server/index.html) | |
4 | 4 | |
5 | 5 | This library a protocol handler for the RFC6455 specification. |
6 | 6 | It contains components for both server and client side handshake and messaging protocol negotation. |
7 | 7 | |
8 | Aspects that are left open to interpertation in the specification are also left open in this library. | |
9 | It is up to the implementation to determine how those interpertations are to be dealt with. | |
8 | Aspects that are left open to interpretation in the specification are also left open in this library. | |
9 | It is up to the implementation to determine how those interpretations are to be dealt with. | |
10 | 10 | |
11 | 11 | This library is independent, framework agnostic, and does not deal with any I/O. |
12 | 12 | HTTP upgrade negotiation integration points are handled with PSR-7 interfaces. |
4 | 4 | "keywords": ["WebSockets", "websocket", "RFC6455"], |
5 | 5 | "homepage": "http://socketo.me", |
6 | 6 | "license": "MIT", |
7 | "authors": [{ | |
8 | "name": "Chris Boden" | |
9 | , "email": "cboden@gmail.com" | |
10 | , "role": "Developer" | |
11 | }], | |
7 | "authors": [ | |
8 | { | |
9 | "name": "Chris Boden" | |
10 | , "email": "cboden@gmail.com" | |
11 | , "role": "Developer" | |
12 | }, | |
13 | { | |
14 | "name": "Matt Bonneau", | |
15 | "role": "Developer" | |
16 | } | |
17 | ], | |
12 | 18 | "support": { |
13 | "forum": "https://groups.google.com/forum/#!forum/ratchet-php" | |
14 | , "issues": "https://github.com/ratchetphp/RFC6455/issues" | |
15 | , "irc": "irc://irc.freenode.org/reactphp" | |
19 | "issues": "https://github.com/ratchetphp/RFC6455/issues", | |
20 | "chat": "https://gitter.im/reactphp/reactphp" | |
16 | 21 | }, |
17 | 22 | "autoload": { |
18 | 23 | "psr-4": { |
24 | 29 | "guzzlehttp/psr7": "^1.0" |
25 | 30 | }, |
26 | 31 | "require-dev": { |
27 | "react/http": "^0.4.1", | |
28 | "react/socket-client": "^0.4.3", | |
29 | "phpunit/phpunit": "4.8.*" | |
32 | "phpunit/phpunit": "5.7.*", | |
33 | "react/socket": "^1.3" | |
34 | }, | |
35 | "scripts": { | |
36 | "abtest-client": "ABTEST=client && sh tests/ab/run_ab_tests.sh", | |
37 | "abtest-server": "ABTEST=server && sh tests/ab/run_ab_tests.sh", | |
38 | "phpunit": "phpunit --colors=always", | |
39 | "test": [ | |
40 | "@abtest-client", | |
41 | "@abtest-server", | |
42 | "@phpunit" | |
43 | ] | |
30 | 44 | } |
31 | 45 | } |
15 | 15 | */ |
16 | 16 | private $defaultHeader; |
17 | 17 | |
18 | function __construct() { | |
18 | function __construct(PermessageDeflateOptions $perMessageDeflateOptions = null) { | |
19 | 19 | $this->verifier = new ResponseVerifier; |
20 | 20 | |
21 | 21 | $this->defaultHeader = new Request('GET', '', [ |
24 | 24 | , 'Sec-WebSocket-Version' => $this->getVersion() |
25 | 25 | , 'User-Agent' => "Ratchet" |
26 | 26 | ]); |
27 | ||
28 | if ($perMessageDeflateOptions === null) { | |
29 | $perMessageDeflateOptions = PermessageDeflateOptions::createDisabled(); | |
30 | } | |
31 | ||
32 | // https://bugs.php.net/bug.php?id=73373 | |
33 | // https://bugs.php.net/bug.php?id=74240 - need >=7.1.4 or >=7.0.18 | |
34 | if ($perMessageDeflateOptions->isEnabled() && | |
35 | !PermessageDeflateOptions::permessageDeflateSupported()) { | |
36 | trigger_error('permessage-deflate is being disabled because it is not support by your PHP version.', E_USER_NOTICE); | |
37 | $perMessageDeflateOptions = PermessageDeflateOptions::createDisabled(); | |
38 | } | |
39 | if ($perMessageDeflateOptions->isEnabled() && !function_exists('deflate_add')) { | |
40 | trigger_error('permessage-deflate is being disabled because you do not have the zlib extension.', E_USER_NOTICE); | |
41 | $perMessageDeflateOptions = PermessageDeflateOptions::createDisabled(); | |
42 | } | |
43 | ||
44 | $this->defaultHeader = $perMessageDeflateOptions->addHeaderToRequest($this->defaultHeader); | |
27 | 45 | } |
28 | 46 | |
29 | 47 | public function generateRequest(UriInterface $uri) { |
0 | <?php | |
1 | ||
2 | namespace Ratchet\RFC6455\Handshake; | |
3 | ||
4 | class InvalidPermessageDeflateOptionsException extends \Exception | |
5 | { | |
6 | }⏎ |
0 | <?php | |
1 | ||
2 | namespace Ratchet\RFC6455\Handshake; | |
3 | ||
4 | use Psr\Http\Message\MessageInterface; | |
5 | use Psr\Http\Message\RequestInterface; | |
6 | use Psr\Http\Message\ResponseInterface; | |
7 | ||
8 | final class PermessageDeflateOptions | |
9 | { | |
10 | const MAX_WINDOW_BITS = 15; | |
11 | /* this is a private instead of const for 5.4 compatibility */ | |
12 | private static $VALID_BITS = ['8', '9', '10', '11', '12', '13', '14', '15']; | |
13 | ||
14 | private $deflateEnabled = false; | |
15 | ||
16 | private $server_no_context_takeover; | |
17 | private $client_no_context_takeover; | |
18 | private $server_max_window_bits; | |
19 | private $client_max_window_bits; | |
20 | ||
21 | private function __construct() { } | |
22 | ||
23 | public static function createEnabled() { | |
24 | $new = new static(); | |
25 | $new->deflateEnabled = true; | |
26 | $new->client_max_window_bits = self::MAX_WINDOW_BITS; | |
27 | $new->client_no_context_takeover = false; | |
28 | $new->server_max_window_bits = self::MAX_WINDOW_BITS; | |
29 | $new->server_no_context_takeover = false; | |
30 | ||
31 | return $new; | |
32 | } | |
33 | ||
34 | public static function createDisabled() { | |
35 | return new static(); | |
36 | } | |
37 | ||
38 | public function withClientNoContextTakeover() { | |
39 | $new = clone $this; | |
40 | $new->client_no_context_takeover = true; | |
41 | } | |
42 | ||
43 | public function withoutClientNoContextTakeover() { | |
44 | $new = clone $this; | |
45 | $new->client_no_context_takeover = false; | |
46 | } | |
47 | ||
48 | public function withServerNoContextTakeover() { | |
49 | $new = clone $this; | |
50 | $new->server_no_context_takeover = true; | |
51 | } | |
52 | ||
53 | public function withoutServerNoContextTakeover() { | |
54 | $new = clone $this; | |
55 | $new->server_no_context_takeover = false; | |
56 | } | |
57 | ||
58 | public function withServerMaxWindowBits($bits = self::MAX_WINDOW_BITS) { | |
59 | if (!in_array($bits, self::$VALID_BITS)) { | |
60 | throw new \Exception('server_max_window_bits must have a value between 8 and 15.'); | |
61 | } | |
62 | $new = clone $this; | |
63 | $new->server_max_window_bits = $bits; | |
64 | } | |
65 | ||
66 | public function withClientMaxWindowBits($bits = self::MAX_WINDOW_BITS) { | |
67 | if (!in_array($bits, self::$VALID_BITS)) { | |
68 | throw new \Exception('client_max_window_bits must have a value between 8 and 15.'); | |
69 | } | |
70 | $new = clone $this; | |
71 | $new->client_max_window_bits = $bits; | |
72 | } | |
73 | ||
74 | /** | |
75 | * https://tools.ietf.org/html/rfc6455#section-9.1 | |
76 | * https://tools.ietf.org/html/rfc7692#section-7 | |
77 | * | |
78 | * @param MessageInterface $requestOrResponse | |
79 | * @return PermessageDeflateOptions[] | |
80 | * @throws \Exception | |
81 | */ | |
82 | public static function fromRequestOrResponse(MessageInterface $requestOrResponse) { | |
83 | $optionSets = []; | |
84 | ||
85 | $extHeader = preg_replace('/\s+/', '', join(', ', $requestOrResponse->getHeader('Sec-Websocket-Extensions'))); | |
86 | ||
87 | $configurationRequests = explode(',', $extHeader); | |
88 | foreach ($configurationRequests as $configurationRequest) { | |
89 | $parts = explode(';', $configurationRequest); | |
90 | if (count($parts) == 0) { | |
91 | continue; | |
92 | } | |
93 | ||
94 | if ($parts[0] !== 'permessage-deflate') { | |
95 | continue; | |
96 | } | |
97 | ||
98 | array_shift($parts); | |
99 | $options = new static(); | |
100 | $options->deflateEnabled = true; | |
101 | foreach ($parts as $part) { | |
102 | $kv = explode('=', $part); | |
103 | $key = $kv[0]; | |
104 | $value = count($kv) > 1 ? $kv[1] : null; | |
105 | ||
106 | switch ($key) { | |
107 | case "server_no_context_takeover": | |
108 | case "client_no_context_takeover": | |
109 | if ($value !== null) { | |
110 | throw new InvalidPermessageDeflateOptionsException($key . ' must not have a value.'); | |
111 | } | |
112 | $value = true; | |
113 | break; | |
114 | case "server_max_window_bits": | |
115 | if (!in_array($value, self::$VALID_BITS)) { | |
116 | throw new InvalidPermessageDeflateOptionsException($key . ' must have a value between 8 and 15.'); | |
117 | } | |
118 | break; | |
119 | case "client_max_window_bits": | |
120 | if ($value === null) { | |
121 | $value = '15'; | |
122 | } | |
123 | if (!in_array($value, self::$VALID_BITS)) { | |
124 | throw new InvalidPermessageDeflateOptionsException($key . ' must have no value or a value between 8 and 15.'); | |
125 | } | |
126 | break; | |
127 | default: | |
128 | throw new InvalidPermessageDeflateOptionsException('Option "' . $key . '"is not valid for permessage deflate'); | |
129 | } | |
130 | ||
131 | if ($options->$key !== null) { | |
132 | throw new InvalidPermessageDeflateOptionsException($key . ' specified more than once. Connection must be declined.'); | |
133 | } | |
134 | ||
135 | $options->$key = $value; | |
136 | } | |
137 | ||
138 | if ($options->getClientMaxWindowBits() === null) { | |
139 | $options->client_max_window_bits = 15; | |
140 | } | |
141 | ||
142 | if ($options->getServerMaxWindowBits() === null) { | |
143 | $options->server_max_window_bits = 15; | |
144 | } | |
145 | ||
146 | $optionSets[] = $options; | |
147 | } | |
148 | ||
149 | // always put a disabled on the end | |
150 | $optionSets[] = new static(); | |
151 | ||
152 | return $optionSets; | |
153 | } | |
154 | ||
155 | /** | |
156 | * @return mixed | |
157 | */ | |
158 | public function getServerNoContextTakeover() | |
159 | { | |
160 | return $this->server_no_context_takeover; | |
161 | } | |
162 | ||
163 | /** | |
164 | * @return mixed | |
165 | */ | |
166 | public function getClientNoContextTakeover() | |
167 | { | |
168 | return $this->client_no_context_takeover; | |
169 | } | |
170 | ||
171 | /** | |
172 | * @return mixed | |
173 | */ | |
174 | public function getServerMaxWindowBits() | |
175 | { | |
176 | return $this->server_max_window_bits; | |
177 | } | |
178 | ||
179 | /** | |
180 | * @return mixed | |
181 | */ | |
182 | public function getClientMaxWindowBits() | |
183 | { | |
184 | return $this->client_max_window_bits; | |
185 | } | |
186 | ||
187 | /** | |
188 | * @return bool | |
189 | */ | |
190 | public function isEnabled() | |
191 | { | |
192 | return $this->deflateEnabled; | |
193 | } | |
194 | ||
195 | /** | |
196 | * @param ResponseInterface $response | |
197 | * @return ResponseInterface | |
198 | */ | |
199 | public function addHeaderToResponse(ResponseInterface $response) | |
200 | { | |
201 | if (!$this->deflateEnabled) { | |
202 | return $response; | |
203 | } | |
204 | ||
205 | $header = 'permessage-deflate'; | |
206 | if ($this->client_max_window_bits != 15) { | |
207 | $header .= '; client_max_window_bits='. $this->client_max_window_bits; | |
208 | } | |
209 | if ($this->client_no_context_takeover) { | |
210 | $header .= '; client_no_context_takeover'; | |
211 | } | |
212 | if ($this->server_max_window_bits != 15) { | |
213 | $header .= '; server_max_window_bits=' . $this->server_max_window_bits; | |
214 | } | |
215 | if ($this->server_no_context_takeover) { | |
216 | $header .= '; server_no_context_takeover'; | |
217 | } | |
218 | ||
219 | return $response->withAddedHeader('Sec-Websocket-Extensions', $header); | |
220 | } | |
221 | ||
222 | public function addHeaderToRequest(RequestInterface $request) { | |
223 | if (!$this->deflateEnabled) { | |
224 | return $request; | |
225 | } | |
226 | ||
227 | $header = 'permessage-deflate'; | |
228 | if ($this->server_no_context_takeover) { | |
229 | $header .= '; server_no_context_takeover'; | |
230 | } | |
231 | if ($this->client_no_context_takeover) { | |
232 | $header .= '; client_no_context_takeover'; | |
233 | } | |
234 | if ($this->server_max_window_bits != 15) { | |
235 | $header .= '; server_max_window_bits=' . $this->server_max_window_bits; | |
236 | } | |
237 | $header .= '; client_max_window_bits'; | |
238 | if ($this->client_max_window_bits != 15) { | |
239 | $header .= '='. $this->client_max_window_bits; | |
240 | } | |
241 | ||
242 | return $request->withAddedHeader('Sec-Websocket-Extensions', $header); | |
243 | } | |
244 | ||
245 | public static function permessageDeflateSupported($version = PHP_VERSION) { | |
246 | if (!function_exists('deflate_init')) { | |
247 | return false; | |
248 | } | |
249 | if (version_compare($version, '7.1.3', '>')) { | |
250 | return true; | |
251 | } | |
252 | if (version_compare($version, '7.0.18', '>=') | |
253 | && version_compare($version, '7.1.0', '<')) { | |
254 | return true; | |
255 | } | |
256 | ||
257 | return false; | |
258 | } | |
259 | } |
118 | 118 | |
119 | 119 | /** |
120 | 120 | * Verify the version passed matches this RFC |
121 | * @param string|int $versionHeader MUST equal 13|"13" | |
121 | * @param string[] $versionHeader MUST equal ["13"] | |
122 | 122 | * @return bool |
123 | 123 | */ |
124 | public function verifyVersion($versionHeader) { | |
124 | public function verifyVersion(array $versionHeader) { | |
125 | 125 | return (1 === count($versionHeader) && static::VERSION === (int)$versionHeader[0]); |
126 | 126 | } |
127 | 127 | |
136 | 136 | */ |
137 | 137 | public function verifyExtensions($val) { |
138 | 138 | } |
139 | ||
140 | public function getPermessageDeflateOptions(array $requestHeader, array $responseHeader) { | |
141 | $deflate = true; | |
142 | if (!isset($requestHeader['Sec-WebSocket-Extensions']) || count(array_filter($requestHeader['Sec-WebSocket-Extensions'], function ($val) { | |
143 | return 'permessage-deflate' === substr($val, 0, strlen('permessage-deflate')); | |
144 | })) === 0) { | |
145 | $deflate = false; | |
146 | } | |
147 | ||
148 | if (!isset($responseHeader['Sec-WebSocket-Extensions']) || count(array_filter($responseHeader['Sec-WebSocket-Extensions'], function ($val) { | |
149 | return 'permessage-deflate' === substr($val, 0, strlen('permessage-deflate')); | |
150 | })) === 0) { | |
151 | $deflate = false; | |
152 | } | |
153 | ||
154 | return [ | |
155 | 'deflate' => $deflate, | |
156 | 'no_context_takeover' => false, | |
157 | 'max_window_bits' => null, | |
158 | 'request_no_context_takeover' => false, | |
159 | 'request_max_window_bits' => null | |
160 | ]; | |
161 | } | |
139 | 162 | } |
17 | 17 | $request->getHeader('Sec-WebSocket-Protocol') |
18 | 18 | , $response->getHeader('Sec-WebSocket-Protocol') |
19 | 19 | ); |
20 | $passes += (int)$this->verifyExtensions( | |
21 | $request->getHeader('Sec-WebSocket-Extensions') | |
22 | , $response->getHeader('Sec-WebSocket-Extensions') | |
23 | ); | |
20 | 24 | |
21 | return (5 === $passes); | |
25 | return (6 === $passes); | |
22 | 26 | } |
23 | 27 | |
24 | 28 | public function verifyStatus($status) { |
46 | 50 | } |
47 | 51 | |
48 | 52 | public function verifySubProtocol(array $requestHeader, array $responseHeader) { |
49 | return 0 === count($responseHeader) || count(array_intersect($responseHeader, $requestHeader)) > 0; | |
53 | if (0 === count($responseHeader)) { | |
54 | return true; | |
55 | } | |
56 | ||
57 | $requestedProtocols = array_map('trim', explode(',', implode(',', $requestHeader))); | |
58 | ||
59 | return count($responseHeader) === 1 && count(array_intersect($responseHeader, $requestedProtocols)) === 1; | |
50 | 60 | } |
51 | }⏎ | |
61 | ||
62 | public function verifyExtensions(array $requestHeader, array $responseHeader) { | |
63 | if (in_array('permessage-deflate', $responseHeader)) { | |
64 | return strpos(implode(',', $requestHeader), 'permessage-deflate') !== false ? 1 : 0; | |
65 | } | |
66 | ||
67 | return 1; | |
68 | } | |
69 | } |
16 | 16 | |
17 | 17 | private $_strictSubProtocols = false; |
18 | 18 | |
19 | public function __construct(RequestVerifier $requestVerifier) { | |
19 | private $enablePerMessageDeflate = false; | |
20 | ||
21 | public function __construct(RequestVerifier $requestVerifier, $enablePerMessageDeflate = false) { | |
20 | 22 | $this->verifier = $requestVerifier; |
23 | ||
24 | // https://bugs.php.net/bug.php?id=73373 | |
25 | // https://bugs.php.net/bug.php?id=74240 - need >=7.1.4 or >=7.0.18 | |
26 | $supported = PermessageDeflateOptions::permessageDeflateSupported(); | |
27 | if ($enablePerMessageDeflate && !$supported) { | |
28 | throw new \Exception('permessage-deflate is not supported by your PHP version (need >=7.1.4 or >=7.0.18).'); | |
29 | } | |
30 | if ($enablePerMessageDeflate && !function_exists('deflate_add')) { | |
31 | throw new \Exception('permessage-deflate is not supported because you do not have the zlib extension.'); | |
32 | } | |
33 | ||
34 | $this->enablePerMessageDeflate = $enablePerMessageDeflate; | |
21 | 35 | } |
22 | 36 | |
23 | 37 | /** |
60 | 74 | 'Sec-WebSocket-Version' => $this->getVersionNumber() |
61 | 75 | ]; |
62 | 76 | if (count($this->_supportedSubProtocols) > 0) { |
63 | $upgradeSuggestion['Sec-WebSocket-Protocol'] = implode(', ', $this->_supportedSubProtocols); | |
77 | $upgradeSuggestion['Sec-WebSocket-Protocol'] = implode(', ', array_keys($this->_supportedSubProtocols)); | |
64 | 78 | } |
65 | 79 | if (true !== $this->verifier->verifyUpgradeRequest($request->getHeader('Upgrade'))) { |
66 | 80 | return new Response(426, $upgradeSuggestion, null, '1.1', 'Upgrade header MUST be provided'); |
96 | 110 | } |
97 | 111 | } |
98 | 112 | |
99 | return new Response(101, array_merge($headers, [ | |
113 | $response = new Response(101, array_merge($headers, [ | |
100 | 114 | 'Upgrade' => 'websocket' |
101 | , 'Connection' => 'Upgrade' | |
102 | , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]) | |
103 | , 'X-Powered-By' => 'Ratchet' | |
115 | , 'Connection' => 'Upgrade' | |
116 | , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]) | |
117 | , 'X-Powered-By' => 'Ratchet' | |
104 | 118 | ])); |
119 | ||
120 | try { | |
121 | $perMessageDeflateRequest = PermessageDeflateOptions::fromRequestOrResponse($request)[0]; | |
122 | } catch (InvalidPermessageDeflateOptionsException $e) { | |
123 | return new Response(400, [], null, '1.1', $e->getMessage()); | |
124 | } | |
125 | ||
126 | if ($this->enablePerMessageDeflate && $perMessageDeflateRequest->isEnabled()) { | |
127 | $response = $perMessageDeflateRequest->addHeaderToResponse($response); | |
128 | } | |
129 | ||
130 | return $response; | |
105 | 131 | } |
106 | 132 | |
107 | 133 | /** |
148 | 148 | return 128 === ($this->firstByte & 128); |
149 | 149 | } |
150 | 150 | |
151 | public function setRsv1($value = true) { | |
152 | if (strlen($this->data) == 0) { | |
153 | throw new \UnderflowException("Cannot set Rsv1 because there is no data."); | |
154 | } | |
155 | ||
156 | $this->firstByte = | |
157 | ($this->isFinal() ? 128 : 0) | |
158 | + $this->getOpcode() | |
159 | + ($value ? 64 : 0) | |
160 | + ($this->getRsv2() ? 32 : 0) | |
161 | + ($this->getRsv3() ? 16 : 0) | |
162 | ; | |
163 | ||
164 | $this->data[0] = chr($this->firstByte); | |
165 | return $this; | |
166 | } | |
167 | ||
151 | 168 | /** |
152 | 169 | * @return boolean |
153 | 170 | * @throws \UnderflowException |
6 | 6 | */ |
7 | 7 | private $_frames; |
8 | 8 | |
9 | /** | |
10 | * @var int | |
11 | */ | |
12 | private $len; | |
13 | ||
9 | 14 | public function __construct() { |
10 | 15 | $this->_frames = new \SplDoublyLinkedList; |
16 | $this->len = 0; | |
11 | 17 | } |
12 | 18 | |
13 | 19 | public function getIterator() { |
38 | 44 | * {@inheritdoc} |
39 | 45 | */ |
40 | 46 | public function addFrame(FrameInterface $fragment) { |
47 | $this->len += $fragment->getPayloadLength(); | |
41 | 48 | $this->_frames->push($fragment); |
42 | 49 | |
43 | 50 | return $this; |
58 | 65 | * {@inheritdoc} |
59 | 66 | */ |
60 | 67 | public function getPayloadLength() { |
61 | $len = 0; | |
62 | ||
63 | foreach ($this->_frames as $frame) { | |
64 | try { | |
65 | $len += $frame->getPayloadLength(); | |
66 | } catch (\UnderflowException $e) { | |
67 | // Not an error, want the current amount buffered | |
68 | } | |
69 | } | |
70 | ||
71 | return $len; | |
68 | return $this->len; | |
72 | 69 | } |
73 | 70 | |
74 | 71 | /** |
119 | 116 | |
120 | 117 | return Frame::OP_BINARY === $this->_frames->bottom()->getOpcode(); |
121 | 118 | } |
119 | ||
120 | /** | |
121 | * @return boolean | |
122 | */ | |
123 | public function getRsv1() { | |
124 | if ($this->_frames->isEmpty()) { | |
125 | return false; | |
126 | //throw new \UnderflowException('Not enough data has been received to determine if message is binary'); | |
127 | } | |
128 | ||
129 | return $this->_frames->bottom()->getRsv1(); | |
130 | } | |
122 | 131 | } |
0 | 0 | <?php |
1 | 1 | namespace Ratchet\RFC6455\Messaging; |
2 | 2 | |
3 | use Ratchet\RFC6455\Handshake\PermessageDeflateOptions; | |
4 | ||
3 | 5 | class MessageBuffer { |
4 | 6 | /** |
5 | 7 | * @var \Ratchet\RFC6455\Messaging\CloseFrameChecker |
35 | 37 | * @var bool |
36 | 38 | */ |
37 | 39 | private $checkForMask; |
40 | ||
41 | /** | |
42 | * @var callable | |
43 | */ | |
44 | private $sender; | |
45 | ||
46 | /** | |
47 | * @var string | |
48 | */ | |
49 | private $leftovers; | |
50 | ||
51 | /** | |
52 | * @var int | |
53 | */ | |
54 | private $streamingMessageOpCode = -1; | |
55 | ||
56 | /** | |
57 | * @var PermessageDeflateOptions | |
58 | */ | |
59 | private $permessageDeflateOptions; | |
60 | ||
61 | /** | |
62 | * @var bool | |
63 | */ | |
64 | private $deflateEnabled = false; | |
65 | ||
66 | /** | |
67 | * @var int | |
68 | */ | |
69 | private $maxMessagePayloadSize; | |
70 | ||
71 | /** | |
72 | * @var int | |
73 | */ | |
74 | private $maxFramePayloadSize; | |
75 | ||
76 | /** | |
77 | * @var bool | |
78 | */ | |
79 | private $compressedMessage; | |
38 | 80 | |
39 | 81 | function __construct( |
40 | 82 | CloseFrameChecker $frameChecker, |
41 | 83 | callable $onMessage, |
42 | 84 | callable $onControl = null, |
43 | 85 | $expectMask = true, |
44 | $exceptionFactory = null | |
86 | $exceptionFactory = null, | |
87 | $maxMessagePayloadSize = null, // null for default - zero for no limit | |
88 | $maxFramePayloadSize = null, // null for default - zero for no limit | |
89 | callable $sender = null, | |
90 | PermessageDeflateOptions $permessageDeflateOptions = null | |
45 | 91 | ) { |
46 | 92 | $this->closeFrameChecker = $frameChecker; |
47 | 93 | $this->checkForMask = (bool)$expectMask; |
48 | 94 | |
49 | $this->exceptionFactory ?: $this->exceptionFactory = function($msg) { | |
95 | $this->exceptionFactory ?: $exceptionFactory = function($msg) { | |
50 | 96 | return new \UnderflowException($msg); |
51 | 97 | }; |
52 | 98 | |
53 | 99 | $this->onMessage = $onMessage; |
54 | 100 | $this->onControl = $onControl ?: function() {}; |
101 | ||
102 | $this->sender = $sender; | |
103 | ||
104 | $this->permessageDeflateOptions = $permessageDeflateOptions ?: PermessageDeflateOptions::createDisabled(); | |
105 | ||
106 | $this->deflateEnabled = $this->permessageDeflateOptions->isEnabled(); | |
107 | ||
108 | if ($this->deflateEnabled && !is_callable($this->sender)) { | |
109 | throw new \InvalidArgumentException('sender must be set when deflate is enabled'); | |
110 | } | |
111 | ||
112 | $this->compressedMessage = false; | |
113 | ||
114 | $this->leftovers = ''; | |
115 | ||
116 | $memory_limit_bytes = static::getMemoryLimit(); | |
117 | ||
118 | if ($maxMessagePayloadSize === null) { | |
119 | $maxMessagePayloadSize = $memory_limit_bytes / 4; | |
120 | } | |
121 | if ($maxFramePayloadSize === null) { | |
122 | $maxFramePayloadSize = $memory_limit_bytes / 4; | |
123 | } | |
124 | ||
125 | if (!is_int($maxFramePayloadSize) || $maxFramePayloadSize > 0x7FFFFFFFFFFFFFFF || $maxFramePayloadSize < 0) { // this should be interesting on non-64 bit systems | |
126 | throw new \InvalidArgumentException($maxFramePayloadSize . ' is not a valid maxFramePayloadSize'); | |
127 | } | |
128 | $this->maxFramePayloadSize = $maxFramePayloadSize; | |
129 | ||
130 | if (!is_int($maxMessagePayloadSize) || $maxMessagePayloadSize > 0x7FFFFFFFFFFFFFFF || $maxMessagePayloadSize < 0) { | |
131 | throw new \InvalidArgumentException($maxMessagePayloadSize . 'is not a valid maxMessagePayloadSize'); | |
132 | } | |
133 | $this->maxMessagePayloadSize = $maxMessagePayloadSize; | |
55 | 134 | } |
56 | 135 | |
57 | 136 | public function onData($data) { |
58 | while (strlen($data) > 0) { | |
59 | $data = $this->processData($data); | |
60 | } | |
137 | $data = $this->leftovers . $data; | |
138 | $dataLen = strlen($data); | |
139 | ||
140 | if ($dataLen < 2) { | |
141 | $this->leftovers = $data; | |
142 | ||
143 | return; | |
144 | } | |
145 | ||
146 | $frameStart = 0; | |
147 | while ($frameStart + 2 <= $dataLen) { | |
148 | $headerSize = 2; | |
149 | $payload_length = unpack('C', $data[$frameStart + 1] & "\x7f")[1]; | |
150 | $isMasked = ($data[$frameStart + 1] & "\x80") === "\x80"; | |
151 | $headerSize += $isMasked ? 4 : 0; | |
152 | if ($payload_length > 125 && ($dataLen - $frameStart < $headerSize + 125)) { | |
153 | // no point of checking - this frame is going to be bigger than the buffer is right now | |
154 | break; | |
155 | } | |
156 | if ($payload_length > 125) { | |
157 | $payloadLenBytes = $payload_length === 126 ? 2 : 8; | |
158 | $headerSize += $payloadLenBytes; | |
159 | $bytesToUpack = substr($data, $frameStart + 2, $payloadLenBytes); | |
160 | $payload_length = $payload_length === 126 | |
161 | ? unpack('n', $bytesToUpack)[1] | |
162 | : unpack('J', $bytesToUpack)[1]; | |
163 | } | |
164 | ||
165 | $closeFrame = null; | |
166 | ||
167 | if ($payload_length < 0) { | |
168 | // this can happen when unpacking in php | |
169 | $closeFrame = $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Invalid frame length'); | |
170 | } | |
171 | ||
172 | if (!$closeFrame && $this->maxFramePayloadSize > 1 && $payload_length > $this->maxFramePayloadSize) { | |
173 | $closeFrame = $this->newCloseFrame(Frame::CLOSE_TOO_BIG, 'Maximum frame size exceeded'); | |
174 | } | |
175 | ||
176 | if (!$closeFrame && $this->maxMessagePayloadSize > 0 | |
177 | && $payload_length + ($this->messageBuffer ? $this->messageBuffer->getPayloadLength() : 0) > $this->maxMessagePayloadSize) { | |
178 | $closeFrame = $this->newCloseFrame(Frame::CLOSE_TOO_BIG, 'Maximum message size exceeded'); | |
179 | } | |
180 | ||
181 | if ($closeFrame !== null) { | |
182 | $onControl = $this->onControl; | |
183 | $onControl($closeFrame); | |
184 | $this->leftovers = ''; | |
185 | ||
186 | return; | |
187 | } | |
188 | ||
189 | $isCoalesced = $dataLen - $frameStart >= $payload_length + $headerSize; | |
190 | if (!$isCoalesced) { | |
191 | break; | |
192 | } | |
193 | $this->processData(substr($data, $frameStart, $payload_length + $headerSize)); | |
194 | $frameStart = $frameStart + $payload_length + $headerSize; | |
195 | } | |
196 | ||
197 | $this->leftovers = substr($data, $frameStart); | |
61 | 198 | } |
62 | 199 | |
63 | 200 | /** |
69 | 206 | $this->frameBuffer ?: $this->frameBuffer = $this->newFrame(); |
70 | 207 | |
71 | 208 | $this->frameBuffer->addBuffer($data); |
72 | if (!$this->frameBuffer->isCoalesced()) { | |
73 | return ''; | |
74 | } | |
75 | 209 | |
76 | 210 | $onMessage = $this->onMessage; |
77 | 211 | $onControl = $this->onControl; |
78 | 212 | |
79 | 213 | $this->frameBuffer = $this->frameCheck($this->frameBuffer); |
80 | 214 | |
81 | $overflow = $this->frameBuffer->extractOverflow(); | |
82 | 215 | $this->frameBuffer->unMaskPayload(); |
83 | 216 | |
84 | 217 | $opcode = $this->frameBuffer->getOpcode(); |
85 | 218 | |
86 | 219 | if ($opcode > 2) { |
87 | $onControl($this->frameBuffer); | |
220 | $onControl($this->frameBuffer, $this); | |
88 | 221 | |
89 | 222 | if (Frame::OP_CLOSE === $opcode) { |
90 | 223 | return ''; |
91 | 224 | } |
92 | 225 | } else { |
226 | if ($this->messageBuffer->count() === 0 && $this->frameBuffer->getRsv1()) { | |
227 | $this->compressedMessage = true; | |
228 | } | |
229 | if ($this->compressedMessage) { | |
230 | $this->frameBuffer = $this->inflateFrame($this->frameBuffer); | |
231 | } | |
232 | ||
93 | 233 | $this->messageBuffer->addFrame($this->frameBuffer); |
94 | 234 | } |
95 | 235 | |
97 | 237 | |
98 | 238 | if ($this->messageBuffer->isCoalesced()) { |
99 | 239 | $msgCheck = $this->checkMessage($this->messageBuffer); |
240 | ||
241 | $msgBuffer = $this->messageBuffer; | |
242 | $this->messageBuffer = null; | |
243 | ||
100 | 244 | if (true !== $msgCheck) { |
101 | $onControl($this->newCloseFrame($msgCheck, 'Ratchet detected an invalid UTF-8 payload')); | |
245 | $onControl($this->newCloseFrame($msgCheck, 'Ratchet detected an invalid UTF-8 payload'), $this); | |
102 | 246 | } else { |
103 | $onMessage($this->messageBuffer); | |
247 | $onMessage($msgBuffer, $this); | |
104 | 248 | } |
105 | 249 | |
106 | 250 | $this->messageBuffer = null; |
107 | } | |
108 | ||
109 | return $overflow; | |
251 | $this->compressedMessage = false; | |
252 | ||
253 | if ($this->permessageDeflateOptions->getServerNoContextTakeover()) { | |
254 | $this->inflator = null; | |
255 | } | |
256 | } | |
110 | 257 | } |
111 | 258 | |
112 | 259 | /** |
115 | 262 | * @return \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface |
116 | 263 | */ |
117 | 264 | public function frameCheck(FrameInterface $frame) { |
118 | if (false !== $frame->getRsv1() || | |
265 | if ((false !== $frame->getRsv1() && !$this->deflateEnabled) || | |
119 | 266 | false !== $frame->getRsv2() || |
120 | 267 | false !== $frame->getRsv3() |
121 | 268 | ) { |
227 | 374 | public function newCloseFrame($code, $reason = '') { |
228 | 375 | return $this->newFrame(pack('n', $code) . $reason, true, Frame::OP_CLOSE); |
229 | 376 | } |
377 | ||
378 | public function sendFrame(Frame $frame) { | |
379 | if ($this->sender === null) { | |
380 | throw new \Exception('To send frames using the MessageBuffer, sender must be set.'); | |
381 | } | |
382 | ||
383 | if ($this->deflateEnabled && | |
384 | ($frame->getOpcode() === Frame::OP_TEXT || $frame->getOpcode() === Frame::OP_BINARY)) { | |
385 | $frame = $this->deflateFrame($frame); | |
386 | } | |
387 | ||
388 | if (!$this->checkForMask) { | |
389 | $frame->maskPayload(); | |
390 | } | |
391 | ||
392 | $sender = $this->sender; | |
393 | $sender($frame->getContents()); | |
394 | } | |
395 | ||
396 | public function sendMessage($messagePayload, $final = true, $isBinary = false) { | |
397 | $opCode = $isBinary ? Frame::OP_BINARY : Frame::OP_TEXT; | |
398 | if ($this->streamingMessageOpCode === -1) { | |
399 | $this->streamingMessageOpCode = $opCode; | |
400 | } | |
401 | ||
402 | if ($this->streamingMessageOpCode !== $opCode) { | |
403 | throw new \Exception('Binary and text message parts cannot be streamed together.'); | |
404 | } | |
405 | ||
406 | $frame = $this->newFrame($messagePayload, $final, $opCode); | |
407 | ||
408 | $this->sendFrame($frame); | |
409 | ||
410 | if ($final) { | |
411 | // reset deflator if client doesn't remember contexts | |
412 | if ($this->getDeflateNoContextTakeover()) { | |
413 | $this->deflator = null; | |
414 | } | |
415 | $this->streamingMessageOpCode = -1; | |
416 | } | |
417 | } | |
418 | ||
419 | private $inflator; | |
420 | ||
421 | private function getDeflateNoContextTakeover() { | |
422 | return $this->checkForMask ? | |
423 | $this->permessageDeflateOptions->getServerNoContextTakeover() : | |
424 | $this->permessageDeflateOptions->getClientNoContextTakeover(); | |
425 | } | |
426 | ||
427 | private function getDeflateWindowBits() { | |
428 | return $this->checkForMask ? $this->permessageDeflateOptions->getServerMaxWindowBits() : $this->permessageDeflateOptions->getClientMaxWindowBits(); | |
429 | } | |
430 | ||
431 | private function getInflateNoContextTakeover() { | |
432 | return $this->checkForMask ? | |
433 | $this->permessageDeflateOptions->getClientNoContextTakeover() : | |
434 | $this->permessageDeflateOptions->getServerNoContextTakeover(); | |
435 | } | |
436 | ||
437 | private function getInflateWindowBits() { | |
438 | return $this->checkForMask ? $this->permessageDeflateOptions->getClientMaxWindowBits() : $this->permessageDeflateOptions->getServerMaxWindowBits(); | |
439 | } | |
440 | ||
441 | private function inflateFrame(Frame $frame) { | |
442 | if ($this->inflator === null) { | |
443 | $this->inflator = inflate_init( | |
444 | ZLIB_ENCODING_RAW, | |
445 | [ | |
446 | 'level' => -1, | |
447 | 'memory' => 8, | |
448 | 'window' => $this->getInflateWindowBits(), | |
449 | 'strategy' => ZLIB_DEFAULT_STRATEGY | |
450 | ] | |
451 | ); | |
452 | } | |
453 | ||
454 | $terminator = ''; | |
455 | if ($frame->isFinal()) { | |
456 | $terminator = "\x00\x00\xff\xff"; | |
457 | } | |
458 | ||
459 | gc_collect_cycles(); // memory runs away if we don't collect ?? | |
460 | ||
461 | return new Frame( | |
462 | inflate_add($this->inflator, $frame->getPayload() . $terminator), | |
463 | $frame->isFinal(), | |
464 | $frame->getOpcode() | |
465 | ); | |
466 | } | |
467 | ||
468 | private $deflator; | |
469 | ||
470 | private function deflateFrame(Frame $frame) | |
471 | { | |
472 | if ($frame->getRsv1()) { | |
473 | return $frame; // frame is already deflated | |
474 | } | |
475 | ||
476 | if ($this->deflator === null) { | |
477 | $bits = (int)$this->getDeflateWindowBits(); | |
478 | if ($bits === 8) { | |
479 | $bits = 9; | |
480 | } | |
481 | $this->deflator = deflate_init( | |
482 | ZLIB_ENCODING_RAW, | |
483 | [ | |
484 | 'level' => -1, | |
485 | 'memory' => 8, | |
486 | 'window' => $bits, | |
487 | 'strategy' => ZLIB_DEFAULT_STRATEGY | |
488 | ] | |
489 | ); | |
490 | } | |
491 | ||
492 | // there is an issue in the zlib extension for php where | |
493 | // deflate_add does not check avail_out to see if the buffer filled | |
494 | // this only seems to be an issue for payloads between 16 and 64 bytes | |
495 | // This if statement is a hack fix to break the output up allowing us | |
496 | // to call deflate_add twice which should clear the buffer issue | |
497 | // if ($frame->getPayloadLength() >= 16 && $frame->getPayloadLength() <= 64) { | |
498 | // // try processing in 8 byte chunks | |
499 | // // https://bugs.php.net/bug.php?id=73373 | |
500 | // $payload = ""; | |
501 | // $orig = $frame->getPayload(); | |
502 | // $partSize = 8; | |
503 | // while (strlen($orig) > 0) { | |
504 | // $part = substr($orig, 0, $partSize); | |
505 | // $orig = substr($orig, strlen($part)); | |
506 | // $flags = strlen($orig) > 0 ? ZLIB_PARTIAL_FLUSH : ZLIB_SYNC_FLUSH; | |
507 | // $payload .= deflate_add($this->deflator, $part, $flags); | |
508 | // } | |
509 | // } else { | |
510 | $payload = deflate_add( | |
511 | $this->deflator, | |
512 | $frame->getPayload(), | |
513 | ZLIB_SYNC_FLUSH | |
514 | ); | |
515 | // } | |
516 | ||
517 | $deflatedFrame = new Frame( | |
518 | substr($payload, 0, $frame->isFinal() ? -4 : strlen($payload)), | |
519 | $frame->isFinal(), | |
520 | $frame->getOpcode() | |
521 | ); | |
522 | ||
523 | if ($frame->isFinal()) { | |
524 | $deflatedFrame->setRsv1(); | |
525 | } | |
526 | ||
527 | return $deflatedFrame; | |
528 | } | |
529 | ||
530 | /** | |
531 | * This is a separate function for testing purposes | |
532 | * $memory_limit is only used for testing | |
533 | * | |
534 | * @param null|string $memory_limit | |
535 | * @return int | |
536 | */ | |
537 | private static function getMemoryLimit($memory_limit = null) { | |
538 | $memory_limit = $memory_limit === null ? \trim(\ini_get('memory_limit')) : $memory_limit; | |
539 | $memory_limit_bytes = 0; | |
540 | if ($memory_limit !== '') { | |
541 | $shifty = ['k' => 0, 'm' => 10, 'g' => 20]; | |
542 | $multiplier = strlen($memory_limit) > 1 ? substr(strtolower($memory_limit), -1) : ''; | |
543 | $memory_limit = (int)$memory_limit; | |
544 | $memory_limit_bytes = in_array($multiplier, array_keys($shifty), true) ? $memory_limit * 1024 << $shifty[$multiplier] : $memory_limit; | |
545 | } | |
546 | ||
547 | return $memory_limit_bytes < 0 ? 0 : $memory_limit_bytes; | |
548 | } | |
230 | 549 | } |
0 | 0 | <?php |
1 | ||
1 | 2 | namespace Ratchet\RFC6455\Test; |
3 | use PHPUnit\Framework\TestCase; | |
2 | 4 | |
3 | class AbResultsTest extends \PHPUnit_Framework_TestCase { | |
5 | class AbResultsTest extends TestCase { | |
4 | 6 | private function verifyAutobahnResults($fileName) { |
5 | 7 | if (!file_exists($fileName)) { |
6 | 8 | return $this->markTestSkipped('Autobahn TestSuite results not found'); |
0 | 0 | <?php |
1 | 1 | use GuzzleHttp\Psr7\Uri; |
2 | use Ratchet\RFC6455\Handshake\InvalidPermessageDeflateOptionsException; | |
3 | use Ratchet\RFC6455\Handshake\PermessageDeflateOptions; | |
4 | use Ratchet\RFC6455\Messaging\MessageBuffer; | |
5 | use Ratchet\RFC6455\Handshake\ClientNegotiator; | |
6 | use Ratchet\RFC6455\Messaging\CloseFrameChecker; | |
7 | use Ratchet\RFC6455\Messaging\MessageInterface; | |
2 | 8 | use React\Promise\Deferred; |
3 | 9 | use Ratchet\RFC6455\Messaging\Frame; |
10 | use React\Socket\ConnectionInterface; | |
11 | use React\Socket\Connector; | |
4 | 12 | |
5 | 13 | require __DIR__ . '/../bootstrap.php'; |
6 | 14 | |
7 | define('AGENT', 'RatchetRFC/0.0.0'); | |
15 | define('AGENT', 'RatchetRFC/0.3'); | |
8 | 16 | |
9 | 17 | $testServer = "127.0.0.1"; |
10 | 18 | |
11 | 19 | $loop = React\EventLoop\Factory::create(); |
12 | 20 | |
13 | $dnsResolverFactory = new React\Dns\Resolver\Factory(); | |
14 | $dnsResolver = $dnsResolverFactory->createCached('8.8.8.8', $loop); | |
15 | ||
16 | $factory = new \React\SocketClient\Connector($loop, $dnsResolver); | |
17 | ||
18 | function echoStreamerFactory($conn) | |
21 | $connector = new Connector($loop); | |
22 | ||
23 | function echoStreamerFactory($conn, $permessageDeflateOptions = null) | |
19 | 24 | { |
25 | $permessageDeflateOptions = $permessageDeflateOptions ?: PermessageDeflateOptions::createDisabled(); | |
26 | ||
20 | 27 | return new \Ratchet\RFC6455\Messaging\MessageBuffer( |
21 | 28 | new \Ratchet\RFC6455\Messaging\CloseFrameChecker, |
22 | function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($conn) { | |
23 | /** @var Frame $frame */ | |
24 | foreach ($msg as $frame) { | |
25 | $frame->maskPayload(); | |
26 | } | |
27 | $conn->write($msg->getContents()); | |
29 | function (\Ratchet\RFC6455\Messaging\MessageInterface $msg, MessageBuffer $messageBuffer) use ($conn) { | |
30 | $messageBuffer->sendMessage($msg->getPayload(), true, $msg->isBinary()); | |
28 | 31 | }, |
29 | function (\Ratchet\RFC6455\Messaging\FrameInterface $frame) use ($conn) { | |
32 | function (\Ratchet\RFC6455\Messaging\FrameInterface $frame, MessageBuffer $messageBuffer) use ($conn) { | |
30 | 33 | switch ($frame->getOpcode()) { |
31 | 34 | case Frame::OP_PING: |
32 | 35 | return $conn->write((new Frame($frame->getPayload(), true, Frame::OP_PONG))->maskPayload()->getContents()); |
36 | 39 | break; |
37 | 40 | } |
38 | 41 | }, |
39 | false | |
42 | false, | |
43 | null, | |
44 | null, | |
45 | null, | |
46 | [$conn, 'write'], | |
47 | $permessageDeflateOptions | |
40 | 48 | ); |
41 | 49 | } |
42 | 50 | |
43 | 51 | function getTestCases() { |
44 | global $factory; | |
45 | 52 | global $testServer; |
53 | global $connector; | |
46 | 54 | |
47 | 55 | $deferred = new Deferred(); |
48 | 56 | |
49 | $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { | |
50 | $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(); | |
57 | $connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred) { | |
58 | $cn = new ClientNegotiator(); | |
51 | 59 | $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001/getCaseCount')); |
52 | 60 | |
53 | 61 | $rawResponse = ""; |
54 | 62 | $response = null; |
55 | 63 | |
56 | /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageBuffer $ms */ | |
64 | /** @var MessageBuffer $ms */ | |
57 | 65 | $ms = null; |
58 | 66 | |
59 | $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { | |
67 | $connection->on('data', function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { | |
60 | 68 | if ($response === null) { |
61 | 69 | $rawResponse .= $data; |
62 | 70 | $pos = strpos($rawResponse, "\r\n\r\n"); |
66 | 74 | $response = \GuzzleHttp\Psr7\parse_response($rawResponse); |
67 | 75 | |
68 | 76 | if (!$cn->validateResponse($cnRequest, $response)) { |
69 | $stream->end(); | |
77 | $connection->end(); | |
70 | 78 | $deferred->reject(); |
71 | 79 | } else { |
72 | $ms = new \Ratchet\RFC6455\Messaging\MessageBuffer( | |
73 | new \Ratchet\RFC6455\Messaging\CloseFrameChecker, | |
74 | function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) { | |
80 | $ms = new MessageBuffer( | |
81 | new CloseFrameChecker, | |
82 | function (MessageInterface $msg) use ($deferred, $connection) { | |
75 | 83 | $deferred->resolve($msg->getPayload()); |
76 | $stream->close(); | |
84 | $connection->close(); | |
77 | 85 | }, |
78 | 86 | null, |
79 | false | |
87 | false, | |
88 | null, | |
89 | null, | |
90 | null, | |
91 | function () {} | |
80 | 92 | ); |
81 | 93 | } |
82 | 94 | } |
88 | 100 | } |
89 | 101 | }); |
90 | 102 | |
91 | $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); | |
103 | $connection->write(\GuzzleHttp\Psr7\str($cnRequest)); | |
92 | 104 | }); |
93 | 105 | |
94 | 106 | return $deferred->promise(); |
95 | 107 | } |
108 | ||
109 | $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator( | |
110 | PermessageDeflateOptions::permessageDeflateSupported() ? PermessageDeflateOptions::createEnabled() : null); | |
96 | 111 | |
97 | 112 | function runTest($case) |
98 | 113 | { |
99 | global $factory; | |
114 | global $connector; | |
100 | 115 | global $testServer; |
116 | global $cn; | |
101 | 117 | |
102 | 118 | $casePath = "/runCase?case={$case}&agent=" . AGENT; |
103 | 119 | |
104 | 120 | $deferred = new Deferred(); |
105 | 121 | |
106 | $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath, $case) { | |
107 | $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(); | |
122 | $connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred, $casePath, $case) { | |
123 | $cn = new ClientNegotiator( | |
124 | PermessageDeflateOptions::permessageDeflateSupported() ? PermessageDeflateOptions::createEnabled() : null); | |
108 | 125 | $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $casePath)); |
109 | 126 | |
110 | 127 | $rawResponse = ""; |
112 | 129 | |
113 | 130 | $ms = null; |
114 | 131 | |
115 | $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { | |
132 | $connection->on('data', function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { | |
116 | 133 | if ($response === null) { |
117 | 134 | $rawResponse .= $data; |
118 | 135 | $pos = strpos($rawResponse, "\r\n\r\n"); |
122 | 139 | $response = \GuzzleHttp\Psr7\parse_response($rawResponse); |
123 | 140 | |
124 | 141 | if (!$cn->validateResponse($cnRequest, $response)) { |
125 | $stream->end(); | |
142 | echo "Invalid response.\n"; | |
143 | $connection->end(); | |
126 | 144 | $deferred->reject(); |
127 | 145 | } else { |
128 | $ms = echoStreamerFactory($stream); | |
146 | try { | |
147 | $permessageDeflateOptions = PermessageDeflateOptions::fromRequestOrResponse($response)[0]; | |
148 | $ms = echoStreamerFactory( | |
149 | $connection, | |
150 | $permessageDeflateOptions | |
151 | ); | |
152 | } catch (InvalidPermessageDeflateOptionsException $e) { | |
153 | $connection->end(); | |
154 | } | |
129 | 155 | } |
130 | 156 | } |
131 | 157 | } |
136 | 162 | } |
137 | 163 | }); |
138 | 164 | |
139 | $stream->on('close', function () use ($deferred) { | |
165 | $connection->on('close', function () use ($deferred) { | |
140 | 166 | $deferred->resolve(); |
141 | 167 | }); |
142 | 168 | |
143 | $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); | |
169 | $connection->write(\GuzzleHttp\Psr7\str($cnRequest)); | |
144 | 170 | }); |
145 | 171 | |
146 | 172 | return $deferred->promise(); |
147 | 173 | } |
148 | 174 | |
149 | 175 | function createReport() { |
150 | global $factory; | |
176 | global $connector; | |
151 | 177 | global $testServer; |
152 | 178 | |
153 | 179 | $deferred = new Deferred(); |
154 | 180 | |
155 | $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { | |
156 | $reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true"; | |
157 | $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(); | |
181 | $connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred) { | |
182 | // $reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true"; | |
183 | // we will stop it using docker now instead of just shutting down | |
184 | $reportPath = "/updateReports?agent=" . AGENT; | |
185 | $cn = new ClientNegotiator(); | |
158 | 186 | $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $reportPath)); |
159 | 187 | |
160 | 188 | $rawResponse = ""; |
161 | 189 | $response = null; |
162 | 190 | |
163 | /** @var \Ratchet\RFC6455\Messaging\MessageBuffer $ms */ | |
191 | /** @var MessageBuffer $ms */ | |
164 | 192 | $ms = null; |
165 | 193 | |
166 | $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { | |
194 | $connection->on('data', function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { | |
167 | 195 | if ($response === null) { |
168 | 196 | $rawResponse .= $data; |
169 | 197 | $pos = strpos($rawResponse, "\r\n\r\n"); |
173 | 201 | $response = \GuzzleHttp\Psr7\parse_response($rawResponse); |
174 | 202 | |
175 | 203 | if (!$cn->validateResponse($cnRequest, $response)) { |
176 | $stream->end(); | |
204 | $connection->end(); | |
177 | 205 | $deferred->reject(); |
178 | 206 | } else { |
179 | $ms = new \Ratchet\RFC6455\Messaging\MessageBuffer( | |
180 | new \Ratchet\RFC6455\Messaging\CloseFrameChecker, | |
181 | function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) { | |
207 | $ms = new MessageBuffer( | |
208 | new CloseFrameChecker, | |
209 | function (MessageInterface $msg) use ($deferred, $connection) { | |
182 | 210 | $deferred->resolve($msg->getPayload()); |
183 | $stream->close(); | |
211 | $connection->close(); | |
184 | 212 | }, |
185 | 213 | null, |
186 | false | |
214 | false, | |
215 | null, | |
216 | null, | |
217 | null, | |
218 | function () {} | |
187 | 219 | ); |
188 | 220 | } |
189 | 221 | } |
195 | 227 | } |
196 | 228 | }); |
197 | 229 | |
198 | $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); | |
230 | $connection->write(\GuzzleHttp\Psr7\str($cnRequest)); | |
199 | 231 | }); |
200 | 232 | |
201 | 233 | return $deferred->promise(); |
213 | 245 | $allDeferred->resolve(); |
214 | 246 | return; |
215 | 247 | } |
216 | runTest($i)->then($runNextCase); | |
248 | echo "Running test $i/$count..."; | |
249 | $startTime = microtime(true); | |
250 | runTest($i) | |
251 | ->then(function () use ($startTime) { | |
252 | echo " completed " . round((microtime(true) - $startTime) * 1000) . " ms\n"; | |
253 | }) | |
254 | ->then($runNextCase); | |
217 | 255 | }; |
218 | 256 | |
219 | 257 | $i = 0; |
0 | #!/bin/bash | |
1 | set -x | |
2 | ||
3 | echo "Running $0" | |
4 | ||
5 | echo Adding "$1 host.ratchet.internal" to /etc/hosts file | |
6 | ||
7 | echo $1 host.ratchet.internal >> /etc/hosts | |
8 | ||
9 | echo /etc/hosts contains: | |
10 | cat /etc/hosts | |
11 | echo |
1 | 1 | "options": { |
2 | 2 | "failByDrop": false |
3 | 3 | } |
4 | , "outdir": "./reports/servers" | |
4 | , "outdir": "/reports/servers" | |
5 | 5 | , "servers": [{ |
6 | "agent": "RatchetRFC/0.1.0" | |
7 | , "url": "ws://localhost:9001" | |
6 | "agent": "RatchetRFC/0.3" | |
7 | , "url": "ws://host.ratchet.internal:9001" | |
8 | 8 | , "options": {"version": 18} |
9 | 9 | }] |
10 | , "cases": ["*"] | |
11 | , "exclude-cases": ["6.4.*", "12.*","13.*"] | |
10 | , "cases": [ | |
11 | "*" | |
12 | ] | |
13 | , "exclude-cases": [] | |
12 | 14 | , "exclude-agent-cases": {} |
13 | 15 | } |
0 | { | |
1 | "options": { | |
2 | "failByDrop": false | |
3 | } | |
4 | , "outdir": "/reports/servers" | |
5 | , "servers": [{ | |
6 | "agent": "RatchetRFC/0.3" | |
7 | , "url": "ws://host.ratchet.internal:9001" | |
8 | , "options": {"version": 18} | |
9 | }] | |
10 | , "cases": ["*"] | |
11 | , "exclude-cases": ["12.*", "13.*"] | |
12 | , "exclude-agent-cases": {} | |
13 | } |
3 | 3 | "failByDrop": false |
4 | 4 | } |
5 | 5 | , "outdir": "./reports/clients" |
6 | , "cases": ["*"] | |
7 | , "exclude-cases": ["6.4.*", "12.*", "13.*"] | |
6 | , "cases": [ | |
7 | "*" | |
8 | ] | |
9 | , "exclude-cases": [] | |
8 | 10 | , "exclude-agent-cases": {} |
9 | 11 | } |
0 | { | |
1 | "url": "ws://127.0.0.1:9001" | |
2 | , "options": { | |
3 | "failByDrop": false | |
4 | } | |
5 | , "outdir": "./reports/clients" | |
6 | , "cases": ["*"] | |
7 | , "exclude-cases": ["12.*", "13.*"] | |
8 | , "exclude-agent-cases": {} | |
9 | } |
0 | set -x | |
0 | 1 | cd tests/ab |
1 | 2 | |
2 | wstest -m fuzzingserver -s fuzzingserver.json & | |
3 | sleep 5 | |
4 | php clientRunner.php | |
3 | SKIP_DEFLATE= | |
4 | if [ "$TRAVIS" = "true" ]; then | |
5 | if [ $(phpenv version-name) = "hhvm" -o $(phpenv version-name) = "5.4" -o $(phpenv version-name) = "5.5" -o $(phpenv version-name) = "5.6" ]; then | |
6 | echo "Skipping deflate autobahn tests for $(phpenv version-name)" | |
7 | SKIP_DEFLATE=_skip_deflate | |
8 | fi | |
9 | fi | |
5 | 10 | |
6 | sleep 2 | |
11 | if [ "$ABTEST" = "client" ]; then | |
12 | docker run --rm \ | |
13 | -d \ | |
14 | -v ${PWD}:/config \ | |
15 | -v ${PWD}/reports:/reports \ | |
16 | -p 9001:9001 \ | |
17 | --name fuzzingserver \ | |
18 | crossbario/autobahn-testsuite wstest -m fuzzingserver -s /config/fuzzingserver$SKIP_DEFLATE.json | |
19 | sleep 5 | |
20 | if [ "$TRAVIS" != "true" ]; then | |
21 | echo "Running tests vs Autobahn test client" | |
22 | ###docker run -it --rm --name abpytest crossbario/autobahn-testsuite wstest --mode testeeclient -w ws://host.docker.internal:9001 | |
23 | fi | |
24 | php -d memory_limit=256M clientRunner.php | |
7 | 25 | |
8 | php startServer.php & | |
9 | sleep 3 | |
10 | wstest -m fuzzingclient -s fuzzingclient.json | |
26 | docker ps -a | |
27 | ||
28 | docker logs fuzzingserver | |
29 | ||
30 | docker stop fuzzingserver | |
31 | ||
32 | sleep 2 | |
33 | fi | |
34 | ||
35 | if [ "$ABTEST" = "server" ]; then | |
36 | php -d memory_limit=256M startServer.php & | |
37 | sleep 3 | |
38 | ||
39 | if [ "$OSTYPE" = "linux-gnu" ]; then | |
40 | IPADDR=`hostname -I | cut -f 1 -d ' '` | |
41 | else | |
42 | IPADDR=`ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' | head -1 | tr -d 'adr:'` | |
43 | fi | |
44 | ||
45 | docker run --rm \ | |
46 | -it \ | |
47 | -v ${PWD}:/config \ | |
48 | -v ${PWD}/reports:/reports \ | |
49 | --name fuzzingclient \ | |
50 | crossbario/autobahn-testsuite /bin/sh -c "sh /config/docker_bootstrap.sh $IPADDR; wstest -m fuzzingclient -s /config/fuzzingclient$SKIP_DEFLATE.json" | |
51 | sleep 1 | |
52 | ||
53 | # send the shutdown command to the PHP echo server | |
54 | wget -O - -q http://127.0.0.1:9001/shutdown | |
55 | fi | |
56 | ||
57 |
0 | 0 | <?php |
1 | ||
2 | use GuzzleHttp\Psr7\Response; | |
3 | use Ratchet\RFC6455\Handshake\PermessageDeflateOptions; | |
4 | use Ratchet\RFC6455\Messaging\MessageBuffer; | |
1 | 5 | use Ratchet\RFC6455\Messaging\MessageInterface; |
2 | 6 | use Ratchet\RFC6455\Messaging\FrameInterface; |
3 | 7 | use Ratchet\RFC6455\Messaging\Frame; |
6 | 10 | |
7 | 11 | $loop = \React\EventLoop\Factory::create(); |
8 | 12 | |
9 | $socket = new \React\Socket\Server($loop); | |
10 | $server = new \React\Http\Server($socket); | |
13 | $socket = new \React\Socket\Server('0.0.0.0:9001', $loop); | |
11 | 14 | |
12 | 15 | $closeFrameChecker = new \Ratchet\RFC6455\Messaging\CloseFrameChecker; |
13 | $negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator(new \Ratchet\RFC6455\Handshake\RequestVerifier); | |
16 | $negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator(new \Ratchet\RFC6455\Handshake\RequestVerifier, PermessageDeflateOptions::permessageDeflateSupported()); | |
14 | 17 | |
15 | 18 | $uException = new \UnderflowException; |
16 | 19 | |
17 | $server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $closeFrameChecker, $uException) { | |
18 | $psrRequest = new \GuzzleHttp\Psr7\Request($request->getMethod(), $request->getPath(), $request->getHeaders()); | |
19 | 20 | |
20 | $negotiatorResponse = $negotiator->handshake($psrRequest); | |
21 | $socket->on('connection', function (React\Socket\ConnectionInterface $connection) use ($negotiator, $closeFrameChecker, $uException, $socket) { | |
22 | $headerComplete = false; | |
23 | $buffer = ''; | |
24 | $parser = null; | |
25 | $connection->on('data', function ($data) use ($connection, &$parser, &$headerComplete, &$buffer, $negotiator, $closeFrameChecker, $uException, $socket) { | |
26 | if ($headerComplete) { | |
27 | $parser->onData($data); | |
28 | return; | |
29 | } | |
21 | 30 | |
22 | $response->writeHead( | |
23 | $negotiatorResponse->getStatusCode(), | |
24 | array_merge( | |
25 | $negotiatorResponse->getHeaders(), | |
26 | ["Content-Length" => "0"] | |
27 | ) | |
28 | ); | |
31 | $buffer .= $data; | |
32 | $parts = explode("\r\n\r\n", $buffer); | |
33 | if (count($parts) < 2) { | |
34 | return; | |
35 | } | |
36 | $headerComplete = true; | |
37 | $psrRequest = \GuzzleHttp\Psr7\parse_request($parts[0] . "\r\n\r\n"); | |
38 | $negotiatorResponse = $negotiator->handshake($psrRequest); | |
29 | 39 | |
30 | if ($negotiatorResponse->getStatusCode() !== 101) { | |
31 | $response->end(); | |
32 | return; | |
33 | } | |
40 | $negotiatorResponse = $negotiatorResponse->withAddedHeader("Content-Length", "0"); | |
34 | 41 | |
35 | $parser = new \Ratchet\RFC6455\Messaging\MessageBuffer($closeFrameChecker, function(MessageInterface $message) use ($response) { | |
36 | $response->write($message->getContents()); | |
37 | }, function(FrameInterface $frame) use ($response, &$parser) { | |
38 | switch ($frame->getOpCode()) { | |
39 | case Frame::OP_CLOSE: | |
40 | $response->end($frame->getContents()); | |
41 | break; | |
42 | case Frame::OP_PING: | |
43 | $response->write($parser->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents()); | |
44 | break; | |
42 | if ($negotiatorResponse->getStatusCode() !== 101 && $psrRequest->getUri()->getPath() === '/shutdown') { | |
43 | $connection->end(\GuzzleHttp\Psr7\str(new Response(200, [], 'Shutting down echo server.' . PHP_EOL))); | |
44 | $socket->close(); | |
45 | return; | |
46 | }; | |
47 | ||
48 | $connection->write(\GuzzleHttp\Psr7\str($negotiatorResponse)); | |
49 | ||
50 | if ($negotiatorResponse->getStatusCode() !== 101) { | |
51 | $connection->end(); | |
52 | return; | |
45 | 53 | } |
46 | }, true, function() use ($uException) { | |
47 | return $uException; | |
54 | ||
55 | // there is no need to look through the client requests | |
56 | // we support any valid permessage deflate | |
57 | $deflateOptions = PermessageDeflateOptions::fromRequestOrResponse($psrRequest)[0]; | |
58 | ||
59 | $parser = new \Ratchet\RFC6455\Messaging\MessageBuffer($closeFrameChecker, | |
60 | function (MessageInterface $message, MessageBuffer $messageBuffer) { | |
61 | $messageBuffer->sendMessage($message->getPayload(), true, $message->isBinary()); | |
62 | }, function (FrameInterface $frame) use ($connection, &$parser) { | |
63 | switch ($frame->getOpCode()) { | |
64 | case Frame::OP_CLOSE: | |
65 | $connection->end($frame->getContents()); | |
66 | break; | |
67 | case Frame::OP_PING: | |
68 | $connection->write($parser->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents()); | |
69 | break; | |
70 | } | |
71 | }, true, function () use ($uException) { | |
72 | return $uException; | |
73 | }, | |
74 | null, | |
75 | null, | |
76 | [$connection, 'write'], | |
77 | $deflateOptions); | |
78 | ||
79 | array_shift($parts); | |
80 | $parser->onData(implode("\r\n\r\n", $parts)); | |
48 | 81 | }); |
49 | ||
50 | $request->on('data', [$parser, 'onData']); | |
51 | 82 | }); |
52 | 83 | |
53 | $socket->listen(9001, '0.0.0.0'); | |
54 | 84 | $loop->run(); |
0 | <?php | |
1 | ||
2 | namespace Ratchet\RFC6455\Test\Unit\Handshake; | |
3 | ||
4 | use Ratchet\RFC6455\Handshake\PermessageDeflateOptions; | |
5 | use PHPUnit\Framework\TestCase; | |
6 | ||
7 | class PermessageDeflateOptionsTest extends TestCase | |
8 | { | |
9 | public static function versionSupportProvider() { | |
10 | return [ | |
11 | ['7.0.17', false], | |
12 | ['7.0.18', true], | |
13 | ['7.0.200', true], | |
14 | ['5.6.0', false], | |
15 | ['7.1.3', false], | |
16 | ['7.1.4', true], | |
17 | ['7.1.200', true], | |
18 | ['10.0.0', true] | |
19 | ]; | |
20 | } | |
21 | ||
22 | /** | |
23 | * @requires function deflate_init | |
24 | * @dataProvider versionSupportProvider | |
25 | */ | |
26 | public function testVersionSupport($version, $supported) { | |
27 | $this->assertEquals($supported, PermessageDeflateOptions::permessageDeflateSupported($version)); | |
28 | } | |
29 | }⏎ |
0 | 0 | <?php |
1 | ||
1 | 2 | namespace Ratchet\RFC6455\Test\Unit\Handshake; |
3 | ||
2 | 4 | use Ratchet\RFC6455\Handshake\RequestVerifier; |
5 | use PHPUnit\Framework\TestCase; | |
3 | 6 | |
4 | 7 | /** |
5 | 8 | * @covers Ratchet\RFC6455\Handshake\RequestVerifier |
6 | 9 | */ |
7 | class RequestVerifierTest extends \PHPUnit_Framework_TestCase { | |
10 | class RequestVerifierTest extends TestCase { | |
8 | 11 | /** |
9 | 12 | * @var RequestVerifier |
10 | 13 | */ |
0 | 0 | <?php |
1 | ||
1 | 2 | namespace Ratchet\RFC6455\Test\Unit\Handshake; |
3 | ||
2 | 4 | use Ratchet\RFC6455\Handshake\ResponseVerifier; |
5 | use PHPUnit\Framework\TestCase; | |
3 | 6 | |
4 | 7 | /** |
5 | 8 | * @covers Ratchet\RFC6455\Handshake\ResponseVerifier |
6 | 9 | */ |
7 | class ResponseVerifierTest extends \PHPUnit_Framework_TestCase { | |
10 | class ResponseVerifierTest extends TestCase { | |
8 | 11 | /** |
9 | 12 | * @var ResponseVerifier |
10 | 13 | */ |
17 | 20 | public static function subProtocolsProvider() { |
18 | 21 | return [ |
19 | 22 | [true, ['a'], ['a']] |
20 | , [true, ['b', 'a'], ['c', 'd', 'a']] | |
21 | , [false, ['a', 'b', 'c'], ['d']] | |
23 | , [true, ['c', 'd', 'a'], ['a']] | |
24 | , [true, ['c, a', 'd'], ['a']] | |
22 | 25 | , [true, [], []] |
23 | 26 | , [true, ['a', 'b'], []] |
27 | , [false, ['c', 'd', 'a'], ['b', 'a']] | |
28 | , [false, ['a', 'b', 'c'], ['d']] | |
24 | 29 | ]; |
25 | 30 | } |
26 | 31 | |
27 | 32 | /** |
28 | 33 | * @dataProvider subProtocolsProvider |
29 | 34 | */ |
30 | public function testVerifySubProtocol($expected, $response, $request) { | |
31 | $this->assertEquals($expected, $this->_v->verifySubProtocol($response, $request)); | |
35 | public function testVerifySubProtocol($expected, $request, $response) { | |
36 | $this->assertEquals($expected, $this->_v->verifySubProtocol($request, $response)); | |
32 | 37 | } |
33 | 38 | } |
3 | 3 | |
4 | 4 | use Ratchet\RFC6455\Handshake\RequestVerifier; |
5 | 5 | use Ratchet\RFC6455\Handshake\ServerNegotiator; |
6 | ||
7 | class ServerNegotiatorTest extends \PHPUnit_Framework_TestCase | |
6 | use PHPUnit\Framework\TestCase; | |
7 | ||
8 | class ServerNegotiatorTest extends TestCase | |
8 | 9 | { |
9 | 10 | public function testNoUpgradeRequested() { |
10 | 11 | $negotiator = new ServerNegotiator(new RequestVerifier()); |
18 | 19 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 |
19 | 20 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 |
20 | 21 | Accept-Encoding: gzip, deflate, sdch, br |
21 | Accept-Language: en-US,en;q=0.8'; | |
22 | Accept-Language: en-US,en;q=0.8 | |
23 | ||
24 | '; | |
22 | 25 | |
23 | 26 | $request = \GuzzleHttp\Psr7\parse_request($requestText); |
24 | 27 | |
45 | 48 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 |
46 | 49 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 |
47 | 50 | Accept-Encoding: gzip, deflate, sdch, br |
48 | Accept-Language: en-US,en;q=0.8'; | |
51 | Accept-Language: en-US,en;q=0.8 | |
52 | ||
53 | '; | |
49 | 54 | |
50 | 55 | $request = \GuzzleHttp\Psr7\parse_request($requestText); |
51 | 56 | |
70 | 75 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 |
71 | 76 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 |
72 | 77 | Accept-Encoding: gzip, deflate, sdch, br |
73 | Accept-Language: en-US,en;q=0.8'; | |
78 | Accept-Language: en-US,en;q=0.8 | |
79 | ||
80 | '; | |
74 | 81 | |
75 | 82 | $request = \GuzzleHttp\Psr7\parse_request($requestText); |
76 | 83 | |
95 | 102 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 |
96 | 103 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 |
97 | 104 | Accept-Encoding: gzip, deflate, sdch, br |
98 | Accept-Language: en-US,en;q=0.8'; | |
105 | Accept-Language: en-US,en;q=0.8 | |
106 | ||
107 | '; | |
99 | 108 | |
100 | 109 | $request = \GuzzleHttp\Psr7\parse_request($requestText); |
101 | 110 | |
127 | 136 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 |
128 | 137 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 |
129 | 138 | Accept-Encoding: gzip, deflate, sdch, br |
130 | Accept-Language: en-US,en;q=0.8'; | |
139 | Accept-Language: en-US,en;q=0.8 | |
140 | ||
141 | '; | |
131 | 142 | |
132 | 143 | $request = \GuzzleHttp\Psr7\parse_request($requestText); |
133 | 144 | |
159 | 170 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 |
160 | 171 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 |
161 | 172 | Accept-Encoding: gzip, deflate, sdch, br |
162 | Accept-Language: en-US,en;q=0.8'; | |
173 | Accept-Language: en-US,en;q=0.8 | |
174 | ||
175 | '; | |
163 | 176 | |
164 | 177 | $request = \GuzzleHttp\Psr7\parse_request($requestText); |
165 | 178 | |
171 | 184 | $this->assertEquals('websocket', $response->getHeaderLine('Upgrade')); |
172 | 185 | $this->assertFalse($response->hasHeader('Sec-WebSocket-Protocol')); |
173 | 186 | } |
174 | }⏎ | |
187 | ||
188 | public function testSuggestsAppropriateSubprotocol() | |
189 | { | |
190 | $negotiator = new ServerNegotiator(new RequestVerifier()); | |
191 | $negotiator->setStrictSubProtocolCheck(true); | |
192 | $negotiator->setSupportedSubProtocols(['someproto']); | |
193 | ||
194 | $requestText = 'GET / HTTP/1.1 | |
195 | Host: localhost:8080 | |
196 | Connection: Upgrade | |
197 | Upgrade: websocket | |
198 | Sec-WebSocket-Version: 13 | |
199 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 | |
200 | Accept-Encoding: gzip, deflate, br | |
201 | Accept-Language: en-US,en;q=0.9 | |
202 | Sec-WebSocket-Key: HGt8eQax7nAOlXUw0/asPQ== | |
203 | Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits | |
204 | ||
205 | '; | |
206 | ||
207 | $request = \GuzzleHttp\Psr7\parse_request($requestText); | |
208 | ||
209 | $response = $negotiator->handshake($request); | |
210 | ||
211 | $this->assertEquals('1.1', $response->getProtocolVersion()); | |
212 | $this->assertEquals(426, $response->getStatusCode()); | |
213 | $this->assertEquals('Upgrade', $response->getHeaderLine('Connection')); | |
214 | $this->assertEquals('websocket', $response->getHeaderLine('Upgrade')); | |
215 | $this->assertEquals('someproto', $response->getHeaderLine('Sec-WebSocket-Protocol')); | |
216 | } | |
217 | } |
0 | 0 | <?php |
1 | ||
1 | 2 | namespace Ratchet\RFC6455\Test\Unit\Messaging; |
3 | ||
2 | 4 | use Ratchet\RFC6455\Messaging\Frame; |
5 | use PHPUnit\Framework\TestCase; | |
3 | 6 | |
4 | 7 | /** |
5 | 8 | * @covers Ratchet\RFC6455\Messaging\Frame |
6 | 9 | * @todo getMaskingKey, getPayloadStartingByte don't have tests yet |
7 | 10 | * @todo Could use some clean up in general, I had to rush to fix a bug for a deadline, sorry. |
8 | 11 | */ |
9 | class FrameTest extends \PHPUnit_Framework_TestCase { | |
12 | class FrameTest extends TestCase { | |
10 | 13 | protected $_firstByteFinText = '10000001'; |
11 | 14 | |
12 | 15 | protected $_secondByteMaskedSPL = '11111101'; |
5 | 5 | use Ratchet\RFC6455\Messaging\Frame; |
6 | 6 | use Ratchet\RFC6455\Messaging\Message; |
7 | 7 | use Ratchet\RFC6455\Messaging\MessageBuffer; |
8 | ||
9 | class MessageBufferTest extends \PHPUnit_Framework_TestCase | |
8 | use React\EventLoop\Factory; | |
9 | use PHPUnit\Framework\TestCase; | |
10 | ||
11 | class MessageBufferTest extends TestCase | |
10 | 12 | { |
11 | 13 | /** |
12 | 14 | * This is to test that MessageBuffer can handle a large receive |
35 | 37 | |
36 | 38 | $this->assertEquals(1000, $messageCount); |
37 | 39 | } |
40 | ||
41 | public function testProcessingMessagesAsynchronouslyWhileBlockingInMessageHandler() { | |
42 | $loop = Factory::create(); | |
43 | ||
44 | $frameA = new Frame('a', true, Frame::OP_TEXT); | |
45 | $frameB = new Frame('b', true, Frame::OP_TEXT); | |
46 | ||
47 | $bReceived = false; | |
48 | ||
49 | $messageBuffer = new MessageBuffer( | |
50 | new CloseFrameChecker(), | |
51 | function (Message $message) use (&$messageCount, &$bReceived, $loop) { | |
52 | $payload = $message->getPayload(); | |
53 | $bReceived = $payload === 'b'; | |
54 | ||
55 | if (!$bReceived) { | |
56 | $loop->run(); | |
57 | } | |
58 | }, | |
59 | null, | |
60 | false | |
61 | ); | |
62 | ||
63 | $loop->addPeriodicTimer(0.1, function () use ($messageBuffer, $frameB, $loop) { | |
64 | $loop->stop(); | |
65 | $messageBuffer->onData($frameB->getContents()); | |
66 | }); | |
67 | ||
68 | $messageBuffer->onData($frameA->getContents()); | |
69 | ||
70 | $this->assertTrue($bReceived); | |
71 | } | |
72 | ||
73 | public function testInvalidFrameLength() { | |
74 | $frame = new Frame(str_repeat('a', 200), true, Frame::OP_TEXT); | |
75 | ||
76 | $frameRaw = $frame->getContents(); | |
77 | ||
78 | $frameRaw[1] = "\x7f"; // 127 in the first spot | |
79 | ||
80 | $frameRaw[2] = "\xff"; // this will unpack to -1 | |
81 | $frameRaw[3] = "\xff"; | |
82 | $frameRaw[4] = "\xff"; | |
83 | $frameRaw[5] = "\xff"; | |
84 | $frameRaw[6] = "\xff"; | |
85 | $frameRaw[7] = "\xff"; | |
86 | $frameRaw[8] = "\xff"; | |
87 | $frameRaw[9] = "\xff"; | |
88 | ||
89 | /** @var Frame $controlFrame */ | |
90 | $controlFrame = null; | |
91 | $messageCount = 0; | |
92 | ||
93 | $messageBuffer = new MessageBuffer( | |
94 | new CloseFrameChecker(), | |
95 | function (Message $message) use (&$messageCount) { | |
96 | $messageCount++; | |
97 | }, | |
98 | function (Frame $frame) use (&$controlFrame) { | |
99 | $this->assertNull($controlFrame); | |
100 | $controlFrame = $frame; | |
101 | }, | |
102 | false, | |
103 | null, | |
104 | 0, | |
105 | 10 | |
106 | ); | |
107 | ||
108 | $messageBuffer->onData($frameRaw); | |
109 | ||
110 | $this->assertEquals(0, $messageCount); | |
111 | $this->assertTrue($controlFrame instanceof Frame); | |
112 | $this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode()); | |
113 | $this->assertEquals([Frame::CLOSE_PROTOCOL], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2)))); | |
114 | ||
115 | } | |
116 | ||
117 | public function testFrameLengthTooBig() { | |
118 | $frame = new Frame(str_repeat('a', 200), true, Frame::OP_TEXT); | |
119 | ||
120 | $frameRaw = $frame->getContents(); | |
121 | ||
122 | $frameRaw[1] = "\x7f"; // 127 in the first spot | |
123 | ||
124 | $frameRaw[2] = "\x7f"; // this will unpack to -1 | |
125 | $frameRaw[3] = "\xff"; | |
126 | $frameRaw[4] = "\xff"; | |
127 | $frameRaw[5] = "\xff"; | |
128 | $frameRaw[6] = "\xff"; | |
129 | $frameRaw[7] = "\xff"; | |
130 | $frameRaw[8] = "\xff"; | |
131 | $frameRaw[9] = "\xff"; | |
132 | ||
133 | /** @var Frame $controlFrame */ | |
134 | $controlFrame = null; | |
135 | $messageCount = 0; | |
136 | ||
137 | $messageBuffer = new MessageBuffer( | |
138 | new CloseFrameChecker(), | |
139 | function (Message $message) use (&$messageCount) { | |
140 | $messageCount++; | |
141 | }, | |
142 | function (Frame $frame) use (&$controlFrame) { | |
143 | $this->assertNull($controlFrame); | |
144 | $controlFrame = $frame; | |
145 | }, | |
146 | false, | |
147 | null, | |
148 | 0, | |
149 | 10 | |
150 | ); | |
151 | ||
152 | $messageBuffer->onData($frameRaw); | |
153 | ||
154 | $this->assertEquals(0, $messageCount); | |
155 | $this->assertTrue($controlFrame instanceof Frame); | |
156 | $this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode()); | |
157 | $this->assertEquals([Frame::CLOSE_TOO_BIG], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2)))); | |
158 | } | |
159 | ||
160 | public function testFrameLengthBiggerThanMaxMessagePayload() { | |
161 | $frame = new Frame(str_repeat('a', 200), true, Frame::OP_TEXT); | |
162 | ||
163 | $frameRaw = $frame->getContents(); | |
164 | ||
165 | /** @var Frame $controlFrame */ | |
166 | $controlFrame = null; | |
167 | $messageCount = 0; | |
168 | ||
169 | $messageBuffer = new MessageBuffer( | |
170 | new CloseFrameChecker(), | |
171 | function (Message $message) use (&$messageCount) { | |
172 | $messageCount++; | |
173 | }, | |
174 | function (Frame $frame) use (&$controlFrame) { | |
175 | $this->assertNull($controlFrame); | |
176 | $controlFrame = $frame; | |
177 | }, | |
178 | false, | |
179 | null, | |
180 | 100, | |
181 | 0 | |
182 | ); | |
183 | ||
184 | $messageBuffer->onData($frameRaw); | |
185 | ||
186 | $this->assertEquals(0, $messageCount); | |
187 | $this->assertTrue($controlFrame instanceof Frame); | |
188 | $this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode()); | |
189 | $this->assertEquals([Frame::CLOSE_TOO_BIG], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2)))); | |
190 | } | |
191 | ||
192 | public function testSecondFrameLengthPushesPastMaxMessagePayload() { | |
193 | $frame = new Frame(str_repeat('a', 200), false, Frame::OP_TEXT); | |
194 | $firstFrameRaw = $frame->getContents(); | |
195 | $frame = new Frame(str_repeat('b', 200), true, Frame::OP_TEXT); | |
196 | $secondFrameRaw = $frame->getContents(); | |
197 | ||
198 | /** @var Frame $controlFrame */ | |
199 | $controlFrame = null; | |
200 | $messageCount = 0; | |
201 | ||
202 | $messageBuffer = new MessageBuffer( | |
203 | new CloseFrameChecker(), | |
204 | function (Message $message) use (&$messageCount) { | |
205 | $messageCount++; | |
206 | }, | |
207 | function (Frame $frame) use (&$controlFrame) { | |
208 | $this->assertNull($controlFrame); | |
209 | $controlFrame = $frame; | |
210 | }, | |
211 | false, | |
212 | null, | |
213 | 300, | |
214 | 0 | |
215 | ); | |
216 | ||
217 | $messageBuffer->onData($firstFrameRaw); | |
218 | // only put part of the second frame in to watch it fail fast | |
219 | $messageBuffer->onData(substr($secondFrameRaw, 0, 150)); | |
220 | ||
221 | $this->assertEquals(0, $messageCount); | |
222 | $this->assertTrue($controlFrame instanceof Frame); | |
223 | $this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode()); | |
224 | $this->assertEquals([Frame::CLOSE_TOO_BIG], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2)))); | |
225 | } | |
226 | ||
227 | /** | |
228 | * Some test cases from memory limit inspired by https://github.com/BrandEmbassy/php-memory | |
229 | * | |
230 | * Here is the license for that project: | |
231 | * MIT License | |
232 | * | |
233 | * Copyright (c) 2018 Brand Embassy | |
234 | * | |
235 | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
236 | * of this software and associated documentation files (the "Software"), to deal | |
237 | * in the Software without restriction, including without limitation the rights | |
238 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
239 | * copies of the Software, and to permit persons to whom the Software is | |
240 | * furnished to do so, subject to the following conditions: | |
241 | * | |
242 | * The above copyright notice and this permission notice shall be included in all | |
243 | * copies or substantial portions of the Software. | |
244 | * | |
245 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
246 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
247 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
248 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
249 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
250 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
251 | * SOFTWARE. | |
252 | */ | |
253 | ||
254 | /** | |
255 | * @dataProvider phpConfigurationProvider | |
256 | * | |
257 | * @param string $phpConfigurationValue | |
258 | * @param int $expectedLimit | |
259 | */ | |
260 | public function testMemoryLimits($phpConfigurationValue, $expectedLimit) { | |
261 | $method = new \ReflectionMethod('Ratchet\RFC6455\Messaging\MessageBuffer', 'getMemoryLimit'); | |
262 | $method->setAccessible(true); | |
263 | $actualLimit = $method->invoke(null, $phpConfigurationValue); | |
264 | ||
265 | $this->assertSame($expectedLimit, $actualLimit); | |
266 | } | |
267 | ||
268 | public function phpConfigurationProvider() { | |
269 | return [ | |
270 | 'without unit type, just bytes' => ['500', 500], | |
271 | '1 GB with big "G"' => ['1G', 1 * 1024 * 1024 * 1024], | |
272 | '128 MB with big "M"' => ['128M', 128 * 1024 * 1024], | |
273 | '128 MB with small "m"' => ['128m', 128 * 1024 * 1024], | |
274 | '24 kB with small "k"' => ['24k', 24 * 1024], | |
275 | '2 GB with small "g"' => ['2g', 2 * 1024 * 1024 * 1024], | |
276 | 'unlimited memory' => ['-1', 0], | |
277 | 'invalid float value' => ['2.5M', 2 * 1024 * 1024], | |
278 | 'empty value' => ['', 0], | |
279 | 'invalid ini setting' => ['whatever it takes', 0] | |
280 | ]; | |
281 | } | |
282 | ||
283 | /** | |
284 | * @expectedException \InvalidArgumentException | |
285 | */ | |
286 | public function testInvalidMaxFramePayloadSizes() { | |
287 | $messageBuffer = new MessageBuffer( | |
288 | new CloseFrameChecker(), | |
289 | function (Message $message) {}, | |
290 | function (Frame $frame) {}, | |
291 | false, | |
292 | null, | |
293 | 0, | |
294 | 0x8000000000000000 | |
295 | ); | |
296 | } | |
297 | ||
298 | /** | |
299 | * @expectedException \InvalidArgumentException | |
300 | */ | |
301 | public function testInvalidMaxMessagePayloadSizes() { | |
302 | $messageBuffer = new MessageBuffer( | |
303 | new CloseFrameChecker(), | |
304 | function (Message $message) {}, | |
305 | function (Frame $frame) {}, | |
306 | false, | |
307 | null, | |
308 | 0x8000000000000000, | |
309 | 0 | |
310 | ); | |
311 | } | |
312 | ||
313 | /** | |
314 | * @dataProvider phpConfigurationProvider | |
315 | * | |
316 | * @param string $phpConfigurationValue | |
317 | * @param int $expectedLimit | |
318 | * | |
319 | * @runInSeparateProcess | |
320 | * @requires PHP 7.0 | |
321 | */ | |
322 | public function testIniSizes($phpConfigurationValue, $expectedLimit) { | |
323 | ini_set('memory_limit', $phpConfigurationValue); | |
324 | $messageBuffer = new MessageBuffer( | |
325 | new CloseFrameChecker(), | |
326 | function (Message $message) {}, | |
327 | function (Frame $frame) {}, | |
328 | false, | |
329 | null | |
330 | ); | |
331 | ||
332 | if ($expectedLimit === -1) { | |
333 | $expectedLimit = 0; | |
334 | } | |
335 | ||
336 | $prop = new \ReflectionProperty($messageBuffer, 'maxMessagePayloadSize'); | |
337 | $prop->setAccessible(true); | |
338 | $this->assertEquals($expectedLimit / 4, $prop->getValue($messageBuffer)); | |
339 | ||
340 | $prop = new \ReflectionProperty($messageBuffer, 'maxFramePayloadSize'); | |
341 | $prop->setAccessible(true); | |
342 | $this->assertEquals($expectedLimit / 4, $prop->getValue($messageBuffer)); | |
343 | } | |
344 | ||
345 | /** | |
346 | * @runInSeparateProcess | |
347 | * @requires PHP 7.0 | |
348 | */ | |
349 | public function testInvalidIniSize() { | |
350 | ini_set('memory_limit', 'lots of memory'); | |
351 | $messageBuffer = new MessageBuffer( | |
352 | new CloseFrameChecker(), | |
353 | function (Message $message) {}, | |
354 | function (Frame $frame) {}, | |
355 | false, | |
356 | null | |
357 | ); | |
358 | ||
359 | $prop = new \ReflectionProperty($messageBuffer, 'maxMessagePayloadSize'); | |
360 | $prop->setAccessible(true); | |
361 | $this->assertEquals(0, $prop->getValue($messageBuffer)); | |
362 | ||
363 | $prop = new \ReflectionProperty($messageBuffer, 'maxFramePayloadSize'); | |
364 | $prop->setAccessible(true); | |
365 | $this->assertEquals(0, $prop->getValue($messageBuffer)); | |
366 | } | |
38 | 367 | }⏎ |
0 | 0 | <?php |
1 | ||
1 | 2 | namespace Ratchet\RFC6455\Test\Unit\Messaging; |
3 | ||
2 | 4 | use Ratchet\RFC6455\Messaging\Frame; |
3 | 5 | use Ratchet\RFC6455\Messaging\Message; |
6 | use PHPUnit\Framework\TestCase; | |
4 | 7 | |
5 | 8 | /** |
6 | 9 | * @covers Ratchet\RFC6455\Messaging\Message |
7 | 10 | */ |
8 | class MessageTest extends \PHPUnit_Framework_TestCase { | |
11 | class MessageTest extends TestCase { | |
9 | 12 | /** @var Message */ |
10 | 13 | protected $message; |
11 | 14 |