pax_global_header00006660000000000000000000000064132723502200014506gustar00rootroot0000000000000052 comment=1612f528c3496ad06e910d0f8b6f16ab97696706 RFC6455-0.2.4/000077500000000000000000000000001327235022000124475ustar00rootroot00000000000000RFC6455-0.2.4/.gitignore000066400000000000000000000000561327235022000144400ustar00rootroot00000000000000composer.lock vendor tests/ab/reports reports RFC6455-0.2.4/.travis.yml000066400000000000000000000005231327235022000145600ustar00rootroot00000000000000language: php php: - 5.4 - 5.5 - 5.6 - 7.0 - 7.1 - 7.2 - hhvm before_install: - export PATH=$HOME/.local/bin:$PATH - pip install --user autobahntestsuite - pip list --user autobahntestsuite before_script: - composer install - sh tests/ab/run_ab_tests.sh script: - vendor/bin/phpunit RFC6455-0.2.4/LICENSE000066400000000000000000000020441327235022000134540ustar00rootroot00000000000000Copyright (c) 2011-2016 Chris Boden Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. RFC6455-0.2.4/README.md000066400000000000000000000013711327235022000137300ustar00rootroot00000000000000# RFC6455 - The WebSocket Protocol [![Build Status](https://travis-ci.org/ratchetphp/RFC6455.svg?branch=master)](https://travis-ci.org/ratchetphp/RFC6455) ![Autobahn Testsuite](https://img.shields.io/badge/Autobahn-passing-brightgreen.svg) This library a protocol handler for the RFC6455 specification. It contains components for both server and client side handshake and messaging protocol negotation. Aspects that are left open to interpertation in the specification are also left open in this library. It is up to the implementation to determine how those interpertations are to be dealt with. This library is independent, framework agnostic, and does not deal with any I/O. HTTP upgrade negotiation integration points are handled with PSR-7 interfaces. RFC6455-0.2.4/composer.json000066400000000000000000000015501327235022000151720ustar00rootroot00000000000000{ "name": "ratchet/rfc6455", "type": "library", "description": "RFC6455 WebSocket protocol handler", "keywords": ["WebSockets", "websocket", "RFC6455"], "homepage": "http://socketo.me", "license": "MIT", "authors": [{ "name": "Chris Boden" , "email": "cboden@gmail.com" , "role": "Developer" }], "support": { "forum": "https://groups.google.com/forum/#!forum/ratchet-php" , "issues": "https://github.com/ratchetphp/RFC6455/issues" , "irc": "irc://irc.freenode.org/reactphp" }, "autoload": { "psr-4": { "Ratchet\\RFC6455\\": "src" } }, "require": { "php": ">=5.4.2", "guzzlehttp/psr7": "^1.0" }, "require-dev": { "react/http": "^0.4.1", "react/socket-client": "^0.4.3", "phpunit/phpunit": "4.8.*" } } RFC6455-0.2.4/phpunit.xml.dist000066400000000000000000000012441327235022000156230ustar00rootroot00000000000000 tests test/ab ./src/ RFC6455-0.2.4/src/000077500000000000000000000000001327235022000132365ustar00rootroot00000000000000RFC6455-0.2.4/src/Handshake/000077500000000000000000000000001327235022000151245ustar00rootroot00000000000000RFC6455-0.2.4/src/Handshake/ClientNegotiator.php000066400000000000000000000027121327235022000211110ustar00rootroot00000000000000verifier = new ResponseVerifier; $this->defaultHeader = new Request('GET', '', [ 'Connection' => 'Upgrade' , 'Upgrade' => 'websocket' , 'Sec-WebSocket-Version' => $this->getVersion() , 'User-Agent' => "Ratchet" ]); } public function generateRequest(UriInterface $uri) { return $this->defaultHeader->withUri($uri) ->withHeader("Sec-WebSocket-Key", $this->generateKey()); } public function validateResponse(RequestInterface $request, ResponseInterface $response) { return $this->verifier->verifyAll($request, $response); } public function generateKey() { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwzyz1234567890+/='; $charRange = strlen($chars) - 1; $key = ''; for ($i = 0; $i < 16; $i++) { $key .= $chars[mt_rand(0, $charRange)]; } return base64_encode($key); } public function getVersion() { return 13; } } RFC6455-0.2.4/src/Handshake/NegotiatorInterface.php000066400000000000000000000030551327235022000215740ustar00rootroot00000000000000verifyMethod($request->getMethod()); $passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion()); $passes += (int)$this->verifyRequestURI($request->getUri()->getPath()); $passes += (int)$this->verifyHost($request->getHeader('Host')); $passes += (int)$this->verifyUpgradeRequest($request->getHeader('Upgrade')); $passes += (int)$this->verifyConnection($request->getHeader('Connection')); $passes += (int)$this->verifyKey($request->getHeader('Sec-WebSocket-Key')); $passes += (int)$this->verifyVersion($request->getHeader('Sec-WebSocket-Version')); return (8 === $passes); } /** * Test the HTTP method. MUST be "GET" * @param string * @return bool */ public function verifyMethod($val) { return ('get' === strtolower($val)); } /** * Test the HTTP version passed. MUST be 1.1 or greater * @param string|int * @return bool */ public function verifyHTTPVersion($val) { return (1.1 <= (double)$val); } /** * @param string * @return bool */ public function verifyRequestURI($val) { if ($val[0] !== '/') { return false; } if (false !== strstr($val, '#')) { return false; } if (!extension_loaded('mbstring')) { return true; } return mb_check_encoding($val, 'US-ASCII'); } /** * @param array $hostHeader * @return bool * @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'] ? */ public function verifyHost(array $hostHeader) { return (1 === count($hostHeader)); } /** * Verify the Upgrade request to WebSockets. * @param array $upgradeHeader MUST equal "websocket" * @return bool */ public function verifyUpgradeRequest(array $upgradeHeader) { return (1 === count($upgradeHeader) && 'websocket' === strtolower($upgradeHeader[0])); } /** * Verify the Connection header * @param array $connectionHeader MUST include "Upgrade" * @return bool */ public function verifyConnection(array $connectionHeader) { foreach ($connectionHeader as $l) { $upgrades = array_filter( array_map('trim', array_map('strtolower', explode(',', $l))), function ($x) { return 'upgrade' === $x; } ); if (count($upgrades) > 0) { return true; } } return false; } /** * This function verifies the nonce is valid (64 big encoded, 16 bytes random string) * @param array $keyHeader * @return bool * @todo The spec says we don't need to base64_decode - can I just check if the length is 24 and not decode? * @todo Check the spec to see what the encoding of the key could be */ public function verifyKey(array $keyHeader) { return (1 === count($keyHeader) && 16 === strlen(base64_decode($keyHeader[0]))); } /** * Verify the version passed matches this RFC * @param string|int $versionHeader MUST equal 13|"13" * @return bool */ public function verifyVersion($versionHeader) { return (1 === count($versionHeader) && static::VERSION === (int)$versionHeader[0]); } /** * @todo Write logic for this method. See section 4.2.1.8 */ public function verifyProtocol($val) { } /** * @todo Write logic for this method. See section 4.2.1.9 */ public function verifyExtensions($val) { } } RFC6455-0.2.4/src/Handshake/ResponseVerifier.php000066400000000000000000000033611327235022000211320ustar00rootroot00000000000000verifyStatus($response->getStatusCode()); $passes += (int)$this->verifyUpgrade($response->getHeader('Upgrade')); $passes += (int)$this->verifyConnection($response->getHeader('Connection')); $passes += (int)$this->verifySecWebSocketAccept( $response->getHeader('Sec-WebSocket-Accept') , $request->getHeader('Sec-WebSocket-Key') ); $passes += (int)$this->verifySubProtocol( $request->getHeader('Sec-WebSocket-Protocol') , $response->getHeader('Sec-WebSocket-Protocol') ); return (5 === $passes); } public function verifyStatus($status) { return ((int)$status === 101); } public function verifyUpgrade(array $upgrade) { return (in_array('websocket', array_map('strtolower', $upgrade))); } public function verifyConnection(array $connection) { return (in_array('upgrade', array_map('strtolower', $connection))); } public function verifySecWebSocketAccept($swa, $key) { return ( 1 === count($swa) && 1 === count($key) && $swa[0] === $this->sign($key[0]) ); } public function sign($key) { return base64_encode(sha1($key . NegotiatorInterface::GUID, true)); } public function verifySubProtocol(array $requestHeader, array $responseHeader) { return 0 === count($responseHeader) || count(array_intersect($responseHeader, $requestHeader)) > 0; } }RFC6455-0.2.4/src/Handshake/ServerNegotiator.php000066400000000000000000000114521327235022000211420ustar00rootroot00000000000000verifier = $requestVerifier; } /** * {@inheritdoc} */ public function isProtocol(RequestInterface $request) { return $this->verifier->verifyVersion($request->getHeader('Sec-WebSocket-Version')); } /** * {@inheritdoc} */ public function getVersionNumber() { return RequestVerifier::VERSION; } /** * {@inheritdoc} */ public function handshake(RequestInterface $request) { if (true !== $this->verifier->verifyMethod($request->getMethod())) { return new Response(405, ['Allow' => 'GET']); } if (true !== $this->verifier->verifyHTTPVersion($request->getProtocolVersion())) { return new Response(505); } if (true !== $this->verifier->verifyRequestURI($request->getUri()->getPath())) { return new Response(400); } if (true !== $this->verifier->verifyHost($request->getHeader('Host'))) { return new Response(400); } $upgradeSuggestion = [ 'Connection' => 'Upgrade', 'Upgrade' => 'websocket', 'Sec-WebSocket-Version' => $this->getVersionNumber() ]; if (count($this->_supportedSubProtocols) > 0) { $upgradeSuggestion['Sec-WebSocket-Protocol'] = implode(', ', $this->_supportedSubProtocols); } if (true !== $this->verifier->verifyUpgradeRequest($request->getHeader('Upgrade'))) { return new Response(426, $upgradeSuggestion, null, '1.1', 'Upgrade header MUST be provided'); } if (true !== $this->verifier->verifyConnection($request->getHeader('Connection'))) { return new Response(400, [], null, '1.1', 'Connection Upgrade MUST be requested'); } if (true !== $this->verifier->verifyKey($request->getHeader('Sec-WebSocket-Key'))) { return new Response(400, [], null, '1.1', 'Invalid Sec-WebSocket-Key'); } if (true !== $this->verifier->verifyVersion($request->getHeader('Sec-WebSocket-Version'))) { return new Response(426, $upgradeSuggestion); } $headers = []; $subProtocols = $request->getHeader('Sec-WebSocket-Protocol'); if (count($subProtocols) > 0 || (count($this->_supportedSubProtocols) > 0 && $this->_strictSubProtocols)) { $subProtocols = array_map('trim', explode(',', implode(',', $subProtocols))); $match = array_reduce($subProtocols, function($accumulator, $protocol) { return $accumulator ?: (isset($this->_supportedSubProtocols[$protocol]) ? $protocol : null); }, null); if ($this->_strictSubProtocols && null === $match) { return new Response(426, $upgradeSuggestion, null, '1.1', 'No Sec-WebSocket-Protocols requested supported'); } if (null !== $match) { $headers['Sec-WebSocket-Protocol'] = $match; } } return new Response(101, array_merge($headers, [ 'Upgrade' => 'websocket' , 'Connection' => 'Upgrade' , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]) , 'X-Powered-By' => 'Ratchet' ])); } /** * Used when doing the handshake to encode the key, verifying client/server are speaking the same language * @param string $key * @return string * @internal */ public function sign($key) { return base64_encode(sha1($key . static::GUID, true)); } /** * @param array $protocols */ function setSupportedSubProtocols(array $protocols) { $this->_supportedSubProtocols = array_flip($protocols); } /** * If enabled and support for a subprotocol has been added handshake * will not upgrade if a match between request and supported subprotocols * @param boolean $enable * @todo Consider extending this interface and moving this there. * The spec does says the server can fail for this reason, but * it is not a requirement. This is an implementation detail. */ function setStrictSubProtocolCheck($enable) { $this->_strictSubProtocols = (boolean)$enable; } } RFC6455-0.2.4/src/Messaging/000077500000000000000000000000001327235022000151535ustar00rootroot00000000000000RFC6455-0.2.4/src/Messaging/CloseFrameChecker.php000066400000000000000000000012031327235022000211650ustar00rootroot00000000000000validCloseCodes = [ Frame::CLOSE_NORMAL, Frame::CLOSE_GOING_AWAY, Frame::CLOSE_PROTOCOL, Frame::CLOSE_BAD_DATA, Frame::CLOSE_BAD_PAYLOAD, Frame::CLOSE_POLICY, Frame::CLOSE_TOO_BIG, Frame::CLOSE_MAND_EXT, Frame::CLOSE_SRV_ERR, ]; } public function __invoke($val) { return ($val >= 3000 && $val <= 4999) || in_array($val, $this->validCloseCodes); } } RFC6455-0.2.4/src/Messaging/DataInterface.php000066400000000000000000000012401327235022000203530ustar00rootroot00000000000000 $ufExceptionFactory */ public function __construct($payload = null, $final = true, $opcode = 1, callable $ufExceptionFactory = null) { $this->ufeg = $ufExceptionFactory ?: static function($msg = '') { return new \UnderflowException($msg); }; if (null === $payload) { return; } $this->defPayLen = strlen($payload); $this->firstByte = ($final ? 128 : 0) + $opcode; $this->secondByte = $this->defPayLen; $this->isCoalesced = true; $ext = ''; if ($this->defPayLen > 65535) { $ext = pack('NN', 0, $this->defPayLen); $this->secondByte = 127; } elseif ($this->defPayLen > 125) { $ext = pack('n', $this->defPayLen); $this->secondByte = 126; } $this->data = chr($this->firstByte) . chr($this->secondByte) . $ext . $payload; $this->bytesRecvd = 2 + strlen($ext) + $this->defPayLen; } /** * {@inheritdoc} */ public function isCoalesced() { if (true === $this->isCoalesced) { return true; } try { $payload_length = $this->getPayloadLength(); $payload_start = $this->getPayloadStartingByte(); } catch (\UnderflowException $e) { return false; } $this->isCoalesced = $this->bytesRecvd >= $payload_length + $payload_start; return $this->isCoalesced; } /** * {@inheritdoc} */ public function addBuffer($buf) { $len = strlen($buf); $this->data .= $buf; $this->bytesRecvd += $len; if ($this->firstByte === -1 && $this->bytesRecvd !== 0) { $this->firstByte = ord($this->data[0]); } if ($this->secondByte === -1 && $this->bytesRecvd >= 2) { $this->secondByte = ord($this->data[1]); } } /** * {@inheritdoc} */ public function isFinal() { if (-1 === $this->firstByte) { throw call_user_func($this->ufeg, 'Not enough bytes received to determine if this is the final frame in message'); } return 128 === ($this->firstByte & 128); } /** * @return boolean * @throws \UnderflowException */ public function getRsv1() { if (-1 === $this->firstByte) { throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit'); } return 64 === ($this->firstByte & 64); } /** * @return boolean * @throws \UnderflowException */ public function getRsv2() { if (-1 === $this->firstByte) { throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit'); } return 32 === ($this->firstByte & 32); } /** * @return boolean * @throws \UnderflowException */ public function getRsv3() { if (-1 === $this->firstByte) { throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit'); } return 16 === ($this->firstByte & 16); } /** * {@inheritdoc} */ public function isMasked() { if (-1 === $this->secondByte) { throw call_user_func($this->ufeg, "Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set"); } return 128 === ($this->secondByte & 128); } /** * {@inheritdoc} */ public function getMaskingKey() { if (!$this->isMasked()) { return ''; } $start = 1 + $this->getNumPayloadBytes(); if ($this->bytesRecvd < $start + static::MASK_LENGTH) { throw call_user_func($this->ufeg, 'Not enough data buffered to calculate the masking key'); } return substr($this->data, $start, static::MASK_LENGTH); } /** * Create a 4 byte masking key * @return string */ public function generateMaskingKey() { $mask = ''; for ($i = 1; $i <= static::MASK_LENGTH; $i++) { $mask .= chr(rand(32, 126)); } return $mask; } /** * Apply a mask to the payload * @param string|null If NULL is passed a masking key will be generated * @throws \OutOfBoundsException * @throws \InvalidArgumentException If there is an issue with the given masking key * @return Frame */ public function maskPayload($maskingKey = null) { if (null === $maskingKey) { $maskingKey = $this->generateMaskingKey(); } if (static::MASK_LENGTH !== strlen($maskingKey)) { throw new \InvalidArgumentException("Masking key must be " . static::MASK_LENGTH ." characters"); } if (extension_loaded('mbstring') && true !== mb_check_encoding($maskingKey, 'US-ASCII')) { throw new \OutOfBoundsException("Masking key MUST be ASCII"); } $this->unMaskPayload(); $this->secondByte = $this->secondByte | 128; $this->data[1] = chr($this->secondByte); $this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0); $this->bytesRecvd += static::MASK_LENGTH; $this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); return $this; } /** * Remove a mask from the payload * @throws \UnderFlowException If the frame is not coalesced * @return Frame */ public function unMaskPayload() { if (!$this->isCoalesced()) { throw call_user_func($this->ufeg, 'Frame must be coalesced before applying mask'); } if (!$this->isMasked()) { return $this; } $maskingKey = $this->getMaskingKey(); $this->secondByte = $this->secondByte & ~128; $this->data[1] = chr($this->secondByte); $this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH); $this->bytesRecvd -= static::MASK_LENGTH; $this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); return $this; } /** * Apply a mask to a string or the payload of the instance * @param string $maskingKey The 4 character masking key to be applied * @param string|null $payload A string to mask or null to use the payload * @throws \UnderflowException If using the payload but enough hasn't been buffered * @return string The masked string */ public function applyMask($maskingKey, $payload = null) { if (null === $payload) { if (!$this->isCoalesced()) { throw call_user_func($this->ufeg, 'Frame must be coalesced to apply a mask'); } $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); } $len = strlen($payload); if (0 === $len) { return ''; } return $payload ^ str_pad('', $len, $maskingKey, STR_PAD_RIGHT); // TODO: Remove this before publish - keeping methods here to compare performance (above is faster but need control against v0.3.3) $applied = ''; for ($i = 0, $len = strlen($payload); $i < $len; $i++) { $applied .= $payload[$i] ^ $maskingKey[$i % static::MASK_LENGTH]; } return $applied; } /** * {@inheritdoc} */ public function getOpcode() { if (-1 === $this->firstByte) { throw call_user_func($this->ufeg, 'Not enough bytes received to determine opcode'); } return ($this->firstByte & ~240); } /** * Gets the decimal value of bits 9 (10th) through 15 inclusive * @return int * @throws \UnderflowException If the buffer doesn't have enough data to determine this */ protected function getFirstPayloadVal() { if (-1 === $this->secondByte) { throw call_user_func($this->ufeg, 'Not enough bytes received'); } return $this->secondByte & 127; } /** * @return int (7|23|71) Number of bits defined for the payload length in the fame * @throws \UnderflowException */ protected function getNumPayloadBits() { if (-1 === $this->secondByte) { throw call_user_func($this->ufeg, 'Not enough bytes received'); } // By default 7 bits are used to describe the payload length // These are bits 9 (10th) through 15 inclusive $bits = 7; // Get the value of those bits $check = $this->getFirstPayloadVal(); // If the value is 126 the 7 bits plus the next 16 are used to describe the payload length if ($check >= 126) { $bits += 16; } // If the value of the initial payload length are is 127 an additional 48 bits are used to describe length // Note: The documentation specifies the length is to be 63 bits, but I think that's a typo and is 64 (16+48) if ($check === 127) { $bits += 48; } return $bits; } /** * This just returns the number of bytes used in the frame to describe the payload length (as opposed to # of bits) * @see getNumPayloadBits */ protected function getNumPayloadBytes() { return (1 + $this->getNumPayloadBits()) / 8; } /** * {@inheritdoc} */ public function getPayloadLength() { if ($this->defPayLen !== -1) { return $this->defPayLen; } $this->defPayLen = $this->getFirstPayloadVal(); if ($this->defPayLen <= 125) { return $this->getPayloadLength(); } $byte_length = $this->getNumPayloadBytes(); if ($this->bytesRecvd < 1 + $byte_length) { $this->defPayLen = -1; throw call_user_func($this->ufeg, 'Not enough data buffered to determine payload length'); } $len = 0; for ($i = 2; $i <= $byte_length; $i++) { $len <<= 8; $len += ord($this->data[$i]); } $this->defPayLen = $len; return $this->getPayloadLength(); } /** * {@inheritdoc} */ public function getPayloadStartingByte() { return 1 + $this->getNumPayloadBytes() + ($this->isMasked() ? static::MASK_LENGTH : 0); } /** * {@inheritdoc} * @todo Consider not checking mask, always returning the payload, masked or not */ public function getPayload() { if (!$this->isCoalesced()) { throw call_user_func($this->ufeg, 'Can not return partial message'); } return $this->__toString(); } /** * Get the raw contents of the frame * @todo This is untested, make sure the substr is right - trying to return the frame w/o the overflow */ public function getContents() { return substr($this->data, 0, $this->getPayloadStartingByte() + $this->getPayloadLength()); } public function __toString() { $payload = (string)substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); if ($this->isMasked()) { $payload = $this->applyMask($this->getMaskingKey(), $payload); } return $payload; } /** * Sometimes clients will concatenate more than one frame over the wire * This method will take the extra bytes off the end and return them * @return string */ public function extractOverflow() { if ($this->isCoalesced()) { $endPoint = $this->getPayloadLength(); $endPoint += $this->getPayloadStartingByte(); if ($this->bytesRecvd > $endPoint) { $overflow = substr($this->data, $endPoint); $this->data = substr($this->data, 0, $endPoint); return $overflow; } } return ''; } } RFC6455-0.2.4/src/Messaging/FrameInterface.php000066400000000000000000000012121327235022000205330ustar00rootroot00000000000000_frames = new \SplDoublyLinkedList; } public function getIterator() { return $this->_frames; } /** * {@inheritdoc} */ public function count() { return count($this->_frames); } /** * {@inheritdoc} */ public function isCoalesced() { if (count($this->_frames) == 0) { return false; } $last = $this->_frames->top(); return ($last->isCoalesced() && $last->isFinal()); } /** * {@inheritdoc} */ public function addFrame(FrameInterface $fragment) { $this->_frames->push($fragment); return $this; } /** * {@inheritdoc} */ public function getOpcode() { if (count($this->_frames) == 0) { throw new \UnderflowException('No frames have been added to this message'); } return $this->_frames->bottom()->getOpcode(); } /** * {@inheritdoc} */ public function getPayloadLength() { $len = 0; foreach ($this->_frames as $frame) { try { $len += $frame->getPayloadLength(); } catch (\UnderflowException $e) { // Not an error, want the current amount buffered } } return $len; } /** * {@inheritdoc} */ public function getPayload() { if (!$this->isCoalesced()) { throw new \UnderflowException('Message has not been put back together yet'); } return $this->__toString(); } /** * {@inheritdoc} */ public function getContents() { if (!$this->isCoalesced()) { throw new \UnderflowException("Message has not been put back together yet"); } $buffer = ''; foreach ($this->_frames as $frame) { $buffer .= $frame->getContents(); } return $buffer; } public function __toString() { $buffer = ''; foreach ($this->_frames as $frame) { $buffer .= $frame->getPayload(); } return $buffer; } /** * @return boolean */ public function isBinary() { if ($this->_frames->isEmpty()) { throw new \UnderflowException('Not enough data has been received to determine if message is binary'); } return Frame::OP_BINARY === $this->_frames->bottom()->getOpcode(); } } RFC6455-0.2.4/src/Messaging/MessageBuffer.php000066400000000000000000000155421327235022000204110ustar00rootroot00000000000000closeFrameChecker = $frameChecker; $this->checkForMask = (bool)$expectMask; $this->exceptionFactory ?: $this->exceptionFactory = function($msg) { return new \UnderflowException($msg); }; $this->onMessage = $onMessage; $this->onControl = $onControl ?: function() {}; } public function onData($data) { while (strlen($data) > 0) { $data = $this->processData($data); } } /** * @param string $data * @return null */ private function processData($data) { $this->messageBuffer ?: $this->messageBuffer = $this->newMessage(); $this->frameBuffer ?: $this->frameBuffer = $this->newFrame(); $this->frameBuffer->addBuffer($data); if (!$this->frameBuffer->isCoalesced()) { return ''; } $onMessage = $this->onMessage; $onControl = $this->onControl; $this->frameBuffer = $this->frameCheck($this->frameBuffer); $overflow = $this->frameBuffer->extractOverflow(); $this->frameBuffer->unMaskPayload(); $opcode = $this->frameBuffer->getOpcode(); if ($opcode > 2) { $onControl($this->frameBuffer); if (Frame::OP_CLOSE === $opcode) { return ''; } } else { $this->messageBuffer->addFrame($this->frameBuffer); } $this->frameBuffer = null; if ($this->messageBuffer->isCoalesced()) { $msgCheck = $this->checkMessage($this->messageBuffer); if (true !== $msgCheck) { $onControl($this->newCloseFrame($msgCheck, 'Ratchet detected an invalid UTF-8 payload')); } else { $onMessage($this->messageBuffer); } $this->messageBuffer = null; } return $overflow; } /** * Check a frame to be added to the current message buffer * @param \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface $frame * @return \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface */ public function frameCheck(FrameInterface $frame) { if (false !== $frame->getRsv1() || false !== $frame->getRsv2() || false !== $frame->getRsv3() ) { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid reserve code'); } if ($this->checkForMask && !$frame->isMasked()) { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an incorrect frame mask'); } $opcode = $frame->getOpcode(); if ($opcode > 2) { if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected a mismatch between final bit and indicated payload length'); } switch ($opcode) { case Frame::OP_CLOSE: $closeCode = 0; $bin = $frame->getPayload(); if (empty($bin)) { return $this->newCloseFrame(Frame::CLOSE_NORMAL); } if (strlen($bin) === 1) { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid close code'); } if (strlen($bin) >= 2) { list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); } $checker = $this->closeFrameChecker; if (!$checker($closeCode)) { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid close code'); } if (!$this->checkUtf8(substr($bin, 2))) { return $this->newCloseFrame(Frame::CLOSE_BAD_PAYLOAD, 'Ratchet detected an invalid UTF-8 payload in the close reason'); } return $frame; break; case Frame::OP_PING: case Frame::OP_PONG: break; default: return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid OP code'); break; } return $frame; } if (Frame::OP_CONTINUE === $frame->getOpcode() && 0 === count($this->messageBuffer)) { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected the first frame of a message was a continue'); } if (count($this->messageBuffer) > 0 && Frame::OP_CONTINUE !== $frame->getOpcode()) { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected invalid OP code when expecting continue frame'); } return $frame; } /** * Determine if a message is valid * @param \Ratchet\RFC6455\Messaging\MessageInterface * @return bool|int true if valid - false if incomplete - int of recommended close code */ public function checkMessage(MessageInterface $message) { if (!$message->isBinary()) { if (!$this->checkUtf8($message->getPayload())) { return Frame::CLOSE_BAD_PAYLOAD; } } return true; } private function checkUtf8($string) { if (extension_loaded('mbstring')) { return mb_check_encoding($string, 'UTF-8'); } return preg_match('//u', $string); } /** * @return \Ratchet\RFC6455\Messaging\MessageInterface */ public function newMessage() { return new Message; } /** * @param string|null $payload * @param bool|null $final * @param int|null $opcode * @return \Ratchet\RFC6455\Messaging\FrameInterface */ public function newFrame($payload = null, $final = null, $opcode = null) { return new Frame($payload, $final, $opcode, $this->exceptionFactory); } public function newCloseFrame($code, $reason = '') { return $this->newFrame(pack('n', $code) . $reason, true, Frame::OP_CLOSE); } } RFC6455-0.2.4/src/Messaging/MessageInterface.php000066400000000000000000000005771327235022000211020ustar00rootroot00000000000000markTestSkipped('Autobahn TestSuite results not found'); } $resultsJson = file_get_contents($fileName); $results = json_decode($resultsJson); $agentName = array_keys(get_object_vars($results))[0]; foreach ($results->$agentName as $name => $result) { if ($result->behavior === "INFORMATIONAL") { continue; } $this->assertTrue(in_array($result->behavior, ["OK", "NON-STRICT"]), "Autobahn test case " . $name . " in " . $fileName); } } public function testAutobahnClientResults() { $this->verifyAutobahnResults(__DIR__ . '/ab/reports/clients/index.json'); } public function testAutobahnServerResults() { $this->verifyAutobahnResults(__DIR__ . '/ab/reports/servers/index.json'); } } RFC6455-0.2.4/tests/ab/000077500000000000000000000000001327235022000141735ustar00rootroot00000000000000RFC6455-0.2.4/tests/ab/clientRunner.php000066400000000000000000000167341327235022000173670ustar00rootroot00000000000000createCached('8.8.8.8', $loop); $factory = new \React\SocketClient\Connector($loop, $dnsResolver); function echoStreamerFactory($conn) { return new \Ratchet\RFC6455\Messaging\MessageBuffer( new \Ratchet\RFC6455\Messaging\CloseFrameChecker, function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($conn) { /** @var Frame $frame */ foreach ($msg as $frame) { $frame->maskPayload(); } $conn->write($msg->getContents()); }, function (\Ratchet\RFC6455\Messaging\FrameInterface $frame) use ($conn) { switch ($frame->getOpcode()) { case Frame::OP_PING: return $conn->write((new Frame($frame->getPayload(), true, Frame::OP_PONG))->maskPayload()->getContents()); break; case Frame::OP_CLOSE: return $conn->end((new Frame($frame->getPayload(), true, Frame::OP_CLOSE))->maskPayload()->getContents()); break; } }, false ); } function getTestCases() { global $factory; global $testServer; $deferred = new Deferred(); $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(); $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001/getCaseCount')); $rawResponse = ""; $response = null; /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageBuffer $ms */ $ms = null; $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); if ($pos) { $data = substr($rawResponse, $pos + 4); $rawResponse = substr($rawResponse, 0, $pos + 4); $response = \GuzzleHttp\Psr7\parse_response($rawResponse); if (!$cn->validateResponse($cnRequest, $response)) { $stream->end(); $deferred->reject(); } else { $ms = new \Ratchet\RFC6455\Messaging\MessageBuffer( new \Ratchet\RFC6455\Messaging\CloseFrameChecker, function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) { $deferred->resolve($msg->getPayload()); $stream->close(); }, null, false ); } } } // feed the message streamer if ($ms) { $ms->onData($data); } }); $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); }); return $deferred->promise(); } function runTest($case) { global $factory; global $testServer; $casePath = "/runCase?case={$case}&agent=" . AGENT; $deferred = new Deferred(); $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath, $case) { $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(); $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $casePath)); $rawResponse = ""; $response = null; $ms = null; $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); if ($pos) { $data = substr($rawResponse, $pos + 4); $rawResponse = substr($rawResponse, 0, $pos + 4); $response = \GuzzleHttp\Psr7\parse_response($rawResponse); if (!$cn->validateResponse($cnRequest, $response)) { $stream->end(); $deferred->reject(); } else { $ms = echoStreamerFactory($stream); } } } // feed the message streamer if ($ms) { $ms->onData($data); } }); $stream->on('close', function () use ($deferred) { $deferred->resolve(); }); $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); }); return $deferred->promise(); } function createReport() { global $factory; global $testServer; $deferred = new Deferred(); $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { $reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true"; $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(); $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $reportPath)); $rawResponse = ""; $response = null; /** @var \Ratchet\RFC6455\Messaging\MessageBuffer $ms */ $ms = null; $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); if ($pos) { $data = substr($rawResponse, $pos + 4); $rawResponse = substr($rawResponse, 0, $pos + 4); $response = \GuzzleHttp\Psr7\parse_response($rawResponse); if (!$cn->validateResponse($cnRequest, $response)) { $stream->end(); $deferred->reject(); } else { $ms = new \Ratchet\RFC6455\Messaging\MessageBuffer( new \Ratchet\RFC6455\Messaging\CloseFrameChecker, function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) { $deferred->resolve($msg->getPayload()); $stream->close(); }, null, false ); } } } // feed the message streamer if ($ms) { $ms->onData($data); } }); $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); }); return $deferred->promise(); } $testPromises = []; getTestCases()->then(function ($count) use ($loop) { $allDeferred = new Deferred(); $runNextCase = function () use (&$i, &$runNextCase, $count, $allDeferred) { $i++; if ($i > $count) { $allDeferred->resolve(); return; } runTest($i)->then($runNextCase); }; $i = 0; $runNextCase(); $allDeferred->promise()->then(function () { createReport(); }); }); $loop->run(); RFC6455-0.2.4/tests/ab/fuzzingclient.json000066400000000000000000000004751327235022000177670ustar00rootroot00000000000000{ "options": { "failByDrop": false } , "outdir": "./reports/servers" , "servers": [{ "agent": "RatchetRFC/0.1.0" , "url": "ws://localhost:9001" , "options": {"version": 18} }] , "cases": ["*"] , "exclude-cases": ["6.4.*", "12.*","13.*"] , "exclude-agent-cases": {} } RFC6455-0.2.4/tests/ab/fuzzingserver.json000066400000000000000000000003321327235022000200070ustar00rootroot00000000000000{ "url": "ws://127.0.0.1:9001" , "options": { "failByDrop": false } , "outdir": "./reports/clients" , "cases": ["*"] , "exclude-cases": ["6.4.*", "12.*", "13.*"] , "exclude-agent-cases": {} } RFC6455-0.2.4/tests/ab/run_ab_tests.sh000066400000000000000000000002601327235022000172150ustar00rootroot00000000000000cd tests/ab wstest -m fuzzingserver -s fuzzingserver.json & sleep 5 php clientRunner.php sleep 2 php startServer.php & sleep 3 wstest -m fuzzingclient -s fuzzingclient.json RFC6455-0.2.4/tests/ab/startServer.php000066400000000000000000000036131327235022000172330ustar00rootroot00000000000000on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $closeFrameChecker, $uException) { $psrRequest = new \GuzzleHttp\Psr7\Request($request->getMethod(), $request->getPath(), $request->getHeaders()); $negotiatorResponse = $negotiator->handshake($psrRequest); $response->writeHead( $negotiatorResponse->getStatusCode(), array_merge( $negotiatorResponse->getHeaders(), ["Content-Length" => "0"] ) ); if ($negotiatorResponse->getStatusCode() !== 101) { $response->end(); return; } $parser = new \Ratchet\RFC6455\Messaging\MessageBuffer($closeFrameChecker, function(MessageInterface $message) use ($response) { $response->write($message->getContents()); }, function(FrameInterface $frame) use ($response, &$parser) { switch ($frame->getOpCode()) { case Frame::OP_CLOSE: $response->end($frame->getContents()); break; case Frame::OP_PING: $response->write($parser->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents()); break; } }, true, function() use ($uException) { return $uException; }); $request->on('data', [$parser, 'onData']); }); $socket->listen(9001, '0.0.0.0'); $loop->run(); RFC6455-0.2.4/tests/bootstrap.php000066400000000000000000000006421327235022000163410ustar00rootroot00000000000000addPsr4('Ratchet\\RFC6455\\Test\\', __DIR__); break; } } RFC6455-0.2.4/tests/unit/000077500000000000000000000000001327235022000145705ustar00rootroot00000000000000RFC6455-0.2.4/tests/unit/Handshake/000077500000000000000000000000001327235022000164565ustar00rootroot00000000000000RFC6455-0.2.4/tests/unit/Handshake/RequestVerifierTest.php000066400000000000000000000123121327235022000231520ustar00rootroot00000000000000_v = new RequestVerifier(); } public static function methodProvider() { return array( array(true, 'GET'), array(true, 'get'), array(true, 'Get'), array(false, 'POST'), array(false, 'DELETE'), array(false, 'PUT'), array(false, 'PATCH') ); } /** * @dataProvider methodProvider */ public function testMethodMustBeGet($result, $in) { $this->assertEquals($result, $this->_v->verifyMethod($in)); } public static function httpVersionProvider() { return array( array(true, 1.1), array(true, '1.1'), array(true, 1.2), array(true, '1.2'), array(true, 2), array(true, '2'), array(true, '2.0'), array(false, '1.0'), array(false, 1), array(false, '0.9'), array(false, ''), array(false, 'hello') ); } /** * @dataProvider httpVersionProvider */ public function testHttpVersionIsAtLeast1Point1($expected, $in) { $this->assertEquals($expected, $this->_v->verifyHTTPVersion($in)); } public static function uRIProvider() { return array( array(true, '/chat'), array(true, '/hello/world?key=val'), array(false, '/chat#bad'), array(false, 'nope'), array(false, '/ ಠ_ಠ '), array(false, '/✖') ); } /** * @dataProvider URIProvider */ public function testRequestUri($expected, $in) { $this->assertEquals($expected, $this->_v->verifyRequestURI($in)); } public static function hostProvider() { return array( array(true, ['server.example.com']), array(false, []) ); } /** * @dataProvider HostProvider */ public function testVerifyHostIsSet($expected, $in) { $this->assertEquals($expected, $this->_v->verifyHost($in)); } public static function upgradeProvider() { return array( array(true, ['websocket']), array(true, ['Websocket']), array(true, ['webSocket']), array(false, []), array(false, ['']) ); } /** * @dataProvider upgradeProvider */ public function testVerifyUpgradeIsWebSocket($expected, $val) { $this->assertEquals($expected, $this->_v->verifyUpgradeRequest($val)); } public static function connectionProvider() { return array( array(true, ['Upgrade']), array(true, ['upgrade']), array(true, ['keep-alive', 'Upgrade']), array(true, ['Upgrade', 'keep-alive']), array(true, ['keep-alive', 'Upgrade', 'something']), // as seen in Firefox 47.0.1 - see https://github.com/ratchetphp/RFC6455/issues/14 array(true, ['keep-alive, Upgrade']), array(true, ['Upgrade, keep-alive']), array(true, ['keep-alive, Upgrade, something']), array(true, ['keep-alive, Upgrade', 'something']), array(false, ['']), array(false, []) ); } /** * @dataProvider connectionProvider */ public function testConnectionHeaderVerification($expected, $val) { $this->assertEquals($expected, $this->_v->verifyConnection($val)); } public static function keyProvider() { return array( array(true, ['hkfa1L7uwN6DCo4IS3iWAw==']), array(true, ['765vVoQpKSGJwPzJIMM2GA==']), array(true, ['AQIDBAUGBwgJCgsMDQ4PEC==']), array(true, ['axa2B/Yz2CdpfQAY2Q5P7w==']), array(false, [0]), array(false, ['Hello World']), array(false, ['1234567890123456']), array(false, ['123456789012345678901234']), array(true, [base64_encode('UTF8allthngs+✓')]), array(true, ['dGhlIHNhbXBsZSBub25jZQ==']), array(false, []), array(false, ['dGhlIHNhbXBsZSBub25jZQ==', 'Some other value']), array(false, ['Some other value', 'dGhlIHNhbXBsZSBub25jZQ==']) ); } /** * @dataProvider keyProvider */ public function testKeyIsBase64Encoded16BitNonce($expected, $val) { $this->assertEquals($expected, $this->_v->verifyKey($val)); } public static function versionProvider() { return array( array(true, [13]), array(true, ['13']), array(false, [12]), array(false, [14]), array(false, ['14']), array(false, ['hi']), array(false, ['']), array(false, []) ); } /** * @dataProvider versionProvider */ public function testVersionEquals13($expected, $in) { $this->assertEquals($expected, $this->_v->verifyVersion($in)); } }RFC6455-0.2.4/tests/unit/Handshake/ResponseVerifierTest.php000066400000000000000000000015641327235022000233270ustar00rootroot00000000000000_v = new ResponseVerifier; } public static function subProtocolsProvider() { return [ [true, ['a'], ['a']] , [true, ['b', 'a'], ['c', 'd', 'a']] , [false, ['a', 'b', 'c'], ['d']] , [true, [], []] , [true, ['a', 'b'], []] ]; } /** * @dataProvider subProtocolsProvider */ public function testVerifySubProtocol($expected, $response, $request) { $this->assertEquals($expected, $this->_v->verifySubProtocol($response, $request)); } } RFC6455-0.2.4/tests/unit/Handshake/ServerNegotiatorTest.php000066400000000000000000000160751327235022000233420ustar00rootroot00000000000000handshake($request); $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(426, $response->getStatusCode()); $this->assertEquals('Upgrade header MUST be provided', $response->getReasonPhrase()); $this->assertEquals('Upgrade', $response->getHeaderLine('Connection')); $this->assertEquals('websocket', $response->getHeaderLine('Upgrade')); $this->assertEquals('13', $response->getHeaderLine('Sec-WebSocket-Version')); } public function testNoConnectionUpgradeRequested() { $negotiator = new ServerNegotiator(new RequestVerifier()); $requestText = 'GET / HTTP/1.1 Host: 127.0.0.1:6789 Connection: keep-alive Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Upgrade-Insecure-Requests: 1 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 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Encoding: gzip, deflate, sdch, br Accept-Language: en-US,en;q=0.8'; $request = \GuzzleHttp\Psr7\parse_request($requestText); $response = $negotiator->handshake($request); $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(400, $response->getStatusCode()); $this->assertEquals('Connection Upgrade MUST be requested', $response->getReasonPhrase()); } public function testInvalidSecWebsocketKey() { $negotiator = new ServerNegotiator(new RequestVerifier()); $requestText = 'GET / HTTP/1.1 Host: 127.0.0.1:6789 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Sec-WebSocket-Key: 12345 Upgrade-Insecure-Requests: 1 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 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Encoding: gzip, deflate, sdch, br Accept-Language: en-US,en;q=0.8'; $request = \GuzzleHttp\Psr7\parse_request($requestText); $response = $negotiator->handshake($request); $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(400, $response->getStatusCode()); $this->assertEquals('Invalid Sec-WebSocket-Key', $response->getReasonPhrase()); } public function testInvalidSecWebsocketVersion() { $negotiator = new ServerNegotiator(new RequestVerifier()); $requestText = 'GET / HTTP/1.1 Host: 127.0.0.1:6789 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Upgrade-Insecure-Requests: 1 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 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Encoding: gzip, deflate, sdch, br Accept-Language: en-US,en;q=0.8'; $request = \GuzzleHttp\Psr7\parse_request($requestText); $response = $negotiator->handshake($request); $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(426, $response->getStatusCode()); $this->assertEquals('Upgrade Required', $response->getReasonPhrase()); $this->assertEquals('Upgrade', $response->getHeaderLine('Connection')); $this->assertEquals('websocket', $response->getHeaderLine('Upgrade')); $this->assertEquals('13', $response->getHeaderLine('Sec-WebSocket-Version')); } public function testBadSubprotocolResponse() { $negotiator = new ServerNegotiator(new RequestVerifier()); $negotiator->setStrictSubProtocolCheck(true); $negotiator->setSupportedSubProtocols([]); $requestText = 'GET / HTTP/1.1 Host: 127.0.0.1:6789 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 Sec-WebSocket-Protocol: someprotocol Upgrade-Insecure-Requests: 1 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 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Encoding: gzip, deflate, sdch, br Accept-Language: en-US,en;q=0.8'; $request = \GuzzleHttp\Psr7\parse_request($requestText); $response = $negotiator->handshake($request); $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(426, $response->getStatusCode()); $this->assertEquals('No Sec-WebSocket-Protocols requested supported', $response->getReasonPhrase()); $this->assertEquals('Upgrade', $response->getHeaderLine('Connection')); $this->assertEquals('websocket', $response->getHeaderLine('Upgrade')); $this->assertEquals('13', $response->getHeaderLine('Sec-WebSocket-Version')); } public function testNonStrictSubprotocolDoesNotIncludeHeaderWhenNoneAgreedOn() { $negotiator = new ServerNegotiator(new RequestVerifier()); $negotiator->setStrictSubProtocolCheck(false); $negotiator->setSupportedSubProtocols(['someproto']); $requestText = 'GET / HTTP/1.1 Host: 127.0.0.1:6789 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 Sec-WebSocket-Protocol: someotherproto Upgrade-Insecure-Requests: 1 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 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Encoding: gzip, deflate, sdch, br Accept-Language: en-US,en;q=0.8'; $request = \GuzzleHttp\Psr7\parse_request($requestText); $response = $negotiator->handshake($request); $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(101, $response->getStatusCode()); $this->assertEquals('Upgrade', $response->getHeaderLine('Connection')); $this->assertEquals('websocket', $response->getHeaderLine('Upgrade')); $this->assertFalse($response->hasHeader('Sec-WebSocket-Protocol')); } }RFC6455-0.2.4/tests/unit/Messaging/000077500000000000000000000000001327235022000165055ustar00rootroot00000000000000RFC6455-0.2.4/tests/unit/Messaging/FrameTest.php000066400000000000000000000450231327235022000211140ustar00rootroot00000000000000_frame = new Frame; } /** * Encode the fake binary string to send over the wire * @param string of 1's and 0's * @return string */ public static function encode($in) { if (strlen($in) > 8) { $out = ''; while (strlen($in) >= 8) { $out .= static::encode(substr($in, 0, 8)); $in = substr($in, 8); } return $out; } return chr(bindec($in)); } /** * This is a data provider * param string The UTF8 message * param string The WebSocket framed message, then base64_encoded */ public static function UnframeMessageProvider() { return array( array('Hello World!', 'gYydAIfa1WXrtvIg0LXvbOP7'), array('!@#$%^&*()-=_+[]{}\|/.,<>`~', 'gZv+h96r38f9j9vZ+IHWrvOWoayF9oX6gtfRqfKXwOeg'), array('ಠ_ಠ', 'gYfnSpu5B/g75gf4Ow=='), array( "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...", 'gf4Amahb14P8M7Kj2S6+4MN7tfHHLLmjzjSvo8IuuvPbe7j1zSn398A+9+/JIa6jzDSwrYh7lu/Ee6Ds2jD34sY/9+3He6fvySL37skwsvCIGL/xwSj34og/ou/Ee7Xs0XX3o+F8uqPcKa7qxjz398d7sObce6fi2y/3sppj9+DAOqXiyy+y8dt7sezae7aj3TW+94gvsvDce7/m2j75rYY=' ) ); } public static function underflowProvider() { return array( array('isFinal', ''), array('getRsv1', ''), array('getRsv2', ''), array('getRsv3', ''), array('getOpcode', ''), array('isMasked', '10000001'), array('getPayloadLength', '10000001'), array('getPayloadLength', '1000000111111110'), array('getMaskingKey', '1000000110000111'), array('getPayload', '100000011000000100011100101010101001100111110100') ); } /** * @dataProvider underflowProvider * * @covers Ratchet\RFC6455\Messaging\Frame::isFinal * @covers Ratchet\RFC6455\Messaging\Frame::getRsv1 * @covers Ratchet\RFC6455\Messaging\Frame::getRsv2 * @covers Ratchet\RFC6455\Messaging\Frame::getRsv3 * @covers Ratchet\RFC6455\Messaging\Frame::getOpcode * @covers Ratchet\RFC6455\Messaging\Frame::isMasked * @covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength * @covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey * @covers Ratchet\RFC6455\Messaging\Frame::getPayload */ public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering($method, $bin) { $this->setExpectedException('\UnderflowException'); if (!empty($bin)) { $this->_frame->addBuffer(static::encode($bin)); } call_user_func(array($this->_frame, $method)); } /** * A data provider for testing the first byte of a WebSocket frame * param bool Given, is the byte indicate this is the final frame * param int Given, what is the expected opcode * param string of 0|1 Each character represents a bit in the byte */ public static function firstByteProvider() { return array( array(false, false, false, true, 8, '00011000'), array(true, false, true, false, 10, '10101010'), array(false, false, false, false, 15, '00001111'), array(true, false, false, false, 1, '10000001'), array(true, true, true, true, 15, '11111111'), array(true, true, false, false, 7, '11000111') ); } /** * @dataProvider firstByteProvider * covers Ratchet\RFC6455\Messaging\Frame::isFinal */ public function testFinCodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { $this->_frame->addBuffer(static::encode($bin)); $this->assertEquals($fin, $this->_frame->isFinal()); } /** * @dataProvider firstByteProvider * covers Ratchet\RFC6455\Messaging\Frame::getRsv1 * covers Ratchet\RFC6455\Messaging\Frame::getRsv2 * covers Ratchet\RFC6455\Messaging\Frame::getRsv3 */ public function testGetRsvFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { $this->_frame->addBuffer(static::encode($bin)); $this->assertEquals($rsv1, $this->_frame->getRsv1()); $this->assertEquals($rsv2, $this->_frame->getRsv2()); $this->assertEquals($rsv3, $this->_frame->getRsv3()); } /** * @dataProvider firstByteProvider * covers Ratchet\RFC6455\Messaging\Frame::getOpcode */ public function testOpcodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { $this->_frame->addBuffer(static::encode($bin)); $this->assertEquals($opcode, $this->_frame->getOpcode()); } /** * @dataProvider UnframeMessageProvider * covers Ratchet\RFC6455\Messaging\Frame::isFinal */ public function testFinCodeFromFullMessage($msg, $encoded) { $this->_frame->addBuffer(base64_decode($encoded)); $this->assertTrue($this->_frame->isFinal()); } /** * @dataProvider UnframeMessageProvider * covers Ratchet\RFC6455\Messaging\Frame::getOpcode */ public function testOpcodeFromFullMessage($msg, $encoded) { $this->_frame->addBuffer(base64_decode($encoded)); $this->assertEquals(1, $this->_frame->getOpcode()); } public static function payloadLengthDescriptionProvider() { return array( array(7, '01110101'), array(7, '01111101'), array(23, '01111110'), array(71, '01111111'), array(7, '00000000'), // Should this throw an exception? Can a payload be empty? array(7, '00000001') ); } /** * @dataProvider payloadLengthDescriptionProvider * covers Ratchet\RFC6455\Messaging\Frame::addBuffer * covers Ratchet\RFC6455\Messaging\Frame::getFirstPayloadVal */ public function testFirstPayloadDesignationValue($bits, $bin) { $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); $this->_frame->addBuffer(static::encode($bin)); $ref = new \ReflectionClass($this->_frame); $cb = $ref->getMethod('getFirstPayloadVal'); $cb->setAccessible(true); $this->assertEquals(bindec($bin), $cb->invoke($this->_frame)); } /** * covers Ratchet\RFC6455\Messaging\Frame::getFirstPayloadVal */ public function testFirstPayloadValUnderflow() { $ref = new \ReflectionClass($this->_frame); $cb = $ref->getMethod('getFirstPayloadVal'); $cb->setAccessible(true); $this->setExpectedException('UnderflowException'); $cb->invoke($this->_frame); } /** * @dataProvider payloadLengthDescriptionProvider * covers Ratchet\RFC6455\Messaging\Frame::getNumPayloadBits */ public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) { $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); $this->_frame->addBuffer(static::encode($bin)); $ref = new \ReflectionClass($this->_frame); $cb = $ref->getMethod('getNumPayloadBits'); $cb->setAccessible(true); $this->assertEquals($expected_bits, $cb->invoke($this->_frame)); } /** * covers Ratchet\RFC6455\Messaging\Frame::getNumPayloadBits */ public function testgetNumPayloadBitsUnderflow() { $ref = new \ReflectionClass($this->_frame); $cb = $ref->getMethod('getNumPayloadBits'); $cb->setAccessible(true); $this->setExpectedException('UnderflowException'); $cb->invoke($this->_frame); } public function secondByteProvider() { return array( array(true, 1, '10000001'), array(false, 1, '00000001'), array(true, 125, $this->_secondByteMaskedSPL) ); } /** * @dataProvider secondByteProvider * covers Ratchet\RFC6455\Messaging\Frame::isMasked */ public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) { $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); $this->_frame->addBuffer(static::encode($bin)); $this->assertEquals($masked, $this->_frame->isMasked()); } /** * @dataProvider UnframeMessageProvider * covers Ratchet\RFC6455\Messaging\Frame::isMasked */ public function testIsMaskedFromFullMessage($msg, $encoded) { $this->_frame->addBuffer(base64_decode($encoded)); $this->assertTrue($this->_frame->isMasked()); } /** * @dataProvider secondByteProvider * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength */ public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) { $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); $this->_frame->addBuffer(static::encode($bin)); $this->assertEquals($payload_length, $this->_frame->getPayloadLength()); } /** * @dataProvider UnframeMessageProvider * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength * @todo Not yet testing when second additional payload length descriptor */ public function testGetPayloadLengthFromFullMessage($msg, $encoded) { $this->_frame->addBuffer(base64_decode($encoded)); $this->assertEquals(strlen($msg), $this->_frame->getPayloadLength()); } public function maskingKeyProvider() { $frame = new Frame; return array( array($frame->generateMaskingKey()), array($frame->generateMaskingKey()), array($frame->generateMaskingKey()) ); } /** * @dataProvider maskingKeyProvider * covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey * @todo I I wrote the dataProvider incorrectly, skipping for now */ public function testGetMaskingKey($mask) { $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); $this->_frame->addBuffer(static::encode($this->_secondByteMaskedSPL)); $this->_frame->addBuffer($mask); $this->assertEquals($mask, $this->_frame->getMaskingKey()); } /** * covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey */ public function testGetMaskingKeyOnUnmaskedPayload() { $frame = new Frame('Hello World!'); $this->assertEquals('', $frame->getMaskingKey()); } /** * @dataProvider UnframeMessageProvider * covers Ratchet\RFC6455\Messaging\Frame::getPayload * @todo Move this test to bottom as it requires all methods of the class */ public function testUnframeFullMessage($unframed, $base_framed) { $this->_frame->addBuffer(base64_decode($base_framed)); $this->assertEquals($unframed, $this->_frame->getPayload()); } public static function messageFragmentProvider() { return array( array(false, '', '', '', '', '') ); } /** * @dataProvider UnframeMessageProvider * covers Ratchet\RFC6455\Messaging\Frame::getPayload */ public function testCheckPiecingTogetherMessage($msg, $encoded) { $framed = base64_decode($encoded); for ($i = 0, $len = strlen($framed);$i < $len; $i++) { $this->_frame->addBuffer(substr($framed, $i, 1)); } $this->assertEquals($msg, $this->_frame->getPayload()); } /** * covers Ratchet\RFC6455\Messaging\Frame::__construct * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength * covers Ratchet\RFC6455\Messaging\Frame::getPayload */ public function testLongCreate() { $len = 65525; $pl = $this->generateRandomString($len); $frame = new Frame($pl, true, Frame::OP_PING); $this->assertTrue($frame->isFinal()); $this->assertEquals(Frame::OP_PING, $frame->getOpcode()); $this->assertFalse($frame->isMasked()); $this->assertEquals($len, $frame->getPayloadLength()); $this->assertEquals($pl, $frame->getPayload()); } /** * covers Ratchet\RFC6455\Messaging\Frame::__construct * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength */ public function testReallyLongCreate() { $len = 65575; $frame = new Frame($this->generateRandomString($len)); $this->assertEquals($len, $frame->getPayloadLength()); } /** * covers Ratchet\RFC6455\Messaging\Frame::__construct * covers Ratchet\RFC6455\Messaging\Frame::extractOverflow */ public function testExtractOverflow() { $string1 = $this->generateRandomString(); $frame1 = new Frame($string1); $string2 = $this->generateRandomString(); $frame2 = new Frame($string2); $cat = new Frame; $cat->addBuffer($frame1->getContents() . $frame2->getContents()); $this->assertEquals($frame1->getContents(), $cat->getContents()); $this->assertEquals($string1, $cat->getPayload()); $uncat = new Frame; $uncat->addBuffer($cat->extractOverflow()); $this->assertEquals($string1, $cat->getPayload()); $this->assertEquals($string2, $uncat->getPayload()); } /** * covers Ratchet\RFC6455\Messaging\Frame::extractOverflow */ public function testEmptyExtractOverflow() { $string = $this->generateRandomString(); $frame = new Frame($string); $this->assertEquals($string, $frame->getPayload()); $this->assertEquals('', $frame->extractOverflow()); $this->assertEquals($string, $frame->getPayload()); } /** * covers Ratchet\RFC6455\Messaging\Frame::getContents */ public function testGetContents() { $msg = 'The quick brown fox jumps over the lazy dog.'; $frame1 = new Frame($msg); $frame2 = new Frame($msg); $frame2->maskPayload(); $this->assertNotEquals($frame1->getContents(), $frame2->getContents()); $this->assertEquals(strlen($frame1->getContents()) + 4, strlen($frame2->getContents())); } /** * covers Ratchet\RFC6455\Messaging\Frame::maskPayload */ public function testMasking() { $msg = 'The quick brown fox jumps over the lazy dog.'; $frame = new Frame($msg); $frame->maskPayload(); $this->assertTrue($frame->isMasked()); $this->assertEquals($msg, $frame->getPayload()); } /** * covers Ratchet\RFC6455\Messaging\Frame::unMaskPayload */ public function testUnMaskPayload() { $string = $this->generateRandomString(); $frame = new Frame($string); $frame->maskPayload()->unMaskPayload(); $this->assertFalse($frame->isMasked()); $this->assertEquals($string, $frame->getPayload()); } /** * covers Ratchet\RFC6455\Messaging\Frame::generateMaskingKey */ public function testGenerateMaskingKey() { $dupe = false; $done = array(); for ($i = 0; $i < 10; $i++) { $new = $this->_frame->generateMaskingKey(); if (in_array($new, $done)) { $dupe = true; } $done[] = $new; } $this->assertEquals(4, strlen($new)); $this->assertFalse($dupe); } /** * covers Ratchet\RFC6455\Messaging\Frame::maskPayload */ public function testGivenMaskIsValid() { $this->setExpectedException('InvalidArgumentException'); $this->_frame->maskPayload('hello world'); } /** * covers Ratchet\RFC6455\Messaging\Frame::maskPayload */ public function testGivenMaskIsValidAscii() { if (!extension_loaded('mbstring')) { $this->markTestSkipped("mbstring required for this test"); return; } $this->setExpectedException('OutOfBoundsException'); $this->_frame->maskPayload('x✖'); } protected function generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) { $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&/()=[]{}'; // ยง $useChars = array(); for($i = 0; $i < $length; $i++) { $useChars[] = $characters[mt_rand(0, strlen($characters) - 1)]; } if($addSpaces === true) { array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' '); } if($addNumbers === true) { array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9)); } shuffle($useChars); $randomString = trim(implode('', $useChars)); $randomString = substr($randomString, 0, $length); return $randomString; } /** * There was a frame boundary issue when the first 3 bytes of a frame with a payload greater than * 126 was added to the frame buffer and then Frame::getPayloadLength was called. It would cause the frame * to set the payload length to 126 and then not recalculate it once the full length information was available. * * This is fixed by setting the defPayLen back to -1 before the underflow exception is thrown. * * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength * covers Ratchet\RFC6455\Messaging\Frame::extractOverflow */ public function testFrameDeliveredOneByteAtATime() { $startHeader = "\x01\x7e\x01\x00"; // header for a text frame of 256 - non-final $framePayload = str_repeat("*", 256); $rawOverflow = "xyz"; $rawFrame = $startHeader . $framePayload . $rawOverflow; $frame = new Frame(); $payloadLen = 256; for ($i = 0; $i < strlen($rawFrame); $i++) { $frame->addBuffer($rawFrame[$i]); try { // payloadLen will $payloadLen = $frame->getPayloadLength(); } catch (\UnderflowException $e) { if ($i > 2) { // we should get an underflow on 0,1,2 $this->fail("Underflow exception when the frame length should be available"); } } if ($payloadLen !== 256) { $this->fail("Payload length of " . $payloadLen . " should have been 256."); } } // make sure the overflow is good $this->assertEquals($rawOverflow, $frame->extractOverflow()); } } RFC6455-0.2.4/tests/unit/Messaging/MessageBufferTest.php000066400000000000000000000021061327235022000225730ustar00rootroot00000000000000getContents(); $data = str_repeat($frameRaw, 1000); $messageCount = 0; $messageBuffer = new MessageBuffer( new CloseFrameChecker(), function (Message $message) use (&$messageCount) { $messageCount++; $this->assertEquals('a', $message->getPayload()); }, null, false ); $messageBuffer->onData($data); $this->assertEquals(1000, $messageCount); } }RFC6455-0.2.4/tests/unit/Messaging/MessageTest.php000066400000000000000000000036151327235022000214470ustar00rootroot00000000000000message = new Message; } public function testNoFrames() { $this->assertFalse($this->message->isCoalesced()); } public function testNoFramesOpCode() { $this->setExpectedException('UnderflowException'); $this->message->getOpCode(); } public function testFragmentationPayload() { $a = 'Hello '; $b = 'World!'; $f1 = new Frame($a, false); $f2 = new Frame($b, true, Frame::OP_CONTINUE); $this->message->addFrame($f1)->addFrame($f2); $this->assertEquals(strlen($a . $b), $this->message->getPayloadLength()); $this->assertEquals($a . $b, $this->message->getPayload()); } public function testUnbufferedFragment() { $this->message->addFrame(new Frame('The quick brow', false)); $this->setExpectedException('UnderflowException'); $this->message->getPayload(); } public function testGetOpCode() { $this->message ->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT)) ->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE)) ->addFrame(new Frame('er the lazy dog', true, Frame::OP_CONTINUE)) ; $this->assertEquals(Frame::OP_TEXT, $this->message->getOpCode()); } public function testGetUnBufferedPayloadLength() { $this->message ->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT)) ->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE)) ; $this->assertEquals(28, $this->message->getPayloadLength()); } }