Codebase list ratchet-rfc6455 / 86d6903
New upstream version 0.2.4 Dominik George 5 years ago
31 changed file(s) with 2839 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 composer.lock
1 vendor
2 tests/ab/reports
3 reports
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 }