From 791ebaeb2455f4a361e84714e04771d1702246ab Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 23 May 2015 12:29:05 -0400 Subject: [PATCH] Replace evenement with callback interface Use strict ContextInterface instead of event emitter Keep message/frame within connection, not parser Expect only 1 of specific WebSocket headers Non-UTF-8 server tests passing :-) --- composer.json | 3 +- src/Encoding/NullValidator.php | 2 +- src/Handshake/RequestVerifier.php | 10 +- src/Messaging/Streaming/ContextInterface.php | 29 +++++ src/Messaging/Streaming/MessageStreamer.php | 122 +++++------------- src/Messaging/Validation/MessageValidator.php | 5 +- tests/ab/fuzzingclient.json | 3 +- tests/ab/startServer.php | 104 +++++++++------ 8 files changed, 134 insertions(+), 144 deletions(-) create mode 100644 src/Messaging/Streaming/ContextInterface.php diff --git a/composer.json b/composer.json index 89d2f6d..88b6f56 100644 --- a/composer.json +++ b/composer.json @@ -22,8 +22,7 @@ }, "require": { "php": ">=5.4.2", - "guzzlehttp/psr7": "^1.0", - "evenement/evenement": "^2.0" + "guzzlehttp/psr7": "^1.0" }, "require-dev": { "react/http": "^0.4.1" diff --git a/src/Encoding/NullValidator.php b/src/Encoding/NullValidator.php index 5db53c8..6cc7515 100644 --- a/src/Encoding/NullValidator.php +++ b/src/Encoding/NullValidator.php @@ -3,7 +3,7 @@ namespace Ratchet\RFC6455\Encoding; class NullValidator implements ValidatorInterface { /** - * What value to return when checkEncoding is valled + * What value to return when checkEncoding is valid * @var boolean */ public $validationResponse = true; diff --git a/src/Handshake/RequestVerifier.php b/src/Handshake/RequestVerifier.php index 4118d8d..e3c57de 100644 --- a/src/Handshake/RequestVerifier.php +++ b/src/Handshake/RequestVerifier.php @@ -55,7 +55,7 @@ class RequestVerifier { * @return bool */ public function verifyRequestURI($val) { - if ($val[0] != '/') { + if ($val[0] !== '/') { return false; } @@ -81,11 +81,11 @@ class RequestVerifier { /** * Verify the Upgrade request to WebSockets. - * @param array $upgradeHeader MUST include "websocket" + * @param array $upgradeHeader MUST equal "websocket" * @return bool */ public function verifyUpgradeRequest(array $upgradeHeader) { - return (in_array('websocket', array_map('strtolower', $upgradeHeader))); + return (1 === count($upgradeHeader) && 'websocket' === strtolower($upgradeHeader[0])); } /** @@ -94,7 +94,7 @@ class RequestVerifier { * @return bool */ public function verifyConnection(array $connectionHeader) { - return in_array('upgrade', array_map('strtolower', $connectionHeader)); + return (1 === count($connectionHeader) && 'upgrade' === strtolower(($connectionHeader[0]))); } /** @@ -105,7 +105,7 @@ class RequestVerifier { * @todo Check the spec to see what the encoding of the key could be */ public function verifyKey(array $keyHeader) { - return in_array(16, array_map('strlen', array_map('base64_decode', $keyHeader))); + return (1 === count($keyHeader) && 16 === strlen(base64_decode($keyHeader[0]))); } /** diff --git a/src/Messaging/Streaming/ContextInterface.php b/src/Messaging/Streaming/ContextInterface.php new file mode 100644 index 0000000..5f29ff8 --- /dev/null +++ b/src/Messaging/Streaming/ContextInterface.php @@ -0,0 +1,29 @@ +checkForMask = !$client; - $this->validator = new MessageValidator(new Validator(), $this->checkForMask); + function __construct(ValidatorInterface $encodingValidator, $expectMask = false) { + $this->validator = new MessageValidator($encodingValidator, !$expectMask); } - public function onData($data) { + public function onData($data, ContextInterface $context) { $overflow = ''; - if (!isset($this->currentMessage)) { - $this->currentMessage = $this->newMessage(); - } + $context->getMessage() || $context->setMessage($this->newMessage()); + $context->getFrame() || $context->setFrame($this->newFrame()); - // There is a frame fragment attached to the connection, add to it - if (!isset($this->currentFrame)) { - $this->currentFrame = $this->newFrame(); - } - - $frame = $this->currentFrame; + $frame = $context->getFrame(); $frame->addBuffer($data); if ($frame->isCoalesced()) { $validFrame = $this->validator->validateFrame($frame); - if ($validFrame !== true) { - $this->emit('close', [$validFrame]); + if (true !== $validFrame) { + $context->onClose($validFrame); + return; } $opcode = $frame->getOpcode(); if ($opcode > 2) { - if ($frame->getPayloadLength() > 125) { - // payload only allowed to 125 on control frames ab 2.5 - $this->emit('close', [$frame::CLOSE_PROTOCOL]); - return; - } switch ($opcode) { - case $frame::OP_CLOSE: - $closeCode = 0; - - $bin = $frame->getPayload(); - - if (empty($bin)) { - $this->emit('close', [null]); - return; - } - - if (strlen($bin) >= 2) { - list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); - } - - if (!$frame->isValidCloseCode($closeCode)) { - $this->emit('close', [$frame::CLOSE_PROTOCOL]); - return; - } - - // todo: - //if (!$this->validator->checkEncoding(substr($bin, 2), 'UTF-8')) { - // $this->emit('close', [$frame::CLOSE_BAD_PAYLOAD]); - // return; - //} - - $this->emit('close', [$closeCode]); - return; - break; case $frame::OP_PING: - // this should probably be automatic - //$from->send($this->newFrame($frame->getPayload(), true, $frame::OP_PONG)); - $this->emit('ping', [$frame]); - break; + $context->onPing($frame); + break; case $frame::OP_PONG: - $this->emit('pong', [$frame]); - break; - default: - $this->emit('close', [$frame::CLOSE_PROTOCOL]); - return; - break; + $context->onPong($frame); + break; } $overflow = $frame->extractOverflow(); - - unset($this->currentFrame, $frame, $opcode); + $context->setFrame(null); if (strlen($overflow) > 0) { - $this->onData($overflow); + $this->onData($overflow, $context); } return; @@ -116,32 +54,30 @@ class MessageStreamer implements EventEmitterInterface { $overflow = $frame->extractOverflow(); - $frameAdded = $this->currentMessage->addFrame($this->currentFrame); - if ($frameAdded !== true) { - $this->emit('close', [$frameAdded]); + $frameAdded = $context->getMessage()->addFrame($frame); + if (true !== $frameAdded) { + $context->onClose($frameAdded); } - unset($this->currentFrame); + $context->setFrame(null); } - if ($this->currentMessage->isCoalesced()) { - $msgCheck = $this->validator->checkMessage($this->currentMessage); + if ($context->getMessage()->isCoalesced()) { + $msgCheck = $this->validator->checkMessage($context->getMessage()); if ($msgCheck !== true) { - if ($msgCheck === false) $msgCheck = null; - $this->emit('close', [$msgCheck]); + $context->onClose($msgCheck || null); return; } - $this->emit('message', [$this->currentMessage]); - //$parsed = $from->WebSocket->message->getPayload(); - unset($this->currentMessage); + $context->onMessage($context->getMessage()); + $context->setMessage(null); } if (strlen($overflow) > 0) { - $this->onData($overflow); + $this->onData($overflow, $context); } } /** - * @return Message + * @return \Ratchet\RFC6455\Messaging\Protocol\MessageInterface */ public function newMessage() { return new Message; @@ -151,7 +87,7 @@ class MessageStreamer implements EventEmitterInterface { * @param string|null $payload * @param bool|null $final * @param int|null $opcode - * @return Frame + * @return \Ratchet\RFC6455\Messaging\Protocol\FrameInterface */ public function newFrame($payload = null, $final = null, $opcode = null) { return new Frame($payload, $final, $opcode); diff --git a/src/Messaging/Validation/MessageValidator.php b/src/Messaging/Validation/MessageValidator.php index e47eb2c..b67eb34 100644 --- a/src/Messaging/Validation/MessageValidator.php +++ b/src/Messaging/Validation/MessageValidator.php @@ -53,6 +53,10 @@ class MessageValidator { return true; } + /** + * @param Frame $frame + * @return bool|int Return true if everything is good, an integer close code if not + */ public function validateFrame(Frame $frame) { if (false !== $frame->getRsv1() || false !== $frame->getRsv2() || @@ -79,7 +83,6 @@ class MessageValidator { $bin = $frame->getPayload(); - if (empty($bin)) { return $frame::CLOSE_NORMAL; } diff --git a/tests/ab/fuzzingclient.json b/tests/ab/fuzzingclient.json index 20e1bd2..28cdd4a 100644 --- a/tests/ab/fuzzingclient.json +++ b/tests/ab/fuzzingclient.json @@ -7,8 +7,7 @@ "url": "ws://localhost:9001", "options": {"version": 18}} ], - - "cases": ["1.*"], + "cases": ["*"], "exclude-cases": ["12.*","13.*"], "exclude-agent-cases": {} } diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 7668bec..47643a6 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -1,23 +1,75 @@ _conn = $connectionContext; + } + + public function setFrame(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame = null) { + $this->_frame = $frame; + } + + public function getFrame() { + return $this->_frame; + } + + public function setMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $message = null) { + $this->_message = $message; + } + + public function getMessage() { + return $this->_message; + } + + public function onMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) { + $frame = new Frame($msg->getPayload(), true, $msg[0]->getOpcode()); + $this->_conn->write($frame->getContents()); + } + + public function onPing(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame) { + $pong = new Frame($frame->getPayload(), true, Frame::OP_PONG); + $this->_conn->write($pong->getContents()); + } + + public function onPong(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $msg) { + // TODO: Implement onPong() method. + } + + public function onClose($code = 1000) { + $frame = new Frame( + pack('n', $code), + true, + Frame::OP_CLOSE + ); + + $this->_conn->end($frame->getContents()); + } +} + +$loop = \React\EventLoop\Factory::create(); +$socket = new \React\Socket\Server($loop); $server = new \React\Http\Server($socket); $server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) { - // saving this for later - $conn = $response; + $conn = new ConnectionContext($response); + + $encodingValidator = new \Ratchet\RFC6455\Encoding\Validator; // make the React Request a Psr7 request (not perfect) $psrRequest = new \GuzzleHttp\Psr7\Request($request->getMethod(), $request->getPath(), $request->getHeaders()); - $negotiator = new \Ratchet\RFC6455\Handshake\Negotiator(new \Ratchet\RFC6455\Encoding\NullValidator()); + $negotiator = new \Ratchet\RFC6455\Handshake\Negotiator($encodingValidator); $negotiatorResponse = $negotiator->handshake($psrRequest); @@ -34,39 +86,11 @@ $server->on('request', function (\React\Http\Request $request, \React\Http\Respo return; } - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($encodingValidator); - $ms->on('message', function (Message $msg) use ($conn) { - $opcode = $msg->isBinary() ? Frame::OP_BINARY : Frame::OP_TEXT; - $frame = new Frame($msg->getPayload(), true, $opcode); - $conn->write($frame->getContents()); - }); - - $ms->on('ping', function (Frame $frame) use ($conn) { - $pong = new Frame($frame->getPayload(), true, Frame::OP_PONG); - $conn->write($pong->getContents()); - }); - - $ms->on('pong', function (Frame $frame) { - echo "got PONG...\n"; - }); - - $ms->on('close', function ($code) use ($conn) { - if ($code === null) { - $conn->close(); - return; - } - $frame = new Frame( - pack('n', $code), - true, - Frame::OP_CLOSE - ); - $conn->end($frame->getContents()); - }); - - $request->on('data', function ($data) use ($ms) { - $ms->onData($data); + $request->on('data', function ($data) use ($ms, $conn) { + $ms->onData($data, $conn); }); }); -$socket->listen(9001); -$loop->run(); \ No newline at end of file +$socket->listen(9001, '0.0.0.0'); +$loop->run();