diff --git a/Makefile b/Makefile index 9bd90f0..4ec818f 100644 --- a/Makefile +++ b/Makefile @@ -13,16 +13,19 @@ abtests: ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver-noutf8.php 8003 StreamSelect & ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8004 LibEv & wstest -m testeeserver -w ws://localhost:8000 & + sleep 1 wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-all.json killall php wstest abtest: ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8000 StreamSelect & + sleep 1 wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-quick.json killall php profile: php -d 'xdebug.profiler_enable=1' tests/autobahn/bin/fuzzingserver.php 8000 LibEvent & + sleep 1 wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-profile.json killall php diff --git a/composer.json b/composer.json index c4a7809..79b22b5 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ , "require": { "php": ">=5.3.9" , "react/socket": "^0.3 || ^0.4" - , "guzzle/http": "^3.6" + , "guzzlehttp/psr7": "^1.0" + , "ratchet/rfc6455": "dev-psr7" , "symfony/http-foundation": "^2.2" , "symfony/routing": "^2.2" } diff --git a/src/Ratchet/Http/CloseResponseTrait.php b/src/Ratchet/Http/CloseResponseTrait.php new file mode 100644 index 0000000..abdf5c4 --- /dev/null +++ b/src/Ratchet/Http/CloseResponseTrait.php @@ -0,0 +1,22 @@ + \Ratchet\VERSION + ], $additional_headers)); + + $conn->send(gPsr\str($response)); + $conn->close(); + } +} \ No newline at end of file diff --git a/src/Ratchet/Http/Guzzle/Http/Message/RequestFactory.php b/src/Ratchet/Http/Guzzle/Http/Message/RequestFactory.php deleted file mode 100644 index 8f68e5e..0000000 --- a/src/Ratchet/Http/Guzzle/Http/Message/RequestFactory.php +++ /dev/null @@ -1,34 +0,0 @@ -entityEnclosingRequestClass; - $request = new $c($method, $url, $headers); - $request->setBody(EntityBody::factory($body)); - - return $request; - } -} diff --git a/src/Ratchet/Http/HttpRequestParser.php b/src/Ratchet/Http/HttpRequestParser.php index cbb5bbd..286a3cb 100644 --- a/src/Ratchet/Http/HttpRequestParser.php +++ b/src/Ratchet/Http/HttpRequestParser.php @@ -2,7 +2,7 @@ namespace Ratchet\Http; use Ratchet\MessageInterface; use Ratchet\ConnectionInterface; -use Ratchet\Http\Guzzle\Http\Message\RequestFactory; +use GuzzleHttp\Psr7 as g7; /** * This class receives streaming data from a client request @@ -22,7 +22,7 @@ class HttpRequestParser implements MessageInterface { /** * @param \Ratchet\ConnectionInterface $context * @param string $data Data stream to buffer - * @return \Guzzle\Http\Message\RequestInterface|null + * @return \Psr\Http\Message\RequestInterface * @throws \OverflowException If the message buffer has become too large */ public function onMessage(ConnectionInterface $context, $data) { @@ -37,7 +37,7 @@ class HttpRequestParser implements MessageInterface { } if ($this->isEom($context->httpBuffer)) { - $request = RequestFactory::getInstance()->fromMessage($context->httpBuffer); + $request = g7\parse_request($context->httpBuffer); unset($context->httpBuffer); diff --git a/src/Ratchet/Http/HttpServer.php b/src/Ratchet/Http/HttpServer.php index f1b8f69..bbd8d53 100644 --- a/src/Ratchet/Http/HttpServer.php +++ b/src/Ratchet/Http/HttpServer.php @@ -2,9 +2,10 @@ namespace Ratchet\Http; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; -use Guzzle\Http\Message\Response; class HttpServer implements MessageComponentInterface { + use CloseResponseTrait; + /** * Buffers incoming HTTP requests returning a Guzzle Request when coalesced * @var HttpRequestParser @@ -72,19 +73,4 @@ class HttpServer implements MessageComponentInterface { $this->close($conn, 500); } } - - /** - * Close a connection with an HTTP response - * @param \Ratchet\ConnectionInterface $conn - * @param int $code HTTP status code - * @return null - */ - protected function close(ConnectionInterface $conn, $code = 400) { - $response = new Response($code, array( - 'X-Powered-By' => \Ratchet\VERSION - )); - - $conn->send((string)$response); - $conn->close(); - } } diff --git a/src/Ratchet/Http/HttpServerInterface.php b/src/Ratchet/Http/HttpServerInterface.php index 79b7d55..2c37c49 100644 --- a/src/Ratchet/Http/HttpServerInterface.php +++ b/src/Ratchet/Http/HttpServerInterface.php @@ -2,12 +2,12 @@ namespace Ratchet\Http; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; -use Guzzle\Http\Message\RequestInterface; +use Psr\Http\Message\RequestInterface; interface HttpServerInterface extends MessageComponentInterface { /** * @param \Ratchet\ConnectionInterface $conn - * @param \Guzzle\Http\Message\RequestInterface $request null is default because PHP won't let me overload; don't pass null!!! + * @param \Psr\Http\Message\RequestInterface $request null is default because PHP won't let me overload; don't pass null!!! * @throws \UnexpectedValueException if a RequestInterface is not passed */ public function onOpen(ConnectionInterface $conn, RequestInterface $request = null); diff --git a/src/Ratchet/Http/OriginCheck.php b/src/Ratchet/Http/OriginCheck.php index 640d3c7..2bdc0f7 100644 --- a/src/Ratchet/Http/OriginCheck.php +++ b/src/Ratchet/Http/OriginCheck.php @@ -1,9 +1,8 @@ _component = $component; $this->allowedOrigins += $allowed; } @@ -31,7 +32,7 @@ class OriginCheck implements HttpServerInterface { * {@inheritdoc} */ public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { - $header = (string)$request->getHeader('Origin'); + $header = (string)$request->getHeader('Origin')[0]; $origin = parse_url($header, PHP_URL_HOST) ?: $header; if (!in_array($origin, $this->allowedOrigins)) { @@ -61,19 +62,4 @@ class OriginCheck implements HttpServerInterface { function onError(ConnectionInterface $conn, \Exception $e) { return $this->_component->onError($conn, $e); } - - /** - * Close a connection with an HTTP response - * @param \Ratchet\ConnectionInterface $conn - * @param int $code HTTP status code - * @return null - */ - protected function close(ConnectionInterface $conn, $code = 400) { - $response = new Response($code, array( - 'X-Powered-By' => \Ratchet\VERSION - )); - - $conn->send((string)$response); - $conn->close(); - } -} +} \ No newline at end of file diff --git a/src/Ratchet/Http/Router.php b/src/Ratchet/Http/Router.php index bfc8193..834115a 100644 --- a/src/Ratchet/Http/Router.php +++ b/src/Ratchet/Http/Router.php @@ -1,14 +1,14 @@ getUri(); + $context = $this->_matcher->getContext(); $context->setMethod($request->getMethod()); - $context->setHost($request->getHost()); + $context->setHost($uri->getHost()); try { - $route = $this->_matcher->match($request->getPath()); + $route = $this->_matcher->match($uri->getPath()); } catch (MethodNotAllowedException $nae) { return $this->close($conn, 403); } catch (ResourceNotFoundException $nfe) { @@ -47,17 +49,18 @@ class Router implements HttpServerInterface { throw new \UnexpectedValueException('All routes must implement Ratchet\Http\HttpServerInterface'); } - $parameters = array(); - foreach($route as $key => $value) { - if ((is_string($key)) && ('_' !== substr($key, 0, 1))) { - $parameters[$key] = $value; - } - } - $parameters = array_merge($parameters, $request->getQuery()->getAll()); - - $url = Url::factory($request->getPath()); - $url->setQuery($parameters); - $request->setUrl($url); +// TODO: Apply Symfony default params to request +// $parameters = []; +// foreach($route as $key => $value) { +// if ((is_string($key)) && ('_' !== substr($key, 0, 1))) { +// $parameters[$key] = $value; +// } +// } +// $parameters = array_merge($parameters, gPsr\parse_query($uri->getQuery())); +// +// $url = Url::factory($request->getPath()); +// $url->setQuery($parameters); +// $request->setUrl($url); $conn->controller = $route['_controller']; $conn->controller->onOpen($conn, $request); @@ -87,19 +90,4 @@ class Router implements HttpServerInterface { $conn->controller->onError($conn, $e); } } - - /** - * Close a connection with an HTTP response - * @param \Ratchet\ConnectionInterface $conn - * @param int $code HTTP status code - * @return null - */ - protected function close(ConnectionInterface $conn, $code = 400) { - $response = new Response($code, array( - 'X-Powered-By' => \Ratchet\VERSION - )); - - $conn->send((string)$response); - $conn->close(); - } -} +} \ No newline at end of file diff --git a/src/Ratchet/WebSocket/ConnectionContext.php b/src/Ratchet/WebSocket/ConnectionContext.php new file mode 100644 index 0000000..92b8ef6 --- /dev/null +++ b/src/Ratchet/WebSocket/ConnectionContext.php @@ -0,0 +1,79 @@ +conn = $conn; + $this->component = $component; + } + + public function detach() { + $conn = $this->conn; + + $this->frame = null; + $this->message = null; + + $this->component = null; + $this->conn = null; + + return $conn; + } + + public function onError(\Exception $e) { + $this->component->onError($this->conn, $e); + } + + public function setFrame(FrameInterface $frame = null) { + $this->frame = $frame; + } + + /** + * @return \Ratchet\RFC6455\Messaging\Protocol\FrameInterface + */ + public function getFrame() { + return $this->frame; + } + + public function setMessage(MessageInterface $message = null) { + $this->message = $message; + } + + /** + * @return \Ratchet\RFC6455\Messaging\Protocol\MessageInterface + */ + public function getMessage() { + return $this->message; + } + + public function onMessage(MessageInterface $msg) { + $this->component->onMessage($this->conn, $msg->getPayload()); + } + + public function onPing(FrameInterface $frame) { + $pong = new \Ratchet\RFC6455\Messaging\Protocol\Frame($frame->getPayload(), true, $frame::OP_PONG); + + $this->conn->send($pong); + } + + public function onPong(FrameInterface $frame) { + // TODO: Implement onPong() method. + } + + /** + * @param $code int + */ + public function onClose($code) { + $this->conn->close($code); + } +} \ No newline at end of file diff --git a/src/Ratchet/WebSocket/Encoding/ToggleableValidator.php b/src/Ratchet/WebSocket/Encoding/ToggleableValidator.php deleted file mode 100644 index edf14bc..0000000 --- a/src/Ratchet/WebSocket/Encoding/ToggleableValidator.php +++ /dev/null @@ -1,31 +0,0 @@ -validator = new Validator; - $this->on = (boolean)$on; - } - - /** - * {@inheritdoc} - */ - public function checkEncoding($str, $encoding) { - if (!(boolean)$this->on) { - return true; - } - - return $this->validator->checkEncoding($str, $encoding); - } -} diff --git a/src/Ratchet/WebSocket/Encoding/Validator.php b/src/Ratchet/WebSocket/Encoding/Validator.php deleted file mode 100644 index 3b02230..0000000 --- a/src/Ratchet/WebSocket/Encoding/Validator.php +++ /dev/null @@ -1,93 +0,0 @@ -hasMbString = extension_loaded('mbstring'); - $this->hasIconv = extension_loaded('iconv'); - } - - /** - * @param string $str The value to check the encoding - * @param string $against The type of encoding to check against - * @return bool - */ - public function checkEncoding($str, $against) { - if ('UTF-8' == $against) { - return $this->isUtf8($str); - } - - if ($this->hasMbString) { - return mb_check_encoding($str, $against); - } elseif ($this->hasIconv) { - return ($str == iconv($against, "{$against}//IGNORE", $str)); - } - - return true; - } - - protected function isUtf8($str) { - if ($this->hasMbString) { - if (false === mb_check_encoding($str, 'UTF-8')) { - return false; - } - } elseif ($this->hasIconv) { - if ($str != iconv('UTF-8', 'UTF-8//IGNORE', $str)) { - return false; - } - } - - $state = static::UTF8_ACCEPT; - - for ($i = 0, $len = strlen($str); $i < $len; $i++) { - $state = static::$dfa[256 + ($state << 4) + static::$dfa[ord($str[$i])]]; - - if (static::UTF8_REJECT === $state) { - return false; - } - } - - return true; - } -} diff --git a/src/Ratchet/WebSocket/Encoding/ValidatorInterface.php b/src/Ratchet/WebSocket/Encoding/ValidatorInterface.php deleted file mode 100644 index 374f220..0000000 --- a/src/Ratchet/WebSocket/Encoding/ValidatorInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -getHeader('Sec-WebSocket-Key2')); - } - - /** - * {@inheritdoc} - */ - public function getVersionNumber() { - return 0; - } - - /** - * @param \Guzzle\Http\Message\RequestInterface $request - * @return \Guzzle\Http\Message\Response - * @throws \UnderflowException If there hasn't been enough data received - */ - public function handshake(RequestInterface $request) { - $body = substr($request->getBody(), 0, 8); - if (8 !== strlen($body)) { - throw new \UnderflowException("Not enough data received to issue challenge response"); - } - - $challenge = $this->sign((string)$request->getHeader('Sec-WebSocket-Key1'), (string)$request->getHeader('Sec-WebSocket-Key2'), $body); - - $headers = array( - 'Upgrade' => 'WebSocket' - , 'Connection' => 'Upgrade' - , 'Sec-WebSocket-Origin' => (string)$request->getHeader('Origin') - , 'Sec-WebSocket-Location' => 'ws://' . (string)$request->getHeader('Host') . $request->getPath() - ); - - $response = new Response(101, $headers, $challenge); - $response->setStatus(101, 'WebSocket Protocol Handshake'); - - return $response; - } - - /** - * {@inheritdoc} - */ - public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) { - $upgraded = new Connection($conn); - - if (!isset($upgraded->WebSocket)) { - $upgraded->WebSocket = new \StdClass; - } - - $upgraded->WebSocket->coalescedCallback = $coalescedCallback; - - return $upgraded; - } - - public function onMessage(ConnectionInterface $from, $data) { - $overflow = ''; - - if (!isset($from->WebSocket->frame)) { - $from->WebSocket->frame = $this->newFrame(); - } - - $from->WebSocket->frame->addBuffer($data); - if ($from->WebSocket->frame->isCoalesced()) { - $overflow = $from->WebSocket->frame->extractOverflow(); - - $parsed = $from->WebSocket->frame->getPayload(); - unset($from->WebSocket->frame); - - $from->WebSocket->coalescedCallback->onMessage($from, $parsed); - - unset($from->WebSocket->frame); - } - - if (strlen($overflow) > 0) { - $this->onMessage($from, $overflow); - } - } - - public function newFrame() { - return new Frame; - } - - public function generateKeyNumber($key) { - if (0 === substr_count($key, ' ')) { - return 0; - } - - return preg_replace('[\D]', '', $key) / substr_count($key, ' '); - } - - protected function sign($key1, $key2, $code) { - return md5( - pack('N', $this->generateKeyNumber($key1)) - . pack('N', $this->generateKeyNumber($key2)) - . $code - , true); - } -} diff --git a/src/Ratchet/WebSocket/Version/Hixie76/Connection.php b/src/Ratchet/WebSocket/Version/Hixie76/Connection.php deleted file mode 100644 index e3d0834..0000000 --- a/src/Ratchet/WebSocket/Version/Hixie76/Connection.php +++ /dev/null @@ -1,26 +0,0 @@ -WebSocket->closing) { - $this->getConnection()->send(chr(0) . $msg . chr(255)); - } - - return $this; - } - - public function close() { - if (!$this->WebSocket->closing) { - $this->getConnection()->send(chr(255)); - $this->getConnection()->close(); - - $this->WebSocket->closing = true; - } - } -} diff --git a/src/Ratchet/WebSocket/Version/Hixie76/Frame.php b/src/Ratchet/WebSocket/Version/Hixie76/Frame.php deleted file mode 100644 index 28eb90e..0000000 --- a/src/Ratchet/WebSocket/Version/Hixie76/Frame.php +++ /dev/null @@ -1,86 +0,0 @@ -_data[0] == chr(0) && substr($this->_data, -1) == chr(255)); - } - - /** - * {@inheritdoc} - */ - public function addBuffer($buf) { - $this->_data .= (string)$buf; - } - - /** - * {@inheritdoc} - */ - public function isFinal() { - return true; - } - - /** - * {@inheritdoc} - */ - public function isMasked() { - return false; - } - - /** - * {@inheritdoc} - */ - public function getOpcode() { - return 1; - } - - /** - * {@inheritdoc} - */ - public function getPayloadLength() { - if (!$this->isCoalesced()) { - throw new \UnderflowException('Not enough of the message has been buffered to determine the length of the payload'); - } - - return strlen($this->_data) - 2; - } - - /** - * {@inheritdoc} - */ - public function getMaskingKey() { - return ''; - } - - /** - * {@inheritdoc} - */ - public function getPayload() { - if (!$this->isCoalesced()) { - return new \UnderflowException('Not enough data buffered to read payload'); - } - - return substr($this->_data, 1, strlen($this->_data) - 2); - } - - public function getContents() { - return $this->_data; - } - - public function extractOverflow() { - return ''; - } -} diff --git a/src/Ratchet/WebSocket/Version/HyBi10.php b/src/Ratchet/WebSocket/Version/HyBi10.php deleted file mode 100644 index a53d338..0000000 --- a/src/Ratchet/WebSocket/Version/HyBi10.php +++ /dev/null @@ -1,15 +0,0 @@ -getHeader('Sec-WebSocket-Version'); - - return ($version >= 6 && $version < 13); - } - - public function getVersionNumber() { - return 6; - } -} diff --git a/src/Ratchet/WebSocket/Version/MessageInterface.php b/src/Ratchet/WebSocket/Version/MessageInterface.php deleted file mode 100644 index 476c091..0000000 --- a/src/Ratchet/WebSocket/Version/MessageInterface.php +++ /dev/null @@ -1,15 +0,0 @@ -_verifier = new HandshakeVerifier; - $this->setCloseCodes(); - - if (null === $validator) { - $validator = new Validator; - } - - $this->validator = $validator; - } - - /** - * {@inheritdoc} - */ - public function isProtocol(RequestInterface $request) { - $version = (int)(string)$request->getHeader('Sec-WebSocket-Version'); - - return ($this->getVersionNumber() === $version); - } - - /** - * {@inheritdoc} - */ - public function getVersionNumber() { - return 13; - } - - /** - * {@inheritdoc} - */ - public function handshake(RequestInterface $request) { - if (true !== $this->_verifier->verifyAll($request)) { - return new Response(400); - } - - return new Response(101, array( - 'Upgrade' => 'websocket' - , 'Connection' => 'Upgrade' - , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')) - )); - } - - /** - * @param \Ratchet\ConnectionInterface $conn - * @param \Ratchet\MessageInterface $coalescedCallback - * @return \Ratchet\WebSocket\Version\RFC6455\Connection - */ - public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) { - $upgraded = new Connection($conn); - - if (!isset($upgraded->WebSocket)) { - $upgraded->WebSocket = new \StdClass; - } - - $upgraded->WebSocket->coalescedCallback = $coalescedCallback; - - return $upgraded; - } - - /** - * @param \Ratchet\WebSocket\Version\RFC6455\Connection $from - * @param string $data - */ - public function onMessage(ConnectionInterface $from, $data) { - $overflow = ''; - - if (!isset($from->WebSocket->message)) { - $from->WebSocket->message = $this->newMessage(); - } - - // There is a frame fragment attached to the connection, add to it - if (!isset($from->WebSocket->frame)) { - $from->WebSocket->frame = $this->newFrame(); - } - - $from->WebSocket->frame->addBuffer($data); - if ($from->WebSocket->frame->isCoalesced()) { - $frame = $from->WebSocket->frame; - - if (false !== $frame->getRsv1() || - false !== $frame->getRsv2() || - false !== $frame->getRsv3() - ) { - return $from->close($frame::CLOSE_PROTOCOL); - } - - if (!$frame->isMasked()) { - return $from->close($frame::CLOSE_PROTOCOL); - } - - $opcode = $frame->getOpcode(); - - if ($opcode > 2) { - if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) { - return $from->close($frame::CLOSE_PROTOCOL); - } - - switch ($opcode) { - case $frame::OP_CLOSE: - $closeCode = 0; - - $bin = $frame->getPayload(); - - if (empty($bin)) { - return $from->close(); - } - - if (strlen($bin) >= 2) { - list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); - } - - if (!$this->isValidCloseCode($closeCode)) { - return $from->close($frame::CLOSE_PROTOCOL); - } - - if (!$this->validator->checkEncoding(substr($bin, 2), 'UTF-8')) { - return $from->close($frame::CLOSE_BAD_PAYLOAD); - } - - return $from->close($frame); - break; - case $frame::OP_PING: - $from->send($this->newFrame($frame->getPayload(), true, $frame::OP_PONG)); - break; - case $frame::OP_PONG: - break; - default: - return $from->close($frame::CLOSE_PROTOCOL); - break; - } - - $overflow = $from->WebSocket->frame->extractOverflow(); - - unset($from->WebSocket->frame, $frame, $opcode); - - if (strlen($overflow) > 0) { - $this->onMessage($from, $overflow); - } - - return; - } - - $overflow = $from->WebSocket->frame->extractOverflow(); - - if ($frame::OP_CONTINUE == $frame->getOpcode() && 0 == count($from->WebSocket->message)) { - return $from->close($frame::CLOSE_PROTOCOL); - } - - if (count($from->WebSocket->message) > 0 && $frame::OP_CONTINUE != $frame->getOpcode()) { - return $from->close($frame::CLOSE_PROTOCOL); - } - - $from->WebSocket->message->addFrame($from->WebSocket->frame); - unset($from->WebSocket->frame); - } - - if ($from->WebSocket->message->isCoalesced()) { - $parsed = $from->WebSocket->message->getPayload(); - unset($from->WebSocket->message); - - if (!$this->validator->checkEncoding($parsed, 'UTF-8')) { - return $from->close(Frame::CLOSE_BAD_PAYLOAD); - } - - $from->WebSocket->coalescedCallback->onMessage($from, $parsed); - } - - if (strlen($overflow) > 0) { - $this->onMessage($from, $overflow); - } - } - - /** - * @return RFC6455\Message - */ - public function newMessage() { - return new Message; - } - - /** - * @param string|null $payload - * @param bool|null $final - * @param int|null $opcode - * @return RFC6455\Frame - */ - public function newFrame($payload = null, $final = null, $opcode = null) { - return new Frame($payload, $final, $opcode); - } - - /** - * 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)); - } - - /** - * Determine if a close code is valid - * @param int|string - * @return bool - */ - public function isValidCloseCode($val) { - if (array_key_exists($val, $this->closeCodes)) { - return true; - } - - if ($val >= 3000 && $val <= 4999) { - return true; - } - - return false; - } - - /** - * Creates a private lookup of valid, private close codes - */ - protected function setCloseCodes() { - $this->closeCodes[Frame::CLOSE_NORMAL] = true; - $this->closeCodes[Frame::CLOSE_GOING_AWAY] = true; - $this->closeCodes[Frame::CLOSE_PROTOCOL] = true; - $this->closeCodes[Frame::CLOSE_BAD_DATA] = true; - //$this->closeCodes[Frame::CLOSE_NO_STATUS] = true; - //$this->closeCodes[Frame::CLOSE_ABNORMAL] = true; - $this->closeCodes[Frame::CLOSE_BAD_PAYLOAD] = true; - $this->closeCodes[Frame::CLOSE_POLICY] = true; - $this->closeCodes[Frame::CLOSE_TOO_BIG] = true; - $this->closeCodes[Frame::CLOSE_MAND_EXT] = true; - $this->closeCodes[Frame::CLOSE_SRV_ERR] = true; - //$this->closeCodes[Frame::CLOSE_TLS] = true; - } -} diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php deleted file mode 100644 index 77d1258..0000000 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ /dev/null @@ -1,451 +0,0 @@ -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 new \UnderflowException('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 new \UnderflowException('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 new \UnderflowException('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 new \UnderflowException('Not enough bytes received to determine reserved bit'); - } - - return 16 == ($this->firstByte & 16); - } - - /** - * {@inheritdoc} - */ - public function isMasked() { - if (-1 === $this->secondByte) { - throw new \UnderflowException("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 new \UnderflowException('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 new \UnderflowException('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 new \UnderflowException('Frame must be coalesced to apply a mask'); - } - - $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); - } - - $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 new \UnderflowException('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 new \UnderflowException('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 new \UnderflowException('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 new \UnderflowException('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 new \UnderflowException('Can not return partial message'); - } - - $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); - - if ($this->isMasked()) { - $payload = $this->applyMask($this->getMaskingKey(), $payload); - } - - return $payload; - } - - /** - * 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()); - } - - /** - * Sometimes clients will concatenate more than one frame over the wire - * This method will take the extra bytes off the end and return them - * @todo Consider returning new Frame - * @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 ''; - } -} diff --git a/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php b/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php deleted file mode 100644 index fd783f6..0000000 --- a/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php +++ /dev/null @@ -1,137 +0,0 @@ -verifyMethod($request->getMethod()); - $passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion()); - $passes += (int)$this->verifyRequestURI($request->getPath()); - $passes += (int)$this->verifyHost((string)$request->getHeader('Host')); - $passes += (int)$this->verifyUpgradeRequest((string)$request->getHeader('Upgrade')); - $passes += (int)$this->verifyConnection((string)$request->getHeader('Connection')); - $passes += (int)$this->verifyKey((string)$request->getHeader('Sec-WebSocket-Key')); - //$passes += (int)$this->verifyVersion($headers['Sec-WebSocket-Version']); // Temporarily breaking functionality - - return (7 === $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 string|null - * @return bool - * @todo Find out if I can find the master socket, ensure the port is attached to header if not 80 or 443 - not sure if this is possible, as I tried to hide it - * @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($val) { - return (null !== $val); - } - - /** - * Verify the Upgrade request to WebSockets. - * @param string $val MUST equal "websocket" - * @return bool - */ - public function verifyUpgradeRequest($val) { - return ('websocket' === strtolower($val)); - } - - /** - * Verify the Connection header - * @param string $val MUST equal "Upgrade" - * @return bool - */ - public function verifyConnection($val) { - $val = strtolower($val); - - if ('upgrade' === $val) { - return true; - } - - $vals = explode(',', str_replace(', ', ',', $val)); - - return (false !== array_search('upgrade', $vals)); - } - - /** - * This function verifies the nonce is valid (64 big encoded, 16 bytes random string) - * @param string|null - * @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($val) { - return (16 === strlen(base64_decode((string)$val))); - } - - /** - * Verify the version passed matches this RFC - * @param string|int MUST equal 13|"13" - * @return bool - * @todo Ran in to a problem here...I'm having HyBi use the RFC files, this breaks it! oops - */ - public function verifyVersion($val) { - return (13 === (int)$val); - } - - /** - * @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) { - } -} diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Message.php b/src/Ratchet/WebSocket/Version/RFC6455/Message.php deleted file mode 100644 index a839f2d..0000000 --- a/src/Ratchet/WebSocket/Version/RFC6455/Message.php +++ /dev/null @@ -1,107 +0,0 @@ -_frames = new \SplDoublyLinkedList; - } - - /** - * {@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} - * @todo Also, I should perhaps check the type...control frames (ping/pong/close) are not to be considered part of a message - */ - 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'); - } - - $buffer = ''; - - foreach ($this->_frames as $frame) { - $buffer .= $frame->getPayload(); - } - - return $buffer; - } - - /** - * {@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; - } -} diff --git a/src/Ratchet/WebSocket/Version/VersionInterface.php b/src/Ratchet/WebSocket/Version/VersionInterface.php deleted file mode 100644 index 5bbe534..0000000 --- a/src/Ratchet/WebSocket/Version/VersionInterface.php +++ /dev/null @@ -1,57 +0,0 @@ -versions as $version) { - if ($version->isProtocol($request)) { - return $version; - } - } - - throw new \InvalidArgumentException("Version not found"); - } - - /** - * @param \Guzzle\Http\Message\RequestInterface - * @return bool - */ - public function isVersionEnabled(RequestInterface $request) { - foreach ($this->versions as $version) { - if ($version->isProtocol($request)) { - return true; - } - } - - return false; - } - - /** - * Enable support for a specific version of the WebSocket protocol - * @param \Ratchet\WebSocket\Version\VersionInterface $version - * @return VersionManager - */ - public function enableVersion(VersionInterface $version) { - $this->versions[$version->getVersionNumber()] = $version; - - if (empty($this->versionString)) { - $this->versionString = (string)$version->getVersionNumber(); - } else { - $this->versionString .= ", {$version->getVersionNumber()}"; - } - - return $this; - } - - /** - * Disable support for a specific WebSocket protocol version - * @param int $versionId The version ID to un-support - * @return VersionManager - */ - public function disableVersion($versionId) { - unset($this->versions[$versionId]); - - $this->versionString = implode(',', array_keys($this->versions)); - - return $this; - } - - /** - * Get a string of version numbers supported (comma delimited) - * @return string - */ - public function getSupportedVersionString() { - return $this->versionString; - } -} diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Connection.php b/src/Ratchet/WebSocket/WsConnection.php similarity index 82% rename from src/Ratchet/WebSocket/Version/RFC6455/Connection.php rename to src/Ratchet/WebSocket/WsConnection.php index a17e382..a4adce2 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Connection.php +++ b/src/Ratchet/WebSocket/WsConnection.php @@ -1,13 +1,14 @@ versioner = new VersionManager; - $this->validator = new ToggleableValidator; - - $this->versioner - ->enableVersion(new Version\RFC6455($this->validator)) - ->enableVersion(new Version\HyBi10($this->validator)) - ->enableVersion(new Version\Hixie76) - ; - $this->component = $component; $this->connections = new \SplObjectStorage; + + $encodingValidator = new \Ratchet\RFC6455\Encoding\Validator; + $this->handshakeNegotiator = new \Ratchet\RFC6455\Handshake\Negotiator($encodingValidator); + $this->messageStreamer = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($encodingValidator); } /** @@ -76,12 +62,33 @@ class WsServer implements HttpServerInterface { throw new \UnexpectedValueException('$request can not be null'); } - $conn->WebSocket = new \StdClass; - $conn->WebSocket->request = $request; - $conn->WebSocket->established = false; - $conn->WebSocket->closing = false; + $conn->httpRequest = $request; // This will replace ->WebSocket->request - $this->attemptUpgrade($conn); + $conn->WebSocket = new \StdClass; + $conn->WebSocket->closing = false; + $conn->WebSocket->request = $request; // deprecated + + $response = $this->handshakeNegotiator->handshake($request)->withHeader('X-Powered-By', \Ratchet\VERSION); + +// Probably moved to RFC lib +// $subHeader = $conn->WebSocket->request->getHeader('Sec-WebSocket-Protocol'); +// if (count($subHeader) > 0) { +// if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($subHeader))) { +// $response = $response->withHeader('Sec-WebSocket-Protocol', $agreedSubProtocols); +// } +// } + + $conn->send(gPsr\str($response)); + + if (101 != $response->getStatusCode()) { + return $conn->close(); + } + + $wsConn = new WsConnection($conn); + $context = new ConnectionContext($wsConn, $this->component); + $this->connections->attach($conn, $context); + + return $this->component->onOpen($wsConn); } /** @@ -92,50 +99,9 @@ class WsServer implements HttpServerInterface { return; } - if (true === $from->WebSocket->established) { - return $from->WebSocket->version->onMessage($this->connections[$from], $msg); - } + $context = $this->connections[$from]; - $this->attemptUpgrade($from, $msg); - } - - protected function attemptUpgrade(ConnectionInterface $conn, $data = '') { - if ('' !== $data) { - $conn->WebSocket->request->getBody()->write($data); - } else { - if (!$this->versioner->isVersionEnabled($conn->WebSocket->request)) { - return $this->close($conn); - } - - $conn->WebSocket->version = $this->versioner->getVersion($conn->WebSocket->request); - } - - try { - $response = $conn->WebSocket->version->handshake($conn->WebSocket->request); - } catch (\UnderflowException $e) { - return; - } - - if (null !== ($subHeader = $conn->WebSocket->request->getHeader('Sec-WebSocket-Protocol'))) { - if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($subHeader->normalize()))) { - $response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols); - } - } - - $response->setHeader('X-Powered-By', \Ratchet\VERSION); - $conn->send((string)$response); - - if (101 != $response->getStatusCode()) { - return $conn->close(); - } - - $upgraded = $conn->WebSocket->version->upgradeConnection($conn, $this->component); - - $this->connections->attach($conn, $upgraded); - - $upgraded->WebSocket->established = true; - - return $this->component->onOpen($upgraded); + $this->messageStreamer->onData($msg, $context); } /** @@ -146,7 +112,9 @@ class WsServer implements HttpServerInterface { $decor = $this->connections[$conn]; $this->connections->detach($conn); - $this->component->onClose($decor); + $conn = $decor->detach(); + + $this->component->onClose($conn); } } @@ -154,31 +122,21 @@ class WsServer implements HttpServerInterface { * {@inheritdoc} */ public function onError(ConnectionInterface $conn, \Exception $e) { - if ($conn->WebSocket->established && $this->connections->contains($conn)) { - $this->component->onError($this->connections[$conn], $e); + if ($this->connections->contains($conn)) { + $context = $this->connections[$conn]; + $context->onError($e); } else { $conn->close(); } } - /** - * Disable a specific version of the WebSocket protocol - * @param int $versionId Version ID to disable - * @return WsServer - */ - public function disableVersion($versionId) { - $this->versioner->disableVersion($versionId); - - return $this; - } - /** * Toggle weather to check encoding of incoming messages * @param bool * @return WsServer */ public function setEncodingChecks($opt) { - $this->validator->on = (boolean)$opt; +// $this->validator->on = (boolean)$opt; return $this; } @@ -214,19 +172,4 @@ class WsServer implements HttpServerInterface { return ''; } - - /** - * Close a connection with an HTTP response - * @param \Ratchet\ConnectionInterface $conn - * @param int $code HTTP status code - */ - protected function close(ConnectionInterface $conn, $code = 400) { - $response = new Response($code, array( - 'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString() - , 'X-Powered-By' => \Ratchet\VERSION - )); - - $conn->send((string)$response); - $conn->close(); - } -} +} \ No newline at end of file