New upstream version 0.2.4
Dominik George
5 years ago
0 | language: php | |
1 | ||
2 | php: | |
3 | - 5.4 | |
4 | - 5.5 | |
5 | - 5.6 | |
6 | - 7.0 | |
7 | - 7.1 | |
8 | - 7.2 | |
9 | - hhvm | |
10 | ||
11 | before_install: | |
12 | - export PATH=$HOME/.local/bin:$PATH | |
13 | - pip install --user autobahntestsuite | |
14 | - pip list --user autobahntestsuite | |
15 | ||
16 | before_script: | |
17 | - composer install | |
18 | - sh tests/ab/run_ab_tests.sh | |
19 | ||
20 | script: | |
21 | - vendor/bin/phpunit |
0 | Copyright (c) 2011-2016 Chris Boden | |
1 | ||
2 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
3 | of this software and associated documentation files (the "Software"), to deal | |
4 | in the Software without restriction, including without limitation the rights | |
5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
6 | copies of the Software, and to permit persons to whom the Software is furnished | |
7 | to do so, subject to the following conditions: | |
8 | ||
9 | The above copyright notice and this permission notice shall be included in all | |
10 | copies or substantial portions of the Software. | |
11 | ||
12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
18 | THE SOFTWARE. |
0 | # RFC6455 - The WebSocket Protocol | |
1 | ||
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) | |
4 | ||
5 | This library a protocol handler for the RFC6455 specification. | |
6 | It contains components for both server and client side handshake and messaging protocol negotation. | |
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. | |
10 | ||
11 | This library is independent, framework agnostic, and does not deal with any I/O. | |
12 | HTTP upgrade negotiation integration points are handled with PSR-7 interfaces. |
0 | { | |
1 | "name": "ratchet/rfc6455", | |
2 | "type": "library", | |
3 | "description": "RFC6455 WebSocket protocol handler", | |
4 | "keywords": ["WebSockets", "websocket", "RFC6455"], | |
5 | "homepage": "http://socketo.me", | |
6 | "license": "MIT", | |
7 | "authors": [{ | |
8 | "name": "Chris Boden" | |
9 | , "email": "cboden@gmail.com" | |
10 | , "role": "Developer" | |
11 | }], | |
12 | "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" | |
16 | }, | |
17 | "autoload": { | |
18 | "psr-4": { | |
19 | "Ratchet\\RFC6455\\": "src" | |
20 | } | |
21 | }, | |
22 | "require": { | |
23 | "php": ">=5.4.2", | |
24 | "guzzlehttp/psr7": "^1.0" | |
25 | }, | |
26 | "require-dev": { | |
27 | "react/http": "^0.4.1", | |
28 | "react/socket-client": "^0.4.3", | |
29 | "phpunit/phpunit": "4.8.*" | |
30 | } | |
31 | } |
0 | <?xml version="1.0" encoding="UTF-8"?> | |
1 | <phpunit | |
2 | forceCoversAnnotation="true" | |
3 | mapTestClassNameToCoveredClassName="true" | |
4 | bootstrap="tests/bootstrap.php" | |
5 | colors="true" | |
6 | backupGlobals="false" | |
7 | backupStaticAttributes="false" | |
8 | syntaxCheck="false" | |
9 | stopOnError="false" | |
10 | > | |
11 | ||
12 | <testsuites> | |
13 | <testsuite name="tests"> | |
14 | <directory>tests</directory> | |
15 | <exclude> | |
16 | <directory>test/ab</directory> | |
17 | </exclude> | |
18 | </testsuite> | |
19 | </testsuites> | |
20 | ||
21 | <filter> | |
22 | <whitelist> | |
23 | <directory>./src/</directory> | |
24 | </whitelist> | |
25 | </filter> | |
26 | </phpunit>⏎ |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Handshake; | |
2 | use Psr\Http\Message\RequestInterface; | |
3 | use Psr\Http\Message\ResponseInterface; | |
4 | use Psr\Http\Message\UriInterface; | |
5 | use GuzzleHttp\Psr7\Request; | |
6 | ||
7 | class ClientNegotiator { | |
8 | /** | |
9 | * @var ResponseVerifier | |
10 | */ | |
11 | private $verifier; | |
12 | ||
13 | /** | |
14 | * @var \Psr\Http\Message\RequestInterface | |
15 | */ | |
16 | private $defaultHeader; | |
17 | ||
18 | function __construct() { | |
19 | $this->verifier = new ResponseVerifier; | |
20 | ||
21 | $this->defaultHeader = new Request('GET', '', [ | |
22 | 'Connection' => 'Upgrade' | |
23 | , 'Upgrade' => 'websocket' | |
24 | , 'Sec-WebSocket-Version' => $this->getVersion() | |
25 | , 'User-Agent' => "Ratchet" | |
26 | ]); | |
27 | } | |
28 | ||
29 | public function generateRequest(UriInterface $uri) { | |
30 | return $this->defaultHeader->withUri($uri) | |
31 | ->withHeader("Sec-WebSocket-Key", $this->generateKey()); | |
32 | } | |
33 | ||
34 | public function validateResponse(RequestInterface $request, ResponseInterface $response) { | |
35 | return $this->verifier->verifyAll($request, $response); | |
36 | } | |
37 | ||
38 | public function generateKey() { | |
39 | $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwzyz1234567890+/='; | |
40 | $charRange = strlen($chars) - 1; | |
41 | $key = ''; | |
42 | for ($i = 0; $i < 16; $i++) { | |
43 | $key .= $chars[mt_rand(0, $charRange)]; | |
44 | } | |
45 | ||
46 | return base64_encode($key); | |
47 | } | |
48 | ||
49 | public function getVersion() { | |
50 | return 13; | |
51 | } | |
52 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Handshake; | |
2 | use Psr\Http\Message\RequestInterface; | |
3 | ||
4 | /** | |
5 | * A standard interface for interacting with the various version of the WebSocket protocol | |
6 | * @todo Look in to extension support | |
7 | */ | |
8 | interface NegotiatorInterface { | |
9 | const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; | |
10 | ||
11 | /** | |
12 | * Given an HTTP header, determine if this version should handle the protocol | |
13 | * @param RequestInterface $request | |
14 | * @return bool | |
15 | */ | |
16 | function isProtocol(RequestInterface $request); | |
17 | ||
18 | /** | |
19 | * Although the version has a name associated with it the integer returned is the proper identification | |
20 | * @return int | |
21 | */ | |
22 | function getVersionNumber(); | |
23 | ||
24 | /** | |
25 | * Perform the handshake and return the response headers | |
26 | * @param RequestInterface $request | |
27 | * @return \Psr\Http\Message\ResponseInterface | |
28 | */ | |
29 | function handshake(RequestInterface $request); | |
30 | ||
31 | /** | |
32 | * Add supported protocols. If the request has any matching the response will include one | |
33 | * @param array $protocols | |
34 | */ | |
35 | function setSupportedSubProtocols(array $protocols); | |
36 | ||
37 | /** | |
38 | * If enabled and support for a subprotocol has been added handshake | |
39 | * will not upgrade if a match between request and supported subprotocols | |
40 | * @param boolean $enable | |
41 | * @todo Consider extending this interface and moving this there. | |
42 | * The spec does says the server can fail for this reason, but | |
43 | * it is not a requirement. This is an implementation detail. | |
44 | */ | |
45 | function setStrictSubProtocolCheck($enable); | |
46 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Handshake; | |
2 | use Psr\Http\Message\RequestInterface; | |
3 | ||
4 | /** | |
5 | * These are checks to ensure the client requested handshake are valid | |
6 | * Verification rules come from section 4.2.1 of the RFC6455 document | |
7 | * @todo Currently just returning invalid - should consider returning appropriate HTTP status code error #s | |
8 | */ | |
9 | class RequestVerifier { | |
10 | const VERSION = 13; | |
11 | ||
12 | /** | |
13 | * Given an array of the headers this method will run through all verification methods | |
14 | * @param RequestInterface $request | |
15 | * @return bool TRUE if all headers are valid, FALSE if 1 or more were invalid | |
16 | */ | |
17 | public function verifyAll(RequestInterface $request) { | |
18 | $passes = 0; | |
19 | ||
20 | $passes += (int)$this->verifyMethod($request->getMethod()); | |
21 | $passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion()); | |
22 | $passes += (int)$this->verifyRequestURI($request->getUri()->getPath()); | |
23 | $passes += (int)$this->verifyHost($request->getHeader('Host')); | |
24 | $passes += (int)$this->verifyUpgradeRequest($request->getHeader('Upgrade')); | |
25 | $passes += (int)$this->verifyConnection($request->getHeader('Connection')); | |
26 | $passes += (int)$this->verifyKey($request->getHeader('Sec-WebSocket-Key')); | |
27 | $passes += (int)$this->verifyVersion($request->getHeader('Sec-WebSocket-Version')); | |
28 | ||
29 | return (8 === $passes); | |
30 | } | |
31 | ||
32 | /** | |
33 | * Test the HTTP method. MUST be "GET" | |
34 | * @param string | |
35 | * @return bool | |
36 | */ | |
37 | public function verifyMethod($val) { | |
38 | return ('get' === strtolower($val)); | |
39 | } | |
40 | ||
41 | /** | |
42 | * Test the HTTP version passed. MUST be 1.1 or greater | |
43 | * @param string|int | |
44 | * @return bool | |
45 | */ | |
46 | public function verifyHTTPVersion($val) { | |
47 | return (1.1 <= (double)$val); | |
48 | } | |
49 | ||
50 | /** | |
51 | * @param string | |
52 | * @return bool | |
53 | */ | |
54 | public function verifyRequestURI($val) { | |
55 | if ($val[0] !== '/') { | |
56 | return false; | |
57 | } | |
58 | ||
59 | if (false !== strstr($val, '#')) { | |
60 | return false; | |
61 | } | |
62 | ||
63 | if (!extension_loaded('mbstring')) { | |
64 | return true; | |
65 | } | |
66 | ||
67 | return mb_check_encoding($val, 'US-ASCII'); | |
68 | } | |
69 | ||
70 | /** | |
71 | * @param array $hostHeader | |
72 | * @return bool | |
73 | * @todo Once I fix HTTP::getHeaders just verify this isn't NULL or empty...or maybe need to verify it's a valid domain??? Or should it equal $_SERVER['HOST'] ? | |
74 | */ | |
75 | public function verifyHost(array $hostHeader) { | |
76 | return (1 === count($hostHeader)); | |
77 | } | |
78 | ||
79 | /** | |
80 | * Verify the Upgrade request to WebSockets. | |
81 | * @param array $upgradeHeader MUST equal "websocket" | |
82 | * @return bool | |
83 | */ | |
84 | public function verifyUpgradeRequest(array $upgradeHeader) { | |
85 | return (1 === count($upgradeHeader) && 'websocket' === strtolower($upgradeHeader[0])); | |
86 | } | |
87 | ||
88 | /** | |
89 | * Verify the Connection header | |
90 | * @param array $connectionHeader MUST include "Upgrade" | |
91 | * @return bool | |
92 | */ | |
93 | public function verifyConnection(array $connectionHeader) { | |
94 | foreach ($connectionHeader as $l) { | |
95 | $upgrades = array_filter( | |
96 | array_map('trim', array_map('strtolower', explode(',', $l))), | |
97 | function ($x) { | |
98 | return 'upgrade' === $x; | |
99 | } | |
100 | ); | |
101 | if (count($upgrades) > 0) { | |
102 | return true; | |
103 | } | |
104 | } | |
105 | return false; | |
106 | } | |
107 | ||
108 | /** | |
109 | * This function verifies the nonce is valid (64 big encoded, 16 bytes random string) | |
110 | * @param array $keyHeader | |
111 | * @return bool | |
112 | * @todo The spec says we don't need to base64_decode - can I just check if the length is 24 and not decode? | |
113 | * @todo Check the spec to see what the encoding of the key could be | |
114 | */ | |
115 | public function verifyKey(array $keyHeader) { | |
116 | return (1 === count($keyHeader) && 16 === strlen(base64_decode($keyHeader[0]))); | |
117 | } | |
118 | ||
119 | /** | |
120 | * Verify the version passed matches this RFC | |
121 | * @param string|int $versionHeader MUST equal 13|"13" | |
122 | * @return bool | |
123 | */ | |
124 | public function verifyVersion($versionHeader) { | |
125 | return (1 === count($versionHeader) && static::VERSION === (int)$versionHeader[0]); | |
126 | } | |
127 | ||
128 | /** | |
129 | * @todo Write logic for this method. See section 4.2.1.8 | |
130 | */ | |
131 | public function verifyProtocol($val) { | |
132 | } | |
133 | ||
134 | /** | |
135 | * @todo Write logic for this method. See section 4.2.1.9 | |
136 | */ | |
137 | public function verifyExtensions($val) { | |
138 | } | |
139 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Handshake; | |
2 | use Psr\Http\Message\RequestInterface; | |
3 | use Psr\Http\Message\ResponseInterface; | |
4 | ||
5 | class ResponseVerifier { | |
6 | public function verifyAll(RequestInterface $request, ResponseInterface $response) { | |
7 | $passes = 0; | |
8 | ||
9 | $passes += (int)$this->verifyStatus($response->getStatusCode()); | |
10 | $passes += (int)$this->verifyUpgrade($response->getHeader('Upgrade')); | |
11 | $passes += (int)$this->verifyConnection($response->getHeader('Connection')); | |
12 | $passes += (int)$this->verifySecWebSocketAccept( | |
13 | $response->getHeader('Sec-WebSocket-Accept') | |
14 | , $request->getHeader('Sec-WebSocket-Key') | |
15 | ); | |
16 | $passes += (int)$this->verifySubProtocol( | |
17 | $request->getHeader('Sec-WebSocket-Protocol') | |
18 | , $response->getHeader('Sec-WebSocket-Protocol') | |
19 | ); | |
20 | ||
21 | return (5 === $passes); | |
22 | } | |
23 | ||
24 | public function verifyStatus($status) { | |
25 | return ((int)$status === 101); | |
26 | } | |
27 | ||
28 | public function verifyUpgrade(array $upgrade) { | |
29 | return (in_array('websocket', array_map('strtolower', $upgrade))); | |
30 | } | |
31 | ||
32 | public function verifyConnection(array $connection) { | |
33 | return (in_array('upgrade', array_map('strtolower', $connection))); | |
34 | } | |
35 | ||
36 | public function verifySecWebSocketAccept($swa, $key) { | |
37 | return ( | |
38 | 1 === count($swa) && | |
39 | 1 === count($key) && | |
40 | $swa[0] === $this->sign($key[0]) | |
41 | ); | |
42 | } | |
43 | ||
44 | public function sign($key) { | |
45 | return base64_encode(sha1($key . NegotiatorInterface::GUID, true)); | |
46 | } | |
47 | ||
48 | public function verifySubProtocol(array $requestHeader, array $responseHeader) { | |
49 | return 0 === count($responseHeader) || count(array_intersect($responseHeader, $requestHeader)) > 0; | |
50 | } | |
51 | }⏎ |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Handshake; | |
2 | use Psr\Http\Message\RequestInterface; | |
3 | use GuzzleHttp\Psr7\Response; | |
4 | ||
5 | /** | |
6 | * The latest version of the WebSocket protocol | |
7 | * @todo Unicode: return mb_convert_encoding(pack("N",$u), mb_internal_encoding(), 'UCS-4BE'); | |
8 | */ | |
9 | class ServerNegotiator implements NegotiatorInterface { | |
10 | /** | |
11 | * @var \Ratchet\RFC6455\Handshake\RequestVerifier | |
12 | */ | |
13 | private $verifier; | |
14 | ||
15 | private $_supportedSubProtocols = []; | |
16 | ||
17 | private $_strictSubProtocols = false; | |
18 | ||
19 | public function __construct(RequestVerifier $requestVerifier) { | |
20 | $this->verifier = $requestVerifier; | |
21 | } | |
22 | ||
23 | /** | |
24 | * {@inheritdoc} | |
25 | */ | |
26 | public function isProtocol(RequestInterface $request) { | |
27 | return $this->verifier->verifyVersion($request->getHeader('Sec-WebSocket-Version')); | |
28 | } | |
29 | ||
30 | /** | |
31 | * {@inheritdoc} | |
32 | */ | |
33 | public function getVersionNumber() { | |
34 | return RequestVerifier::VERSION; | |
35 | } | |
36 | ||
37 | /** | |
38 | * {@inheritdoc} | |
39 | */ | |
40 | public function handshake(RequestInterface $request) { | |
41 | if (true !== $this->verifier->verifyMethod($request->getMethod())) { | |
42 | return new Response(405, ['Allow' => 'GET']); | |
43 | } | |
44 | ||
45 | if (true !== $this->verifier->verifyHTTPVersion($request->getProtocolVersion())) { | |
46 | return new Response(505); | |
47 | } | |
48 | ||
49 | if (true !== $this->verifier->verifyRequestURI($request->getUri()->getPath())) { | |
50 | return new Response(400); | |
51 | } | |
52 | ||
53 | if (true !== $this->verifier->verifyHost($request->getHeader('Host'))) { | |
54 | return new Response(400); | |
55 | } | |
56 | ||
57 | $upgradeSuggestion = [ | |
58 | 'Connection' => 'Upgrade', | |
59 | 'Upgrade' => 'websocket', | |
60 | 'Sec-WebSocket-Version' => $this->getVersionNumber() | |
61 | ]; | |
62 | if (count($this->_supportedSubProtocols) > 0) { | |
63 | $upgradeSuggestion['Sec-WebSocket-Protocol'] = implode(', ', $this->_supportedSubProtocols); | |
64 | } | |
65 | if (true !== $this->verifier->verifyUpgradeRequest($request->getHeader('Upgrade'))) { | |
66 | return new Response(426, $upgradeSuggestion, null, '1.1', 'Upgrade header MUST be provided'); | |
67 | } | |
68 | ||
69 | if (true !== $this->verifier->verifyConnection($request->getHeader('Connection'))) { | |
70 | return new Response(400, [], null, '1.1', 'Connection Upgrade MUST be requested'); | |
71 | } | |
72 | ||
73 | if (true !== $this->verifier->verifyKey($request->getHeader('Sec-WebSocket-Key'))) { | |
74 | return new Response(400, [], null, '1.1', 'Invalid Sec-WebSocket-Key'); | |
75 | } | |
76 | ||
77 | if (true !== $this->verifier->verifyVersion($request->getHeader('Sec-WebSocket-Version'))) { | |
78 | return new Response(426, $upgradeSuggestion); | |
79 | } | |
80 | ||
81 | $headers = []; | |
82 | $subProtocols = $request->getHeader('Sec-WebSocket-Protocol'); | |
83 | if (count($subProtocols) > 0 || (count($this->_supportedSubProtocols) > 0 && $this->_strictSubProtocols)) { | |
84 | $subProtocols = array_map('trim', explode(',', implode(',', $subProtocols))); | |
85 | ||
86 | $match = array_reduce($subProtocols, function($accumulator, $protocol) { | |
87 | return $accumulator ?: (isset($this->_supportedSubProtocols[$protocol]) ? $protocol : null); | |
88 | }, null); | |
89 | ||
90 | if ($this->_strictSubProtocols && null === $match) { | |
91 | return new Response(426, $upgradeSuggestion, null, '1.1', 'No Sec-WebSocket-Protocols requested supported'); | |
92 | } | |
93 | ||
94 | if (null !== $match) { | |
95 | $headers['Sec-WebSocket-Protocol'] = $match; | |
96 | } | |
97 | } | |
98 | ||
99 | return new Response(101, array_merge($headers, [ | |
100 | 'Upgrade' => 'websocket' | |
101 | , 'Connection' => 'Upgrade' | |
102 | , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]) | |
103 | , 'X-Powered-By' => 'Ratchet' | |
104 | ])); | |
105 | } | |
106 | ||
107 | /** | |
108 | * Used when doing the handshake to encode the key, verifying client/server are speaking the same language | |
109 | * @param string $key | |
110 | * @return string | |
111 | * @internal | |
112 | */ | |
113 | public function sign($key) { | |
114 | return base64_encode(sha1($key . static::GUID, true)); | |
115 | } | |
116 | ||
117 | /** | |
118 | * @param array $protocols | |
119 | */ | |
120 | function setSupportedSubProtocols(array $protocols) { | |
121 | $this->_supportedSubProtocols = array_flip($protocols); | |
122 | } | |
123 | ||
124 | /** | |
125 | * If enabled and support for a subprotocol has been added handshake | |
126 | * will not upgrade if a match between request and supported subprotocols | |
127 | * @param boolean $enable | |
128 | * @todo Consider extending this interface and moving this there. | |
129 | * The spec does says the server can fail for this reason, but | |
130 | * it is not a requirement. This is an implementation detail. | |
131 | */ | |
132 | function setStrictSubProtocolCheck($enable) { | |
133 | $this->_strictSubProtocols = (boolean)$enable; | |
134 | } | |
135 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Messaging; | |
2 | ||
3 | class CloseFrameChecker { | |
4 | private $validCloseCodes = []; | |
5 | ||
6 | public function __construct() { | |
7 | $this->validCloseCodes = [ | |
8 | Frame::CLOSE_NORMAL, | |
9 | Frame::CLOSE_GOING_AWAY, | |
10 | Frame::CLOSE_PROTOCOL, | |
11 | Frame::CLOSE_BAD_DATA, | |
12 | Frame::CLOSE_BAD_PAYLOAD, | |
13 | Frame::CLOSE_POLICY, | |
14 | Frame::CLOSE_TOO_BIG, | |
15 | Frame::CLOSE_MAND_EXT, | |
16 | Frame::CLOSE_SRV_ERR, | |
17 | ]; | |
18 | } | |
19 | ||
20 | public function __invoke($val) { | |
21 | return ($val >= 3000 && $val <= 4999) || in_array($val, $this->validCloseCodes); | |
22 | } | |
23 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Messaging; | |
2 | ||
3 | interface DataInterface { | |
4 | /** | |
5 | * Determine if the message is complete or still fragmented | |
6 | * @return bool | |
7 | */ | |
8 | function isCoalesced(); | |
9 | ||
10 | /** | |
11 | * Get the number of bytes the payload is set to be | |
12 | * @return int | |
13 | */ | |
14 | function getPayloadLength(); | |
15 | ||
16 | /** | |
17 | * Get the payload (message) sent from peer | |
18 | * @return string | |
19 | */ | |
20 | function getPayload(); | |
21 | ||
22 | /** | |
23 | * Get raw contents of the message | |
24 | * @return string | |
25 | */ | |
26 | function getContents(); | |
27 | ||
28 | /** | |
29 | * Should return the unmasked payload received from peer | |
30 | * @return string | |
31 | */ | |
32 | function __toString(); | |
33 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Messaging; | |
2 | ||
3 | class Frame implements FrameInterface { | |
4 | const OP_CONTINUE = 0; | |
5 | const OP_TEXT = 1; | |
6 | const OP_BINARY = 2; | |
7 | const OP_CLOSE = 8; | |
8 | const OP_PING = 9; | |
9 | const OP_PONG = 10; | |
10 | ||
11 | const CLOSE_NORMAL = 1000; | |
12 | const CLOSE_GOING_AWAY = 1001; | |
13 | const CLOSE_PROTOCOL = 1002; | |
14 | const CLOSE_BAD_DATA = 1003; | |
15 | const CLOSE_NO_STATUS = 1005; | |
16 | const CLOSE_ABNORMAL = 1006; | |
17 | const CLOSE_BAD_PAYLOAD = 1007; | |
18 | const CLOSE_POLICY = 1008; | |
19 | const CLOSE_TOO_BIG = 1009; | |
20 | const CLOSE_MAND_EXT = 1010; | |
21 | const CLOSE_SRV_ERR = 1011; | |
22 | const CLOSE_TLS = 1015; | |
23 | ||
24 | const MASK_LENGTH = 4; | |
25 | ||
26 | /** | |
27 | * The contents of the frame | |
28 | * @var string | |
29 | */ | |
30 | protected $data = ''; | |
31 | ||
32 | /** | |
33 | * Number of bytes received from the frame | |
34 | * @var int | |
35 | */ | |
36 | public $bytesRecvd = 0; | |
37 | ||
38 | /** | |
39 | * Number of bytes in the payload (as per framing protocol) | |
40 | * @var int | |
41 | */ | |
42 | protected $defPayLen = -1; | |
43 | ||
44 | /** | |
45 | * If the frame is coalesced this is true | |
46 | * This is to prevent doing math every time ::isCoalesced is called | |
47 | * @var boolean | |
48 | */ | |
49 | private $isCoalesced = false; | |
50 | ||
51 | /** | |
52 | * The unpacked first byte of the frame | |
53 | * @var int | |
54 | */ | |
55 | protected $firstByte = -1; | |
56 | ||
57 | /** | |
58 | * The unpacked second byte of the frame | |
59 | * @var int | |
60 | */ | |
61 | protected $secondByte = -1; | |
62 | ||
63 | /** | |
64 | * @var callable | |
65 | * @returns \UnderflowException | |
66 | */ | |
67 | private $ufeg; | |
68 | ||
69 | /** | |
70 | * @param string|null $payload | |
71 | * @param bool $final | |
72 | * @param int $opcode | |
73 | * @param callable<\UnderflowException> $ufExceptionFactory | |
74 | */ | |
75 | public function __construct($payload = null, $final = true, $opcode = 1, callable $ufExceptionFactory = null) { | |
76 | $this->ufeg = $ufExceptionFactory ?: static function($msg = '') { | |
77 | return new \UnderflowException($msg); | |
78 | }; | |
79 | ||
80 | if (null === $payload) { | |
81 | return; | |
82 | } | |
83 | ||
84 | $this->defPayLen = strlen($payload); | |
85 | $this->firstByte = ($final ? 128 : 0) + $opcode; | |
86 | $this->secondByte = $this->defPayLen; | |
87 | $this->isCoalesced = true; | |
88 | ||
89 | $ext = ''; | |
90 | if ($this->defPayLen > 65535) { | |
91 | $ext = pack('NN', 0, $this->defPayLen); | |
92 | $this->secondByte = 127; | |
93 | } elseif ($this->defPayLen > 125) { | |
94 | $ext = pack('n', $this->defPayLen); | |
95 | $this->secondByte = 126; | |
96 | } | |
97 | ||
98 | $this->data = chr($this->firstByte) . chr($this->secondByte) . $ext . $payload; | |
99 | $this->bytesRecvd = 2 + strlen($ext) + $this->defPayLen; | |
100 | } | |
101 | ||
102 | /** | |
103 | * {@inheritdoc} | |
104 | */ | |
105 | public function isCoalesced() { | |
106 | if (true === $this->isCoalesced) { | |
107 | return true; | |
108 | } | |
109 | ||
110 | try { | |
111 | $payload_length = $this->getPayloadLength(); | |
112 | $payload_start = $this->getPayloadStartingByte(); | |
113 | } catch (\UnderflowException $e) { | |
114 | return false; | |
115 | } | |
116 | ||
117 | $this->isCoalesced = $this->bytesRecvd >= $payload_length + $payload_start; | |
118 | ||
119 | return $this->isCoalesced; | |
120 | } | |
121 | ||
122 | /** | |
123 | * {@inheritdoc} | |
124 | */ | |
125 | public function addBuffer($buf) { | |
126 | $len = strlen($buf); | |
127 | ||
128 | $this->data .= $buf; | |
129 | $this->bytesRecvd += $len; | |
130 | ||
131 | if ($this->firstByte === -1 && $this->bytesRecvd !== 0) { | |
132 | $this->firstByte = ord($this->data[0]); | |
133 | } | |
134 | ||
135 | if ($this->secondByte === -1 && $this->bytesRecvd >= 2) { | |
136 | $this->secondByte = ord($this->data[1]); | |
137 | } | |
138 | } | |
139 | ||
140 | /** | |
141 | * {@inheritdoc} | |
142 | */ | |
143 | public function isFinal() { | |
144 | if (-1 === $this->firstByte) { | |
145 | throw call_user_func($this->ufeg, 'Not enough bytes received to determine if this is the final frame in message'); | |
146 | } | |
147 | ||
148 | return 128 === ($this->firstByte & 128); | |
149 | } | |
150 | ||
151 | /** | |
152 | * @return boolean | |
153 | * @throws \UnderflowException | |
154 | */ | |
155 | public function getRsv1() { | |
156 | if (-1 === $this->firstByte) { | |
157 | throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit'); | |
158 | } | |
159 | ||
160 | return 64 === ($this->firstByte & 64); | |
161 | } | |
162 | ||
163 | /** | |
164 | * @return boolean | |
165 | * @throws \UnderflowException | |
166 | */ | |
167 | public function getRsv2() { | |
168 | if (-1 === $this->firstByte) { | |
169 | throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit'); | |
170 | } | |
171 | ||
172 | return 32 === ($this->firstByte & 32); | |
173 | } | |
174 | ||
175 | /** | |
176 | * @return boolean | |
177 | * @throws \UnderflowException | |
178 | */ | |
179 | public function getRsv3() { | |
180 | if (-1 === $this->firstByte) { | |
181 | throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit'); | |
182 | } | |
183 | ||
184 | return 16 === ($this->firstByte & 16); | |
185 | } | |
186 | ||
187 | /** | |
188 | * {@inheritdoc} | |
189 | */ | |
190 | public function isMasked() { | |
191 | if (-1 === $this->secondByte) { | |
192 | throw call_user_func($this->ufeg, "Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set"); | |
193 | } | |
194 | ||
195 | return 128 === ($this->secondByte & 128); | |
196 | } | |
197 | ||
198 | /** | |
199 | * {@inheritdoc} | |
200 | */ | |
201 | public function getMaskingKey() { | |
202 | if (!$this->isMasked()) { | |
203 | return ''; | |
204 | } | |
205 | ||
206 | $start = 1 + $this->getNumPayloadBytes(); | |
207 | ||
208 | if ($this->bytesRecvd < $start + static::MASK_LENGTH) { | |
209 | throw call_user_func($this->ufeg, 'Not enough data buffered to calculate the masking key'); | |
210 | } | |
211 | ||
212 | return substr($this->data, $start, static::MASK_LENGTH); | |
213 | } | |
214 | ||
215 | /** | |
216 | * Create a 4 byte masking key | |
217 | * @return string | |
218 | */ | |
219 | public function generateMaskingKey() { | |
220 | $mask = ''; | |
221 | ||
222 | for ($i = 1; $i <= static::MASK_LENGTH; $i++) { | |
223 | $mask .= chr(rand(32, 126)); | |
224 | } | |
225 | ||
226 | return $mask; | |
227 | } | |
228 | ||
229 | /** | |
230 | * Apply a mask to the payload | |
231 | * @param string|null If NULL is passed a masking key will be generated | |
232 | * @throws \OutOfBoundsException | |
233 | * @throws \InvalidArgumentException If there is an issue with the given masking key | |
234 | * @return Frame | |
235 | */ | |
236 | public function maskPayload($maskingKey = null) { | |
237 | if (null === $maskingKey) { | |
238 | $maskingKey = $this->generateMaskingKey(); | |
239 | } | |
240 | ||
241 | if (static::MASK_LENGTH !== strlen($maskingKey)) { | |
242 | throw new \InvalidArgumentException("Masking key must be " . static::MASK_LENGTH ." characters"); | |
243 | } | |
244 | ||
245 | if (extension_loaded('mbstring') && true !== mb_check_encoding($maskingKey, 'US-ASCII')) { | |
246 | throw new \OutOfBoundsException("Masking key MUST be ASCII"); | |
247 | } | |
248 | ||
249 | $this->unMaskPayload(); | |
250 | ||
251 | $this->secondByte = $this->secondByte | 128; | |
252 | $this->data[1] = chr($this->secondByte); | |
253 | ||
254 | $this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0); | |
255 | ||
256 | $this->bytesRecvd += static::MASK_LENGTH; | |
257 | $this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); | |
258 | ||
259 | return $this; | |
260 | } | |
261 | ||
262 | /** | |
263 | * Remove a mask from the payload | |
264 | * @throws \UnderFlowException If the frame is not coalesced | |
265 | * @return Frame | |
266 | */ | |
267 | public function unMaskPayload() { | |
268 | if (!$this->isCoalesced()) { | |
269 | throw call_user_func($this->ufeg, 'Frame must be coalesced before applying mask'); | |
270 | } | |
271 | ||
272 | if (!$this->isMasked()) { | |
273 | return $this; | |
274 | } | |
275 | ||
276 | $maskingKey = $this->getMaskingKey(); | |
277 | ||
278 | $this->secondByte = $this->secondByte & ~128; | |
279 | $this->data[1] = chr($this->secondByte); | |
280 | ||
281 | $this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH); | |
282 | ||
283 | $this->bytesRecvd -= static::MASK_LENGTH; | |
284 | $this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); | |
285 | ||
286 | return $this; | |
287 | } | |
288 | ||
289 | /** | |
290 | * Apply a mask to a string or the payload of the instance | |
291 | * @param string $maskingKey The 4 character masking key to be applied | |
292 | * @param string|null $payload A string to mask or null to use the payload | |
293 | * @throws \UnderflowException If using the payload but enough hasn't been buffered | |
294 | * @return string The masked string | |
295 | */ | |
296 | public function applyMask($maskingKey, $payload = null) { | |
297 | if (null === $payload) { | |
298 | if (!$this->isCoalesced()) { | |
299 | throw call_user_func($this->ufeg, 'Frame must be coalesced to apply a mask'); | |
300 | } | |
301 | ||
302 | $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); | |
303 | } | |
304 | ||
305 | $len = strlen($payload); | |
306 | ||
307 | if (0 === $len) { | |
308 | return ''; | |
309 | } | |
310 | ||
311 | return $payload ^ str_pad('', $len, $maskingKey, STR_PAD_RIGHT); | |
312 | ||
313 | // TODO: Remove this before publish - keeping methods here to compare performance (above is faster but need control against v0.3.3) | |
314 | ||
315 | $applied = ''; | |
316 | for ($i = 0, $len = strlen($payload); $i < $len; $i++) { | |
317 | $applied .= $payload[$i] ^ $maskingKey[$i % static::MASK_LENGTH]; | |
318 | } | |
319 | ||
320 | return $applied; | |
321 | } | |
322 | ||
323 | /** | |
324 | * {@inheritdoc} | |
325 | */ | |
326 | public function getOpcode() { | |
327 | if (-1 === $this->firstByte) { | |
328 | throw call_user_func($this->ufeg, 'Not enough bytes received to determine opcode'); | |
329 | } | |
330 | ||
331 | return ($this->firstByte & ~240); | |
332 | } | |
333 | ||
334 | /** | |
335 | * Gets the decimal value of bits 9 (10th) through 15 inclusive | |
336 | * @return int | |
337 | * @throws \UnderflowException If the buffer doesn't have enough data to determine this | |
338 | */ | |
339 | protected function getFirstPayloadVal() { | |
340 | if (-1 === $this->secondByte) { | |
341 | throw call_user_func($this->ufeg, 'Not enough bytes received'); | |
342 | } | |
343 | ||
344 | return $this->secondByte & 127; | |
345 | } | |
346 | ||
347 | /** | |
348 | * @return int (7|23|71) Number of bits defined for the payload length in the fame | |
349 | * @throws \UnderflowException | |
350 | */ | |
351 | protected function getNumPayloadBits() { | |
352 | if (-1 === $this->secondByte) { | |
353 | throw call_user_func($this->ufeg, 'Not enough bytes received'); | |
354 | } | |
355 | ||
356 | // By default 7 bits are used to describe the payload length | |
357 | // These are bits 9 (10th) through 15 inclusive | |
358 | $bits = 7; | |
359 | ||
360 | // Get the value of those bits | |
361 | $check = $this->getFirstPayloadVal(); | |
362 | ||
363 | // If the value is 126 the 7 bits plus the next 16 are used to describe the payload length | |
364 | if ($check >= 126) { | |
365 | $bits += 16; | |
366 | } | |
367 | ||
368 | // If the value of the initial payload length are is 127 an additional 48 bits are used to describe length | |
369 | // Note: The documentation specifies the length is to be 63 bits, but I think that's a typo and is 64 (16+48) | |
370 | if ($check === 127) { | |
371 | $bits += 48; | |
372 | } | |
373 | ||
374 | return $bits; | |
375 | } | |
376 | ||
377 | /** | |
378 | * This just returns the number of bytes used in the frame to describe the payload length (as opposed to # of bits) | |
379 | * @see getNumPayloadBits | |
380 | */ | |
381 | protected function getNumPayloadBytes() { | |
382 | return (1 + $this->getNumPayloadBits()) / 8; | |
383 | } | |
384 | ||
385 | /** | |
386 | * {@inheritdoc} | |
387 | */ | |
388 | public function getPayloadLength() { | |
389 | if ($this->defPayLen !== -1) { | |
390 | return $this->defPayLen; | |
391 | } | |
392 | ||
393 | $this->defPayLen = $this->getFirstPayloadVal(); | |
394 | if ($this->defPayLen <= 125) { | |
395 | return $this->getPayloadLength(); | |
396 | } | |
397 | ||
398 | $byte_length = $this->getNumPayloadBytes(); | |
399 | if ($this->bytesRecvd < 1 + $byte_length) { | |
400 | $this->defPayLen = -1; | |
401 | throw call_user_func($this->ufeg, 'Not enough data buffered to determine payload length'); | |
402 | } | |
403 | ||
404 | $len = 0; | |
405 | for ($i = 2; $i <= $byte_length; $i++) { | |
406 | $len <<= 8; | |
407 | $len += ord($this->data[$i]); | |
408 | } | |
409 | ||
410 | $this->defPayLen = $len; | |
411 | ||
412 | return $this->getPayloadLength(); | |
413 | } | |
414 | ||
415 | /** | |
416 | * {@inheritdoc} | |
417 | */ | |
418 | public function getPayloadStartingByte() { | |
419 | return 1 + $this->getNumPayloadBytes() + ($this->isMasked() ? static::MASK_LENGTH : 0); | |
420 | } | |
421 | ||
422 | /** | |
423 | * {@inheritdoc} | |
424 | * @todo Consider not checking mask, always returning the payload, masked or not | |
425 | */ | |
426 | public function getPayload() { | |
427 | if (!$this->isCoalesced()) { | |
428 | throw call_user_func($this->ufeg, 'Can not return partial message'); | |
429 | } | |
430 | ||
431 | return $this->__toString(); | |
432 | } | |
433 | ||
434 | /** | |
435 | * Get the raw contents of the frame | |
436 | * @todo This is untested, make sure the substr is right - trying to return the frame w/o the overflow | |
437 | */ | |
438 | public function getContents() { | |
439 | return substr($this->data, 0, $this->getPayloadStartingByte() + $this->getPayloadLength()); | |
440 | } | |
441 | ||
442 | public function __toString() { | |
443 | $payload = (string)substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); | |
444 | ||
445 | if ($this->isMasked()) { | |
446 | $payload = $this->applyMask($this->getMaskingKey(), $payload); | |
447 | } | |
448 | ||
449 | return $payload; | |
450 | } | |
451 | ||
452 | /** | |
453 | * Sometimes clients will concatenate more than one frame over the wire | |
454 | * This method will take the extra bytes off the end and return them | |
455 | * @return string | |
456 | */ | |
457 | public function extractOverflow() { | |
458 | if ($this->isCoalesced()) { | |
459 | $endPoint = $this->getPayloadLength(); | |
460 | $endPoint += $this->getPayloadStartingByte(); | |
461 | ||
462 | if ($this->bytesRecvd > $endPoint) { | |
463 | $overflow = substr($this->data, $endPoint); | |
464 | $this->data = substr($this->data, 0, $endPoint); | |
465 | ||
466 | return $overflow; | |
467 | } | |
468 | } | |
469 | ||
470 | return ''; | |
471 | } | |
472 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Messaging; | |
2 | ||
3 | interface FrameInterface extends DataInterface { | |
4 | /** | |
5 | * Add incoming data to the frame from peer | |
6 | * @param string | |
7 | */ | |
8 | function addBuffer($buf); | |
9 | ||
10 | /** | |
11 | * Is this the final frame in a fragmented message? | |
12 | * @return bool | |
13 | */ | |
14 | function isFinal(); | |
15 | ||
16 | /** | |
17 | * Is the payload masked? | |
18 | * @return bool | |
19 | */ | |
20 | function isMasked(); | |
21 | ||
22 | /** | |
23 | * @return int | |
24 | */ | |
25 | function getOpcode(); | |
26 | ||
27 | /** | |
28 | * @return int | |
29 | */ | |
30 | //function getReceivedPayloadLength(); | |
31 | ||
32 | /** | |
33 | * 32-big string | |
34 | * @return string | |
35 | */ | |
36 | function getMaskingKey(); | |
37 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Messaging; | |
2 | ||
3 | class Message implements \IteratorAggregate, MessageInterface { | |
4 | /** | |
5 | * @var \SplDoublyLinkedList | |
6 | */ | |
7 | private $_frames; | |
8 | ||
9 | public function __construct() { | |
10 | $this->_frames = new \SplDoublyLinkedList; | |
11 | } | |
12 | ||
13 | public function getIterator() { | |
14 | return $this->_frames; | |
15 | } | |
16 | ||
17 | /** | |
18 | * {@inheritdoc} | |
19 | */ | |
20 | public function count() { | |
21 | return count($this->_frames); | |
22 | } | |
23 | ||
24 | /** | |
25 | * {@inheritdoc} | |
26 | */ | |
27 | public function isCoalesced() { | |
28 | if (count($this->_frames) == 0) { | |
29 | return false; | |
30 | } | |
31 | ||
32 | $last = $this->_frames->top(); | |
33 | ||
34 | return ($last->isCoalesced() && $last->isFinal()); | |
35 | } | |
36 | ||
37 | /** | |
38 | * {@inheritdoc} | |
39 | */ | |
40 | public function addFrame(FrameInterface $fragment) { | |
41 | $this->_frames->push($fragment); | |
42 | ||
43 | return $this; | |
44 | } | |
45 | ||
46 | /** | |
47 | * {@inheritdoc} | |
48 | */ | |
49 | public function getOpcode() { | |
50 | if (count($this->_frames) == 0) { | |
51 | throw new \UnderflowException('No frames have been added to this message'); | |
52 | } | |
53 | ||
54 | return $this->_frames->bottom()->getOpcode(); | |
55 | } | |
56 | ||
57 | /** | |
58 | * {@inheritdoc} | |
59 | */ | |
60 | 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; | |
72 | } | |
73 | ||
74 | /** | |
75 | * {@inheritdoc} | |
76 | */ | |
77 | public function getPayload() { | |
78 | if (!$this->isCoalesced()) { | |
79 | throw new \UnderflowException('Message has not been put back together yet'); | |
80 | } | |
81 | ||
82 | return $this->__toString(); | |
83 | } | |
84 | ||
85 | /** | |
86 | * {@inheritdoc} | |
87 | */ | |
88 | public function getContents() { | |
89 | if (!$this->isCoalesced()) { | |
90 | throw new \UnderflowException("Message has not been put back together yet"); | |
91 | } | |
92 | ||
93 | $buffer = ''; | |
94 | ||
95 | foreach ($this->_frames as $frame) { | |
96 | $buffer .= $frame->getContents(); | |
97 | } | |
98 | ||
99 | return $buffer; | |
100 | } | |
101 | ||
102 | public function __toString() { | |
103 | $buffer = ''; | |
104 | ||
105 | foreach ($this->_frames as $frame) { | |
106 | $buffer .= $frame->getPayload(); | |
107 | } | |
108 | ||
109 | return $buffer; | |
110 | } | |
111 | ||
112 | /** | |
113 | * @return boolean | |
114 | */ | |
115 | public function isBinary() { | |
116 | if ($this->_frames->isEmpty()) { | |
117 | throw new \UnderflowException('Not enough data has been received to determine if message is binary'); | |
118 | } | |
119 | ||
120 | return Frame::OP_BINARY === $this->_frames->bottom()->getOpcode(); | |
121 | } | |
122 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Messaging; | |
2 | ||
3 | class MessageBuffer { | |
4 | /** | |
5 | * @var \Ratchet\RFC6455\Messaging\CloseFrameChecker | |
6 | */ | |
7 | private $closeFrameChecker; | |
8 | ||
9 | /** | |
10 | * @var callable | |
11 | */ | |
12 | private $exceptionFactory; | |
13 | ||
14 | /** | |
15 | * @var \Ratchet\RFC6455\Messaging\Message | |
16 | */ | |
17 | private $messageBuffer; | |
18 | ||
19 | /** | |
20 | * @var \Ratchet\RFC6455\Messaging\Frame | |
21 | */ | |
22 | private $frameBuffer; | |
23 | ||
24 | /** | |
25 | * @var callable | |
26 | */ | |
27 | private $onMessage; | |
28 | ||
29 | /** | |
30 | * @var callable | |
31 | */ | |
32 | private $onControl; | |
33 | ||
34 | /** | |
35 | * @var bool | |
36 | */ | |
37 | private $checkForMask; | |
38 | ||
39 | function __construct( | |
40 | CloseFrameChecker $frameChecker, | |
41 | callable $onMessage, | |
42 | callable $onControl = null, | |
43 | $expectMask = true, | |
44 | $exceptionFactory = null | |
45 | ) { | |
46 | $this->closeFrameChecker = $frameChecker; | |
47 | $this->checkForMask = (bool)$expectMask; | |
48 | ||
49 | $this->exceptionFactory ?: $this->exceptionFactory = function($msg) { | |
50 | return new \UnderflowException($msg); | |
51 | }; | |
52 | ||
53 | $this->onMessage = $onMessage; | |
54 | $this->onControl = $onControl ?: function() {}; | |
55 | } | |
56 | ||
57 | public function onData($data) { | |
58 | while (strlen($data) > 0) { | |
59 | $data = $this->processData($data); | |
60 | } | |
61 | } | |
62 | ||
63 | /** | |
64 | * @param string $data | |
65 | * @return null | |
66 | */ | |
67 | private function processData($data) { | |
68 | $this->messageBuffer ?: $this->messageBuffer = $this->newMessage(); | |
69 | $this->frameBuffer ?: $this->frameBuffer = $this->newFrame(); | |
70 | ||
71 | $this->frameBuffer->addBuffer($data); | |
72 | if (!$this->frameBuffer->isCoalesced()) { | |
73 | return ''; | |
74 | } | |
75 | ||
76 | $onMessage = $this->onMessage; | |
77 | $onControl = $this->onControl; | |
78 | ||
79 | $this->frameBuffer = $this->frameCheck($this->frameBuffer); | |
80 | ||
81 | $overflow = $this->frameBuffer->extractOverflow(); | |
82 | $this->frameBuffer->unMaskPayload(); | |
83 | ||
84 | $opcode = $this->frameBuffer->getOpcode(); | |
85 | ||
86 | if ($opcode > 2) { | |
87 | $onControl($this->frameBuffer); | |
88 | ||
89 | if (Frame::OP_CLOSE === $opcode) { | |
90 | return ''; | |
91 | } | |
92 | } else { | |
93 | $this->messageBuffer->addFrame($this->frameBuffer); | |
94 | } | |
95 | ||
96 | $this->frameBuffer = null; | |
97 | ||
98 | if ($this->messageBuffer->isCoalesced()) { | |
99 | $msgCheck = $this->checkMessage($this->messageBuffer); | |
100 | if (true !== $msgCheck) { | |
101 | $onControl($this->newCloseFrame($msgCheck, 'Ratchet detected an invalid UTF-8 payload')); | |
102 | } else { | |
103 | $onMessage($this->messageBuffer); | |
104 | } | |
105 | ||
106 | $this->messageBuffer = null; | |
107 | } | |
108 | ||
109 | return $overflow; | |
110 | } | |
111 | ||
112 | /** | |
113 | * Check a frame to be added to the current message buffer | |
114 | * @param \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface $frame | |
115 | * @return \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface | |
116 | */ | |
117 | public function frameCheck(FrameInterface $frame) { | |
118 | if (false !== $frame->getRsv1() || | |
119 | false !== $frame->getRsv2() || | |
120 | false !== $frame->getRsv3() | |
121 | ) { | |
122 | return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid reserve code'); | |
123 | } | |
124 | ||
125 | if ($this->checkForMask && !$frame->isMasked()) { | |
126 | return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an incorrect frame mask'); | |
127 | } | |
128 | ||
129 | $opcode = $frame->getOpcode(); | |
130 | ||
131 | if ($opcode > 2) { | |
132 | if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) { | |
133 | return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected a mismatch between final bit and indicated payload length'); | |
134 | } | |
135 | ||
136 | switch ($opcode) { | |
137 | case Frame::OP_CLOSE: | |
138 | $closeCode = 0; | |
139 | ||
140 | $bin = $frame->getPayload(); | |
141 | ||
142 | if (empty($bin)) { | |
143 | return $this->newCloseFrame(Frame::CLOSE_NORMAL); | |
144 | } | |
145 | ||
146 | if (strlen($bin) === 1) { | |
147 | return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid close code'); | |
148 | } | |
149 | ||
150 | if (strlen($bin) >= 2) { | |
151 | list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); | |
152 | } | |
153 | ||
154 | $checker = $this->closeFrameChecker; | |
155 | if (!$checker($closeCode)) { | |
156 | return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid close code'); | |
157 | } | |
158 | ||
159 | if (!$this->checkUtf8(substr($bin, 2))) { | |
160 | return $this->newCloseFrame(Frame::CLOSE_BAD_PAYLOAD, 'Ratchet detected an invalid UTF-8 payload in the close reason'); | |
161 | } | |
162 | ||
163 | return $frame; | |
164 | break; | |
165 | case Frame::OP_PING: | |
166 | case Frame::OP_PONG: | |
167 | break; | |
168 | default: | |
169 | return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid OP code'); | |
170 | break; | |
171 | } | |
172 | ||
173 | return $frame; | |
174 | } | |
175 | ||
176 | if (Frame::OP_CONTINUE === $frame->getOpcode() && 0 === count($this->messageBuffer)) { | |
177 | return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected the first frame of a message was a continue'); | |
178 | } | |
179 | ||
180 | if (count($this->messageBuffer) > 0 && Frame::OP_CONTINUE !== $frame->getOpcode()) { | |
181 | return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected invalid OP code when expecting continue frame'); | |
182 | } | |
183 | ||
184 | return $frame; | |
185 | } | |
186 | ||
187 | /** | |
188 | * Determine if a message is valid | |
189 | * @param \Ratchet\RFC6455\Messaging\MessageInterface | |
190 | * @return bool|int true if valid - false if incomplete - int of recommended close code | |
191 | */ | |
192 | public function checkMessage(MessageInterface $message) { | |
193 | if (!$message->isBinary()) { | |
194 | if (!$this->checkUtf8($message->getPayload())) { | |
195 | return Frame::CLOSE_BAD_PAYLOAD; | |
196 | } | |
197 | } | |
198 | ||
199 | return true; | |
200 | } | |
201 | ||
202 | private function checkUtf8($string) { | |
203 | if (extension_loaded('mbstring')) { | |
204 | return mb_check_encoding($string, 'UTF-8'); | |
205 | } | |
206 | ||
207 | return preg_match('//u', $string); | |
208 | } | |
209 | ||
210 | /** | |
211 | * @return \Ratchet\RFC6455\Messaging\MessageInterface | |
212 | */ | |
213 | public function newMessage() { | |
214 | return new Message; | |
215 | } | |
216 | ||
217 | /** | |
218 | * @param string|null $payload | |
219 | * @param bool|null $final | |
220 | * @param int|null $opcode | |
221 | * @return \Ratchet\RFC6455\Messaging\FrameInterface | |
222 | */ | |
223 | public function newFrame($payload = null, $final = null, $opcode = null) { | |
224 | return new Frame($payload, $final, $opcode, $this->exceptionFactory); | |
225 | } | |
226 | ||
227 | public function newCloseFrame($code, $reason = '') { | |
228 | return $this->newFrame(pack('n', $code) . $reason, true, Frame::OP_CLOSE); | |
229 | } | |
230 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Messaging; | |
2 | ||
3 | interface MessageInterface extends DataInterface, \Traversable, \Countable { | |
4 | /** | |
5 | * @param FrameInterface $fragment | |
6 | * @return MessageInterface | |
7 | */ | |
8 | function addFrame(FrameInterface $fragment); | |
9 | ||
10 | /** | |
11 | * @return int | |
12 | */ | |
13 | function getOpcode(); | |
14 | ||
15 | /** | |
16 | * @return bool | |
17 | */ | |
18 | function isBinary(); | |
19 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Test; | |
2 | ||
3 | class AbResultsTest extends \PHPUnit_Framework_TestCase { | |
4 | private function verifyAutobahnResults($fileName) { | |
5 | if (!file_exists($fileName)) { | |
6 | return $this->markTestSkipped('Autobahn TestSuite results not found'); | |
7 | } | |
8 | ||
9 | $resultsJson = file_get_contents($fileName); | |
10 | $results = json_decode($resultsJson); | |
11 | $agentName = array_keys(get_object_vars($results))[0]; | |
12 | ||
13 | foreach ($results->$agentName as $name => $result) { | |
14 | if ($result->behavior === "INFORMATIONAL") { | |
15 | continue; | |
16 | } | |
17 | ||
18 | $this->assertTrue(in_array($result->behavior, ["OK", "NON-STRICT"]), "Autobahn test case " . $name . " in " . $fileName); | |
19 | } | |
20 | } | |
21 | ||
22 | public function testAutobahnClientResults() { | |
23 | $this->verifyAutobahnResults(__DIR__ . '/ab/reports/clients/index.json'); | |
24 | } | |
25 | ||
26 | public function testAutobahnServerResults() { | |
27 | $this->verifyAutobahnResults(__DIR__ . '/ab/reports/servers/index.json'); | |
28 | } | |
29 | } |
0 | <?php | |
1 | use GuzzleHttp\Psr7\Uri; | |
2 | use React\Promise\Deferred; | |
3 | use Ratchet\RFC6455\Messaging\Frame; | |
4 | ||
5 | require __DIR__ . '/../bootstrap.php'; | |
6 | ||
7 | define('AGENT', 'RatchetRFC/0.0.0'); | |
8 | ||
9 | $testServer = "127.0.0.1"; | |
10 | ||
11 | $loop = React\EventLoop\Factory::create(); | |
12 | ||
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) | |
19 | { | |
20 | return new \Ratchet\RFC6455\Messaging\MessageBuffer( | |
21 | 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()); | |
28 | }, | |
29 | function (\Ratchet\RFC6455\Messaging\FrameInterface $frame) use ($conn) { | |
30 | switch ($frame->getOpcode()) { | |
31 | case Frame::OP_PING: | |
32 | return $conn->write((new Frame($frame->getPayload(), true, Frame::OP_PONG))->maskPayload()->getContents()); | |
33 | break; | |
34 | case Frame::OP_CLOSE: | |
35 | return $conn->end((new Frame($frame->getPayload(), true, Frame::OP_CLOSE))->maskPayload()->getContents()); | |
36 | break; | |
37 | } | |
38 | }, | |
39 | false | |
40 | ); | |
41 | } | |
42 | ||
43 | function getTestCases() { | |
44 | global $factory; | |
45 | global $testServer; | |
46 | ||
47 | $deferred = new Deferred(); | |
48 | ||
49 | $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { | |
50 | $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(); | |
51 | $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001/getCaseCount')); | |
52 | ||
53 | $rawResponse = ""; | |
54 | $response = null; | |
55 | ||
56 | /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageBuffer $ms */ | |
57 | $ms = null; | |
58 | ||
59 | $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { | |
60 | if ($response === null) { | |
61 | $rawResponse .= $data; | |
62 | $pos = strpos($rawResponse, "\r\n\r\n"); | |
63 | if ($pos) { | |
64 | $data = substr($rawResponse, $pos + 4); | |
65 | $rawResponse = substr($rawResponse, 0, $pos + 4); | |
66 | $response = \GuzzleHttp\Psr7\parse_response($rawResponse); | |
67 | ||
68 | if (!$cn->validateResponse($cnRequest, $response)) { | |
69 | $stream->end(); | |
70 | $deferred->reject(); | |
71 | } else { | |
72 | $ms = new \Ratchet\RFC6455\Messaging\MessageBuffer( | |
73 | new \Ratchet\RFC6455\Messaging\CloseFrameChecker, | |
74 | function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) { | |
75 | $deferred->resolve($msg->getPayload()); | |
76 | $stream->close(); | |
77 | }, | |
78 | null, | |
79 | false | |
80 | ); | |
81 | } | |
82 | } | |
83 | } | |
84 | ||
85 | // feed the message streamer | |
86 | if ($ms) { | |
87 | $ms->onData($data); | |
88 | } | |
89 | }); | |
90 | ||
91 | $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); | |
92 | }); | |
93 | ||
94 | return $deferred->promise(); | |
95 | } | |
96 | ||
97 | function runTest($case) | |
98 | { | |
99 | global $factory; | |
100 | global $testServer; | |
101 | ||
102 | $casePath = "/runCase?case={$case}&agent=" . AGENT; | |
103 | ||
104 | $deferred = new Deferred(); | |
105 | ||
106 | $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath, $case) { | |
107 | $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(); | |
108 | $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $casePath)); | |
109 | ||
110 | $rawResponse = ""; | |
111 | $response = null; | |
112 | ||
113 | $ms = null; | |
114 | ||
115 | $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { | |
116 | if ($response === null) { | |
117 | $rawResponse .= $data; | |
118 | $pos = strpos($rawResponse, "\r\n\r\n"); | |
119 | if ($pos) { | |
120 | $data = substr($rawResponse, $pos + 4); | |
121 | $rawResponse = substr($rawResponse, 0, $pos + 4); | |
122 | $response = \GuzzleHttp\Psr7\parse_response($rawResponse); | |
123 | ||
124 | if (!$cn->validateResponse($cnRequest, $response)) { | |
125 | $stream->end(); | |
126 | $deferred->reject(); | |
127 | } else { | |
128 | $ms = echoStreamerFactory($stream); | |
129 | } | |
130 | } | |
131 | } | |
132 | ||
133 | // feed the message streamer | |
134 | if ($ms) { | |
135 | $ms->onData($data); | |
136 | } | |
137 | }); | |
138 | ||
139 | $stream->on('close', function () use ($deferred) { | |
140 | $deferred->resolve(); | |
141 | }); | |
142 | ||
143 | $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); | |
144 | }); | |
145 | ||
146 | return $deferred->promise(); | |
147 | } | |
148 | ||
149 | function createReport() { | |
150 | global $factory; | |
151 | global $testServer; | |
152 | ||
153 | $deferred = new Deferred(); | |
154 | ||
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(); | |
158 | $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $reportPath)); | |
159 | ||
160 | $rawResponse = ""; | |
161 | $response = null; | |
162 | ||
163 | /** @var \Ratchet\RFC6455\Messaging\MessageBuffer $ms */ | |
164 | $ms = null; | |
165 | ||
166 | $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { | |
167 | if ($response === null) { | |
168 | $rawResponse .= $data; | |
169 | $pos = strpos($rawResponse, "\r\n\r\n"); | |
170 | if ($pos) { | |
171 | $data = substr($rawResponse, $pos + 4); | |
172 | $rawResponse = substr($rawResponse, 0, $pos + 4); | |
173 | $response = \GuzzleHttp\Psr7\parse_response($rawResponse); | |
174 | ||
175 | if (!$cn->validateResponse($cnRequest, $response)) { | |
176 | $stream->end(); | |
177 | $deferred->reject(); | |
178 | } else { | |
179 | $ms = new \Ratchet\RFC6455\Messaging\MessageBuffer( | |
180 | new \Ratchet\RFC6455\Messaging\CloseFrameChecker, | |
181 | function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) { | |
182 | $deferred->resolve($msg->getPayload()); | |
183 | $stream->close(); | |
184 | }, | |
185 | null, | |
186 | false | |
187 | ); | |
188 | } | |
189 | } | |
190 | } | |
191 | ||
192 | // feed the message streamer | |
193 | if ($ms) { | |
194 | $ms->onData($data); | |
195 | } | |
196 | }); | |
197 | ||
198 | $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); | |
199 | }); | |
200 | ||
201 | return $deferred->promise(); | |
202 | } | |
203 | ||
204 | ||
205 | $testPromises = []; | |
206 | ||
207 | getTestCases()->then(function ($count) use ($loop) { | |
208 | $allDeferred = new Deferred(); | |
209 | ||
210 | $runNextCase = function () use (&$i, &$runNextCase, $count, $allDeferred) { | |
211 | $i++; | |
212 | if ($i > $count) { | |
213 | $allDeferred->resolve(); | |
214 | return; | |
215 | } | |
216 | runTest($i)->then($runNextCase); | |
217 | }; | |
218 | ||
219 | $i = 0; | |
220 | $runNextCase(); | |
221 | ||
222 | $allDeferred->promise()->then(function () { | |
223 | createReport(); | |
224 | }); | |
225 | }); | |
226 | ||
227 | $loop->run(); |
0 | { | |
1 | "options": { | |
2 | "failByDrop": false | |
3 | } | |
4 | , "outdir": "./reports/servers" | |
5 | , "servers": [{ | |
6 | "agent": "RatchetRFC/0.1.0" | |
7 | , "url": "ws://localhost:9001" | |
8 | , "options": {"version": 18} | |
9 | }] | |
10 | , "cases": ["*"] | |
11 | , "exclude-cases": ["6.4.*", "12.*","13.*"] | |
12 | , "exclude-agent-cases": {} | |
13 | } |
0 | { | |
1 | "url": "ws://127.0.0.1:9001" | |
2 | , "options": { | |
3 | "failByDrop": false | |
4 | } | |
5 | , "outdir": "./reports/clients" | |
6 | , "cases": ["*"] | |
7 | , "exclude-cases": ["6.4.*", "12.*", "13.*"] | |
8 | , "exclude-agent-cases": {} | |
9 | } |
0 | cd tests/ab | |
1 | ||
2 | wstest -m fuzzingserver -s fuzzingserver.json & | |
3 | sleep 5 | |
4 | php clientRunner.php | |
5 | ||
6 | sleep 2 | |
7 | ||
8 | php startServer.php & | |
9 | sleep 3 | |
10 | wstest -m fuzzingclient -s fuzzingclient.json |
0 | <?php | |
1 | use Ratchet\RFC6455\Messaging\MessageInterface; | |
2 | use Ratchet\RFC6455\Messaging\FrameInterface; | |
3 | use Ratchet\RFC6455\Messaging\Frame; | |
4 | ||
5 | require_once __DIR__ . "/../bootstrap.php"; | |
6 | ||
7 | $loop = \React\EventLoop\Factory::create(); | |
8 | ||
9 | $socket = new \React\Socket\Server($loop); | |
10 | $server = new \React\Http\Server($socket); | |
11 | ||
12 | $closeFrameChecker = new \Ratchet\RFC6455\Messaging\CloseFrameChecker; | |
13 | $negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator(new \Ratchet\RFC6455\Handshake\RequestVerifier); | |
14 | ||
15 | $uException = new \UnderflowException; | |
16 | ||
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 | $negotiatorResponse = $negotiator->handshake($psrRequest); | |
21 | ||
22 | $response->writeHead( | |
23 | $negotiatorResponse->getStatusCode(), | |
24 | array_merge( | |
25 | $negotiatorResponse->getHeaders(), | |
26 | ["Content-Length" => "0"] | |
27 | ) | |
28 | ); | |
29 | ||
30 | if ($negotiatorResponse->getStatusCode() !== 101) { | |
31 | $response->end(); | |
32 | return; | |
33 | } | |
34 | ||
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; | |
45 | } | |
46 | }, true, function() use ($uException) { | |
47 | return $uException; | |
48 | }); | |
49 | ||
50 | $request->on('data', [$parser, 'onData']); | |
51 | }); | |
52 | ||
53 | $socket->listen(9001, '0.0.0.0'); | |
54 | $loop->run(); |
0 | <?php | |
1 | ||
2 | /** | |
3 | * Find the auto loader file | |
4 | */ | |
5 | $files = [ | |
6 | __DIR__ . '/../vendor/autoload.php', | |
7 | __DIR__ . '/../../vendor/autoload.php', | |
8 | __DIR__ . '/../../../vendor/autoload.php', | |
9 | __DIR__ . '/../../../../vendor/autoload.php', | |
10 | ]; | |
11 | ||
12 | foreach ($files as $file) { | |
13 | if (file_exists($file)) { | |
14 | $loader = require $file; | |
15 | $loader->addPsr4('Ratchet\\RFC6455\\Test\\', __DIR__); | |
16 | break; | |
17 | } | |
18 | } |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Test\Unit\Handshake; | |
2 | use Ratchet\RFC6455\Handshake\RequestVerifier; | |
3 | ||
4 | /** | |
5 | * @covers Ratchet\RFC6455\Handshake\RequestVerifier | |
6 | */ | |
7 | class RequestVerifierTest extends \PHPUnit_Framework_TestCase { | |
8 | /** | |
9 | * @var RequestVerifier | |
10 | */ | |
11 | protected $_v; | |
12 | ||
13 | public function setUp() { | |
14 | $this->_v = new RequestVerifier(); | |
15 | } | |
16 | ||
17 | public static function methodProvider() { | |
18 | return array( | |
19 | array(true, 'GET'), | |
20 | array(true, 'get'), | |
21 | array(true, 'Get'), | |
22 | array(false, 'POST'), | |
23 | array(false, 'DELETE'), | |
24 | array(false, 'PUT'), | |
25 | array(false, 'PATCH') | |
26 | ); | |
27 | } | |
28 | /** | |
29 | * @dataProvider methodProvider | |
30 | */ | |
31 | public function testMethodMustBeGet($result, $in) { | |
32 | $this->assertEquals($result, $this->_v->verifyMethod($in)); | |
33 | } | |
34 | ||
35 | public static function httpVersionProvider() { | |
36 | return array( | |
37 | array(true, 1.1), | |
38 | array(true, '1.1'), | |
39 | array(true, 1.2), | |
40 | array(true, '1.2'), | |
41 | array(true, 2), | |
42 | array(true, '2'), | |
43 | array(true, '2.0'), | |
44 | array(false, '1.0'), | |
45 | array(false, 1), | |
46 | array(false, '0.9'), | |
47 | array(false, ''), | |
48 | array(false, 'hello') | |
49 | ); | |
50 | } | |
51 | ||
52 | /** | |
53 | * @dataProvider httpVersionProvider | |
54 | */ | |
55 | public function testHttpVersionIsAtLeast1Point1($expected, $in) { | |
56 | $this->assertEquals($expected, $this->_v->verifyHTTPVersion($in)); | |
57 | } | |
58 | ||
59 | public static function uRIProvider() { | |
60 | return array( | |
61 | array(true, '/chat'), | |
62 | array(true, '/hello/world?key=val'), | |
63 | array(false, '/chat#bad'), | |
64 | array(false, 'nope'), | |
65 | array(false, '/ ಠ_ಠ '), | |
66 | array(false, '/✖') | |
67 | ); | |
68 | } | |
69 | ||
70 | /** | |
71 | * @dataProvider URIProvider | |
72 | */ | |
73 | public function testRequestUri($expected, $in) { | |
74 | $this->assertEquals($expected, $this->_v->verifyRequestURI($in)); | |
75 | } | |
76 | ||
77 | public static function hostProvider() { | |
78 | return array( | |
79 | array(true, ['server.example.com']), | |
80 | array(false, []) | |
81 | ); | |
82 | } | |
83 | ||
84 | /** | |
85 | * @dataProvider HostProvider | |
86 | */ | |
87 | public function testVerifyHostIsSet($expected, $in) { | |
88 | $this->assertEquals($expected, $this->_v->verifyHost($in)); | |
89 | } | |
90 | ||
91 | public static function upgradeProvider() { | |
92 | return array( | |
93 | array(true, ['websocket']), | |
94 | array(true, ['Websocket']), | |
95 | array(true, ['webSocket']), | |
96 | array(false, []), | |
97 | array(false, ['']) | |
98 | ); | |
99 | } | |
100 | ||
101 | /** | |
102 | * @dataProvider upgradeProvider | |
103 | */ | |
104 | public function testVerifyUpgradeIsWebSocket($expected, $val) { | |
105 | $this->assertEquals($expected, $this->_v->verifyUpgradeRequest($val)); | |
106 | } | |
107 | ||
108 | public static function connectionProvider() { | |
109 | return array( | |
110 | array(true, ['Upgrade']), | |
111 | array(true, ['upgrade']), | |
112 | array(true, ['keep-alive', 'Upgrade']), | |
113 | array(true, ['Upgrade', 'keep-alive']), | |
114 | array(true, ['keep-alive', 'Upgrade', 'something']), | |
115 | // as seen in Firefox 47.0.1 - see https://github.com/ratchetphp/RFC6455/issues/14 | |
116 | array(true, ['keep-alive, Upgrade']), | |
117 | array(true, ['Upgrade, keep-alive']), | |
118 | array(true, ['keep-alive, Upgrade, something']), | |
119 | array(true, ['keep-alive, Upgrade', 'something']), | |
120 | array(false, ['']), | |
121 | array(false, []) | |
122 | ); | |
123 | } | |
124 | ||
125 | /** | |
126 | * @dataProvider connectionProvider | |
127 | */ | |
128 | public function testConnectionHeaderVerification($expected, $val) { | |
129 | $this->assertEquals($expected, $this->_v->verifyConnection($val)); | |
130 | } | |
131 | ||
132 | public static function keyProvider() { | |
133 | return array( | |
134 | array(true, ['hkfa1L7uwN6DCo4IS3iWAw==']), | |
135 | array(true, ['765vVoQpKSGJwPzJIMM2GA==']), | |
136 | array(true, ['AQIDBAUGBwgJCgsMDQ4PEC==']), | |
137 | array(true, ['axa2B/Yz2CdpfQAY2Q5P7w==']), | |
138 | array(false, [0]), | |
139 | array(false, ['Hello World']), | |
140 | array(false, ['1234567890123456']), | |
141 | array(false, ['123456789012345678901234']), | |
142 | array(true, [base64_encode('UTF8allthngs+✓')]), | |
143 | array(true, ['dGhlIHNhbXBsZSBub25jZQ==']), | |
144 | array(false, []), | |
145 | array(false, ['dGhlIHNhbXBsZSBub25jZQ==', 'Some other value']), | |
146 | array(false, ['Some other value', 'dGhlIHNhbXBsZSBub25jZQ==']) | |
147 | ); | |
148 | } | |
149 | ||
150 | /** | |
151 | * @dataProvider keyProvider | |
152 | */ | |
153 | public function testKeyIsBase64Encoded16BitNonce($expected, $val) { | |
154 | $this->assertEquals($expected, $this->_v->verifyKey($val)); | |
155 | } | |
156 | ||
157 | public static function versionProvider() { | |
158 | return array( | |
159 | array(true, [13]), | |
160 | array(true, ['13']), | |
161 | array(false, [12]), | |
162 | array(false, [14]), | |
163 | array(false, ['14']), | |
164 | array(false, ['hi']), | |
165 | array(false, ['']), | |
166 | array(false, []) | |
167 | ); | |
168 | } | |
169 | ||
170 | /** | |
171 | * @dataProvider versionProvider | |
172 | */ | |
173 | public function testVersionEquals13($expected, $in) { | |
174 | $this->assertEquals($expected, $this->_v->verifyVersion($in)); | |
175 | } | |
176 | }⏎ |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Test\Unit\Handshake; | |
2 | use Ratchet\RFC6455\Handshake\ResponseVerifier; | |
3 | ||
4 | /** | |
5 | * @covers Ratchet\RFC6455\Handshake\ResponseVerifier | |
6 | */ | |
7 | class ResponseVerifierTest extends \PHPUnit_Framework_TestCase { | |
8 | /** | |
9 | * @var ResponseVerifier | |
10 | */ | |
11 | protected $_v; | |
12 | ||
13 | public function setUp() { | |
14 | $this->_v = new ResponseVerifier; | |
15 | } | |
16 | ||
17 | public static function subProtocolsProvider() { | |
18 | return [ | |
19 | [true, ['a'], ['a']] | |
20 | , [true, ['b', 'a'], ['c', 'd', 'a']] | |
21 | , [false, ['a', 'b', 'c'], ['d']] | |
22 | , [true, [], []] | |
23 | , [true, ['a', 'b'], []] | |
24 | ]; | |
25 | } | |
26 | ||
27 | /** | |
28 | * @dataProvider subProtocolsProvider | |
29 | */ | |
30 | public function testVerifySubProtocol($expected, $response, $request) { | |
31 | $this->assertEquals($expected, $this->_v->verifySubProtocol($response, $request)); | |
32 | } | |
33 | } |
0 | <?php | |
1 | ||
2 | namespace Ratchet\RFC6455\Test\Unit\Handshake; | |
3 | ||
4 | use Ratchet\RFC6455\Handshake\RequestVerifier; | |
5 | use Ratchet\RFC6455\Handshake\ServerNegotiator; | |
6 | ||
7 | class ServerNegotiatorTest extends \PHPUnit_Framework_TestCase | |
8 | { | |
9 | public function testNoUpgradeRequested() { | |
10 | $negotiator = new ServerNegotiator(new RequestVerifier()); | |
11 | ||
12 | $requestText = 'GET / HTTP/1.1 | |
13 | Host: 127.0.0.1:6789 | |
14 | Connection: keep-alive | |
15 | Pragma: no-cache | |
16 | Cache-Control: no-cache | |
17 | Upgrade-Insecure-Requests: 1 | |
18 | 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 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 | |
20 | Accept-Encoding: gzip, deflate, sdch, br | |
21 | Accept-Language: en-US,en;q=0.8'; | |
22 | ||
23 | $request = \GuzzleHttp\Psr7\parse_request($requestText); | |
24 | ||
25 | $response = $negotiator->handshake($request); | |
26 | ||
27 | $this->assertEquals('1.1', $response->getProtocolVersion()); | |
28 | $this->assertEquals(426, $response->getStatusCode()); | |
29 | $this->assertEquals('Upgrade header MUST be provided', $response->getReasonPhrase()); | |
30 | $this->assertEquals('Upgrade', $response->getHeaderLine('Connection')); | |
31 | $this->assertEquals('websocket', $response->getHeaderLine('Upgrade')); | |
32 | $this->assertEquals('13', $response->getHeaderLine('Sec-WebSocket-Version')); | |
33 | } | |
34 | ||
35 | public function testNoConnectionUpgradeRequested() { | |
36 | $negotiator = new ServerNegotiator(new RequestVerifier()); | |
37 | ||
38 | $requestText = 'GET / HTTP/1.1 | |
39 | Host: 127.0.0.1:6789 | |
40 | Connection: keep-alive | |
41 | Pragma: no-cache | |
42 | Cache-Control: no-cache | |
43 | Upgrade: websocket | |
44 | Upgrade-Insecure-Requests: 1 | |
45 | 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 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 | |
47 | Accept-Encoding: gzip, deflate, sdch, br | |
48 | Accept-Language: en-US,en;q=0.8'; | |
49 | ||
50 | $request = \GuzzleHttp\Psr7\parse_request($requestText); | |
51 | ||
52 | $response = $negotiator->handshake($request); | |
53 | ||
54 | $this->assertEquals('1.1', $response->getProtocolVersion()); | |
55 | $this->assertEquals(400, $response->getStatusCode()); | |
56 | $this->assertEquals('Connection Upgrade MUST be requested', $response->getReasonPhrase()); | |
57 | } | |
58 | ||
59 | public function testInvalidSecWebsocketKey() { | |
60 | $negotiator = new ServerNegotiator(new RequestVerifier()); | |
61 | ||
62 | $requestText = 'GET / HTTP/1.1 | |
63 | Host: 127.0.0.1:6789 | |
64 | Connection: Upgrade | |
65 | Pragma: no-cache | |
66 | Cache-Control: no-cache | |
67 | Upgrade: websocket | |
68 | Sec-WebSocket-Key: 12345 | |
69 | Upgrade-Insecure-Requests: 1 | |
70 | 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 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 | |
72 | Accept-Encoding: gzip, deflate, sdch, br | |
73 | Accept-Language: en-US,en;q=0.8'; | |
74 | ||
75 | $request = \GuzzleHttp\Psr7\parse_request($requestText); | |
76 | ||
77 | $response = $negotiator->handshake($request); | |
78 | ||
79 | $this->assertEquals('1.1', $response->getProtocolVersion()); | |
80 | $this->assertEquals(400, $response->getStatusCode()); | |
81 | $this->assertEquals('Invalid Sec-WebSocket-Key', $response->getReasonPhrase()); | |
82 | } | |
83 | ||
84 | public function testInvalidSecWebsocketVersion() { | |
85 | $negotiator = new ServerNegotiator(new RequestVerifier()); | |
86 | ||
87 | $requestText = 'GET / HTTP/1.1 | |
88 | Host: 127.0.0.1:6789 | |
89 | Connection: Upgrade | |
90 | Pragma: no-cache | |
91 | Cache-Control: no-cache | |
92 | Upgrade: websocket | |
93 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== | |
94 | Upgrade-Insecure-Requests: 1 | |
95 | 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 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 | |
97 | Accept-Encoding: gzip, deflate, sdch, br | |
98 | Accept-Language: en-US,en;q=0.8'; | |
99 | ||
100 | $request = \GuzzleHttp\Psr7\parse_request($requestText); | |
101 | ||
102 | $response = $negotiator->handshake($request); | |
103 | ||
104 | $this->assertEquals('1.1', $response->getProtocolVersion()); | |
105 | $this->assertEquals(426, $response->getStatusCode()); | |
106 | $this->assertEquals('Upgrade Required', $response->getReasonPhrase()); | |
107 | $this->assertEquals('Upgrade', $response->getHeaderLine('Connection')); | |
108 | $this->assertEquals('websocket', $response->getHeaderLine('Upgrade')); | |
109 | $this->assertEquals('13', $response->getHeaderLine('Sec-WebSocket-Version')); | |
110 | } | |
111 | ||
112 | public function testBadSubprotocolResponse() { | |
113 | $negotiator = new ServerNegotiator(new RequestVerifier()); | |
114 | $negotiator->setStrictSubProtocolCheck(true); | |
115 | $negotiator->setSupportedSubProtocols([]); | |
116 | ||
117 | $requestText = 'GET / HTTP/1.1 | |
118 | Host: 127.0.0.1:6789 | |
119 | Connection: Upgrade | |
120 | Pragma: no-cache | |
121 | Cache-Control: no-cache | |
122 | Upgrade: websocket | |
123 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== | |
124 | Sec-WebSocket-Version: 13 | |
125 | Sec-WebSocket-Protocol: someprotocol | |
126 | Upgrade-Insecure-Requests: 1 | |
127 | 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 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 | |
129 | Accept-Encoding: gzip, deflate, sdch, br | |
130 | Accept-Language: en-US,en;q=0.8'; | |
131 | ||
132 | $request = \GuzzleHttp\Psr7\parse_request($requestText); | |
133 | ||
134 | $response = $negotiator->handshake($request); | |
135 | ||
136 | $this->assertEquals('1.1', $response->getProtocolVersion()); | |
137 | $this->assertEquals(426, $response->getStatusCode()); | |
138 | $this->assertEquals('No Sec-WebSocket-Protocols requested supported', $response->getReasonPhrase()); | |
139 | $this->assertEquals('Upgrade', $response->getHeaderLine('Connection')); | |
140 | $this->assertEquals('websocket', $response->getHeaderLine('Upgrade')); | |
141 | $this->assertEquals('13', $response->getHeaderLine('Sec-WebSocket-Version')); | |
142 | } | |
143 | ||
144 | public function testNonStrictSubprotocolDoesNotIncludeHeaderWhenNoneAgreedOn() { | |
145 | $negotiator = new ServerNegotiator(new RequestVerifier()); | |
146 | $negotiator->setStrictSubProtocolCheck(false); | |
147 | $negotiator->setSupportedSubProtocols(['someproto']); | |
148 | ||
149 | $requestText = 'GET / HTTP/1.1 | |
150 | Host: 127.0.0.1:6789 | |
151 | Connection: Upgrade | |
152 | Pragma: no-cache | |
153 | Cache-Control: no-cache | |
154 | Upgrade: websocket | |
155 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== | |
156 | Sec-WebSocket-Version: 13 | |
157 | Sec-WebSocket-Protocol: someotherproto | |
158 | Upgrade-Insecure-Requests: 1 | |
159 | 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 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 | |
161 | Accept-Encoding: gzip, deflate, sdch, br | |
162 | Accept-Language: en-US,en;q=0.8'; | |
163 | ||
164 | $request = \GuzzleHttp\Psr7\parse_request($requestText); | |
165 | ||
166 | $response = $negotiator->handshake($request); | |
167 | ||
168 | $this->assertEquals('1.1', $response->getProtocolVersion()); | |
169 | $this->assertEquals(101, $response->getStatusCode()); | |
170 | $this->assertEquals('Upgrade', $response->getHeaderLine('Connection')); | |
171 | $this->assertEquals('websocket', $response->getHeaderLine('Upgrade')); | |
172 | $this->assertFalse($response->hasHeader('Sec-WebSocket-Protocol')); | |
173 | } | |
174 | }⏎ |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Test\Unit\Messaging; | |
2 | use Ratchet\RFC6455\Messaging\Frame; | |
3 | ||
4 | /** | |
5 | * @covers Ratchet\RFC6455\Messaging\Frame | |
6 | * @todo getMaskingKey, getPayloadStartingByte don't have tests yet | |
7 | * @todo Could use some clean up in general, I had to rush to fix a bug for a deadline, sorry. | |
8 | */ | |
9 | class FrameTest extends \PHPUnit_Framework_TestCase { | |
10 | protected $_firstByteFinText = '10000001'; | |
11 | ||
12 | protected $_secondByteMaskedSPL = '11111101'; | |
13 | ||
14 | /** @var Frame */ | |
15 | protected $_frame; | |
16 | ||
17 | protected $_packer; | |
18 | ||
19 | public function setUp() { | |
20 | $this->_frame = new Frame; | |
21 | } | |
22 | ||
23 | /** | |
24 | * Encode the fake binary string to send over the wire | |
25 | * @param string of 1's and 0's | |
26 | * @return string | |
27 | */ | |
28 | public static function encode($in) { | |
29 | if (strlen($in) > 8) { | |
30 | $out = ''; | |
31 | while (strlen($in) >= 8) { | |
32 | $out .= static::encode(substr($in, 0, 8)); | |
33 | $in = substr($in, 8); | |
34 | } | |
35 | return $out; | |
36 | } | |
37 | return chr(bindec($in)); | |
38 | } | |
39 | ||
40 | /** | |
41 | * This is a data provider | |
42 | * param string The UTF8 message | |
43 | * param string The WebSocket framed message, then base64_encoded | |
44 | */ | |
45 | public static function UnframeMessageProvider() { | |
46 | return array( | |
47 | array('Hello World!', 'gYydAIfa1WXrtvIg0LXvbOP7'), | |
48 | array('!@#$%^&*()-=_+[]{}\|/.,<>`~', 'gZv+h96r38f9j9vZ+IHWrvOWoayF9oX6gtfRqfKXwOeg'), | |
49 | array('ಠ_ಠ', 'gYfnSpu5B/g75gf4Ow=='), | |
50 | array( | |
51 | "The quick brown fox jumps over the lazy dog. All work and no play makes Chris a dull boy. I'm trying to get past 128 characters for a unit test here...", | |
52 | 'gf4Amahb14P8M7Kj2S6+4MN7tfHHLLmjzjSvo8IuuvPbe7j1zSn398A+9+/JIa6jzDSwrYh7lu/Ee6Ds2jD34sY/9+3He6fvySL37skwsvCIGL/xwSj34og/ou/Ee7Xs0XX3o+F8uqPcKa7qxjz398d7sObce6fi2y/3sppj9+DAOqXiyy+y8dt7sezae7aj3TW+94gvsvDce7/m2j75rYY=' | |
53 | ) | |
54 | ); | |
55 | } | |
56 | ||
57 | public static function underflowProvider() { | |
58 | return array( | |
59 | array('isFinal', ''), | |
60 | array('getRsv1', ''), | |
61 | array('getRsv2', ''), | |
62 | array('getRsv3', ''), | |
63 | array('getOpcode', ''), | |
64 | array('isMasked', '10000001'), | |
65 | array('getPayloadLength', '10000001'), | |
66 | array('getPayloadLength', '1000000111111110'), | |
67 | array('getMaskingKey', '1000000110000111'), | |
68 | array('getPayload', '100000011000000100011100101010101001100111110100') | |
69 | ); | |
70 | } | |
71 | ||
72 | /** | |
73 | * @dataProvider underflowProvider | |
74 | * | |
75 | * @covers Ratchet\RFC6455\Messaging\Frame::isFinal | |
76 | * @covers Ratchet\RFC6455\Messaging\Frame::getRsv1 | |
77 | * @covers Ratchet\RFC6455\Messaging\Frame::getRsv2 | |
78 | * @covers Ratchet\RFC6455\Messaging\Frame::getRsv3 | |
79 | * @covers Ratchet\RFC6455\Messaging\Frame::getOpcode | |
80 | * @covers Ratchet\RFC6455\Messaging\Frame::isMasked | |
81 | * @covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength | |
82 | * @covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey | |
83 | * @covers Ratchet\RFC6455\Messaging\Frame::getPayload | |
84 | */ | |
85 | public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering($method, $bin) { | |
86 | $this->setExpectedException('\UnderflowException'); | |
87 | if (!empty($bin)) { | |
88 | $this->_frame->addBuffer(static::encode($bin)); | |
89 | } | |
90 | call_user_func(array($this->_frame, $method)); | |
91 | } | |
92 | ||
93 | /** | |
94 | * A data provider for testing the first byte of a WebSocket frame | |
95 | * param bool Given, is the byte indicate this is the final frame | |
96 | * param int Given, what is the expected opcode | |
97 | * param string of 0|1 Each character represents a bit in the byte | |
98 | */ | |
99 | public static function firstByteProvider() { | |
100 | return array( | |
101 | array(false, false, false, true, 8, '00011000'), | |
102 | array(true, false, true, false, 10, '10101010'), | |
103 | array(false, false, false, false, 15, '00001111'), | |
104 | array(true, false, false, false, 1, '10000001'), | |
105 | array(true, true, true, true, 15, '11111111'), | |
106 | array(true, true, false, false, 7, '11000111') | |
107 | ); | |
108 | } | |
109 | ||
110 | /** | |
111 | * @dataProvider firstByteProvider | |
112 | * covers Ratchet\RFC6455\Messaging\Frame::isFinal | |
113 | */ | |
114 | public function testFinCodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { | |
115 | $this->_frame->addBuffer(static::encode($bin)); | |
116 | $this->assertEquals($fin, $this->_frame->isFinal()); | |
117 | } | |
118 | ||
119 | /** | |
120 | * @dataProvider firstByteProvider | |
121 | * covers Ratchet\RFC6455\Messaging\Frame::getRsv1 | |
122 | * covers Ratchet\RFC6455\Messaging\Frame::getRsv2 | |
123 | * covers Ratchet\RFC6455\Messaging\Frame::getRsv3 | |
124 | */ | |
125 | public function testGetRsvFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { | |
126 | $this->_frame->addBuffer(static::encode($bin)); | |
127 | $this->assertEquals($rsv1, $this->_frame->getRsv1()); | |
128 | $this->assertEquals($rsv2, $this->_frame->getRsv2()); | |
129 | $this->assertEquals($rsv3, $this->_frame->getRsv3()); | |
130 | } | |
131 | ||
132 | /** | |
133 | * @dataProvider firstByteProvider | |
134 | * covers Ratchet\RFC6455\Messaging\Frame::getOpcode | |
135 | */ | |
136 | public function testOpcodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { | |
137 | $this->_frame->addBuffer(static::encode($bin)); | |
138 | $this->assertEquals($opcode, $this->_frame->getOpcode()); | |
139 | } | |
140 | ||
141 | /** | |
142 | * @dataProvider UnframeMessageProvider | |
143 | * covers Ratchet\RFC6455\Messaging\Frame::isFinal | |
144 | */ | |
145 | public function testFinCodeFromFullMessage($msg, $encoded) { | |
146 | $this->_frame->addBuffer(base64_decode($encoded)); | |
147 | $this->assertTrue($this->_frame->isFinal()); | |
148 | } | |
149 | ||
150 | /** | |
151 | * @dataProvider UnframeMessageProvider | |
152 | * covers Ratchet\RFC6455\Messaging\Frame::getOpcode | |
153 | */ | |
154 | public function testOpcodeFromFullMessage($msg, $encoded) { | |
155 | $this->_frame->addBuffer(base64_decode($encoded)); | |
156 | $this->assertEquals(1, $this->_frame->getOpcode()); | |
157 | } | |
158 | ||
159 | public static function payloadLengthDescriptionProvider() { | |
160 | return array( | |
161 | array(7, '01110101'), | |
162 | array(7, '01111101'), | |
163 | array(23, '01111110'), | |
164 | array(71, '01111111'), | |
165 | array(7, '00000000'), // Should this throw an exception? Can a payload be empty? | |
166 | array(7, '00000001') | |
167 | ); | |
168 | } | |
169 | ||
170 | /** | |
171 | * @dataProvider payloadLengthDescriptionProvider | |
172 | * covers Ratchet\RFC6455\Messaging\Frame::addBuffer | |
173 | * covers Ratchet\RFC6455\Messaging\Frame::getFirstPayloadVal | |
174 | */ | |
175 | public function testFirstPayloadDesignationValue($bits, $bin) { | |
176 | $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); | |
177 | $this->_frame->addBuffer(static::encode($bin)); | |
178 | $ref = new \ReflectionClass($this->_frame); | |
179 | $cb = $ref->getMethod('getFirstPayloadVal'); | |
180 | $cb->setAccessible(true); | |
181 | $this->assertEquals(bindec($bin), $cb->invoke($this->_frame)); | |
182 | } | |
183 | ||
184 | /** | |
185 | * covers Ratchet\RFC6455\Messaging\Frame::getFirstPayloadVal | |
186 | */ | |
187 | public function testFirstPayloadValUnderflow() { | |
188 | $ref = new \ReflectionClass($this->_frame); | |
189 | $cb = $ref->getMethod('getFirstPayloadVal'); | |
190 | $cb->setAccessible(true); | |
191 | $this->setExpectedException('UnderflowException'); | |
192 | $cb->invoke($this->_frame); | |
193 | } | |
194 | ||
195 | /** | |
196 | * @dataProvider payloadLengthDescriptionProvider | |
197 | * covers Ratchet\RFC6455\Messaging\Frame::getNumPayloadBits | |
198 | */ | |
199 | public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) { | |
200 | $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); | |
201 | $this->_frame->addBuffer(static::encode($bin)); | |
202 | $ref = new \ReflectionClass($this->_frame); | |
203 | $cb = $ref->getMethod('getNumPayloadBits'); | |
204 | $cb->setAccessible(true); | |
205 | $this->assertEquals($expected_bits, $cb->invoke($this->_frame)); | |
206 | } | |
207 | ||
208 | /** | |
209 | * covers Ratchet\RFC6455\Messaging\Frame::getNumPayloadBits | |
210 | */ | |
211 | public function testgetNumPayloadBitsUnderflow() { | |
212 | $ref = new \ReflectionClass($this->_frame); | |
213 | $cb = $ref->getMethod('getNumPayloadBits'); | |
214 | $cb->setAccessible(true); | |
215 | $this->setExpectedException('UnderflowException'); | |
216 | $cb->invoke($this->_frame); | |
217 | } | |
218 | ||
219 | public function secondByteProvider() { | |
220 | return array( | |
221 | array(true, 1, '10000001'), | |
222 | array(false, 1, '00000001'), | |
223 | array(true, 125, $this->_secondByteMaskedSPL) | |
224 | ); | |
225 | } | |
226 | /** | |
227 | * @dataProvider secondByteProvider | |
228 | * covers Ratchet\RFC6455\Messaging\Frame::isMasked | |
229 | */ | |
230 | public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) { | |
231 | $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); | |
232 | $this->_frame->addBuffer(static::encode($bin)); | |
233 | $this->assertEquals($masked, $this->_frame->isMasked()); | |
234 | } | |
235 | ||
236 | /** | |
237 | * @dataProvider UnframeMessageProvider | |
238 | * covers Ratchet\RFC6455\Messaging\Frame::isMasked | |
239 | */ | |
240 | public function testIsMaskedFromFullMessage($msg, $encoded) { | |
241 | $this->_frame->addBuffer(base64_decode($encoded)); | |
242 | $this->assertTrue($this->_frame->isMasked()); | |
243 | } | |
244 | ||
245 | /** | |
246 | * @dataProvider secondByteProvider | |
247 | * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength | |
248 | */ | |
249 | public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) { | |
250 | $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); | |
251 | $this->_frame->addBuffer(static::encode($bin)); | |
252 | $this->assertEquals($payload_length, $this->_frame->getPayloadLength()); | |
253 | } | |
254 | ||
255 | /** | |
256 | * @dataProvider UnframeMessageProvider | |
257 | * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength | |
258 | * @todo Not yet testing when second additional payload length descriptor | |
259 | */ | |
260 | public function testGetPayloadLengthFromFullMessage($msg, $encoded) { | |
261 | $this->_frame->addBuffer(base64_decode($encoded)); | |
262 | $this->assertEquals(strlen($msg), $this->_frame->getPayloadLength()); | |
263 | } | |
264 | ||
265 | public function maskingKeyProvider() { | |
266 | $frame = new Frame; | |
267 | return array( | |
268 | array($frame->generateMaskingKey()), | |
269 | array($frame->generateMaskingKey()), | |
270 | array($frame->generateMaskingKey()) | |
271 | ); | |
272 | } | |
273 | ||
274 | /** | |
275 | * @dataProvider maskingKeyProvider | |
276 | * covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey | |
277 | * @todo I I wrote the dataProvider incorrectly, skipping for now | |
278 | */ | |
279 | public function testGetMaskingKey($mask) { | |
280 | $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); | |
281 | $this->_frame->addBuffer(static::encode($this->_secondByteMaskedSPL)); | |
282 | $this->_frame->addBuffer($mask); | |
283 | $this->assertEquals($mask, $this->_frame->getMaskingKey()); | |
284 | } | |
285 | ||
286 | /** | |
287 | * covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey | |
288 | */ | |
289 | public function testGetMaskingKeyOnUnmaskedPayload() { | |
290 | $frame = new Frame('Hello World!'); | |
291 | $this->assertEquals('', $frame->getMaskingKey()); | |
292 | } | |
293 | ||
294 | /** | |
295 | * @dataProvider UnframeMessageProvider | |
296 | * covers Ratchet\RFC6455\Messaging\Frame::getPayload | |
297 | * @todo Move this test to bottom as it requires all methods of the class | |
298 | */ | |
299 | public function testUnframeFullMessage($unframed, $base_framed) { | |
300 | $this->_frame->addBuffer(base64_decode($base_framed)); | |
301 | $this->assertEquals($unframed, $this->_frame->getPayload()); | |
302 | } | |
303 | ||
304 | public static function messageFragmentProvider() { | |
305 | return array( | |
306 | array(false, '', '', '', '', '') | |
307 | ); | |
308 | } | |
309 | ||
310 | /** | |
311 | * @dataProvider UnframeMessageProvider | |
312 | * covers Ratchet\RFC6455\Messaging\Frame::getPayload | |
313 | */ | |
314 | public function testCheckPiecingTogetherMessage($msg, $encoded) { | |
315 | $framed = base64_decode($encoded); | |
316 | for ($i = 0, $len = strlen($framed);$i < $len; $i++) { | |
317 | $this->_frame->addBuffer(substr($framed, $i, 1)); | |
318 | } | |
319 | $this->assertEquals($msg, $this->_frame->getPayload()); | |
320 | } | |
321 | ||
322 | /** | |
323 | * covers Ratchet\RFC6455\Messaging\Frame::__construct | |
324 | * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength | |
325 | * covers Ratchet\RFC6455\Messaging\Frame::getPayload | |
326 | */ | |
327 | public function testLongCreate() { | |
328 | $len = 65525; | |
329 | $pl = $this->generateRandomString($len); | |
330 | $frame = new Frame($pl, true, Frame::OP_PING); | |
331 | $this->assertTrue($frame->isFinal()); | |
332 | $this->assertEquals(Frame::OP_PING, $frame->getOpcode()); | |
333 | $this->assertFalse($frame->isMasked()); | |
334 | $this->assertEquals($len, $frame->getPayloadLength()); | |
335 | $this->assertEquals($pl, $frame->getPayload()); | |
336 | } | |
337 | ||
338 | /** | |
339 | * covers Ratchet\RFC6455\Messaging\Frame::__construct | |
340 | * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength | |
341 | */ | |
342 | public function testReallyLongCreate() { | |
343 | $len = 65575; | |
344 | $frame = new Frame($this->generateRandomString($len)); | |
345 | $this->assertEquals($len, $frame->getPayloadLength()); | |
346 | } | |
347 | /** | |
348 | * covers Ratchet\RFC6455\Messaging\Frame::__construct | |
349 | * covers Ratchet\RFC6455\Messaging\Frame::extractOverflow | |
350 | */ | |
351 | public function testExtractOverflow() { | |
352 | $string1 = $this->generateRandomString(); | |
353 | $frame1 = new Frame($string1); | |
354 | $string2 = $this->generateRandomString(); | |
355 | $frame2 = new Frame($string2); | |
356 | $cat = new Frame; | |
357 | $cat->addBuffer($frame1->getContents() . $frame2->getContents()); | |
358 | $this->assertEquals($frame1->getContents(), $cat->getContents()); | |
359 | $this->assertEquals($string1, $cat->getPayload()); | |
360 | $uncat = new Frame; | |
361 | $uncat->addBuffer($cat->extractOverflow()); | |
362 | $this->assertEquals($string1, $cat->getPayload()); | |
363 | $this->assertEquals($string2, $uncat->getPayload()); | |
364 | } | |
365 | ||
366 | /** | |
367 | * covers Ratchet\RFC6455\Messaging\Frame::extractOverflow | |
368 | */ | |
369 | public function testEmptyExtractOverflow() { | |
370 | $string = $this->generateRandomString(); | |
371 | $frame = new Frame($string); | |
372 | $this->assertEquals($string, $frame->getPayload()); | |
373 | $this->assertEquals('', $frame->extractOverflow()); | |
374 | $this->assertEquals($string, $frame->getPayload()); | |
375 | } | |
376 | ||
377 | /** | |
378 | * covers Ratchet\RFC6455\Messaging\Frame::getContents | |
379 | */ | |
380 | public function testGetContents() { | |
381 | $msg = 'The quick brown fox jumps over the lazy dog.'; | |
382 | $frame1 = new Frame($msg); | |
383 | $frame2 = new Frame($msg); | |
384 | $frame2->maskPayload(); | |
385 | $this->assertNotEquals($frame1->getContents(), $frame2->getContents()); | |
386 | $this->assertEquals(strlen($frame1->getContents()) + 4, strlen($frame2->getContents())); | |
387 | } | |
388 | ||
389 | /** | |
390 | * covers Ratchet\RFC6455\Messaging\Frame::maskPayload | |
391 | */ | |
392 | public function testMasking() { | |
393 | $msg = 'The quick brown fox jumps over the lazy dog.'; | |
394 | $frame = new Frame($msg); | |
395 | $frame->maskPayload(); | |
396 | $this->assertTrue($frame->isMasked()); | |
397 | $this->assertEquals($msg, $frame->getPayload()); | |
398 | } | |
399 | ||
400 | /** | |
401 | * covers Ratchet\RFC6455\Messaging\Frame::unMaskPayload | |
402 | */ | |
403 | public function testUnMaskPayload() { | |
404 | $string = $this->generateRandomString(); | |
405 | $frame = new Frame($string); | |
406 | $frame->maskPayload()->unMaskPayload(); | |
407 | $this->assertFalse($frame->isMasked()); | |
408 | $this->assertEquals($string, $frame->getPayload()); | |
409 | } | |
410 | ||
411 | /** | |
412 | * covers Ratchet\RFC6455\Messaging\Frame::generateMaskingKey | |
413 | */ | |
414 | public function testGenerateMaskingKey() { | |
415 | $dupe = false; | |
416 | $done = array(); | |
417 | for ($i = 0; $i < 10; $i++) { | |
418 | $new = $this->_frame->generateMaskingKey(); | |
419 | if (in_array($new, $done)) { | |
420 | $dupe = true; | |
421 | } | |
422 | $done[] = $new; | |
423 | } | |
424 | $this->assertEquals(4, strlen($new)); | |
425 | $this->assertFalse($dupe); | |
426 | } | |
427 | ||
428 | /** | |
429 | * covers Ratchet\RFC6455\Messaging\Frame::maskPayload | |
430 | */ | |
431 | public function testGivenMaskIsValid() { | |
432 | $this->setExpectedException('InvalidArgumentException'); | |
433 | $this->_frame->maskPayload('hello world'); | |
434 | } | |
435 | ||
436 | /** | |
437 | * covers Ratchet\RFC6455\Messaging\Frame::maskPayload | |
438 | */ | |
439 | public function testGivenMaskIsValidAscii() { | |
440 | if (!extension_loaded('mbstring')) { | |
441 | $this->markTestSkipped("mbstring required for this test"); | |
442 | return; | |
443 | } | |
444 | $this->setExpectedException('OutOfBoundsException'); | |
445 | $this->_frame->maskPayload('x✖'); | |
446 | } | |
447 | ||
448 | protected function generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) { | |
449 | $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&/()=[]{}'; // ยง | |
450 | $useChars = array(); | |
451 | for($i = 0; $i < $length; $i++) { | |
452 | $useChars[] = $characters[mt_rand(0, strlen($characters) - 1)]; | |
453 | } | |
454 | if($addSpaces === true) { | |
455 | array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' '); | |
456 | } | |
457 | if($addNumbers === true) { | |
458 | array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9)); | |
459 | } | |
460 | shuffle($useChars); | |
461 | $randomString = trim(implode('', $useChars)); | |
462 | $randomString = substr($randomString, 0, $length); | |
463 | return $randomString; | |
464 | } | |
465 | ||
466 | /** | |
467 | * There was a frame boundary issue when the first 3 bytes of a frame with a payload greater than | |
468 | * 126 was added to the frame buffer and then Frame::getPayloadLength was called. It would cause the frame | |
469 | * to set the payload length to 126 and then not recalculate it once the full length information was available. | |
470 | * | |
471 | * This is fixed by setting the defPayLen back to -1 before the underflow exception is thrown. | |
472 | * | |
473 | * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength | |
474 | * covers Ratchet\RFC6455\Messaging\Frame::extractOverflow | |
475 | */ | |
476 | public function testFrameDeliveredOneByteAtATime() { | |
477 | $startHeader = "\x01\x7e\x01\x00"; // header for a text frame of 256 - non-final | |
478 | $framePayload = str_repeat("*", 256); | |
479 | $rawOverflow = "xyz"; | |
480 | $rawFrame = $startHeader . $framePayload . $rawOverflow; | |
481 | $frame = new Frame(); | |
482 | $payloadLen = 256; | |
483 | for ($i = 0; $i < strlen($rawFrame); $i++) { | |
484 | $frame->addBuffer($rawFrame[$i]); | |
485 | try { | |
486 | // payloadLen will | |
487 | $payloadLen = $frame->getPayloadLength(); | |
488 | } catch (\UnderflowException $e) { | |
489 | if ($i > 2) { // we should get an underflow on 0,1,2 | |
490 | $this->fail("Underflow exception when the frame length should be available"); | |
491 | } | |
492 | } | |
493 | if ($payloadLen !== 256) { | |
494 | $this->fail("Payload length of " . $payloadLen . " should have been 256."); | |
495 | } | |
496 | } | |
497 | // make sure the overflow is good | |
498 | $this->assertEquals($rawOverflow, $frame->extractOverflow()); | |
499 | } | |
500 | } |
0 | <?php | |
1 | ||
2 | namespace Ratchet\RFC6455\Test\Unit\Messaging; | |
3 | ||
4 | use Ratchet\RFC6455\Messaging\CloseFrameChecker; | |
5 | use Ratchet\RFC6455\Messaging\Frame; | |
6 | use Ratchet\RFC6455\Messaging\Message; | |
7 | use Ratchet\RFC6455\Messaging\MessageBuffer; | |
8 | ||
9 | class MessageBufferTest extends \PHPUnit_Framework_TestCase | |
10 | { | |
11 | /** | |
12 | * This is to test that MessageBuffer can handle a large receive | |
13 | * buffer with many many frames without blowing the stack (pre-v0.4 issue) | |
14 | */ | |
15 | public function testProcessingLotsOfFramesInASingleChunk() { | |
16 | $frame = new Frame('a', true, Frame::OP_TEXT); | |
17 | ||
18 | $frameRaw = $frame->getContents(); | |
19 | ||
20 | $data = str_repeat($frameRaw, 1000); | |
21 | ||
22 | $messageCount = 0; | |
23 | ||
24 | $messageBuffer = new MessageBuffer( | |
25 | new CloseFrameChecker(), | |
26 | function (Message $message) use (&$messageCount) { | |
27 | $messageCount++; | |
28 | $this->assertEquals('a', $message->getPayload()); | |
29 | }, | |
30 | null, | |
31 | false | |
32 | ); | |
33 | ||
34 | $messageBuffer->onData($data); | |
35 | ||
36 | $this->assertEquals(1000, $messageCount); | |
37 | } | |
38 | }⏎ |
0 | <?php | |
1 | namespace Ratchet\RFC6455\Test\Unit\Messaging; | |
2 | use Ratchet\RFC6455\Messaging\Frame; | |
3 | use Ratchet\RFC6455\Messaging\Message; | |
4 | ||
5 | /** | |
6 | * @covers Ratchet\RFC6455\Messaging\Message | |
7 | */ | |
8 | class MessageTest extends \PHPUnit_Framework_TestCase { | |
9 | /** @var Message */ | |
10 | protected $message; | |
11 | ||
12 | public function setUp() { | |
13 | $this->message = new Message; | |
14 | } | |
15 | ||
16 | public function testNoFrames() { | |
17 | $this->assertFalse($this->message->isCoalesced()); | |
18 | } | |
19 | ||
20 | public function testNoFramesOpCode() { | |
21 | $this->setExpectedException('UnderflowException'); | |
22 | $this->message->getOpCode(); | |
23 | } | |
24 | ||
25 | public function testFragmentationPayload() { | |
26 | $a = 'Hello '; | |
27 | $b = 'World!'; | |
28 | $f1 = new Frame($a, false); | |
29 | $f2 = new Frame($b, true, Frame::OP_CONTINUE); | |
30 | $this->message->addFrame($f1)->addFrame($f2); | |
31 | $this->assertEquals(strlen($a . $b), $this->message->getPayloadLength()); | |
32 | $this->assertEquals($a . $b, $this->message->getPayload()); | |
33 | } | |
34 | ||
35 | public function testUnbufferedFragment() { | |
36 | $this->message->addFrame(new Frame('The quick brow', false)); | |
37 | $this->setExpectedException('UnderflowException'); | |
38 | $this->message->getPayload(); | |
39 | } | |
40 | ||
41 | public function testGetOpCode() { | |
42 | $this->message | |
43 | ->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT)) | |
44 | ->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE)) | |
45 | ->addFrame(new Frame('er the lazy dog', true, Frame::OP_CONTINUE)) | |
46 | ; | |
47 | $this->assertEquals(Frame::OP_TEXT, $this->message->getOpCode()); | |
48 | } | |
49 | ||
50 | public function testGetUnBufferedPayloadLength() { | |
51 | $this->message | |
52 | ->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT)) | |
53 | ->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE)) | |
54 | ; | |
55 | $this->assertEquals(28, $this->message->getPayloadLength()); | |
56 | } | |
57 | }⏎ |