From 8884b40f0045a0c1edc001f4722eeea1e639ff7b Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 30 Aug 2014 08:42:57 -0400 Subject: [PATCH 01/56] Update namespace to RFC6455, PHP 5.4 --- composer.json | 4 ++-- src/Encoding/ToggleableValidator.php | 2 +- src/Encoding/Validator.php | 2 +- src/Encoding/ValidatorInterface.php | 2 +- src/Version/DataInterface.php | 2 +- src/Version/FrameInterface.php | 2 +- src/Version/MessageInterface.php | 2 +- src/Version/RFC6455.php | 2 +- src/Version/RFC6455/Connection.php | 4 ++-- src/Version/RFC6455/Frame.php | 4 ++-- src/Version/RFC6455/HandshakeVerifier.php | 2 +- src/Version/RFC6455/Message.php | 6 +++--- src/Version/VersionInterface.php | 2 +- 13 files changed, 18 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index e8c64c4..9a352fe 100644 --- a/composer.json +++ b/composer.json @@ -19,11 +19,11 @@ } , "autoload": { "psr-4": { - "Ratchet\\WebSocket\\": "src" + "Ratchet\\RFC6455\\": "src" } } , "require": { - "php": ">=5.3.9" + "php": ">=5.4.2" , "guzzle/http": "~3.6" } } diff --git a/src/Encoding/ToggleableValidator.php b/src/Encoding/ToggleableValidator.php index edf14bc..46152e4 100644 --- a/src/Encoding/ToggleableValidator.php +++ b/src/Encoding/ToggleableValidator.php @@ -1,5 +1,5 @@ Date: Mon, 8 Sep 2014 22:05:03 -0400 Subject: [PATCH 02/56] Separating protocol parsing, message handling --- src/{Version/RFC6455 => }/Connection.php | 1 + .../RFC6455.php => Handshake/Negotiator.php} | 52 ++++++++++--------- .../NegotiatorInterface.php} | 36 ++++++------- .../RequestVerifier.php} | 4 +- .../Protocol}/DataInterface.php | 2 +- .../RFC6455 => Messaging/Protocol}/Frame.php | 3 +- .../Protocol}/FrameInterface.php | 2 +- .../Protocol}/Message.php | 4 +- .../Protocol}/MessageInterface.php | 2 +- 9 files changed, 50 insertions(+), 56 deletions(-) rename src/{Version/RFC6455 => }/Connection.php (98%) rename src/{Version/RFC6455.php => Handshake/Negotiator.php} (89%) rename src/{Version/VersionInterface.php => Handshake/NegotiatorInterface.php} (59%) rename src/{Version/RFC6455/HandshakeVerifier.php => Handshake/RequestVerifier.php} (98%) rename src/{Version => Messaging/Protocol}/DataInterface.php (91%) rename src/{Version/RFC6455 => Messaging/Protocol}/Frame.php (99%) rename src/{Version => Messaging/Protocol}/FrameInterface.php (93%) rename src/{Version/RFC6455 => Messaging/Protocol}/Message.php (94%) rename src/{Version => Messaging/Protocol}/MessageInterface.php (84%) diff --git a/src/Version/RFC6455/Connection.php b/src/Connection.php similarity index 98% rename from src/Version/RFC6455/Connection.php rename to src/Connection.php index be6d63f..fd404f3 100644 --- a/src/Version/RFC6455/Connection.php +++ b/src/Connection.php @@ -4,6 +4,7 @@ use Ratchet\AbstractConnectionDecorator; use Ratchet\RFC6455\Version\DataInterface; /** + * @deprecated * {@inheritdoc} * @property \StdClass $WebSocket */ diff --git a/src/Version/RFC6455.php b/src/Handshake/Negotiator.php similarity index 89% rename from src/Version/RFC6455.php rename to src/Handshake/Negotiator.php index e70e209..ccddb07 100644 --- a/src/Version/RFC6455.php +++ b/src/Handshake/Negotiator.php @@ -1,48 +1,43 @@ verifier = new RequestVerifier; - public function __construct(ValidatorInterface $validator = null) { - $this->_verifier = new HandshakeVerifier; $this->setCloseCodes(); - if (null === $validator) { - $validator = new Validator; - } - $this->validator = $validator; } @@ -66,7 +61,7 @@ class RFC6455 implements VersionInterface { * {@inheritdoc} */ public function handshake(RequestInterface $request) { - if (true !== $this->_verifier->verifyAll($request)) { + if (true !== $this->verifier->verifyAll($request)) { return new Response(400); } @@ -78,6 +73,7 @@ class RFC6455 implements VersionInterface { } /** + * @deprecated * @param \Ratchet\ConnectionInterface $conn * @param \Ratchet\MessageInterface $coalescedCallback * @return \Ratchet\WebSocket\Version\RFC6455\Connection @@ -95,6 +91,7 @@ class RFC6455 implements VersionInterface { } /** + * @deprecated - The logic belons somewhere else * @param \Ratchet\WebSocket\Version\RFC6455\Connection $from * @param string $data */ @@ -121,6 +118,7 @@ class RFC6455 implements VersionInterface { return $from->close($frame::CLOSE_PROTOCOL); } + // This is server-side specific logic if (!$frame->isMasked()) { return $from->close($frame::CLOSE_PROTOCOL); } @@ -208,6 +206,7 @@ class RFC6455 implements VersionInterface { } /** + * @deprecated * @return RFC6455\Message */ public function newMessage() { @@ -215,6 +214,7 @@ class RFC6455 implements VersionInterface { } /** + * @deprecated * @param string|null $payload * @param bool|null $final * @param int|null $opcode @@ -235,6 +235,7 @@ class RFC6455 implements VersionInterface { } /** + * @deprecated * Determine if a close code is valid * @param int|string * @return bool @@ -252,6 +253,7 @@ class RFC6455 implements VersionInterface { } /** + * @deprecated * Creates a private lookup of valid, private close codes */ protected function setCloseCodes() { diff --git a/src/Version/VersionInterface.php b/src/Handshake/NegotiatorInterface.php similarity index 59% rename from src/Version/VersionInterface.php rename to src/Handshake/NegotiatorInterface.php index 4fdb9f4..624772d 100644 --- a/src/Version/VersionInterface.php +++ b/src/Handshake/NegotiatorInterface.php @@ -1,13 +1,16 @@ Date: Wed, 10 Sep 2014 21:21:00 -0400 Subject: [PATCH 03/56] Null Validator --- src/Encoding/NullValidator.php | 14 ++++++++++++++ src/Encoding/ToggleableValidator.php | 3 +++ src/Encoding/ValidatorInterface.php | 3 +++ 3 files changed, 20 insertions(+) create mode 100644 src/Encoding/NullValidator.php diff --git a/src/Encoding/NullValidator.php b/src/Encoding/NullValidator.php new file mode 100644 index 0000000..5db53c8 --- /dev/null +++ b/src/Encoding/NullValidator.php @@ -0,0 +1,14 @@ +validationResponse; + } +} \ No newline at end of file diff --git a/src/Encoding/ToggleableValidator.php b/src/Encoding/ToggleableValidator.php index 46152e4..3178bbc 100644 --- a/src/Encoding/ToggleableValidator.php +++ b/src/Encoding/ToggleableValidator.php @@ -1,6 +1,9 @@ Date: Sat, 29 Nov 2014 13:08:04 -0500 Subject: [PATCH 04/56] Separate negotiation and validation --- src/Handshake/Negotiator.php | 32 +----- src/Handshake/NegotiatorInterface.php | 2 - src/Handshake/RequestVerifier.php | 9 +- src/Messaging/Validation/MessageValidator.php | 105 ++++++++++++++++++ 4 files changed, 111 insertions(+), 37 deletions(-) create mode 100644 src/Messaging/Validation/MessageValidator.php diff --git a/src/Handshake/Negotiator.php b/src/Handshake/Negotiator.php index ccddb07..69a5fe7 100644 --- a/src/Handshake/Negotiator.php +++ b/src/Handshake/Negotiator.php @@ -54,7 +54,7 @@ class Negotiator implements NegotiatorInterface { * {@inheritdoc} */ public function getVersionNumber() { - return 13; + return RequestVerifier::VERSION; } /** @@ -111,25 +111,7 @@ class Negotiator implements NegotiatorInterface { 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); - } - - // This is server-side specific logic - 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; @@ -177,14 +159,6 @@ class Negotiator implements NegotiatorInterface { $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); } @@ -193,10 +167,6 @@ class Negotiator implements NegotiatorInterface { $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); } diff --git a/src/Handshake/NegotiatorInterface.php b/src/Handshake/NegotiatorInterface.php index 624772d..718d760 100644 --- a/src/Handshake/NegotiatorInterface.php +++ b/src/Handshake/NegotiatorInterface.php @@ -15,7 +15,6 @@ interface NegotiatorInterface { * Given an HTTP header, determine if this version should handle the protocol * @param \Guzzle\Http\Message\RequestInterface $request * @return bool - * @throws \UnderflowException If the protocol thinks the headers are still fragmented */ function isProtocol(RequestInterface $request); @@ -29,7 +28,6 @@ interface NegotiatorInterface { * Perform the handshake and return the response headers * @param \Guzzle\Http\Message\RequestInterface $request * @return \Guzzle\Http\Message\Response - * @throws \UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version) */ function handshake(RequestInterface $request); diff --git a/src/Handshake/RequestVerifier.php b/src/Handshake/RequestVerifier.php index 1e9e2a6..e9f8d6d 100644 --- a/src/Handshake/RequestVerifier.php +++ b/src/Handshake/RequestVerifier.php @@ -8,6 +8,8 @@ use Guzzle\Http\Message\RequestInterface; * @todo Currently just returning invalid - should consider returning appropriate HTTP status code error #s */ class RequestVerifier { + const VERSION = 13; + /** * Given an array of the headers this method will run through all verification methods * @param \Guzzle\Http\Message\RequestInterface $request @@ -23,9 +25,9 @@ class RequestVerifier { $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 + $passes += (int)$this->verifyVersion($headers['Sec-WebSocket-Version']); - return (7 === $passes); + return (8 === $passes); } /** @@ -117,10 +119,9 @@ class RequestVerifier { * 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); + return (static::VERSION === (int)$val); } /** diff --git a/src/Messaging/Validation/MessageValidator.php b/src/Messaging/Validation/MessageValidator.php new file mode 100644 index 0000000..b15b726 --- /dev/null +++ b/src/Messaging/Validation/MessageValidator.php @@ -0,0 +1,105 @@ +validator = $validator; + } + + /** + * Determine if a message is valid + * @param \Ratchet\RFC6455\Messaging\Protocol\MessageInterface + * @return bool|int true if valid - false if incomplete - int of recomended close code + */ + public function checkMessage(MessageInterface $message) { + // Need a progressive and complete check...this is only satisfying complete + if (!$message->isCoalesced()) { + return false; + } + + $frame = $message[0]; + + $frameCheck = $this->checkFrame($frame); + if (true !== $frameCheck) { + return $frameCheck; + } + + // This seems incorrect - how could a frame exist with message count being 0? + if ($frame::OP_CONTINUE === $frame->getOpcode() && 0 === count($message)) { + return $frame::CLOSE_PROTOCOL; + } + + if (count($message) > 0 && $frame::OP_CONTINUE !== $frame->getOpcode()) { + return $frame::CLOSE_PROTOCOL; + } + + $parsed = $message->getPayload(); + if (!$this->validator->checkEncoding($parsed, 'UTF-8')) { + return $frame::CLOSE_BAD_PAYLOAD; + } + + return true; + } + + public function validateFrame(FrameInterface $frame) { + if (false !== $frame->getRsv1() || + false !== $frame->getRsv2() || + false !== $frame->getRsv3() + ) { + return $frame::CLOSE_PROTOCOL; + } + + // Should be checking all frames + if ($this->checkForMask && !$frame->isMasked()) { + return $frame::CLOSE_PROTOCOL; + } + + $opcode = $frame->getOpcode(); + + if ($opcode > 2) { + if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) { + return $frame::CLOSE_PROTOCOL; + } + + switch ($opcode) { + case $frame::OP_CLOSE: + $closeCode = 0; + + $bin = $frame->getPayload(); + + if (empty($bin)) { + return $frame::CLOSE_NORMAL; + } + + if (strlen($bin) >= 2) { + list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); + } + + if (!$this->isValidCloseCode($closeCode)) { + return $frame::CLOSE_PROTOCOL; + } + + if (!$this->validator->checkEncoding(substr($bin, 2), 'UTF-8')) { + return $frame::CLOSE_BAD_PAYLOAD; + } + + return $frame::CLOSE_NORMAL; + break; + case $frame::OP_PING: + case $frame::OP_PONG: + break; + default: + return $frame::CLOSE_PROTOCOL; + break; + } + } + + return true; + } +} From c1027be9a68a9c7d708f3ce1c837e7d3c974cc15 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 29 Nov 2014 13:08:39 -0500 Subject: [PATCH 05/56] Spiking SPL interfaces --- src/Messaging/Protocol/Message.php | 20 ++++++++++++++++++-- src/Messaging/Protocol/MessageInterface.php | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index 17e8537..96a6b4a 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -1,11 +1,11 @@ _frames = new \SplDoublyLinkedList; @@ -18,6 +18,22 @@ class Message implements MessageInterface, \Countable { return count($this->_frames); } + public function offsetExists($index) { + return $this->_frames->offsetExists($index); + } + + public function offsetGet($index) { + return $this->_frames->offsetGet($index); + } + + public function offsetSet($index, $newval) { + throw new \DomainException('Frame access in messages is read-only'); + } + + public function offsetUnset($index) { + throw new \DomainException('Frame access in messages is read-only'); + } + /** * {@inheritdoc} */ diff --git a/src/Messaging/Protocol/MessageInterface.php b/src/Messaging/Protocol/MessageInterface.php index 7cefa0a..a103145 100644 --- a/src/Messaging/Protocol/MessageInterface.php +++ b/src/Messaging/Protocol/MessageInterface.php @@ -1,7 +1,7 @@ Date: Sun, 15 Mar 2015 21:49:26 -0400 Subject: [PATCH 06/56] Changed deps --- composer.json | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/composer.json b/composer.json index 9a352fe..2b7ed91 100644 --- a/composer.json +++ b/composer.json @@ -1,29 +1,29 @@ { - "name": "ratchet/rfc6455" - , "type": "library" - , "description": "RFC6455 protocol handler" - , "keywords": ["WebSockets"] - , "homepage": "http://socketo.me" - , "license": "MIT" - , "authors": [ + "name": "ratchet/rfc6455", + "type": "library", + "description": "RFC6455 protocol handler", + "keywords": ["WebSockets"], + "homepage": "http://socketo.me", + "license": "MIT", + "authors": [ { - "name": "Chris Boden" - , "email": "cboden@gmail.com" - , "role": "Developer" + "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": { + ], + "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" - , "guzzle/http": "~3.6" + }, + "require": { + "php": ">=5.4.2", + "guzzlehttp/psr7": "dev-master", + "psr/http-message": "0.9.*", + "evenement/evenement": "~2.0" } } From 8653b92115529ee9097ee122ef54dc4ad2e4382b Mon Sep 17 00:00:00 2001 From: matt Date: Sun, 15 Mar 2015 23:15:41 -0400 Subject: [PATCH 07/56] Create MessageStreamer, move some things --- src/Handshake/Negotiator.php | 170 +------------------- src/Handshake/NegotiatorInterface.php | 16 +- src/Handshake/RequestVerifier.php | 3 +- src/Messaging/Streaming/MessageStreamer.php | 162 +++++++++++++++++++ 4 files changed, 178 insertions(+), 173 deletions(-) create mode 100644 src/Messaging/Streaming/MessageStreamer.php diff --git a/src/Handshake/Negotiator.php b/src/Handshake/Negotiator.php index 69a5fe7..4b6373c 100644 --- a/src/Handshake/Negotiator.php +++ b/src/Handshake/Negotiator.php @@ -1,15 +1,9 @@ verifier = new RequestVerifier; @@ -44,7 +38,7 @@ class Negotiator implements NegotiatorInterface { /** * {@inheritdoc} */ - public function isProtocol(RequestInterface $request) { + public function isProtocol(ServerRequestInterface $request) { $version = (int)(string)$request->getHeader('Sec-WebSocket-Version'); return ($this->getVersionNumber() === $version); @@ -60,34 +54,16 @@ class Negotiator implements NegotiatorInterface { /** * {@inheritdoc} */ - public function handshake(RequestInterface $request) { + public function handshake(ServerRequestInterface $request) { if (true !== $this->verifier->verifyAll($request)) { return new Response(400); } - return new Response(101, array( + return new Response(101, [ 'Upgrade' => 'websocket' , 'Connection' => 'Upgrade' , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')) - )); - } - - /** - * @deprecated - * @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; + ]); } /** @@ -96,102 +72,7 @@ class Negotiator implements NegotiatorInterface { * @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 ($opcode > 2) { - 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(); - - $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); - - $from->WebSocket->coalescedCallback->onMessage($from, $parsed); - } - - if (strlen($overflow) > 0) { - $this->onMessage($from, $overflow); - } - } - - /** - * @deprecated - * @return RFC6455\Message - */ - public function newMessage() { - return new Message; - } - - /** - * @deprecated - * @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); } /** @@ -203,41 +84,4 @@ class Negotiator implements NegotiatorInterface { public function sign($key) { return base64_encode(sha1($key . static::GUID, true)); } - - /** - * @deprecated - * 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; - } - - /** - * @deprecated - * 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/Handshake/NegotiatorInterface.php b/src/Handshake/NegotiatorInterface.php index 718d760..cb7cbaf 100644 --- a/src/Handshake/NegotiatorInterface.php +++ b/src/Handshake/NegotiatorInterface.php @@ -1,8 +1,8 @@ currentMessage)) { + $this->currentMessage = $this->newMessage(); + } + + // There is a frame fragment attached to the connection, add to it + if (!isset($this->currentFrame)) { + $this->currentFrame = $this->newFrame(); + } + + $frame = $this->currentFrame; + + $frame->addBuffer($data); + if ($frame->isCoalesced()) { + $opcode = $frame->getOpcode(); + if ($opcode > 2) { + 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 (!$this->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; + case $frame::OP_PONG: + $this->emit('pong', [$frame]); + break; + default: + $this->emit('close', [$frame::CLOSE_PROTOCOL]); + return; + break; + } + + $overflow = $frame->extractOverflow(); + + unset($this->currentFrame, $frame, $opcode); + + if (strlen($overflow) > 0) { + $this->onData($overflow); + } + + return; + } + + $overflow = $frame->extractOverflow(); + + $this->currentMessage->addFrame($this->currentFrame); + unset($this->currentFrame); + } + + if ($this->currentMessage->isCoalesced()) { + $this->emit('message', [$this->currentMessage]); + //$parsed = $from->WebSocket->message->getPayload(); + unset($this->currentMessage); + } + + if (strlen($overflow) > 0) { + $this->onData($overflow); + } + } + + /** + * @return Message + */ + public function newMessage() { + return new Message; + } + + /** + * @param string|null $payload + * @param bool|null $final + * @param int|null $opcode + * @return Frame + */ + public function newFrame($payload = null, $final = null, $opcode = null) { + return new Frame($payload, $final, $opcode); + } + + /** + * 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; + } +} \ No newline at end of file From 1833a0f3eca3de6b2c19e1cb9784151564d9426e Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 16 Mar 2015 00:15:33 -0400 Subject: [PATCH 08/56] Passing some ab tests --- src/Handshake/Negotiator.php | 43 ++++++++++++++------- src/Handshake/NegotiatorInterface.php | 10 ++--- src/Handshake/RequestVerifier.php | 7 +++- src/Messaging/Protocol/Message.php | 15 +++++++ src/Messaging/Streaming/MessageStreamer.php | 11 ++++++ 5 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/Handshake/Negotiator.php b/src/Handshake/Negotiator.php index 4b6373c..315824b 100644 --- a/src/Handshake/Negotiator.php +++ b/src/Handshake/Negotiator.php @@ -2,7 +2,7 @@ namespace Ratchet\RFC6455\Handshake; use GuzzleHttp\Psr7\Response; -use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\RequestInterface; use Ratchet\RFC6455\Encoding\ValidatorInterface; /** @@ -20,25 +20,16 @@ class Negotiator implements NegotiatorInterface { */ private $validator; - /** - * A lookup of the valid close codes that can be sent in a frame - * @var array - * @deprecated - */ - private $closeCodes = []; - public function __construct(ValidatorInterface $validator) { $this->verifier = new RequestVerifier; - $this->setCloseCodes(); - $this->validator = $validator; } /** * {@inheritdoc} */ - public function isProtocol(ServerRequestInterface $request) { + public function isProtocol(RequestInterface $request) { $version = (int)(string)$request->getHeader('Sec-WebSocket-Version'); return ($this->getVersionNumber() === $version); @@ -54,7 +45,7 @@ class Negotiator implements NegotiatorInterface { /** * {@inheritdoc} */ - public function handshake(ServerRequestInterface $request) { + public function handshake(RequestInterface $request) { if (true !== $this->verifier->verifyAll($request)) { return new Response(400); } @@ -71,9 +62,9 @@ class Negotiator implements NegotiatorInterface { * @param \Ratchet\WebSocket\Version\RFC6455\Connection $from * @param string $data */ - public function onMessage(ConnectionInterface $from, $data) { - - } +// public function onMessage(ConnectionInterface $from, $data) { +// +// } /** * Used when doing the handshake to encode the key, verifying client/server are speaking the same language @@ -84,4 +75,26 @@ class Negotiator implements NegotiatorInterface { public function sign($key) { return base64_encode(sha1($key . static::GUID, true)); } + + /** + * Add supported protocols. If the request has any matching the response will include one + * @param string $id + */ + function addSupportedSubProtocol($id) + { + // TODO: Implement addSupportedSubProtocol() method. + } + + /** + * 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) + { + // TODO: Implement setStrictSubProtocolCheck() method. + } } diff --git a/src/Handshake/NegotiatorInterface.php b/src/Handshake/NegotiatorInterface.php index cb7cbaf..a5032f8 100644 --- a/src/Handshake/NegotiatorInterface.php +++ b/src/Handshake/NegotiatorInterface.php @@ -1,8 +1,8 @@ verifyMethod($request->getMethod()); $passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion()); - $passes += (int)$this->verifyRequestURI($request->getPath()); + $passes += (int)$this->verifyRequestURI($request->getUri()->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']); + $passes += (int)$this->verifyVersion((string)$request->getHeader('Sec-WebSocket-Version')); return (8 === $passes); } diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index 96a6b4a..1ff8fd6 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -7,6 +7,9 @@ class Message implements MessageInterface { */ private $_frames; + /** @var bool */ + private $binary = false; + public function __construct() { $this->_frames = new \SplDoublyLinkedList; } @@ -50,8 +53,12 @@ class Message implements MessageInterface { /** * {@inheritdoc} * @todo Also, I should perhaps check the type...control frames (ping/pong/close) are not to be considered part of a message + * @todo What should we do if there are binary and text mixed together? */ public function addFrame(FrameInterface $fragment) { + if ($this->_frames->isEmpty()) { + $this->binary = $fragment->getOpcode() == Frame::OP_BINARY; + } $this->_frames->push($fragment); return $this; @@ -118,4 +125,12 @@ class Message implements MessageInterface { return $buffer; } + + /** + * @return boolean + */ + public function isBinary() + { + return $this->binary; + } } diff --git a/src/Messaging/Streaming/MessageStreamer.php b/src/Messaging/Streaming/MessageStreamer.php index 2ed1885..9f13957 100644 --- a/src/Messaging/Streaming/MessageStreamer.php +++ b/src/Messaging/Streaming/MessageStreamer.php @@ -19,6 +19,12 @@ class MessageStreamer implements EventEmitterInterface { /** @var array */ private $closeCodes = []; + function __construct() + { + $this->setCloseCodes(); + } + + public function onData($data) { $overflow = ''; @@ -37,6 +43,11 @@ class MessageStreamer implements EventEmitterInterface { if ($frame->isCoalesced()) { $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; From 1970699b75b8565d6da7b2fff63f568e0ae18c97 Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 16 Mar 2015 00:22:38 -0400 Subject: [PATCH 09/56] Autobahn test script --- .gitignore | 1 + tests/ab/fuzzingclient.json | 15 ++++++++ tests/ab/startServer.php | 72 +++++++++++++++++++++++++++++++++++++ tests/bootstrap.php | 18 ++++++++++ 4 files changed, 106 insertions(+) create mode 100644 tests/ab/fuzzingclient.json create mode 100644 tests/ab/startServer.php create mode 100644 tests/bootstrap.php diff --git a/.gitignore b/.gitignore index 987e2a2..06a6b3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ composer.lock vendor +tests/ab/reports diff --git a/tests/ab/fuzzingclient.json b/tests/ab/fuzzingclient.json new file mode 100644 index 0000000..f2fea0a --- /dev/null +++ b/tests/ab/fuzzingclient.json @@ -0,0 +1,15 @@ +{ + "options": {"failByDrop": false}, + "outdir": "./reports/servers", + + "servers": [ + {"agent": "AutobahnServer", + "url": "ws://localhost:9001", + "options": {"version": 18}} + ], + + "casesy": ["1.1.1", "1.1.2"], + "cases": ["1.*", "2.*", "3.*", "4.*","5.*"], + "exclude-cases": [], + "exclude-agent-cases": {} +} diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php new file mode 100644 index 0000000..7668bec --- /dev/null +++ b/tests/ab/startServer.php @@ -0,0 +1,72 @@ +on('request', function (\React\Http\Request $request, \React\Http\Response $response) { + // saving this for later + $conn = $response; + + // 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()); + + $negotiatorResponse = $negotiator->handshake($psrRequest); + + $response->writeHead( + $negotiatorResponse->getStatusCode(), + array_merge( + $negotiatorResponse->getHeaders(), + ["Content-Length" => "0"] + ) + ); + + if ($negotiatorResponse->getStatusCode() !== 101) { + $response->end(); + return; + } + + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + + $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); + }); +}); +$socket->listen(9001); +$loop->run(); \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..6fa5dc9 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,18 @@ + Date: Mon, 16 Mar 2015 16:23:01 -0400 Subject: [PATCH 10/56] Client side tests and components --- src/Handshake/ClientNegotiator.php | 84 ++++++++ src/Handshake/ClientNegotiatorInterface.php | 11 + src/Handshake/ResponseVerifier.php | 44 ++++ tests/ab/clientRunner.php | 227 ++++++++++++++++++++ tests/ab/fuzzingserver.json | 12 ++ 5 files changed, 378 insertions(+) create mode 100644 src/Handshake/ClientNegotiator.php create mode 100644 src/Handshake/ClientNegotiatorInterface.php create mode 100644 src/Handshake/ResponseVerifier.php create mode 100644 tests/ab/clientRunner.php create mode 100644 tests/ab/fuzzingserver.json diff --git a/src/Handshake/ClientNegotiator.php b/src/Handshake/ClientNegotiator.php new file mode 100644 index 0000000..b4a4e16 --- /dev/null +++ b/src/Handshake/ClientNegotiator.php @@ -0,0 +1,84 @@ + 'Upgrade' + , 'Cache-Control' => 'no-cache' + , 'Pragma' => 'no-cache' + , 'Upgrade' => 'websocket' + , 'Sec-WebSocket-Version' => 13 + , 'User-Agent' => "RatchetRFC/0.0.0" + ]; + + /** @var Request */ + public $request; + + /** @var Response */ + public $response; + + /** @var ResponseVerifier */ + public $verifier; + + private $websocketKey = ''; + + function __construct($path = null) + { + if (!is_string($path)) $path = "/"; + $request = new Request("GET", $path); + + $request = $request->withUri(new Uri("ws://127.0.0.1:9001" . $path)); + + $this->request = $request; + + $this->verifier = new ResponseVerifier(); + + $this->websocketKey = $this->generateKey(); + } + + public function addRequiredHeaders() { + foreach ($this->defaultHeaders as $k => $v) { + // remove any header that is there now + $this->request = $this->request->withoutHeader($k); + $this->request = $this->request->withHeader($k, $v); + } + $this->request = $this->request->withoutHeader("Sec-WebSocket-Key"); + $this->request = $this->request->withHeader("Sec-WebSocket-Key", $this->websocketKey); + $this->request = $this->request->withoutHeader("Host") + ->withHeader("Host", $this->request->getUri()->getHost() . ":" . $this->request->getUri()->getPort()); + } + + public function getRequest() { + $this->addRequiredHeaders(); + return $this->request; + } + + public function getResponse() { + return $this->response; + } + + public function validateResponse(Response $response) { + $this->response = $response; + + return $this->verifier->verifyAll($this->getRequest(), $response); + } + + protected 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); + } + +} \ No newline at end of file diff --git a/src/Handshake/ClientNegotiatorInterface.php b/src/Handshake/ClientNegotiatorInterface.php new file mode 100644 index 0000000..c95c1ac --- /dev/null +++ b/src/Handshake/ClientNegotiatorInterface.php @@ -0,0 +1,11 @@ +verifyStatus($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') + ); + + return (4 == $passes); + } + + public function verifyStatus($status) { + return ($status == 101); + } + + public function verifyUpgrade($upgrade) { + return (strtolower($upgrade) == "websocket"); + } + + public function verifyConnection($connection) { + return (strtolower($connection) == "upgrade"); + } + + public function verifySecWebSocketAccept($swa, $key) { + return ($swa == $this->sign($key)); + } + + public function sign($key) { + return base64_encode(sha1($key . Negotiator::GUID, true)); + } +} \ No newline at end of file diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php new file mode 100644 index 0000000..ade81d0 --- /dev/null +++ b/tests/ab/clientRunner.php @@ -0,0 +1,227 @@ +createCached('8.8.8.8', $loop); + +$factory = new \React\SocketClient\Connector($loop, $dnsResolver); + +function getTestCases() { + global $factory; + + $deferred = new Deferred(); + + $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { + $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator("/getCaseCount"); + $cnRequest = $cn->getRequest(); + + $rawResponse = ""; + $response = null; + + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + + $ms->on('message', function (Message $msg) use ($stream, $deferred) { + $deferred->resolve($msg->getPayload()); + + $closeFrame = new Frame(pack('n', Frame::CLOSE_NORMAL), true, Frame::OP_CLOSE); + $closeFrame->maskPayload(); + $stream->end($closeFrame->getContents()); + }); + + $ms->on('close', function ($code) use ($stream) { + if ($code === null) { + $stream->close(); + return; + } + $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); + $frame->maskPayload(); + $stream->end($frame->getContents()); + }); + + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred) { + 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($response)) { + $stream->end(); + $deferred->reject(); + } + } + } + + // feed the message streamer + if ($response) { + $ms->onData($data); + } + }); + + $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); + }); + + return $deferred->promise(); +} + +function runTest($case) +{ + global $factory; + + $casePath = "/runCase?case={$case}&agent=" . AGENT; + + $deferred = new Deferred(); + + $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath) { + $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator($casePath); + $cnRequest = $cn->getRequest(); + + $rawResponse = ""; + $response = null; + + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + + $ms->on('message', function (Message $msg) use ($stream, $deferred) { + $opcode = $msg->isBinary() ? Frame::OP_BINARY : Frame::OP_TEXT; + $frame = new Frame($msg->getPayload(), true, $opcode); + $frame->maskPayload(); + + $stream->write($frame->getContents()); + }); + + $ms->on('ping', function (Frame $frame) use ($stream) { + $response = new Frame($frame->getPayload(), true, Frame::OP_PONG); + $response->maskPayload(); + $stream->write($response->getContents()); + }); + + $ms->on('close', function ($code) use ($stream, $deferred) { + if ($code === null) { + $stream->close(); + return; + } + $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); + $frame->maskPayload(); + $stream->end($frame->getContents()); + }); + + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred) { + 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($response)) { + $stream->end(); + $deferred->reject(); + } + } + } + + // feed the message streamer + if ($response) { + $ms->onData($data); + } + }); + + $stream->on('close', function () use ($deferred) { + $deferred->resolve(); + }); + + $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); + }); + + return $deferred->promise(); +} + + + +function createReport() { + global $factory; + + $deferred = new Deferred(); + + $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { + $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator('/updateReports?agent=' . AGENT); + $cnRequest = $cn->getRequest(); + + $rawResponse = ""; + $response = null; + + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + + $ms->on('message', function (Message $msg) use ($stream, $deferred) { + $deferred->resolve($msg->getPayload()); + + $closeFrame = new Frame(pack('n', Frame::CLOSE_NORMAL), true, Frame::OP_CLOSE); + $closeFrame->maskPayload(); + $stream->end($closeFrame->getContents()); + }); + + $ms->on('close', function ($code) use ($stream) { + if ($code === null) { + $stream->close(); + return; + } + $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); + $frame->maskPayload(); + $stream->end($frame->getContents()); + }); + + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred) { + 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($response)) { + $stream->end(); + $deferred->reject(); + } + } + } + + // feed the message streamer + if ($response) { + $ms->onData($data); + } + }); + + $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); + }); + + return $deferred->promise(); +} + + +$testPromises = []; + +getTestCases()->then(function ($count) { + echo "Running " . $count . " test cases.\n"; + + for ($i = 0; $i < $count; $i++) { + $testPromises[] = runTest($i + 1); + } + + \React\Promise\all($testPromises)->then(function () { + createReport(); + }); +}); + +$loop->run(); diff --git a/tests/ab/fuzzingserver.json b/tests/ab/fuzzingserver.json new file mode 100644 index 0000000..4193e62 --- /dev/null +++ b/tests/ab/fuzzingserver.json @@ -0,0 +1,12 @@ +{ + "url": "ws://127.0.0.1:9001" + , "options": { + "failByDrop": false +} + , "outdir": "./reports/clients" + , "casesy": ["*"] + , "cases": ["1.*", "2.*", "3.*", "4.*", "5.*"] + , "casesx": ["1.1.8"] + , "exclude-cases": ["9.8.6"] + , "exclude-agent-cases": {} +} \ No newline at end of file From c2a51b62ef6f463a56a40d25d4920e822222fc3f Mon Sep 17 00:00:00 2001 From: matt Date: Wed, 18 Mar 2015 11:12:11 -0400 Subject: [PATCH 11/56] Passing Autobahn tests (except compression) --- src/Messaging/Protocol/Frame.php | 31 +++++++- src/Messaging/Protocol/Message.php | 20 +++++- src/Messaging/Protocol/MessageInterface.php | 5 ++ src/Messaging/Streaming/MessageStreamer.php | 70 ++++++++----------- src/Messaging/Validation/MessageValidator.php | 33 ++++++--- tests/ab/clientRunner.php | 39 +++++++---- tests/ab/fuzzingclient.json | 5 +- tests/ab/fuzzingserver.json | 9 +-- 8 files changed, 136 insertions(+), 76 deletions(-) diff --git a/src/Messaging/Protocol/Frame.php b/src/Messaging/Protocol/Frame.php index 89ba2b9..13e8bfb 100644 --- a/src/Messaging/Protocol/Frame.php +++ b/src/Messaging/Protocol/Frame.php @@ -61,7 +61,6 @@ class Frame implements FrameInterface { */ protected $secondByte = -1; - /** * @param string|null $payload * @param bool $final @@ -446,4 +445,34 @@ class Frame implements FrameInterface { return ''; } + + /** + * Determine if a close code is valid + * @param int|string + * @return bool + */ + public function isValidCloseCode($val) { + if (in_array($val, [ + static::CLOSE_NORMAL, + static::CLOSE_GOING_AWAY, + static::CLOSE_PROTOCOL, + static::CLOSE_BAD_DATA, + //static::CLOSE_NO_STATUS, + //static::CLOSE_ABNORMAL, + static::CLOSE_BAD_PAYLOAD, + static::CLOSE_POLICY, + static::CLOSE_TOO_BIG, + static::CLOSE_MAND_EXT, + static::CLOSE_SRV_ERR, + //static::CLOSE_TLS, + ])) { + return true; + } + + if ($val >= 3000 && $val <= 4999) { + return true; + } + + return false; + } } diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index 1ff8fd6..7d78c71 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -53,15 +53,29 @@ class Message implements MessageInterface { /** * {@inheritdoc} * @todo Also, I should perhaps check the type...control frames (ping/pong/close) are not to be considered part of a message - * @todo What should we do if there are binary and text mixed together? */ public function addFrame(FrameInterface $fragment) { + // should the validation stuff be somewhere else? + // it really needs the context of the message to know whether there is a problem if ($this->_frames->isEmpty()) { $this->binary = $fragment->getOpcode() == Frame::OP_BINARY; } + + // check to see if this is a continuation frame when there is no + // frames yet added + if ($this->_frames->count() == 0 && $fragment->getOpcode() == Frame::OP_CONTINUE) { + return Frame::CLOSE_PROTOCOL; + } + + // check to see if this is not a continuation frame when there is already frames + if ($this->_frames->count() > 0 && $fragment->getOpcode() != Frame::OP_CONTINUE) { + return Frame::CLOSE_PROTOCOL; + } + $this->_frames->push($fragment); - return $this; + return true; + //return $this; } /** @@ -106,6 +120,8 @@ class Message implements MessageInterface { $buffer .= $frame->getPayload(); } + echo "Reassembled " . strlen($buffer) . " bytes in " . $this->_frames->count() . " frames\n"; + return $buffer; } diff --git a/src/Messaging/Protocol/MessageInterface.php b/src/Messaging/Protocol/MessageInterface.php index a103145..f3d8a64 100644 --- a/src/Messaging/Protocol/MessageInterface.php +++ b/src/Messaging/Protocol/MessageInterface.php @@ -12,4 +12,9 @@ interface MessageInterface extends DataInterface, \ArrayAccess, \Countable { * @return int */ function getOpcode(); + + /** + * @return bool + */ + function isBinary(); } diff --git a/src/Messaging/Streaming/MessageStreamer.php b/src/Messaging/Streaming/MessageStreamer.php index 9f13957..5c38e8b 100644 --- a/src/Messaging/Streaming/MessageStreamer.php +++ b/src/Messaging/Streaming/MessageStreamer.php @@ -4,8 +4,10 @@ namespace Ratchet\RFC6455\Messaging\Streaming; use Evenement\EventEmitterInterface; use Evenement\EventEmitterTrait; +use Ratchet\RFC6455\Encoding\Validator; use Ratchet\RFC6455\Messaging\Protocol\Frame; use Ratchet\RFC6455\Messaging\Protocol\Message; +use Ratchet\RFC6455\Messaging\Validation\MessageValidator; class MessageStreamer implements EventEmitterInterface { use EventEmitterTrait; @@ -16,12 +18,16 @@ class MessageStreamer implements EventEmitterInterface { /** @var Message */ private $currentMessage; - /** @var array */ - private $closeCodes = []; + /** @var MessageValidator */ + private $validator; - function __construct() + /** @var bool */ + private $checkForMask; + + function __construct($client = false) { - $this->setCloseCodes(); + $this->checkForMask = !$client; + $this->validator = new MessageValidator(new Validator(), $this->checkForMask); } @@ -41,6 +47,12 @@ class MessageStreamer implements EventEmitterInterface { $frame->addBuffer($data); if ($frame->isCoalesced()) { + $validFrame = $this->validator->validateFrame($frame); + if ($validFrame !== true) { + $this->emit('close', [$validFrame]); + return; + } + $opcode = $frame->getOpcode(); if ($opcode > 2) { if ($frame->getPayloadLength() > 125) { @@ -63,7 +75,7 @@ class MessageStreamer implements EventEmitterInterface { list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); } - if (!$this->isValidCloseCode($closeCode)) { + if (!$frame->isValidCloseCode($closeCode)) { $this->emit('close', [$frame::CLOSE_PROTOCOL]); return; } @@ -104,11 +116,20 @@ class MessageStreamer implements EventEmitterInterface { $overflow = $frame->extractOverflow(); - $this->currentMessage->addFrame($this->currentFrame); + $frameAdded = $this->currentMessage->addFrame($this->currentFrame); + if ($frameAdded !== true) { + $this->emit('close', [$frameAdded]); + } unset($this->currentFrame); } if ($this->currentMessage->isCoalesced()) { + $msgCheck = $this->validator->checkMessage($this->currentMessage); + if ($msgCheck !== true) { + if ($msgCheck === false) $msgCheck = null; + $this->emit('close', [$msgCheck]); + return; + } $this->emit('message', [$this->currentMessage]); //$parsed = $from->WebSocket->message->getPayload(); unset($this->currentMessage); @@ -135,39 +156,4 @@ class MessageStreamer implements EventEmitterInterface { public function newFrame($payload = null, $final = null, $opcode = null) { return new Frame($payload, $final, $opcode); } - - /** - * 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; - } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Messaging/Validation/MessageValidator.php b/src/Messaging/Validation/MessageValidator.php index b15b726..e47eb2c 100644 --- a/src/Messaging/Validation/MessageValidator.php +++ b/src/Messaging/Validation/MessageValidator.php @@ -1,15 +1,18 @@ validator = $validator; + $this->checkForMask = $checkForMask; } /** @@ -25,7 +28,7 @@ class MessageValidator { $frame = $message[0]; - $frameCheck = $this->checkFrame($frame); + $frameCheck = $this->validateFrame($frame); if (true !== $frameCheck) { return $frameCheck; } @@ -35,19 +38,22 @@ class MessageValidator { return $frame::CLOSE_PROTOCOL; } - if (count($message) > 0 && $frame::OP_CONTINUE !== $frame->getOpcode()) { - return $frame::CLOSE_PROTOCOL; - } + // I (mbonneau) don't understand this - seems to always kill the tests +// if (count($message) > 0 && $frame::OP_CONTINUE !== $frame->getOpcode()) { +// return $frame::CLOSE_PROTOCOL; +// } - $parsed = $message->getPayload(); - if (!$this->validator->checkEncoding($parsed, 'UTF-8')) { - return $frame::CLOSE_BAD_PAYLOAD; + if (!$message->isBinary()) { + $parsed = $message->getPayload(); + if (!$this->validator->checkEncoding($parsed, 'UTF-8')) { + return $frame::CLOSE_BAD_PAYLOAD; + } } return true; } - public function validateFrame(FrameInterface $frame) { + public function validateFrame(Frame $frame) { if (false !== $frame->getRsv1() || false !== $frame->getRsv2() || false !== $frame->getRsv3() @@ -73,15 +79,20 @@ class MessageValidator { $bin = $frame->getPayload(); + if (empty($bin)) { return $frame::CLOSE_NORMAL; } + if (strlen($bin) == 1) { + return $frame::CLOSE_PROTOCOL; + } + if (strlen($bin) >= 2) { list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); } - if (!$this->isValidCloseCode($closeCode)) { + if (!$frame->isValidCloseCode($closeCode)) { return $frame::CLOSE_PROTOCOL; } diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php index ade81d0..e1621e2 100644 --- a/tests/ab/clientRunner.php +++ b/tests/ab/clientRunner.php @@ -26,7 +26,7 @@ function getTestCases() { $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(true); $ms->on('message', function (Message $msg) use ($stream, $deferred) { $deferred->resolve($msg->getPayload()); @@ -38,7 +38,7 @@ function getTestCases() { $ms->on('close', function ($code) use ($stream) { if ($code === null) { - $stream->close(); + $stream->end(); return; } $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); @@ -82,16 +82,17 @@ function runTest($case) $deferred = new Deferred(); - $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath) { + $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath, $case) { $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator($casePath); $cnRequest = $cn->getRequest(); $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(true); - $ms->on('message', function (Message $msg) use ($stream, $deferred) { + $ms->on('message', function (Message $msg) use ($stream, $deferred, $case) { + echo "Got message on case " . $case . "\n"; $opcode = $msg->isBinary() ? Frame::OP_BINARY : Frame::OP_TEXT; $frame = new Frame($msg->getPayload(), true, $opcode); $frame->maskPayload(); @@ -107,7 +108,7 @@ function runTest($case) $ms->on('close', function ($code) use ($stream, $deferred) { if ($code === null) { - $stream->close(); + $stream->end(); return; } $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); @@ -161,7 +162,7 @@ function createReport() { $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(true); $ms->on('message', function (Message $msg) use ($stream, $deferred) { $deferred->resolve($msg->getPayload()); @@ -173,7 +174,7 @@ function createReport() { $ms->on('close', function ($code) use ($stream) { if ($code === null) { - $stream->close(); + $stream->end(); return; } $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); @@ -212,14 +213,26 @@ function createReport() { $testPromises = []; -getTestCases()->then(function ($count) { +getTestCases()->then(function ($count) use ($loop) { echo "Running " . $count . " test cases.\n"; - for ($i = 0; $i < $count; $i++) { - $testPromises[] = runTest($i + 1); - } + $allDeferred = new Deferred(); - \React\Promise\all($testPromises)->then(function () { + $runNextCase = function () use (&$i, &$runNextCase, $count, $allDeferred) { + $i++; + if ($i > $count) { + $allDeferred->resolve(); + return; + } + echo "Running " . $i . "\n"; + runTest($i)->then($runNextCase); + }; + + $i = 0; + $runNextCase(); + + $allDeferred->promise()->then(function () { + echo "Generating report...\n"; createReport(); }); }); diff --git a/tests/ab/fuzzingclient.json b/tests/ab/fuzzingclient.json index f2fea0a..20e1bd2 100644 --- a/tests/ab/fuzzingclient.json +++ b/tests/ab/fuzzingclient.json @@ -8,8 +8,7 @@ "options": {"version": 18}} ], - "casesy": ["1.1.1", "1.1.2"], - "cases": ["1.*", "2.*", "3.*", "4.*","5.*"], - "exclude-cases": [], + "cases": ["1.*"], + "exclude-cases": ["12.*","13.*"], "exclude-agent-cases": {} } diff --git a/tests/ab/fuzzingserver.json b/tests/ab/fuzzingserver.json index 4193e62..8041bf1 100644 --- a/tests/ab/fuzzingserver.json +++ b/tests/ab/fuzzingserver.json @@ -4,9 +4,10 @@ "failByDrop": false } , "outdir": "./reports/clients" - , "casesy": ["*"] - , "cases": ["1.*", "2.*", "3.*", "4.*", "5.*"] - , "casesx": ["1.1.8"] - , "exclude-cases": ["9.8.6"] + , "casesa": ["*"] + , "casesj": ["9.3.2", "9.3.3", "9.3.4", "9.4.2", "9.4.3", "9.4.4", "9.7.6", "9.8.6"] + , "cases": ["9.3.1", "9.3.2"] + , "casesx": ["1.*","2.*"] + , "exclude-cases": ["12.*", "13.*"] , "exclude-agent-cases": {} } \ No newline at end of file From c280d8137fd232a4c878d3e2d8957a162ea495e1 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 22 May 2015 09:49:14 -0400 Subject: [PATCH 12/56] Update to PSR-7 v1, update to match API changes --- composer.json | 8 +++-- src/Handshake/Negotiator.php | 2 +- src/Handshake/RequestVerifier.php | 49 +++++++++++++------------------ 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/composer.json b/composer.json index 2b7ed91..89d2f6d 100644 --- a/composer.json +++ b/composer.json @@ -22,8 +22,10 @@ }, "require": { "php": ">=5.4.2", - "guzzlehttp/psr7": "dev-master", - "psr/http-message": "0.9.*", - "evenement/evenement": "~2.0" + "guzzlehttp/psr7": "^1.0", + "evenement/evenement": "^2.0" + }, + "require-dev": { + "react/http": "^0.4.1" } } diff --git a/src/Handshake/Negotiator.php b/src/Handshake/Negotiator.php index 315824b..2edb2d5 100644 --- a/src/Handshake/Negotiator.php +++ b/src/Handshake/Negotiator.php @@ -53,7 +53,7 @@ class Negotiator implements NegotiatorInterface { return new Response(101, [ 'Upgrade' => 'websocket' , 'Connection' => 'Upgrade' - , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')) + , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]) ]); } diff --git a/src/Handshake/RequestVerifier.php b/src/Handshake/RequestVerifier.php index 3aa12d0..4118d8d 100644 --- a/src/Handshake/RequestVerifier.php +++ b/src/Handshake/RequestVerifier.php @@ -23,11 +23,11 @@ class RequestVerifier { $passes += (int)$this->verifyMethod($request->getMethod()); $passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion()); $passes += (int)$this->verifyRequestURI($request->getUri()->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((string)$request->getHeader('Sec-WebSocket-Version')); + $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); } @@ -71,59 +71,50 @@ class RequestVerifier { } /** - * @param string|null + * @param array $hostHeader * @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); + public function verifyHost(array $hostHeader) { + return (1 === count($hostHeader)); } /** * Verify the Upgrade request to WebSockets. - * @param string $val MUST equal "websocket" + * @param array $upgradeHeader MUST include "websocket" * @return bool */ - public function verifyUpgradeRequest($val) { - return ('websocket' === strtolower($val)); + public function verifyUpgradeRequest(array $upgradeHeader) { + return (in_array('websocket', array_map('strtolower', $upgradeHeader))); } /** * Verify the Connection header - * @param string $val MUST equal "Upgrade" + * @param array $connectionHeader MUST include "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)); + public function verifyConnection(array $connectionHeader) { + return in_array('upgrade', array_map('strtolower', $connectionHeader)); } /** * This function verifies the nonce is valid (64 big encoded, 16 bytes random string) - * @param string|null + * @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($val) { - return (16 === strlen(base64_decode((string)$val))); + public function verifyKey(array $keyHeader) { + return in_array(16, array_map('strlen', array_map('base64_decode', $keyHeader))); } /** * Verify the version passed matches this RFC - * @param string|int MUST equal 13|"13" + * @param string|int $versionHeader MUST equal 13|"13" * @return bool */ - public function verifyVersion($val) { - return (static::VERSION === (int)$val); + public function verifyVersion($versionHeader) { + return (1 === count($versionHeader) && static::VERSION === (int)$versionHeader[0]); } /** From de7686984782f1fe12408f7b55971586328cdc31 Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 22 May 2015 16:50:07 -0400 Subject: [PATCH 13/56] Fixed up ResponseVerifier for PSR-7 --- src/Handshake/ResponseVerifier.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Handshake/ResponseVerifier.php b/src/Handshake/ResponseVerifier.php index 2567a45..c1bd67a 100644 --- a/src/Handshake/ResponseVerifier.php +++ b/src/Handshake/ResponseVerifier.php @@ -26,16 +26,19 @@ class ResponseVerifier { return ($status == 101); } - public function verifyUpgrade($upgrade) { - return (strtolower($upgrade) == "websocket"); + public function verifyUpgrade(array $upgrade) { + return (in_array('websocket', array_map('strtolower', $upgrade))); } - public function verifyConnection($connection) { - return (strtolower($connection) == "upgrade"); + public function verifyConnection(array $connection) { + return (in_array('upgrade', array_map('strtolower', $connection))); } public function verifySecWebSocketAccept($swa, $key) { - return ($swa == $this->sign($key)); + return ( + 1 === count($swa) && + 1 === count($key) && + $swa[0] == $this->sign($key[0])); } public function sign($key) { From 791ebaeb2455f4a361e84714e04771d1702246ab Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 23 May 2015 12:29:05 -0400 Subject: [PATCH 14/56] 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(); From 1c6a486e8adc43a204337cfe5483cdc758b3a10b Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 24 May 2015 11:12:59 -0400 Subject: [PATCH 15/56] Fixed failing UTF-8 tests, increased performance --- src/Messaging/Protocol/Message.php | 6 +++++- src/Messaging/Protocol/MessageInterface.php | 2 +- src/Messaging/Streaming/MessageStreamer.php | 2 +- tests/ab/startServer.php | 7 +++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index 7d78c71..f22f91a 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -1,7 +1,7 @@ _frames = new \SplDoublyLinkedList; } + public function getIterator() { + return $this->_frames; + } + /** * {@inheritdoc} */ diff --git a/src/Messaging/Protocol/MessageInterface.php b/src/Messaging/Protocol/MessageInterface.php index f3d8a64..2913d82 100644 --- a/src/Messaging/Protocol/MessageInterface.php +++ b/src/Messaging/Protocol/MessageInterface.php @@ -1,7 +1,7 @@ getMessage()->isCoalesced()) { $msgCheck = $this->validator->checkMessage($context->getMessage()); if ($msgCheck !== true) { - $context->onClose($msgCheck || null); + $context->onClose($msgCheck); return; } $context->onMessage($context->getMessage()); diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 47643a6..fa7dbd8 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -33,8 +33,11 @@ class ConnectionContext implements Ratchet\RFC6455\Messaging\Streaming\ContextIn } public function onMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) { - $frame = new Frame($msg->getPayload(), true, $msg[0]->getOpcode()); - $this->_conn->write($frame->getContents()); + foreach ($msg as $frame) { + $frame->unMaskPayload(); + } + + $this->_conn->write($msg->getContents()); } public function onPing(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame) { From 6676b05d02f44e48af0c239cf10bbf8543c83f91 Mon Sep 17 00:00:00 2001 From: matt Date: Sun, 24 May 2015 19:50:51 -0400 Subject: [PATCH 16/56] Client tests using ContextInterface --- tests/ab/AbConnectionContext.php | 67 ++++++++++++++++ tests/ab/clientRunner.php | 129 ++++++++++++++----------------- tests/ab/fuzzingserver.json | 6 +- 3 files changed, 126 insertions(+), 76 deletions(-) create mode 100644 tests/ab/AbConnectionContext.php diff --git a/tests/ab/AbConnectionContext.php b/tests/ab/AbConnectionContext.php new file mode 100644 index 0000000..8f106e1 --- /dev/null +++ b/tests/ab/AbConnectionContext.php @@ -0,0 +1,67 @@ +_conn = $connectionContext; + $this->maskPayload = $maskPayload; + } + + 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 \Ratchet\RFC6455\Messaging\Protocol\Frame($msg->getPayload(), true, $msg[0]->getOpcode()); + if ($this->maskPayload) { + $frame->maskPayload(); + } + $this->_conn->write($frame->getContents()); + } + + public function onPing(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame) { + $pong = new \Ratchet\RFC6455\Messaging\Protocol\Frame($frame->getPayload(), true, \Ratchet\RFC6455\Messaging\Protocol\Frame::OP_PONG); + if ($this->maskPayload) { + $pong->maskPayload(); + } + $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 \Ratchet\RFC6455\Messaging\Protocol\Frame( + pack('n', $code), + true, + \Ratchet\RFC6455\Messaging\Protocol\Frame::OP_CLOSE + ); + if ($this->maskPayload) { + $frame->maskPayload(); + } + + $this->_conn->end($frame->getContents()); + } +} \ No newline at end of file diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php index e1621e2..0b5f321 100644 --- a/tests/ab/clientRunner.php +++ b/tests/ab/clientRunner.php @@ -4,9 +4,32 @@ use Ratchet\RFC6455\Messaging\Protocol\Frame; use Ratchet\RFC6455\Messaging\Protocol\Message; require __DIR__ . '/../bootstrap.php'; +require __DIR__ . '/AbConnectionContext.php'; define('AGENT', 'RatchetRFC/0.0.0'); + +class EmConnectionContext extends AbConnectionContext implements \Evenement\EventEmitterInterface, Ratchet\RFC6455\Messaging\Streaming\ContextInterface { + use \Evenement\EventEmitterTrait; + + public function onMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) { + $this->emit('message', [$msg]); + } + + public function sendMessage(Frame $frame) { + if ($this->maskPayload) { + $frame->maskPayload(); + } + $this->_conn->write($frame->getContents()); + } + + public function close($closeCode = Frame::CLOSE_NORMAL) { + $closeFrame = new Frame(pack('n', $closeCode), true, Frame::OP_CLOSE); + $closeFrame->maskPayload(); + $this->_conn->end($closeFrame->getContents()); + } +} + $loop = React\EventLoop\Factory::create(); $dnsResolverFactory = new React\Dns\Resolver\Factory(); @@ -26,27 +49,12 @@ function getTestCases() { $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(true); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(new \Ratchet\RFC6455\Encoding\Validator(), true); - $ms->on('message', function (Message $msg) use ($stream, $deferred) { - $deferred->resolve($msg->getPayload()); + /** @var EmConnectionContext $context */ + $context = null; - $closeFrame = new Frame(pack('n', Frame::CLOSE_NORMAL), true, Frame::OP_CLOSE); - $closeFrame->maskPayload(); - $stream->end($closeFrame->getContents()); - }); - - $ms->on('close', function ($code) use ($stream) { - if ($code === null) { - $stream->end(); - return; - } - $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); - $frame->maskPayload(); - $stream->end($frame->getContents()); - }); - - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred) { + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred, &$context) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); @@ -58,13 +66,20 @@ function getTestCases() { if (!$cn->validateResponse($response)) { $stream->end(); $deferred->reject(); + } else { + $context = new EmConnectionContext($stream, true); + + $context->on('message', function (Message $msg) use ($stream, $deferred, $context) { + $deferred->resolve($msg->getPayload()); + $context->close(); + }); } } } // feed the message streamer - if ($response) { - $ms->onData($data); + if ($response && $context) { + $ms->onData($data, $context); } }); @@ -89,34 +104,12 @@ function runTest($case) $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(true); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(new \Ratchet\RFC6455\Encoding\Validator(), true); - $ms->on('message', function (Message $msg) use ($stream, $deferred, $case) { - echo "Got message on case " . $case . "\n"; - $opcode = $msg->isBinary() ? Frame::OP_BINARY : Frame::OP_TEXT; - $frame = new Frame($msg->getPayload(), true, $opcode); - $frame->maskPayload(); + /** @var AbConnectionContext $context */ + $context = null; - $stream->write($frame->getContents()); - }); - - $ms->on('ping', function (Frame $frame) use ($stream) { - $response = new Frame($frame->getPayload(), true, Frame::OP_PONG); - $response->maskPayload(); - $stream->write($response->getContents()); - }); - - $ms->on('close', function ($code) use ($stream, $deferred) { - if ($code === null) { - $stream->end(); - return; - } - $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); - $frame->maskPayload(); - $stream->end($frame->getContents()); - }); - - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred) { + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred, &$context) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); @@ -128,13 +121,15 @@ function runTest($case) if (!$cn->validateResponse($response)) { $stream->end(); $deferred->reject(); + } else { + $context = new AbConnectionContext($stream, true); } } } // feed the message streamer - if ($response) { - $ms->onData($data); + if ($response && $context) { + $ms->onData($data, $context); } }); @@ -148,8 +143,6 @@ function runTest($case) return $deferred->promise(); } - - function createReport() { global $factory; @@ -162,27 +155,12 @@ function createReport() { $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(true); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(new \Ratchet\RFC6455\Encoding\Validator(), true); - $ms->on('message', function (Message $msg) use ($stream, $deferred) { - $deferred->resolve($msg->getPayload()); + /** @var EmConnectionContext $context */ + $context = null; - $closeFrame = new Frame(pack('n', Frame::CLOSE_NORMAL), true, Frame::OP_CLOSE); - $closeFrame->maskPayload(); - $stream->end($closeFrame->getContents()); - }); - - $ms->on('close', function ($code) use ($stream) { - if ($code === null) { - $stream->end(); - return; - } - $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); - $frame->maskPayload(); - $stream->end($frame->getContents()); - }); - - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred) { + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred, &$context) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); @@ -194,13 +172,20 @@ function createReport() { if (!$cn->validateResponse($response)) { $stream->end(); $deferred->reject(); + } else { + $context = new EmConnectionContext($stream, true); + + $context->on('message', function (Message $msg) use ($stream, $deferred, $context) { + $deferred->resolve($msg->getPayload()); + $context->close(); + }); } } } // feed the message streamer - if ($response) { - $ms->onData($data); + if ($response && $context) { + $ms->onData($data, $context); } }); diff --git a/tests/ab/fuzzingserver.json b/tests/ab/fuzzingserver.json index 8041bf1..29e9a22 100644 --- a/tests/ab/fuzzingserver.json +++ b/tests/ab/fuzzingserver.json @@ -4,10 +4,8 @@ "failByDrop": false } , "outdir": "./reports/clients" - , "casesa": ["*"] - , "casesj": ["9.3.2", "9.3.3", "9.3.4", "9.4.2", "9.4.3", "9.4.4", "9.7.6", "9.8.6"] - , "cases": ["9.3.1", "9.3.2"] - , "casesx": ["1.*","2.*"] + , "cases": ["*"] + , "cases_failing": ["9.3.2", "9.3.3", "9.3.4", "9.4.2", "9.4.3", "9.4.4"] , "exclude-cases": ["12.*", "13.*"] , "exclude-agent-cases": {} } \ No newline at end of file From f1451e0bd85726843354e62d5735b2d0acc8b5d1 Mon Sep 17 00:00:00 2001 From: matt Date: Sun, 24 May 2015 23:50:02 -0400 Subject: [PATCH 17/56] Fixed issue with client tests --- src/Messaging/Protocol/Frame.php | 1 + tests/ab/clientRunner.php | 11 ++++++++--- tests/ab/fuzzingserver.json | 1 - 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Messaging/Protocol/Frame.php b/src/Messaging/Protocol/Frame.php index 13e8bfb..fd767c4 100644 --- a/src/Messaging/Protocol/Frame.php +++ b/src/Messaging/Protocol/Frame.php @@ -377,6 +377,7 @@ class Frame implements FrameInterface { $byte_length = $this->getNumPayloadBytes(); if ($this->bytesRecvd < 1 + $byte_length) { + $this->defPayLen = -1; throw new \UnderflowException('Not enough data buffered to determine payload length'); } diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php index 0b5f321..94109f2 100644 --- a/tests/ab/clientRunner.php +++ b/tests/ab/clientRunner.php @@ -8,6 +8,8 @@ require __DIR__ . '/AbConnectionContext.php'; define('AGENT', 'RatchetRFC/0.0.0'); +$testServer = "127.0.0.1"; + class EmConnectionContext extends AbConnectionContext implements \Evenement\EventEmitterInterface, Ratchet\RFC6455\Messaging\Streaming\ContextInterface { use \Evenement\EventEmitterTrait; @@ -39,10 +41,11 @@ $factory = new \React\SocketClient\Connector($loop, $dnsResolver); function getTestCases() { global $factory; + global $testServer; $deferred = new Deferred(); - $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { + $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator("/getCaseCount"); $cnRequest = $cn->getRequest(); @@ -92,12 +95,13 @@ function getTestCases() { function runTest($case) { global $factory; + global $testServer; $casePath = "/runCase?case={$case}&agent=" . AGENT; $deferred = new Deferred(); - $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath, $case) { + $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath, $case) { $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator($casePath); $cnRequest = $cn->getRequest(); @@ -145,10 +149,11 @@ function runTest($case) function createReport() { global $factory; + global $testServer; $deferred = new Deferred(); - $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { + $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator('/updateReports?agent=' . AGENT); $cnRequest = $cn->getRequest(); diff --git a/tests/ab/fuzzingserver.json b/tests/ab/fuzzingserver.json index 29e9a22..70db183 100644 --- a/tests/ab/fuzzingserver.json +++ b/tests/ab/fuzzingserver.json @@ -5,7 +5,6 @@ } , "outdir": "./reports/clients" , "cases": ["*"] - , "cases_failing": ["9.3.2", "9.3.3", "9.3.4", "9.4.2", "9.4.3", "9.4.4"] , "exclude-cases": ["12.*", "13.*"] , "exclude-agent-cases": {} } \ No newline at end of file From c8ce2adcb18b8592e1211f19ef970986a8caf330 Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 25 May 2015 09:51:50 -0400 Subject: [PATCH 18/56] Removed echo --- src/Messaging/Protocol/Message.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index f22f91a..be87123 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -124,8 +124,6 @@ class Message implements \IteratorAggregate, MessageInterface { $buffer .= $frame->getPayload(); } - echo "Reassembled " . strlen($buffer) . " bytes in " . $this->_frames->count() . " frames\n"; - return $buffer; } From af15a56cb411541c9b00ad7608982ea39f2ca04d Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Tue, 26 May 2015 19:07:26 -0400 Subject: [PATCH 19/56] Custom error responses for failed handshake Including React SocketClient for client tests Use re-entrants in test server --- composer.json | 3 +- src/Handshake/Negotiator.php | 47 ++++++++++++++++++--------- src/Handshake/NegotiatorInterface.php | 4 +-- tests/ab/startServer.php | 12 +++---- 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/composer.json b/composer.json index 88b6f56..7d0f4e7 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "guzzlehttp/psr7": "^1.0" }, "require-dev": { - "react/http": "^0.4.1" + "react/http": "^0.4.1", + "react/socket-client": "^0.4.3" } } diff --git a/src/Handshake/Negotiator.php b/src/Handshake/Negotiator.php index 2edb2d5..9fe26c0 100644 --- a/src/Handshake/Negotiator.php +++ b/src/Handshake/Negotiator.php @@ -1,8 +1,7 @@ getHeader('Sec-WebSocket-Version'); - - return ($this->getVersionNumber() === $version); + return $this->verifier->verifyVersion($request->getHeader('Sec-WebSocket-Version')); } /** @@ -46,26 +43,46 @@ class Negotiator implements NegotiatorInterface { * {@inheritdoc} */ public function handshake(RequestInterface $request) { - if (true !== $this->verifier->verifyAll($request)) { + if (true !== $this->verifier->verifyMethod($request->getMethod())) { + return new Response(405); + } + + 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); + } + + if (true !== $this->verifier->verifyUpgradeRequest($request->getHeader('Upgrade'))) { + return new Response(400, [], '1.1', null, 'Upgrade header MUST be provided'); + } + + if (true !== $this->verifier->verifyConnection($request->getHeader('Connection'))) { + return new Response(400, [], '1.1', null, 'Connection header MUST be provided'); + } + + if (true !== $this->verifier->verifyKey($request->getHeader('Sec-WebSocket-Key'))) { + return new Response(400, [], '1.1', null, 'Invalid Sec-WebSocket-Key'); + } + + if (true !== $this->verifier->verifyVersion($request->getHeader('Sec-WebSocket-Version'))) { + return new Response(426, ['Sec-WebSocket-Version' => $this->getVersionNumber()]); + } + return new Response(101, [ 'Upgrade' => 'websocket' , 'Connection' => 'Upgrade' , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]) + , 'X-Powered-By' => 'Ratchet' ]); } - /** - * @deprecated - The logic belons somewhere else - * @param \Ratchet\WebSocket\Version\RFC6455\Connection $from - * @param string $data - */ -// public function onMessage(ConnectionInterface $from, $data) { -// -// } - /** * Used when doing the handshake to encode the key, verifying client/server are speaking the same language * @param string $key diff --git a/src/Handshake/NegotiatorInterface.php b/src/Handshake/NegotiatorInterface.php index a5032f8..1eb2cbe 100644 --- a/src/Handshake/NegotiatorInterface.php +++ b/src/Handshake/NegotiatorInterface.php @@ -1,8 +1,6 @@ on('request', function (\React\Http\Request $request, \React\Http\Response $response) { - $conn = new ConnectionContext($response); +$encodingValidator = new \Ratchet\RFC6455\Encoding\Validator; +$negotiator = new \Ratchet\RFC6455\Handshake\Negotiator($encodingValidator); +$ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($encodingValidator); - $encodingValidator = new \Ratchet\RFC6455\Encoding\Validator; +$server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $ms) { + $conn = new ConnectionContext($response); // 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($encodingValidator); - $negotiatorResponse = $negotiator->handshake($psrRequest); $response->writeHead( @@ -89,8 +89,6 @@ $server->on('request', function (\React\Http\Request $request, \React\Http\Respo return; } - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($encodingValidator); - $request->on('data', function ($data) use ($ms, $conn) { $ms->onData($data, $conn); }); From 5cdd8959dc0ad2f22171ca9e0deb05fc5ef378f8 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 30 May 2015 23:24:15 -0400 Subject: [PATCH 20/56] Refactoring onMessage delivers unMaked payload msg validation moved from Message to MessageValidation Unify return types Context return should be input Remove deprecated Connection --- src/Connection.php | 45 ------------- src/Messaging/Protocol/Message.php | 33 ++-------- src/Messaging/Streaming/ContextInterface.php | 8 +++ src/Messaging/Streaming/MessageStreamer.php | 42 ++++++------ src/Messaging/Validation/MessageValidator.php | 65 ++++++++----------- tests/ab/startServer.php | 8 +-- 6 files changed, 67 insertions(+), 134 deletions(-) delete mode 100644 src/Connection.php diff --git a/src/Connection.php b/src/Connection.php deleted file mode 100644 index fd404f3..0000000 --- a/src/Connection.php +++ /dev/null @@ -1,45 +0,0 @@ -WebSocket->closing) { - if (!($msg instanceof DataInterface)) { - $msg = new Frame($msg); - } - - $this->getConnection()->send($msg->getContents()); - } - - return $this; - } - - /** - * {@inheritdoc} - */ - public function close($code = 1000) { - if ($this->WebSocket->closing) { - return; - } - - if ($code instanceof DataInterface) { - $this->send($code); - } else { - $this->send(new Frame(pack('n', $code), true, Frame::OP_CLOSE)); - } - - $this->getConnection()->close(); - - $this->WebSocket->closing = true; - } -} diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index be87123..2ac28a7 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -7,9 +7,6 @@ class Message implements \IteratorAggregate, MessageInterface { */ private $_frames; - /** @var bool */ - private $binary = false; - public function __construct() { $this->_frames = new \SplDoublyLinkedList; } @@ -56,30 +53,9 @@ class Message implements \IteratorAggregate, MessageInterface { /** * {@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) { - // should the validation stuff be somewhere else? - // it really needs the context of the message to know whether there is a problem - if ($this->_frames->isEmpty()) { - $this->binary = $fragment->getOpcode() == Frame::OP_BINARY; - } - - // check to see if this is a continuation frame when there is no - // frames yet added - if ($this->_frames->count() == 0 && $fragment->getOpcode() == Frame::OP_CONTINUE) { - return Frame::CLOSE_PROTOCOL; - } - - // check to see if this is not a continuation frame when there is already frames - if ($this->_frames->count() > 0 && $fragment->getOpcode() != Frame::OP_CONTINUE) { - return Frame::CLOSE_PROTOCOL; - } - $this->_frames->push($fragment); - - return true; - //return $this; } /** @@ -147,8 +123,11 @@ class Message implements \IteratorAggregate, MessageInterface { /** * @return boolean */ - public function isBinary() - { - return $this->binary; + 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(); } } diff --git a/src/Messaging/Streaming/ContextInterface.php b/src/Messaging/Streaming/ContextInterface.php index 5f29ff8..0bd101f 100644 --- a/src/Messaging/Streaming/ContextInterface.php +++ b/src/Messaging/Streaming/ContextInterface.php @@ -4,6 +4,10 @@ use Ratchet\RFC6455\Messaging\Protocol\MessageInterface; use Ratchet\RFC6455\Messaging\Protocol\FrameInterface; interface ContextInterface { + /** + * @param FrameInterface $frame + * @return FrameInterface + */ public function setFrame(FrameInterface $frame = null); /** @@ -11,6 +15,10 @@ interface ContextInterface { */ public function getFrame(); + /** + * @param MessageInterface $message + * @return MessageInterface + */ public function setMessage(MessageInterface $message = null); /** diff --git a/src/Messaging/Streaming/MessageStreamer.php b/src/Messaging/Streaming/MessageStreamer.php index d45fe5a..4c541e0 100644 --- a/src/Messaging/Streaming/MessageStreamer.php +++ b/src/Messaging/Streaming/MessageStreamer.php @@ -6,7 +6,9 @@ use Ratchet\RFC6455\Messaging\Protocol\Message; use Ratchet\RFC6455\Messaging\Validation\MessageValidator; class MessageStreamer { - /** @var MessageValidator */ + /** + * @var MessageValidator + */ private $validator; function __construct(ValidatorInterface $encodingValidator, $expectMask = false) { @@ -17,27 +19,27 @@ class MessageStreamer { public function onData($data, ContextInterface $context) { $overflow = ''; - $context->getMessage() || $context->setMessage($this->newMessage()); - $context->getFrame() || $context->setFrame($this->newFrame()); - - $frame = $context->getFrame(); + $message = $context->getMessage() ?: $context->setMessage($this->newMessage()); + $frame = $context->getFrame() ?: $context->setFrame($this->newFrame()); $frame->addBuffer($data); if ($frame->isCoalesced()) { - $validFrame = $this->validator->validateFrame($frame); - if (true !== $validFrame) { - $context->onClose($validFrame); + $frameCount = $message->count(); + $prevFrame = $frameCount > 0 ? $message[$frameCount - 1] : null; - return; + $frameStatus = $this->validator->validateFrame($frame, $prevFrame); + + if (0 !== $frameStatus) { + return $context->onClose($frameStatus); } $opcode = $frame->getOpcode(); if ($opcode > 2) { switch ($opcode) { - case $frame::OP_PING: + case Frame::OP_PING: $context->onPing($frame); break; - case $frame::OP_PONG: + case Frame::OP_PONG: $context->onPong($frame); break; } @@ -54,20 +56,18 @@ class MessageStreamer { $overflow = $frame->extractOverflow(); - $frameAdded = $context->getMessage()->addFrame($frame); - if (true !== $frameAdded) { - $context->onClose($frameAdded); - } + $frame->unMaskPayload(); + $message->addFrame($frame); $context->setFrame(null); } - if ($context->getMessage()->isCoalesced()) { - $msgCheck = $this->validator->checkMessage($context->getMessage()); - if ($msgCheck !== true) { - $context->onClose($msgCheck); - return; + if ($message->isCoalesced()) { + $msgCheck = $this->validator->checkMessage($message); + if (true !== $msgCheck) { + return $context->onClose($msgCheck); } - $context->onMessage($context->getMessage()); + + $context->onMessage($message); $context->setMessage(null); } diff --git a/src/Messaging/Validation/MessageValidator.php b/src/Messaging/Validation/MessageValidator.php index b67eb34..de51a9c 100644 --- a/src/Messaging/Validation/MessageValidator.php +++ b/src/Messaging/Validation/MessageValidator.php @@ -18,31 +18,11 @@ class MessageValidator { /** * Determine if a message is valid * @param \Ratchet\RFC6455\Messaging\Protocol\MessageInterface - * @return bool|int true if valid - false if incomplete - int of recomended close code + * @return bool|int true if valid - false if incomplete - int of recommended close code */ public function checkMessage(MessageInterface $message) { - // Need a progressive and complete check...this is only satisfying complete - if (!$message->isCoalesced()) { - return false; - } - $frame = $message[0]; - $frameCheck = $this->validateFrame($frame); - if (true !== $frameCheck) { - return $frameCheck; - } - - // This seems incorrect - how could a frame exist with message count being 0? - if ($frame::OP_CONTINUE === $frame->getOpcode() && 0 === count($message)) { - return $frame::CLOSE_PROTOCOL; - } - - // I (mbonneau) don't understand this - seems to always kill the tests -// if (count($message) > 0 && $frame::OP_CONTINUE !== $frame->getOpcode()) { -// return $frame::CLOSE_PROTOCOL; -// } - if (!$message->isBinary()) { $parsed = $message->getPayload(); if (!$this->validator->checkEncoding($parsed, 'UTF-8')) { @@ -54,41 +34,42 @@ class MessageValidator { } /** - * @param Frame $frame - * @return bool|int Return true if everything is good, an integer close code if not + * @param FrameInterface $frame + * @param FrameInterface $previousFrame + * @return int Return 0 if everything is good, an integer close code if not */ - public function validateFrame(Frame $frame) { + public function validateFrame(FrameInterface $frame, FrameInterface $previousFrame = null) { if (false !== $frame->getRsv1() || false !== $frame->getRsv2() || false !== $frame->getRsv3() ) { - return $frame::CLOSE_PROTOCOL; + return Frame::CLOSE_PROTOCOL; } // Should be checking all frames if ($this->checkForMask && !$frame->isMasked()) { - return $frame::CLOSE_PROTOCOL; + return Frame::CLOSE_PROTOCOL; } $opcode = $frame->getOpcode(); if ($opcode > 2) { if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) { - return $frame::CLOSE_PROTOCOL; + return Frame::CLOSE_PROTOCOL; } switch ($opcode) { - case $frame::OP_CLOSE: + case Frame::OP_CLOSE: $closeCode = 0; $bin = $frame->getPayload(); if (empty($bin)) { - return $frame::CLOSE_NORMAL; + return Frame::CLOSE_NORMAL; } if (strlen($bin) == 1) { - return $frame::CLOSE_PROTOCOL; + return Frame::CLOSE_PROTOCOL; } if (strlen($bin) >= 2) { @@ -96,24 +77,34 @@ class MessageValidator { } if (!$frame->isValidCloseCode($closeCode)) { - return $frame::CLOSE_PROTOCOL; + return Frame::CLOSE_PROTOCOL; } if (!$this->validator->checkEncoding(substr($bin, 2), 'UTF-8')) { - return $frame::CLOSE_BAD_PAYLOAD; + return Frame::CLOSE_BAD_PAYLOAD; } - return $frame::CLOSE_NORMAL; + return Frame::CLOSE_NORMAL; break; - case $frame::OP_PING: - case $frame::OP_PONG: + case Frame::OP_PING: + case Frame::OP_PONG: break; default: - return $frame::CLOSE_PROTOCOL; + return Frame::CLOSE_PROTOCOL; break; } + + return 0; } - return true; + if (Frame::OP_CONTINUE === $frame->getOpcode() && null === $previousFrame) { + return Frame::CLOSE_PROTOCOL; + } + + if (null !== $previousFrame && Frame::OP_CONTINUE != $frame->getOpcode()) { + return Frame::CLOSE_PROTOCOL; + } + + return 0; } } diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 0d8ef88..3cdde1e 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -18,6 +18,8 @@ class ConnectionContext implements Ratchet\RFC6455\Messaging\Streaming\ContextIn public function setFrame(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame = null) { $this->_frame = $frame; + + return $frame; } public function getFrame() { @@ -26,6 +28,8 @@ class ConnectionContext implements Ratchet\RFC6455\Messaging\Streaming\ContextIn public function setMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $message = null) { $this->_message = $message; + + return $message; } public function getMessage() { @@ -33,10 +37,6 @@ class ConnectionContext implements Ratchet\RFC6455\Messaging\Streaming\ContextIn } public function onMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) { - foreach ($msg as $frame) { - $frame->unMaskPayload(); - } - $this->_conn->write($msg->getContents()); } From 621b8f836c39aba43f3360d69e6207be38db4bf7 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 30 May 2015 23:27:54 -0400 Subject: [PATCH 21/56] Re-use exception UnderflowExceptions have been used as flow control Now have a factory to re-throw the same one to not generate a stack trace. --- src/Messaging/Protocol/Frame.php | 39 +++++++++++++-------- src/Messaging/Streaming/MessageStreamer.php | 9 ++++- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/Messaging/Protocol/Frame.php b/src/Messaging/Protocol/Frame.php index fd767c4..4018d7a 100644 --- a/src/Messaging/Protocol/Frame.php +++ b/src/Messaging/Protocol/Frame.php @@ -61,12 +61,23 @@ class Frame implements FrameInterface { */ protected $secondByte = -1; + /** + * @var callable + * @returns \UnderflowException + */ + private $ufeg; + /** * @param string|null $payload * @param bool $final * @param int $opcode + * @param callable $ufExceptionFactory<\UnderflowException> */ - public function __construct($payload = null, $final = true, $opcode = 1) { + public function __construct($payload = null, $final = true, $opcode = 1, callable $ufExceptionFactory = null) { + $this->ufeg = $ufExceptionFactory ?: function($msg = '') { + return new \UnderflowException($msg); + }; + if (null === $payload) { return; } @@ -132,7 +143,7 @@ class Frame implements FrameInterface { */ public function isFinal() { if (-1 === $this->firstByte) { - throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message'); + 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); @@ -144,7 +155,7 @@ class Frame implements FrameInterface { */ public function getRsv1() { if (-1 === $this->firstByte) { - throw new \UnderflowException('Not enough bytes received to determine reserved bit'); + throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit'); } return 64 === ($this->firstByte & 64); @@ -156,7 +167,7 @@ class Frame implements FrameInterface { */ public function getRsv2() { if (-1 === $this->firstByte) { - throw new \UnderflowException('Not enough bytes received to determine reserved bit'); + throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit'); } return 32 === ($this->firstByte & 32); @@ -168,7 +179,7 @@ class Frame implements FrameInterface { */ public function getRsv3() { if (-1 === $this->firstByte) { - throw new \UnderflowException('Not enough bytes received to determine reserved bit'); + throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit'); } return 16 == ($this->firstByte & 16); @@ -179,7 +190,7 @@ class Frame implements FrameInterface { */ public function isMasked() { if (-1 === $this->secondByte) { - throw new \UnderflowException("Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set"); + throw call_user_func($this->ufeg, "Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set"); } return 128 === ($this->secondByte & 128); @@ -196,7 +207,7 @@ class Frame implements FrameInterface { $start = 1 + $this->getNumPayloadBytes(); if ($this->bytesRecvd < $start + static::MASK_LENGTH) { - throw new \UnderflowException('Not enough data buffered to calculate the masking key'); + throw call_user_func($this->ufeg, 'Not enough data buffered to calculate the masking key'); } return substr($this->data, $start, static::MASK_LENGTH); @@ -256,7 +267,7 @@ class Frame implements FrameInterface { */ public function unMaskPayload() { if (!$this->isCoalesced()) { - throw new \UnderflowException('Frame must be coalesced before applying mask'); + throw call_user_func($this->ufeg, 'Frame must be coalesced before applying mask'); } if (!$this->isMasked()) { @@ -286,7 +297,7 @@ class Frame implements FrameInterface { public function applyMask($maskingKey, $payload = null) { if (null === $payload) { if (!$this->isCoalesced()) { - throw new \UnderflowException('Frame must be coalesced to apply a mask'); + throw call_user_func($this->ufeg, 'Frame must be coalesced to apply a mask'); } $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); @@ -305,7 +316,7 @@ class Frame implements FrameInterface { */ public function getOpcode() { if (-1 === $this->firstByte) { - throw new \UnderflowException('Not enough bytes received to determine opcode'); + throw call_user_func($this->ufeg, 'Not enough bytes received to determine opcode'); } return ($this->firstByte & ~240); @@ -318,7 +329,7 @@ class Frame implements FrameInterface { */ protected function getFirstPayloadVal() { if (-1 === $this->secondByte) { - throw new \UnderflowException('Not enough bytes received'); + throw call_user_func($this->ufeg, 'Not enough bytes received'); } return $this->secondByte & 127; @@ -330,7 +341,7 @@ class Frame implements FrameInterface { */ protected function getNumPayloadBits() { if (-1 === $this->secondByte) { - throw new \UnderflowException('Not enough bytes received'); + throw call_user_func($this->ufeg, 'Not enough bytes received'); } // By default 7 bits are used to describe the payload length @@ -378,7 +389,7 @@ class Frame implements FrameInterface { $byte_length = $this->getNumPayloadBytes(); if ($this->bytesRecvd < 1 + $byte_length) { $this->defPayLen = -1; - throw new \UnderflowException('Not enough data buffered to determine payload length'); + throw call_user_func($this->ufeg, 'Not enough data buffered to determine payload length'); } $len = 0; @@ -405,7 +416,7 @@ class Frame implements FrameInterface { */ public function getPayload() { if (!$this->isCoalesced()) { - throw new \UnderflowException('Can not return partial message'); + throw call_user_func($this->ufeg, 'Can not return partial message'); } $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); diff --git a/src/Messaging/Streaming/MessageStreamer.php b/src/Messaging/Streaming/MessageStreamer.php index 4c541e0..7746dbd 100644 --- a/src/Messaging/Streaming/MessageStreamer.php +++ b/src/Messaging/Streaming/MessageStreamer.php @@ -11,8 +11,15 @@ class MessageStreamer { */ private $validator; + private $exceptionFactory; + function __construct(ValidatorInterface $encodingValidator, $expectMask = false) { $this->validator = new MessageValidator($encodingValidator, !$expectMask); + + $exception = new \UnderflowException; + $this->exceptionFactory = function() use ($exception) { + return $exception; + }; } @@ -90,6 +97,6 @@ class MessageStreamer { * @return \Ratchet\RFC6455\Messaging\Protocol\FrameInterface */ public function newFrame($payload = null, $final = null, $opcode = null) { - return new Frame($payload, $final, $opcode); + return new Frame($payload, $final, $opcode, $this->exceptionFactory); } } \ No newline at end of file From ce50c6ceb414c28ac949176b41dbaa6cea748b09 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 30 May 2015 23:28:31 -0400 Subject: [PATCH 22/56] Frame masking perf refs ratchetphp/ratchet#226 --- src/Messaging/Protocol/Frame.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Messaging/Protocol/Frame.php b/src/Messaging/Protocol/Frame.php index 4018d7a..a06d22e 100644 --- a/src/Messaging/Protocol/Frame.php +++ b/src/Messaging/Protocol/Frame.php @@ -303,6 +303,16 @@ class Frame implements FrameInterface { $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]; From e45cd158bd881b92f590c066aa12ef8e53555e1e Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 31 May 2015 15:24:44 -0400 Subject: [PATCH 23/56] Add support for Sub-Protocol selection --- src/Handshake/Negotiator.php | 37 ++++++++++++++++++--------- src/Handshake/NegotiatorInterface.php | 6 ++--- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/Handshake/Negotiator.php b/src/Handshake/Negotiator.php index 9fe26c0..fb72156 100644 --- a/src/Handshake/Negotiator.php +++ b/src/Handshake/Negotiator.php @@ -19,6 +19,10 @@ class Negotiator implements NegotiatorInterface { */ private $validator; + private $_supportedSubProtocols = []; + + private $_strictSubProtocols = true; + public function __construct(ValidatorInterface $validator) { $this->verifier = new RequestVerifier; @@ -75,12 +79,27 @@ class Negotiator implements NegotiatorInterface { return new Response(426, ['Sec-WebSocket-Version' => $this->getVersionNumber()]); } - return new Response(101, [ + $headers = []; + if (count($this->_supportedSubProtocols) > 0) { + $subProtocols = $request->getHeader('Sec-WebSocket-Protocol'); + + $match = array_reduce($subProtocols, function($accumulator, $protocol) { + return $accumulator ?: (isset($this->_supportedSubProtocols[$protocol]) ? $protocol : null); + }, null); + + if ($this->_strictSubProtocols && null === $match) { + return new Response(400, [], '1.1', null ,'No Sec-WebSocket-Protocols requested supported'); + } + + $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' - ]); + ])); } /** @@ -93,13 +112,8 @@ class Negotiator implements NegotiatorInterface { return base64_encode(sha1($key . static::GUID, true)); } - /** - * Add supported protocols. If the request has any matching the response will include one - * @param string $id - */ - function addSupportedSubProtocol($id) - { - // TODO: Implement addSupportedSubProtocol() method. + function setSupportedSubProtocols(array $protocols) { + $this->_supportedSubProtocols = array_flip($protocols); } /** @@ -110,8 +124,7 @@ class Negotiator implements NegotiatorInterface { * 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) - { - // TODO: Implement setStrictSubProtocolCheck() method. + function setStrictSubProtocolCheck($enable) { + $this->_strictSubProtocols = (boolean)$enable; } } diff --git a/src/Handshake/NegotiatorInterface.php b/src/Handshake/NegotiatorInterface.php index 1eb2cbe..662ae95 100644 --- a/src/Handshake/NegotiatorInterface.php +++ b/src/Handshake/NegotiatorInterface.php @@ -31,15 +31,15 @@ interface NegotiatorInterface { /** * Add supported protocols. If the request has any matching the response will include one - * @param string $id + * @param array $protocols */ - function addSupportedSubProtocol($id); + function setSupportedSubProtocols(array $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. + * @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. */ From d8babac7e7a97b6395dba0df7a33b8f50ce96251 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 31 May 2015 15:43:58 -0400 Subject: [PATCH 24/56] Fixed sub-protocol handling --- src/Handshake/Negotiator.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Handshake/Negotiator.php b/src/Handshake/Negotiator.php index fb72156..3c9c7d0 100644 --- a/src/Handshake/Negotiator.php +++ b/src/Handshake/Negotiator.php @@ -80,8 +80,9 @@ class Negotiator implements NegotiatorInterface { } $headers = []; - if (count($this->_supportedSubProtocols) > 0) { - $subProtocols = $request->getHeader('Sec-WebSocket-Protocol'); + $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); @@ -91,14 +92,16 @@ class Negotiator implements NegotiatorInterface { return new Response(400, [], '1.1', null ,'No Sec-WebSocket-Protocols requested supported'); } - $headers['Sec-WebSocket-Protocol'] = $match; + 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' + , 'Connection' => 'Upgrade' + , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]) + , 'X-Powered-By' => 'Ratchet' ])); } From 59464f855cb38fc8c46b35dbe35446d8b00d3026 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 1 Jun 2015 22:39:17 -0400 Subject: [PATCH 25/56] Replace ContextInterface with callable's Replace ContextInterface with callable's Move message/frame validation back into streamer Always return frame objects from check Move close code validation to validator, not base element --- src/Messaging/Protocol/Frame.php | 33 +-- src/Messaging/Protocol/Message.php | 2 +- src/Messaging/Streaming/ContextInterface.php | 37 --- src/Messaging/Streaming/MessageStreamer.php | 212 +++++++++++++++--- src/Messaging/Validation/MessageValidator.php | 110 --------- tests/ab/startServer.php | 78 ++----- 6 files changed, 195 insertions(+), 277 deletions(-) delete mode 100644 src/Messaging/Streaming/ContextInterface.php delete mode 100644 src/Messaging/Validation/MessageValidator.php diff --git a/src/Messaging/Protocol/Frame.php b/src/Messaging/Protocol/Frame.php index a06d22e..88aa818 100644 --- a/src/Messaging/Protocol/Frame.php +++ b/src/Messaging/Protocol/Frame.php @@ -71,7 +71,7 @@ class Frame implements FrameInterface { * @param string|null $payload * @param bool $final * @param int $opcode - * @param callable $ufExceptionFactory<\UnderflowException> + * @param callable<\UnderflowException> $ufExceptionFactory */ public function __construct($payload = null, $final = true, $opcode = 1, callable $ufExceptionFactory = null) { $this->ufeg = $ufExceptionFactory ?: function($msg = '') { @@ -449,7 +449,6 @@ class Frame implements FrameInterface { /** * 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() { @@ -467,34 +466,4 @@ class Frame implements FrameInterface { return ''; } - - /** - * Determine if a close code is valid - * @param int|string - * @return bool - */ - public function isValidCloseCode($val) { - if (in_array($val, [ - static::CLOSE_NORMAL, - static::CLOSE_GOING_AWAY, - static::CLOSE_PROTOCOL, - static::CLOSE_BAD_DATA, - //static::CLOSE_NO_STATUS, - //static::CLOSE_ABNORMAL, - static::CLOSE_BAD_PAYLOAD, - static::CLOSE_POLICY, - static::CLOSE_TOO_BIG, - static::CLOSE_MAND_EXT, - static::CLOSE_SRV_ERR, - //static::CLOSE_TLS, - ])) { - return true; - } - - if ($val >= 3000 && $val <= 4999) { - return true; - } - - return false; - } } diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index 2ac28a7..0a95f19 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -35,7 +35,7 @@ class Message implements \IteratorAggregate, MessageInterface { } public function offsetUnset($index) { - throw new \DomainException('Frame access in messages is read-only'); + unset($this->_frames[$index]); } /** diff --git a/src/Messaging/Streaming/ContextInterface.php b/src/Messaging/Streaming/ContextInterface.php deleted file mode 100644 index 0bd101f..0000000 --- a/src/Messaging/Streaming/ContextInterface.php +++ /dev/null @@ -1,37 +0,0 @@ -validator = new MessageValidator($encodingValidator, !$expectMask); + /** + * @var bool + */ + private $checkForMask; + + /** + * @var array + */ + private $validCloseCodes; + + function __construct(ValidatorInterface $encodingValidator, $expectMask = true) { + $this->validator = $encodingValidator; + $this->checkForMask = (bool)$expectMask; $exception = new \UnderflowException; $this->exceptionFactory = function() use ($exception) { return $exception; }; + + $this->noop = function() {}; + + $this->validCloseCodes = [ + 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 onData($data, ContextInterface $context) { + /** + * @param $data + * @param mixed $context + * @param MessageInterface $message + * @param callable(MessageInterface) $onMessage + * @param callable(FrameInterface) $onControl + * @return MessageInterface + */ + public function onData($data, $context, MessageInterface $message = null, callable $onMessage, callable $onControl = null) { $overflow = ''; - $message = $context->getMessage() ?: $context->setMessage($this->newMessage()); - $frame = $context->getFrame() ?: $context->setFrame($this->newFrame()); + $onControl ?: $this->noop; + $message ?: $message = $this->newMessage(); + + $prevFrame = null; + $frameCount = count($message); + + if ($frameCount > 0) { + $frame = $message[$frameCount - 1]; + + if ($frame->isCoalesced()) { + $prevFrame = $frame; + $frame = $this->newFrame(); + $message->addFrame($frame); + $frameCount++; + } elseif ($frameCount > 1) { + $prevFrame = $message[$frameCount - 2]; + } + } else { + $frame = $this->newFrame(); + $message->addFrame($frame); + $frameCount++; + } $frame->addBuffer($data); if ($frame->isCoalesced()) { - $frameCount = $message->count(); - $prevFrame = $frameCount > 0 ? $message[$frameCount - 1] : null; - - $frameStatus = $this->validator->validateFrame($frame, $prevFrame); - - if (0 !== $frameStatus) { - return $context->onClose($frameStatus); - } + $frame = $this->frameCheck($frame, $prevFrame); $opcode = $frame->getOpcode(); if ($opcode > 2) { - switch ($opcode) { - case Frame::OP_PING: - $context->onPing($frame); - break; - case Frame::OP_PONG: - $context->onPong($frame); - break; - } + $onControl($frame, $context); + unset($message[$frameCount - 1]); $overflow = $frame->extractOverflow(); - $context->setFrame(null); if (strlen($overflow) > 0) { - $this->onData($overflow, $context); + $message = $this->onData($overflow, $context, $message, $onMessage, $onControl); } - return; + return $message; } $overflow = $frame->extractOverflow(); $frame->unMaskPayload(); - $message->addFrame($frame); - $context->setFrame(null); } if ($message->isCoalesced()) { - $msgCheck = $this->validator->checkMessage($message); + $msgCheck = $this->checkMessage($message); if (true !== $msgCheck) { - return $context->onClose($msgCheck); + $onControl($this->newCloseFrame($msgCheck), $context); + + return $this->newMessage(); } - $context->onMessage($message); - $context->setMessage(null); + $onMessage($message, $context); + $message = $this->newMessage(); } if (strlen($overflow) > 0) { - $this->onData($overflow, $context); + $this->onData($overflow, $context, $message, $onMessage, $onControl); } + + return $message; + } + + /** + * Check a frame and previous frame in a message; returns the frame that should be dealt with + * @param \Ratchet\RFC6455\Messaging\Protocol\FrameInterface|FrameInterface $frame + * @param \Ratchet\RFC6455\Messaging\Protocol\FrameInterface|FrameInterface $previousFrame + * @return \Ratchet\RFC6455\Messaging\Protocol\FrameInterface|FrameInterface + */ + public function frameCheck(FrameInterface $frame, FrameInterface $previousFrame = null) { + if (false !== $frame->getRsv1() || + false !== $frame->getRsv2() || + false !== $frame->getRsv3() + ) { + return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); + } + + if ($this->checkForMask && !$frame->isMasked()) { + return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); + } + + $opcode = $frame->getOpcode(); + + if ($opcode > 2) { + if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) { + return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); + } + + 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); + } + + if (strlen($bin) >= 2) { + list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); + } + + if (!$this->isValidCloseCode($closeCode)) { + return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); + } + + if (!$this->validator->checkEncoding(substr($bin, 2), 'UTF-8')) { + return $this->newCloseFrame(Frame::CLOSE_BAD_PAYLOAD); + } + + return $this->newCloseFrame(Frame::CLOSE_NORMAL); + break; + case Frame::OP_PING: + case Frame::OP_PONG: + break; + default: + return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); + break; + } + + return $frame; + } + + if (Frame::OP_CONTINUE === $frame->getOpcode() && null === $previousFrame) { + return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); + } + + if (null !== $previousFrame && Frame::OP_CONTINUE != $frame->getOpcode()) { + return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); + } + + return $frame; + } + + /** + * Determine if a message is valid + * @param \Ratchet\RFC6455\Messaging\Protocol\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->validator->checkEncoding($message->getPayload(), 'UTF-8')) { + return Frame::CLOSE_BAD_PAYLOAD; + } + } + + return true; } /** @@ -99,4 +233,12 @@ class MessageStreamer { public function newFrame($payload = null, $final = null, $opcode = null) { return new Frame($payload, $final, $opcode, $this->exceptionFactory); } + + public function newCloseFrame($code) { + return $this->newFrame(pack('n', $code), true, Frame::OP_CLOSE); + } + + public function isValidCloseCode($val) { + return ($val >= 3000 && $val <= 4999) || in_array($val, $this->validCloseCodes); + } } \ No newline at end of file diff --git a/src/Messaging/Validation/MessageValidator.php b/src/Messaging/Validation/MessageValidator.php deleted file mode 100644 index de51a9c..0000000 --- a/src/Messaging/Validation/MessageValidator.php +++ /dev/null @@ -1,110 +0,0 @@ -validator = $validator; - $this->checkForMask = $checkForMask; - } - - /** - * Determine if a message is valid - * @param \Ratchet\RFC6455\Messaging\Protocol\MessageInterface - * @return bool|int true if valid - false if incomplete - int of recommended close code - */ - public function checkMessage(MessageInterface $message) { - $frame = $message[0]; - - if (!$message->isBinary()) { - $parsed = $message->getPayload(); - if (!$this->validator->checkEncoding($parsed, 'UTF-8')) { - return $frame::CLOSE_BAD_PAYLOAD; - } - } - - return true; - } - - /** - * @param FrameInterface $frame - * @param FrameInterface $previousFrame - * @return int Return 0 if everything is good, an integer close code if not - */ - public function validateFrame(FrameInterface $frame, FrameInterface $previousFrame = null) { - if (false !== $frame->getRsv1() || - false !== $frame->getRsv2() || - false !== $frame->getRsv3() - ) { - return Frame::CLOSE_PROTOCOL; - } - - // Should be checking all frames - if ($this->checkForMask && !$frame->isMasked()) { - return Frame::CLOSE_PROTOCOL; - } - - $opcode = $frame->getOpcode(); - - if ($opcode > 2) { - if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) { - return Frame::CLOSE_PROTOCOL; - } - - switch ($opcode) { - case Frame::OP_CLOSE: - $closeCode = 0; - - $bin = $frame->getPayload(); - - if (empty($bin)) { - return Frame::CLOSE_NORMAL; - } - - if (strlen($bin) == 1) { - return Frame::CLOSE_PROTOCOL; - } - - if (strlen($bin) >= 2) { - list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); - } - - if (!$frame->isValidCloseCode($closeCode)) { - return Frame::CLOSE_PROTOCOL; - } - - if (!$this->validator->checkEncoding(substr($bin, 2), 'UTF-8')) { - return Frame::CLOSE_BAD_PAYLOAD; - } - - return Frame::CLOSE_NORMAL; - break; - case Frame::OP_PING: - case Frame::OP_PONG: - break; - default: - return Frame::CLOSE_PROTOCOL; - break; - } - - return 0; - } - - if (Frame::OP_CONTINUE === $frame->getOpcode() && null === $previousFrame) { - return Frame::CLOSE_PROTOCOL; - } - - if (null !== $previousFrame && Frame::OP_CONTINUE != $frame->getOpcode()) { - return Frame::CLOSE_PROTOCOL; - } - - return 0; - } -} diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 3cdde1e..b316323 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -1,65 +1,10 @@ _conn = $connectionContext; - } - - public function setFrame(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame = null) { - $this->_frame = $frame; - - return $frame; - } - - public function getFrame() { - return $this->_frame; - } - - public function setMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $message = null) { - $this->_message = $message; - - return $message; - } - - public function getMessage() { - return $this->_message; - } - - public function onMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) { - $this->_conn->write($msg->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); @@ -69,9 +14,6 @@ $negotiator = new \Ratchet\RFC6455\Handshake\Negotiator($encodingValidator); $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($encodingValidator); $server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $ms) { - $conn = new ConnectionContext($response); - - // make the React Request a Psr7 request (not perfect) $psrRequest = new \GuzzleHttp\Psr7\Request($request->getMethod(), $request->getPath(), $request->getHeaders()); $negotiatorResponse = $negotiator->handshake($psrRequest); @@ -89,8 +31,20 @@ $server->on('request', function (\React\Http\Request $request, \React\Http\Respo return; } - $request->on('data', function ($data) use ($ms, $conn) { - $ms->onData($data, $conn); + $msg = null; + $request->on('data', function($data) use ($ms, $response, &$msg) { + $msg = $ms->onData($data, $response, $msg, function(MessageInterface $msg, \React\Http\Response $conn) { + $conn->write($msg->getContents()); + }, function(FrameInterface $frame, \React\Http\Response $conn) use ($ms) { + switch ($frame->getOpCode()) { + case Frame::OP_CLOSE: + $conn->end($frame->getContents()); + break; + case Frame::OP_PING: + $conn->write($ms->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents()); + break; + } + }); }); }); $socket->listen(9001, '0.0.0.0'); From 06263cd9a540ade2b7a81ad21a9227816fc66655 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Thu, 4 Jun 2015 23:20:05 -0400 Subject: [PATCH 26/56] Reverse the order of context to make optional --- src/Messaging/Streaming/MessageStreamer.php | 6 +++--- tests/ab/startServer.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Messaging/Streaming/MessageStreamer.php b/src/Messaging/Streaming/MessageStreamer.php index 47ed27b..0869612 100644 --- a/src/Messaging/Streaming/MessageStreamer.php +++ b/src/Messaging/Streaming/MessageStreamer.php @@ -59,7 +59,7 @@ class MessageStreamer { * @param callable(FrameInterface) $onControl * @return MessageInterface */ - public function onData($data, $context, MessageInterface $message = null, callable $onMessage, callable $onControl = null) { + public function onData($data, MessageInterface $message = null, callable $onMessage, callable $onControl = null, $context = null) { $overflow = ''; $onControl ?: $this->noop; @@ -97,7 +97,7 @@ class MessageStreamer { $overflow = $frame->extractOverflow(); if (strlen($overflow) > 0) { - $message = $this->onData($overflow, $context, $message, $onMessage, $onControl); + $message = $this->onData($overflow, $message, $onMessage, $onControl, $context); } return $message; @@ -121,7 +121,7 @@ class MessageStreamer { } if (strlen($overflow) > 0) { - $this->onData($overflow, $context, $message, $onMessage, $onControl); + $this->onData($overflow, $message, $onMessage, $onControl, $context); } return $message; diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index b316323..2fa6f8c 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -33,7 +33,7 @@ $server->on('request', function (\React\Http\Request $request, \React\Http\Respo $msg = null; $request->on('data', function($data) use ($ms, $response, &$msg) { - $msg = $ms->onData($data, $response, $msg, function(MessageInterface $msg, \React\Http\Response $conn) { + $msg = $ms->onData($data, $msg, function(MessageInterface $msg, \React\Http\Response $conn) { $conn->write($msg->getContents()); }, function(FrameInterface $frame, \React\Http\Response $conn) use ($ms) { switch ($frame->getOpCode()) { @@ -44,7 +44,7 @@ $server->on('request', function (\React\Http\Request $request, \React\Http\Respo $conn->write($ms->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents()); break; } - }); + }, $response); }); }); $socket->listen(9001, '0.0.0.0'); From 3c3588fc8b43fe921448fe0c957db68ef64ce818 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Tue, 22 Dec 2015 20:16:55 -0500 Subject: [PATCH 27/56] MessageStreamer refactor Remove notion of context and nested callbacks Each connection will create an instance of MessageParser to hold message/frame state --- src/Handshake/Negotiator.php | 6 +- src/Messaging/Protocol/CloseFrameChecker.php | 24 +++ src/Messaging/Protocol/Message.php | 16 -- src/Messaging/Protocol/MessageInterface.php | 2 +- src/Messaging/Streaming/MessageStreamer.php | 160 +++++++++---------- tests/ab/startServer.php | 28 ++-- 6 files changed, 116 insertions(+), 120 deletions(-) create mode 100644 src/Messaging/Protocol/CloseFrameChecker.php diff --git a/src/Handshake/Negotiator.php b/src/Handshake/Negotiator.php index 3c9c7d0..80075cd 100644 --- a/src/Handshake/Negotiator.php +++ b/src/Handshake/Negotiator.php @@ -99,9 +99,9 @@ class Negotiator implements NegotiatorInterface { 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' + , 'Connection' => 'Upgrade' + , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]) + , 'X-Powered-By' => 'Ratchet' ])); } diff --git a/src/Messaging/Protocol/CloseFrameChecker.php b/src/Messaging/Protocol/CloseFrameChecker.php new file mode 100644 index 0000000..7556b97 --- /dev/null +++ b/src/Messaging/Protocol/CloseFrameChecker.php @@ -0,0 +1,24 @@ +validCloseCodes = [ + 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); + } +} diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index 0a95f19..1b1ed17 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -22,22 +22,6 @@ class Message implements \IteratorAggregate, MessageInterface { return count($this->_frames); } - public function offsetExists($index) { - return $this->_frames->offsetExists($index); - } - - public function offsetGet($index) { - return $this->_frames->offsetGet($index); - } - - public function offsetSet($index, $newval) { - throw new \DomainException('Frame access in messages is read-only'); - } - - public function offsetUnset($index) { - unset($this->_frames[$index]); - } - /** * {@inheritdoc} */ diff --git a/src/Messaging/Protocol/MessageInterface.php b/src/Messaging/Protocol/MessageInterface.php index 2913d82..f153686 100644 --- a/src/Messaging/Protocol/MessageInterface.php +++ b/src/Messaging/Protocol/MessageInterface.php @@ -1,7 +1,7 @@ validator = $encodingValidator; + function __construct( + ValidatorInterface $encodingValidator, + CloseFrameChecker $frameChecker, + callable $onMessage, + callable $onControl = null, + $expectMask = true + ) { + $this->validator = $encodingValidator; + $this->closeFrameChecker = $frameChecker; $this->checkForMask = (bool)$expectMask; $exception = new \UnderflowException; @@ -36,104 +64,67 @@ class MessageStreamer { return $exception; }; - $this->noop = function() {}; - - $this->validCloseCodes = [ - 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, - ]; + $this->onMessage = $onMessage; + $this->onControl = $onControl ?: function() {}; } /** - * @param $data - * @param mixed $context - * @param MessageInterface $message - * @param callable(MessageInterface) $onMessage - * @param callable(FrameInterface) $onControl - * @return MessageInterface + * @param string $data + * @return null */ - public function onData($data, MessageInterface $message = null, callable $onMessage, callable $onControl = null, $context = null) { - $overflow = ''; + public function onData($data) { + $this->messageBuffer ?: $this->messageBuffer = $this->newMessage(); + $this->frameBuffer ?: $this->frameBuffer = $this->newFrame(); - $onControl ?: $this->noop; - $message ?: $message = $this->newMessage(); + $this->frameBuffer->addBuffer($data); + if (!$this->frameBuffer->isCoalesced()) { + return; + } - $prevFrame = null; - $frameCount = count($message); + $onMessage = $this->onMessage; + $onControl = $this->onControl; - if ($frameCount > 0) { - $frame = $message[$frameCount - 1]; + $this->frameBuffer = $this->frameCheck($this->frameBuffer); - if ($frame->isCoalesced()) { - $prevFrame = $frame; - $frame = $this->newFrame(); - $message->addFrame($frame); - $frameCount++; - } elseif ($frameCount > 1) { - $prevFrame = $message[$frameCount - 2]; + $overflow = $this->frameBuffer->extractOverflow(); + $this->frameBuffer->unMaskPayload(); + + $opcode = $this->frameBuffer->getOpcode(); + + if ($opcode > 2) { + $onControl($this->frameBuffer); + + if (Frame::OP_CLOSE === $opcode) { + return; } } else { - $frame = $this->newFrame(); - $message->addFrame($frame); - $frameCount++; + $this->messageBuffer->addFrame($this->frameBuffer); } - $frame->addBuffer($data); - if ($frame->isCoalesced()) { - $frame = $this->frameCheck($frame, $prevFrame); + $this->frameBuffer = null; - $opcode = $frame->getOpcode(); - if ($opcode > 2) { - $onControl($frame, $context); - unset($message[$frameCount - 1]); - - $overflow = $frame->extractOverflow(); - - if (strlen($overflow) > 0) { - $message = $this->onData($overflow, $message, $onMessage, $onControl, $context); - } - - return $message; - } - - $overflow = $frame->extractOverflow(); - - $frame->unMaskPayload(); - } - - if ($message->isCoalesced()) { - $msgCheck = $this->checkMessage($message); + if ($this->messageBuffer->isCoalesced()) { + $msgCheck = $this->checkMessage($this->messageBuffer); if (true !== $msgCheck) { - $onControl($this->newCloseFrame($msgCheck), $context); - - return $this->newMessage(); + $onControl($this->newCloseFrame($msgCheck)); + } else { + $onMessage($this->messageBuffer); } - $onMessage($message, $context); - $message = $this->newMessage(); + $this->messageBuffer = null; } if (strlen($overflow) > 0) { - $this->onData($overflow, $message, $onMessage, $onControl, $context); + $this->onData($overflow); // PHP doesn't do tail recursion :( } - - return $message; } /** - * Check a frame and previous frame in a message; returns the frame that should be dealt with + * Check a frame to be added to the current message buffer * @param \Ratchet\RFC6455\Messaging\Protocol\FrameInterface|FrameInterface $frame - * @param \Ratchet\RFC6455\Messaging\Protocol\FrameInterface|FrameInterface $previousFrame * @return \Ratchet\RFC6455\Messaging\Protocol\FrameInterface|FrameInterface */ - public function frameCheck(FrameInterface $frame, FrameInterface $previousFrame = null) { + public function frameCheck(FrameInterface $frame) { if (false !== $frame->getRsv1() || false !== $frame->getRsv2() || false !== $frame->getRsv3() @@ -170,7 +161,8 @@ class MessageStreamer { list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); } - if (!$this->isValidCloseCode($closeCode)) { + $checker = $this->closeFrameChecker; + if (!$checker($closeCode)) { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); } @@ -191,11 +183,11 @@ class MessageStreamer { return $frame; } - if (Frame::OP_CONTINUE === $frame->getOpcode() && null === $previousFrame) { + if (Frame::OP_CONTINUE == $frame->getOpcode() && 0 == count($this->messageBuffer)) { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); } - if (null !== $previousFrame && Frame::OP_CONTINUE != $frame->getOpcode()) { + if (count($this->messageBuffer) > 0 && Frame::OP_CONTINUE != $frame->getOpcode()) { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); } @@ -237,8 +229,4 @@ class MessageStreamer { public function newCloseFrame($code) { return $this->newFrame(pack('n', $code), true, Frame::OP_CLOSE); } - - public function isValidCloseCode($val) { - return ($val >= 3000 && $val <= 4999) || in_array($val, $this->validCloseCodes); - } } \ No newline at end of file diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 2fa6f8c..6be6e62 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -10,10 +10,10 @@ $socket = new \React\Socket\Server($loop); $server = new \React\Http\Server($socket); $encodingValidator = new \Ratchet\RFC6455\Encoding\Validator; +$closeFrameChecker = new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker; $negotiator = new \Ratchet\RFC6455\Handshake\Negotiator($encodingValidator); -$ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($encodingValidator); -$server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $ms) { +$server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $encodingValidator, $closeFrameChecker) { $psrRequest = new \GuzzleHttp\Psr7\Request($request->getMethod(), $request->getPath(), $request->getHeaders()); $negotiatorResponse = $negotiator->handshake($psrRequest); @@ -31,21 +31,21 @@ $server->on('request', function (\React\Http\Request $request, \React\Http\Respo return; } - $msg = null; - $request->on('data', function($data) use ($ms, $response, &$msg) { - $msg = $ms->onData($data, $msg, function(MessageInterface $msg, \React\Http\Response $conn) { - $conn->write($msg->getContents()); - }, function(FrameInterface $frame, \React\Http\Response $conn) use ($ms) { - switch ($frame->getOpCode()) { - case Frame::OP_CLOSE: - $conn->end($frame->getContents()); + $parser = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($encodingValidator, $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: - $conn->write($ms->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents()); + case Frame::OP_PING: + $response->write($parser->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents()); break; - } - }, $response); + } }); + + $request->on('data', [$parser, 'onData']); }); + $socket->listen(9001, '0.0.0.0'); $loop->run(); From 1579666238f611876e3662a105f7043a34189250 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Tue, 22 Dec 2015 21:11:46 -0500 Subject: [PATCH 28/56] Accept exception factory for performance gains --- src/Messaging/Streaming/MessageStreamer.php | 8 ++++---- tests/ab/startServer.php | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Messaging/Streaming/MessageStreamer.php b/src/Messaging/Streaming/MessageStreamer.php index b14e8ee..3549b07 100644 --- a/src/Messaging/Streaming/MessageStreamer.php +++ b/src/Messaging/Streaming/MessageStreamer.php @@ -53,15 +53,15 @@ class MessageStreamer { CloseFrameChecker $frameChecker, callable $onMessage, callable $onControl = null, - $expectMask = true + $expectMask = true, + $exceptionFactory = null ) { $this->validator = $encodingValidator; $this->closeFrameChecker = $frameChecker; $this->checkForMask = (bool)$expectMask; - $exception = new \UnderflowException; - $this->exceptionFactory = function() use ($exception) { - return $exception; + $this->exceptionFactory ?: $this->exceptionFactory = function($msg) { + return new \UnderflowException($msg); }; $this->onMessage = $onMessage; diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 6be6e62..47a5316 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -13,7 +13,9 @@ $encodingValidator = new \Ratchet\RFC6455\Encoding\Validator; $closeFrameChecker = new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker; $negotiator = new \Ratchet\RFC6455\Handshake\Negotiator($encodingValidator); -$server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $encodingValidator, $closeFrameChecker) { +$uException = new \UnderflowException; + +$server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $encodingValidator, $closeFrameChecker, $uException) { $psrRequest = new \GuzzleHttp\Psr7\Request($request->getMethod(), $request->getPath(), $request->getHeaders()); $negotiatorResponse = $negotiator->handshake($psrRequest); @@ -42,6 +44,8 @@ $server->on('request', function (\React\Http\Request $request, \React\Http\Respo $response->write($parser->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents()); break; } + }, true, function() use ($uException) { + return $uException; }); $request->on('data', [$parser, 'onData']); From 01ed6ecf72b34301aa59d8950c495ff41c7e3154 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 25 Dec 2015 12:41:26 -0500 Subject: [PATCH 29/56] Strict comparisons --- src/Encoding/Validator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Encoding/Validator.php b/src/Encoding/Validator.php index 7165faa..5b2f7f4 100644 --- a/src/Encoding/Validator.php +++ b/src/Encoding/Validator.php @@ -54,14 +54,14 @@ class Validator implements ValidatorInterface { * @return bool */ public function checkEncoding($str, $against) { - if ('UTF-8' == $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 ($str === iconv($against, "{$against}//IGNORE", $str)); } return true; @@ -73,7 +73,7 @@ class Validator implements ValidatorInterface { return false; } } elseif ($this->hasIconv) { - if ($str != iconv('UTF-8', 'UTF-8//IGNORE', $str)) { + if ($str !== iconv('UTF-8', 'UTF-8//IGNORE', $str)) { return false; } } From 31d2618057175e65282218e3a2f7a4a60d777132 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 25 Dec 2015 13:14:15 -0500 Subject: [PATCH 30/56] Added __toString fn to DataInterface --- src/Messaging/Protocol/DataInterface.php | 6 ++++++ src/Messaging/Protocol/Frame.php | 18 +++++++++++------- src/Messaging/Protocol/Message.php | 18 +++++++++++------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/Messaging/Protocol/DataInterface.php b/src/Messaging/Protocol/DataInterface.php index 8d67774..2b0c675 100644 --- a/src/Messaging/Protocol/DataInterface.php +++ b/src/Messaging/Protocol/DataInterface.php @@ -25,4 +25,10 @@ interface DataInterface { * @return string */ function getContents(); + + /** + * Should return the unmasked payload received from peer + * @return string + */ + function __toString(); } diff --git a/src/Messaging/Protocol/Frame.php b/src/Messaging/Protocol/Frame.php index 88aa818..aaf8e0e 100644 --- a/src/Messaging/Protocol/Frame.php +++ b/src/Messaging/Protocol/Frame.php @@ -429,13 +429,7 @@ class Frame implements FrameInterface { throw call_user_func($this->ufeg, 'Can not return partial message'); } - $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); - - if ($this->isMasked()) { - $payload = $this->applyMask($this->getMaskingKey(), $payload); - } - - return $payload; + return $this->__toString(); } /** @@ -446,6 +440,16 @@ class Frame implements FrameInterface { 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 diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index 1b1ed17..b06e4b4 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -78,13 +78,7 @@ class Message implements \IteratorAggregate, MessageInterface { throw new \UnderflowException('Message has not been put back together yet'); } - $buffer = ''; - - foreach ($this->_frames as $frame) { - $buffer .= $frame->getPayload(); - } - - return $buffer; + return $this->__toString(); } /** @@ -104,6 +98,16 @@ class Message implements \IteratorAggregate, MessageInterface { return $buffer; } + public function __toString() { + $buffer = ''; + + foreach ($this->_frames as $frame) { + $buffer .= $frame->getPayload(); + } + + return $buffer; + } + /** * @return boolean */ From a44254bd08c368d424a9a7fd0a1ba18195b269a6 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 25 Dec 2015 13:14:36 -0500 Subject: [PATCH 31/56] Cleanup --- LICENSE | 2 +- src/Handshake/RequestVerifier.php | 2 -- src/Messaging/Streaming/MessageStreamer.php | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index 66857ea..7f8c128 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2014 Chris Boden +Copyright (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 diff --git a/src/Handshake/RequestVerifier.php b/src/Handshake/RequestVerifier.php index e3c57de..87d069a 100644 --- a/src/Handshake/RequestVerifier.php +++ b/src/Handshake/RequestVerifier.php @@ -1,9 +1,7 @@ newFrame(pack('n', $code), true, Frame::OP_CLOSE); } -} \ No newline at end of file +} From c31bea9f301c0774138ffe8391c60bf8dcf3076d Mon Sep 17 00:00:00 2001 From: matt Date: Sat, 26 Dec 2015 14:31:45 -0500 Subject: [PATCH 32/56] Fixup tests for new MessageStreamer --- .travis.yml | 24 ++++++ phpunit.xml.dist | 27 +++++++ tests/AbResultsTest.php | 28 +++++++ tests/TestCase.php | 8 ++ tests/ab/AbConnectionContext.php | 67 ---------------- tests/ab/clientRunner.php | 121 +++++++++++++++-------------- tests/ab/fuzzingclient.travis.json | 13 ++++ tests/ab/fuzzingserver.travis.json | 10 +++ tests/ab/run_ab_tests.sh | 12 +++ tests/ab/startServer.php | 8 ++ tests/bootstrap.php | 9 ++- 11 files changed, 198 insertions(+), 129 deletions(-) create mode 100644 .travis.yml create mode 100644 phpunit.xml.dist create mode 100644 tests/AbResultsTest.php create mode 100644 tests/TestCase.php delete mode 100644 tests/ab/AbConnectionContext.php create mode 100644 tests/ab/fuzzingclient.travis.json create mode 100644 tests/ab/fuzzingserver.travis.json create mode 100644 tests/ab/run_ab_tests.sh diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b5758b7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7 + - hhvm + +matrix: + allow_failures: + - php: hhvm + +before_install: + - export PATH=$HOME/.local/bin:$PATH + - pip install autobahntestsuite --user `whoami` + - pip list autobahntestsuite --user `whoami` + +before_script: + - composer install + - sh tests/ab/run_ab_tests.sh + +script: + - phpunit \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..8f2e7d1 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + + tests + + test/ab + + + + + + + ./src/ + + + \ No newline at end of file diff --git a/tests/AbResultsTest.php b/tests/AbResultsTest.php new file mode 100644 index 0000000..22afcff --- /dev/null +++ b/tests/AbResultsTest.php @@ -0,0 +1,28 @@ +assertFileExists($fileName); + $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'); + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..92cb3c9 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,8 @@ +_conn = $connectionContext; - $this->maskPayload = $maskPayload; - } - - 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 \Ratchet\RFC6455\Messaging\Protocol\Frame($msg->getPayload(), true, $msg[0]->getOpcode()); - if ($this->maskPayload) { - $frame->maskPayload(); - } - $this->_conn->write($frame->getContents()); - } - - public function onPing(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame) { - $pong = new \Ratchet\RFC6455\Messaging\Protocol\Frame($frame->getPayload(), true, \Ratchet\RFC6455\Messaging\Protocol\Frame::OP_PONG); - if ($this->maskPayload) { - $pong->maskPayload(); - } - $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 \Ratchet\RFC6455\Messaging\Protocol\Frame( - pack('n', $code), - true, - \Ratchet\RFC6455\Messaging\Protocol\Frame::OP_CLOSE - ); - if ($this->maskPayload) { - $frame->maskPayload(); - } - - $this->_conn->end($frame->getContents()); - } -} \ No newline at end of file diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php index 94109f2..8d5c24a 100644 --- a/tests/ab/clientRunner.php +++ b/tests/ab/clientRunner.php @@ -4,34 +4,11 @@ use Ratchet\RFC6455\Messaging\Protocol\Frame; use Ratchet\RFC6455\Messaging\Protocol\Message; require __DIR__ . '/../bootstrap.php'; -require __DIR__ . '/AbConnectionContext.php'; define('AGENT', 'RatchetRFC/0.0.0'); $testServer = "127.0.0.1"; - -class EmConnectionContext extends AbConnectionContext implements \Evenement\EventEmitterInterface, Ratchet\RFC6455\Messaging\Streaming\ContextInterface { - use \Evenement\EventEmitterTrait; - - public function onMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) { - $this->emit('message', [$msg]); - } - - public function sendMessage(Frame $frame) { - if ($this->maskPayload) { - $frame->maskPayload(); - } - $this->_conn->write($frame->getContents()); - } - - public function close($closeCode = Frame::CLOSE_NORMAL) { - $closeFrame = new Frame(pack('n', $closeCode), true, Frame::OP_CLOSE); - $closeFrame->maskPayload(); - $this->_conn->end($closeFrame->getContents()); - } -} - $loop = React\EventLoop\Factory::create(); $dnsResolverFactory = new React\Dns\Resolver\Factory(); @@ -39,6 +16,32 @@ $dnsResolver = $dnsResolverFactory->createCached('8.8.8.8', $loop); $factory = new \React\SocketClient\Connector($loop, $dnsResolver); +function echoStreamerFactory($conn) +{ + return new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( + new \Ratchet\RFC6455\Encoding\Validator, + new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, + function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($conn) { + /** @var Frame $frame */ + foreach ($msg as $frame) { + $frame->maskPayload(); + } + $conn->write($msg->getContents()); + }, + function (\Ratchet\RFC6455\Messaging\Protocol\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; @@ -52,12 +55,10 @@ function getTestCases() { $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(new \Ratchet\RFC6455\Encoding\Validator(), true); + /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer $ms */ + $ms = null; - /** @var EmConnectionContext $context */ - $context = null; - - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred, &$context) { + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); @@ -70,19 +71,23 @@ function getTestCases() { $stream->end(); $deferred->reject(); } else { - $context = new EmConnectionContext($stream, true); - - $context->on('message', function (Message $msg) use ($stream, $deferred, $context) { - $deferred->resolve($msg->getPayload()); - $context->close(); - }); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( + new \Ratchet\RFC6455\Encoding\Validator, + new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, + function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($deferred, $stream) { + $deferred->resolve($msg->getPayload()); + $stream->close(); + }, + null, + false + ); } } } // feed the message streamer - if ($response && $context) { - $ms->onData($data, $context); + if ($ms) { + $ms->onData($data); } }); @@ -108,12 +113,9 @@ function runTest($case) $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(new \Ratchet\RFC6455\Encoding\Validator(), true); + $ms = null; - /** @var AbConnectionContext $context */ - $context = null; - - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred, &$context) { + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); @@ -126,14 +128,14 @@ function runTest($case) $stream->end(); $deferred->reject(); } else { - $context = new AbConnectionContext($stream, true); + $ms = echoStreamerFactory($stream); } } } // feed the message streamer - if ($response && $context) { - $ms->onData($data, $context); + if ($ms) { + $ms->onData($data); } }); @@ -154,18 +156,17 @@ function createReport() { $deferred = new Deferred(); $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { - $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator('/updateReports?agent=' . AGENT); + $reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true"; + $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator($reportPath); $cnRequest = $cn->getRequest(); $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(new \Ratchet\RFC6455\Encoding\Validator(), true); + /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer $ms */ + $ms = null; - /** @var EmConnectionContext $context */ - $context = null; - - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred, &$context) { + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); @@ -178,19 +179,23 @@ function createReport() { $stream->end(); $deferred->reject(); } else { - $context = new EmConnectionContext($stream, true); - - $context->on('message', function (Message $msg) use ($stream, $deferred, $context) { - $deferred->resolve($msg->getPayload()); - $context->close(); - }); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( + new \Ratchet\RFC6455\Encoding\Validator, + new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, + function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($deferred, $stream) { + $deferred->resolve($msg->getPayload()); + $stream->close(); + }, + null, + false + ); } } } // feed the message streamer - if ($response && $context) { - $ms->onData($data, $context); + if ($ms) { + $ms->onData($data); } }); diff --git a/tests/ab/fuzzingclient.travis.json b/tests/ab/fuzzingclient.travis.json new file mode 100644 index 0000000..d61bc6a --- /dev/null +++ b/tests/ab/fuzzingclient.travis.json @@ -0,0 +1,13 @@ +{ + "options": {"failByDrop": false}, + "outdir": "./reports/servers", + + "servers": [ + {"agent": "RatchetRFC/0.0.0", + "url": "ws://localhost:9001", + "options": {"version": 18}} + ], + "cases": ["1.*", "2.*", "3.*", "4.*", "5.*", "6.*", "7.*"], + "exclude-cases": [], + "exclude-agent-cases": {} +} diff --git a/tests/ab/fuzzingserver.travis.json b/tests/ab/fuzzingserver.travis.json new file mode 100644 index 0000000..4ef6af3 --- /dev/null +++ b/tests/ab/fuzzingserver.travis.json @@ -0,0 +1,10 @@ +{ + "url": "ws://127.0.0.1:9001" + , "options": { + "failByDrop": false +} + , "outdir": "./reports/clients" + , "cases": ["1.*", "2.*", "3.*", "4.*", "5.*", "6.*", "7.*"] + , "exclude-cases": [] + , "exclude-agent-cases": {} +} \ No newline at end of file diff --git a/tests/ab/run_ab_tests.sh b/tests/ab/run_ab_tests.sh new file mode 100644 index 0000000..9cd8467 --- /dev/null +++ b/tests/ab/run_ab_tests.sh @@ -0,0 +1,12 @@ +cd tests/ab + +wstest -m fuzzingserver -s fuzzingserver.travis.json & +sleep 5 +php clientRunner.php + +sleep 2 + +php startServer.php 25 & +sleep 3 +wstest -m fuzzingclient -s fuzzingclient.travis.json +sleep 2 \ No newline at end of file diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 47a5316..f0f9d8d 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -6,6 +6,14 @@ use Ratchet\RFC6455\Messaging\Protocol\Frame; require_once __DIR__ . "/../bootstrap.php"; $loop = \React\EventLoop\Factory::create(); + +if ($argc > 1 && is_numeric($argv[1])) { + echo "Setting test server to stop in " . $argv[1] . " seconds.\n"; + $loop->addTimer($argv[1], function () { + exit; + }); +} + $socket = new \React\Socket\Server($loop); $server = new \React\Http\Server($socket); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 6fa5dc9..511b041 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -4,15 +4,16 @@ * Find the auto loader file */ $files = [ - __DIR__ . '/../../../../vendor/autoload.php', - __DIR__ . '/../../../vendor/autoload.php', - __DIR__ . '/../../vendor/autoload.php', __DIR__ . '/../vendor/autoload.php', + __DIR__ . '/../../vendor/autoload.php', + __DIR__ . '/../../../vendor/autoload.php', + __DIR__ . '/../../../../vendor/autoload.php', ]; foreach ($files as $file) { if (file_exists($file)) { - require $file; + $loader = require $file; + $loader->addPsr4('Ratchet\\RFC6455\\Test\\', __DIR__); break; } } From 26f995abba26e5c6b49e3b79d147ba9ed48147a3 Mon Sep 17 00:00:00 2001 From: matt Date: Wed, 30 Dec 2015 13:39:27 -0500 Subject: [PATCH 33/56] Remove unused TestCase class --- tests/AbResultsTest.php | 2 +- tests/TestCase.php | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 tests/TestCase.php diff --git a/tests/AbResultsTest.php b/tests/AbResultsTest.php index 22afcff..871d324 100644 --- a/tests/AbResultsTest.php +++ b/tests/AbResultsTest.php @@ -2,7 +2,7 @@ namespace Ratchet\RFC6455\Test; -class AbResultsTest extends TestCase +class AbResultsTest extends \PHPUnit_Framework_TestCase { private function verifyAutobahnResults($fileName) { diff --git a/tests/TestCase.php b/tests/TestCase.php deleted file mode 100644 index 92cb3c9..0000000 --- a/tests/TestCase.php +++ /dev/null @@ -1,8 +0,0 @@ - Date: Wed, 30 Dec 2015 19:38:54 -0500 Subject: [PATCH 34/56] Port RFC6455 tests from Ratchet --- src/Handshake/RequestVerifier.php | 4 +- src/Messaging/Protocol/Message.php | 2 + tests/Unit/Handshake/RequestVerifierTest.php | 174 ++++++ tests/Unit/Messaging/Protocol/FrameTest.php | 502 ++++++++++++++++++ tests/Unit/Messaging/Protocol/MessageTest.php | 60 +++ 5 files changed, 741 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Handshake/RequestVerifierTest.php create mode 100644 tests/Unit/Messaging/Protocol/FrameTest.php create mode 100644 tests/Unit/Messaging/Protocol/MessageTest.php diff --git a/src/Handshake/RequestVerifier.php b/src/Handshake/RequestVerifier.php index e3c57de..51000b8 100644 --- a/src/Handshake/RequestVerifier.php +++ b/src/Handshake/RequestVerifier.php @@ -94,7 +94,9 @@ class RequestVerifier { * @return bool */ public function verifyConnection(array $connectionHeader) { - return (1 === count($connectionHeader) && 'upgrade' === strtolower(($connectionHeader[0]))); + return count(array_filter($connectionHeader, function ($x) { + return 'upgrade' === strtolower($x); + })) > 0; } /** diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index 1b1ed17..23f8701 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -40,6 +40,8 @@ class Message implements \IteratorAggregate, MessageInterface { */ public function addFrame(FrameInterface $fragment) { $this->_frames->push($fragment); + + return $this; } /** diff --git a/tests/Unit/Handshake/RequestVerifierTest.php b/tests/Unit/Handshake/RequestVerifierTest.php new file mode 100644 index 0000000..a7277ff --- /dev/null +++ b/tests/Unit/Handshake/RequestVerifierTest.php @@ -0,0 +1,174 @@ +_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']), + 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)); + } +} \ No newline at end of file diff --git a/tests/Unit/Messaging/Protocol/FrameTest.php b/tests/Unit/Messaging/Protocol/FrameTest.php new file mode 100644 index 0000000..7622599 --- /dev/null +++ b/tests/Unit/Messaging/Protocol/FrameTest.php @@ -0,0 +1,502 @@ +_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\WebSocket\Version\RFC6455\Frame::isFinal + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv1 + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv2 + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv3 + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getOpcode + * covers Ratchet\WebSocket\Version\RFC6455\Frame::isMasked + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getMaskingKey + * covers Ratchet\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\Frame::getRsv1 + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv2 + * covers Ratchet\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\Frame::isFinal + */ + public function testFinCodeFromFullMessage($msg, $encoded) { + $this->_frame->addBuffer(base64_decode($encoded)); + $this->assertTrue($this->_frame->isFinal()); + } + + /** + * @dataProvider UnframeMessageProvider + * covers Ratchet\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\Frame::addBuffer + * covers Ratchet\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\Frame::isMasked + */ + public function testIsMaskedFromFullMessage($msg, $encoded) { + $this->_frame->addBuffer(base64_decode($encoded)); + $this->assertTrue($this->_frame->isMasked()); + } + + /** + * @dataProvider secondByteProvider + * covers Ratchet\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\Frame::getMaskingKey + */ + public function testGetMaskingKeyOnUnmaskedPayload() { + $frame = new Frame('Hello World!'); + $this->assertEquals('', $frame->getMaskingKey()); + } + + /** + * @dataProvider UnframeMessageProvider + * covers Ratchet\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\Frame::__construct + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + * covers Ratchet\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\Frame::__construct + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + */ + public function testReallyLongCreate() { + $len = 65575; + $frame = new Frame($this->generateRandomString($len)); + $this->assertEquals($len, $frame->getPayloadLength()); + } + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::__construct + * covers Ratchet\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\Frame::maskPayload + */ + public function testGivenMaskIsValid() { + $this->setExpectedException('InvalidArgumentException'); + $this->_frame->maskPayload('hello world'); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\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\WebSocket\Version\RFC6455\Frame::getPayloadLength + * covers Ratchet\WebSocket\Version\RFC6455\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()); + } +} \ No newline at end of file diff --git a/tests/Unit/Messaging/Protocol/MessageTest.php b/tests/Unit/Messaging/Protocol/MessageTest.php new file mode 100644 index 0000000..f0e8711 --- /dev/null +++ b/tests/Unit/Messaging/Protocol/MessageTest.php @@ -0,0 +1,60 @@ +message = 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()); + } +} \ No newline at end of file From dce1ef5272f364a2603918cfb26d4cfe876facd4 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Fri, 1 Jan 2016 00:39:39 -0500 Subject: [PATCH 35/56] Rename Unit to unit --- tests/{Unit => unit}/Handshake/RequestVerifierTest.php | 0 tests/{Unit => unit}/Messaging/Protocol/FrameTest.php | 0 tests/{Unit => unit}/Messaging/Protocol/MessageTest.php | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/{Unit => unit}/Handshake/RequestVerifierTest.php (100%) rename tests/{Unit => unit}/Messaging/Protocol/FrameTest.php (100%) rename tests/{Unit => unit}/Messaging/Protocol/MessageTest.php (100%) diff --git a/tests/Unit/Handshake/RequestVerifierTest.php b/tests/unit/Handshake/RequestVerifierTest.php similarity index 100% rename from tests/Unit/Handshake/RequestVerifierTest.php rename to tests/unit/Handshake/RequestVerifierTest.php diff --git a/tests/Unit/Messaging/Protocol/FrameTest.php b/tests/unit/Messaging/Protocol/FrameTest.php similarity index 100% rename from tests/Unit/Messaging/Protocol/FrameTest.php rename to tests/unit/Messaging/Protocol/FrameTest.php diff --git a/tests/Unit/Messaging/Protocol/MessageTest.php b/tests/unit/Messaging/Protocol/MessageTest.php similarity index 100% rename from tests/Unit/Messaging/Protocol/MessageTest.php rename to tests/unit/Messaging/Protocol/MessageTest.php From 35fa78c0c1163cbc5dd180ddc8d80d159f0fd317 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 10 Jan 2016 10:42:48 -0500 Subject: [PATCH 36/56] Cleanup test automation --- .gitignore | 1 + tests/AbResultsTest.php | 24 +++++++++++++----------- tests/ab/clientRunner.php | 4 ---- tests/ab/fuzzingclient.json | 8 ++++---- tests/ab/fuzzingclient.travis.json | 13 ------------- tests/ab/fuzzingserver.travis.json | 10 ---------- tests/ab/run_ab_tests.sh | 10 ++++++---- tests/ab/startServer.php | 7 ------- 8 files changed, 24 insertions(+), 53 deletions(-) delete mode 100644 tests/ab/fuzzingclient.travis.json delete mode 100644 tests/ab/fuzzingserver.travis.json diff --git a/.gitignore b/.gitignore index 06a6b3b..42ab5d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ composer.lock vendor tests/ab/reports +reports diff --git a/tests/AbResultsTest.php b/tests/AbResultsTest.php index 871d324..9bc502d 100644 --- a/tests/AbResultsTest.php +++ b/tests/AbResultsTest.php @@ -1,28 +1,30 @@ assertFileExists($fileName); +class AbResultsTest extends \PHPUnit_Framework_TestCase { + private function verifyAutobahnResults($fileName) { + if (!file_exists($fileName)) { + return $this->markTestSkipped('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() - { + + public function testAutobahnClientResults() { $this->verifyAutobahnResults(__DIR__ . '/ab/reports/clients/index.json'); } - public function testAutobahnServerResults() - { + + public function testAutobahnServerResults() { $this->verifyAutobahnResults(__DIR__ . '/ab/reports/servers/index.json'); } -} \ No newline at end of file +} diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php index 8d5c24a..13a99a7 100644 --- a/tests/ab/clientRunner.php +++ b/tests/ab/clientRunner.php @@ -209,8 +209,6 @@ function createReport() { $testPromises = []; getTestCases()->then(function ($count) use ($loop) { - echo "Running " . $count . " test cases.\n"; - $allDeferred = new Deferred(); $runNextCase = function () use (&$i, &$runNextCase, $count, $allDeferred) { @@ -219,7 +217,6 @@ getTestCases()->then(function ($count) use ($loop) { $allDeferred->resolve(); return; } - echo "Running " . $i . "\n"; runTest($i)->then($runNextCase); }; @@ -227,7 +224,6 @@ getTestCases()->then(function ($count) use ($loop) { $runNextCase(); $allDeferred->promise()->then(function () { - echo "Generating report...\n"; createReport(); }); }); diff --git a/tests/ab/fuzzingclient.json b/tests/ab/fuzzingclient.json index 28cdd4a..75b1cc9 100644 --- a/tests/ab/fuzzingclient.json +++ b/tests/ab/fuzzingclient.json @@ -2,11 +2,11 @@ "options": {"failByDrop": false}, "outdir": "./reports/servers", - "servers": [ - {"agent": "AutobahnServer", + "servers": [{ + "agent": "RatchetRFC/0.1.0", "url": "ws://localhost:9001", - "options": {"version": 18}} - ], + "options": {"version": 18} + }], "cases": ["*"], "exclude-cases": ["12.*","13.*"], "exclude-agent-cases": {} diff --git a/tests/ab/fuzzingclient.travis.json b/tests/ab/fuzzingclient.travis.json deleted file mode 100644 index d61bc6a..0000000 --- a/tests/ab/fuzzingclient.travis.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": {"failByDrop": false}, - "outdir": "./reports/servers", - - "servers": [ - {"agent": "RatchetRFC/0.0.0", - "url": "ws://localhost:9001", - "options": {"version": 18}} - ], - "cases": ["1.*", "2.*", "3.*", "4.*", "5.*", "6.*", "7.*"], - "exclude-cases": [], - "exclude-agent-cases": {} -} diff --git a/tests/ab/fuzzingserver.travis.json b/tests/ab/fuzzingserver.travis.json deleted file mode 100644 index 4ef6af3..0000000 --- a/tests/ab/fuzzingserver.travis.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "url": "ws://127.0.0.1:9001" - , "options": { - "failByDrop": false -} - , "outdir": "./reports/clients" - , "cases": ["1.*", "2.*", "3.*", "4.*", "5.*", "6.*", "7.*"] - , "exclude-cases": [] - , "exclude-agent-cases": {} -} \ No newline at end of file diff --git a/tests/ab/run_ab_tests.sh b/tests/ab/run_ab_tests.sh index 9cd8467..aeb62d9 100644 --- a/tests/ab/run_ab_tests.sh +++ b/tests/ab/run_ab_tests.sh @@ -1,12 +1,14 @@ cd tests/ab -wstest -m fuzzingserver -s fuzzingserver.travis.json & +wstest -m fuzzingserver -s fuzzingserver.json & sleep 5 php clientRunner.php sleep 2 -php startServer.php 25 & +php startServer.php & sleep 3 -wstest -m fuzzingclient -s fuzzingclient.travis.json -sleep 2 \ No newline at end of file +wstest -m fuzzingclient -s fuzzingclient.json +sleep 2 + +killall php wstest diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index f0f9d8d..58b7814 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -7,13 +7,6 @@ require_once __DIR__ . "/../bootstrap.php"; $loop = \React\EventLoop\Factory::create(); -if ($argc > 1 && is_numeric($argv[1])) { - echo "Setting test server to stop in " . $argv[1] . " seconds.\n"; - $loop->addTimer($argv[1], function () { - exit; - }); -} - $socket = new \React\Socket\Server($loop); $server = new \React\Http\Server($socket); From 4f15d6558e5ef5048974ad1a3d78c2f1f913c498 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 10 Jan 2016 10:54:32 -0500 Subject: [PATCH 37/56] Try to fix travis build --- tests/ab/run_ab_tests.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/ab/run_ab_tests.sh b/tests/ab/run_ab_tests.sh index aeb62d9..8fa9ced 100644 --- a/tests/ab/run_ab_tests.sh +++ b/tests/ab/run_ab_tests.sh @@ -9,6 +9,3 @@ sleep 2 php startServer.php & sleep 3 wstest -m fuzzingclient -s fuzzingclient.json -sleep 2 - -killall php wstest From 59a30c3b721afdd7958718cf79496e1c5eafcbe7 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Tue, 12 Jan 2016 20:44:07 -0500 Subject: [PATCH 38/56] Replace slow validator with preg_match UTF8 check --- src/Encoding/NullValidator.php | 14 ---- src/Encoding/ToggleableValidator.php | 34 -------- src/Encoding/Validator.php | 93 --------------------- src/Encoding/ValidatorInterface.php | 15 ---- src/Handshake/Negotiator.php | 10 +-- src/Messaging/Streaming/MessageStreamer.php | 12 +-- tests/ab/clientRunner.php | 3 - tests/ab/startServer.php | 7 +- 8 files changed, 6 insertions(+), 182 deletions(-) delete mode 100644 src/Encoding/NullValidator.php delete mode 100644 src/Encoding/ToggleableValidator.php delete mode 100644 src/Encoding/Validator.php delete mode 100644 src/Encoding/ValidatorInterface.php diff --git a/src/Encoding/NullValidator.php b/src/Encoding/NullValidator.php deleted file mode 100644 index 6cc7515..0000000 --- a/src/Encoding/NullValidator.php +++ /dev/null @@ -1,14 +0,0 @@ -validationResponse; - } -} \ No newline at end of file diff --git a/src/Encoding/ToggleableValidator.php b/src/Encoding/ToggleableValidator.php deleted file mode 100644 index 3178bbc..0000000 --- a/src/Encoding/ToggleableValidator.php +++ /dev/null @@ -1,34 +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/Encoding/Validator.php b/src/Encoding/Validator.php deleted file mode 100644 index 5b2f7f4..0000000 --- a/src/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/Encoding/ValidatorInterface.php b/src/Encoding/ValidatorInterface.php deleted file mode 100644 index 3870157..0000000 --- a/src/Encoding/ValidatorInterface.php +++ /dev/null @@ -1,15 +0,0 @@ -verifier = new RequestVerifier; - - $this->validator = $validator; } /** diff --git a/src/Messaging/Streaming/MessageStreamer.php b/src/Messaging/Streaming/MessageStreamer.php index fea0c9f..e0ef3bd 100644 --- a/src/Messaging/Streaming/MessageStreamer.php +++ b/src/Messaging/Streaming/MessageStreamer.php @@ -1,6 +1,5 @@ validator = $encodingValidator; $this->closeFrameChecker = $frameChecker; $this->checkForMask = (bool)$expectMask; @@ -166,7 +158,7 @@ class MessageStreamer { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); } - if (!$this->validator->checkEncoding(substr($bin, 2), 'UTF-8')) { + if (!preg_match('//u', substr($bin, 2))) { return $this->newCloseFrame(Frame::CLOSE_BAD_PAYLOAD); } @@ -201,7 +193,7 @@ class MessageStreamer { */ public function checkMessage(MessageInterface $message) { if (!$message->isBinary()) { - if (!$this->validator->checkEncoding($message->getPayload(), 'UTF-8')) { + if (!preg_match('//u', $message->getPayload())) { return Frame::CLOSE_BAD_PAYLOAD; } } diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php index 13a99a7..ee8897f 100644 --- a/tests/ab/clientRunner.php +++ b/tests/ab/clientRunner.php @@ -19,7 +19,6 @@ $factory = new \React\SocketClient\Connector($loop, $dnsResolver); function echoStreamerFactory($conn) { return new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( - new \Ratchet\RFC6455\Encoding\Validator, new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($conn) { /** @var Frame $frame */ @@ -72,7 +71,6 @@ function getTestCases() { $deferred->reject(); } else { $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( - new \Ratchet\RFC6455\Encoding\Validator, new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($deferred, $stream) { $deferred->resolve($msg->getPayload()); @@ -180,7 +178,6 @@ function createReport() { $deferred->reject(); } else { $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( - new \Ratchet\RFC6455\Encoding\Validator, new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($deferred, $stream) { $deferred->resolve($msg->getPayload()); diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 58b7814..778318d 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -10,13 +10,12 @@ $loop = \React\EventLoop\Factory::create(); $socket = new \React\Socket\Server($loop); $server = new \React\Http\Server($socket); -$encodingValidator = new \Ratchet\RFC6455\Encoding\Validator; $closeFrameChecker = new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker; -$negotiator = new \Ratchet\RFC6455\Handshake\Negotiator($encodingValidator); +$negotiator = new \Ratchet\RFC6455\Handshake\Negotiator; $uException = new \UnderflowException; -$server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $encodingValidator, $closeFrameChecker, $uException) { +$server->on('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); @@ -34,7 +33,7 @@ $server->on('request', function (\React\Http\Request $request, \React\Http\Respo return; } - $parser = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($encodingValidator, $closeFrameChecker, function(MessageInterface $message) use ($response) { + $parser = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($closeFrameChecker, function(MessageInterface $message) use ($response) { $response->write($message->getContents()); }, function(FrameInterface $frame) use ($response, &$parser) { switch ($frame->getOpCode()) { From 8aef77c11887ddd5aa4b8017fc2e3e66ed32d631 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 27 Jan 2016 18:41:35 -0500 Subject: [PATCH 39/56] Attempt newer HHVM on TravisCI --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b5758b7..9f61e42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ php: - 5.5 - 5.6 - 7 - - hhvm + - hhvm-nightly matrix: allow_failures: @@ -21,4 +21,4 @@ before_script: - sh tests/ab/run_ab_tests.sh script: - - phpunit \ No newline at end of file + - phpunit From 04a7b41d5a0a733f07a8ffdf0dc57e95eaea72ba Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 27 Jan 2016 18:55:28 -0500 Subject: [PATCH 40/56] Revert HHVM version change on TravisCI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9f61e42..24a8308 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ php: - 5.5 - 5.6 - 7 - - hhvm-nightly + - hhvm matrix: allow_failures: From f6bf0ca07c1b05c9e881e9b9cd40a8299c611a32 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 27 Jan 2016 19:59:58 -0500 Subject: [PATCH 41/56] Use mbstring if available HHVM seems to have mbstring loaded/enabled by default --- src/Messaging/Streaming/MessageStreamer.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Messaging/Streaming/MessageStreamer.php b/src/Messaging/Streaming/MessageStreamer.php index e0ef3bd..3b999d9 100644 --- a/src/Messaging/Streaming/MessageStreamer.php +++ b/src/Messaging/Streaming/MessageStreamer.php @@ -158,7 +158,7 @@ class MessageStreamer { return $this->newCloseFrame(Frame::CLOSE_PROTOCOL); } - if (!preg_match('//u', substr($bin, 2))) { + if (!$this->checkUtf8(substr($bin, 2))) { return $this->newCloseFrame(Frame::CLOSE_BAD_PAYLOAD); } @@ -193,7 +193,7 @@ class MessageStreamer { */ public function checkMessage(MessageInterface $message) { if (!$message->isBinary()) { - if (!preg_match('//u', $message->getPayload())) { + if (!$this->checkUtf8($message->getPayload())) { return Frame::CLOSE_BAD_PAYLOAD; } } @@ -201,6 +201,14 @@ class MessageStreamer { 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\Protocol\MessageInterface */ From 797df1b318518cd8020ffb17a41e8687e64e1850 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 27 Jan 2016 23:41:03 -0500 Subject: [PATCH 42/56] HHVM not allowed to fail --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 24a8308..b1c7c1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,6 @@ php: - 7 - hhvm -matrix: - allow_failures: - - php: hhvm - before_install: - export PATH=$HOME/.local/bin:$PATH - pip install autobahntestsuite --user `whoami` From affba40d16e899dcf08bd7086fc933c143a1dab1 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 7 Feb 2016 13:40:38 -0500 Subject: [PATCH 43/56] Client refactor Class is now re-entrant instead of keeping state Remove non-specified default RFC headers Accept strict URI type to cut down on error handling --- src/Handshake/ClientNegotiator.php | 89 ++++++++++-------------------- src/Handshake/ResponseVerifier.php | 10 +--- tests/ab/clientRunner.php | 26 ++++----- 3 files changed, 44 insertions(+), 81 deletions(-) diff --git a/src/Handshake/ClientNegotiator.php b/src/Handshake/ClientNegotiator.php index b4a4e16..c393194 100644 --- a/src/Handshake/ClientNegotiator.php +++ b/src/Handshake/ClientNegotiator.php @@ -1,83 +1,50 @@ 'Upgrade' - , 'Cache-Control' => 'no-cache' - , 'Pragma' => 'no-cache' - , 'Upgrade' => 'websocket' - , 'Sec-WebSocket-Version' => 13 - , 'User-Agent' => "RatchetRFC/0.0.0" - ]; + /** + * @var ResponseVerifier + */ + private $verifier; - /** @var Request */ - public $request; + /** + * @var \Psr\Http\Message\RequestInterface + */ + public $defaultHeader; - /** @var Response */ - public $response; + function __construct() { + $this->verifier = new ResponseVerifier; - /** @var ResponseVerifier */ - public $verifier; - - private $websocketKey = ''; - - function __construct($path = null) - { - if (!is_string($path)) $path = "/"; - $request = new Request("GET", $path); - - $request = $request->withUri(new Uri("ws://127.0.0.1:9001" . $path)); - - $this->request = $request; - - $this->verifier = new ResponseVerifier(); - - $this->websocketKey = $this->generateKey(); + $this->defaultHeader = new Request('GET', '', [ + 'Connection' => 'Upgrade' + , 'Upgrade' => 'websocket' + , 'Sec-WebSocket-Version' => 13 + , 'User-Agent' => "RatchetRFC/0.0.0" + ]); } - public function addRequiredHeaders() { - foreach ($this->defaultHeaders as $k => $v) { - // remove any header that is there now - $this->request = $this->request->withoutHeader($k); - $this->request = $this->request->withHeader($k, $v); - } - $this->request = $this->request->withoutHeader("Sec-WebSocket-Key"); - $this->request = $this->request->withHeader("Sec-WebSocket-Key", $this->websocketKey); - $this->request = $this->request->withoutHeader("Host") - ->withHeader("Host", $this->request->getUri()->getHost() . ":" . $this->request->getUri()->getPort()); + public function generateRequest(UriInterface $uri) { + return $this->defaultHeader->withUri($uri) + ->withoutHeader("Sec-WebSocket-Key") + ->withHeader("Sec-WebSocket-Key", $this->generateKey()); } - public function getRequest() { - $this->addRequiredHeaders(); - return $this->request; + public function validateResponse(RequestInterface $request, ResponseInterface $response) { + return $this->verifier->verifyAll($request, $response); } - public function getResponse() { - return $this->response; - } - - public function validateResponse(Response $response) { - $this->response = $response; - - return $this->verifier->verifyAll($this->getRequest(), $response); - } - - protected function generateKey() { + public function generateKey() { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwzyz1234567890+/='; $charRange = strlen($chars) - 1; $key = ''; - for ($i = 0;$i < 16;$i++) { + for ($i = 0; $i < 16; $i++) { $key .= $chars[mt_rand(0, $charRange)]; } + return base64_encode($key); } diff --git a/src/Handshake/ResponseVerifier.php b/src/Handshake/ResponseVerifier.php index c1bd67a..58d05be 100644 --- a/src/Handshake/ResponseVerifier.php +++ b/src/Handshake/ResponseVerifier.php @@ -1,14 +1,10 @@ verifyStatus($response->getStatusCode()); diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php index ee8897f..bb58832 100644 --- a/tests/ab/clientRunner.php +++ b/tests/ab/clientRunner.php @@ -1,7 +1,7 @@ create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { - $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator("/getCaseCount"); - $cnRequest = $cn->getRequest(); + $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(); + $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001/getCaseCount')); $rawResponse = ""; $response = null; @@ -57,7 +57,7 @@ function getTestCases() { /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer $ms */ $ms = null; - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context) { + $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"); @@ -66,7 +66,7 @@ function getTestCases() { $rawResponse = substr($rawResponse, 0, $pos + 4); $response = \GuzzleHttp\Psr7\parse_response($rawResponse); - if (!$cn->validateResponse($response)) { + if (!$cn->validateResponse($cnRequest, $response)) { $stream->end(); $deferred->reject(); } else { @@ -105,15 +105,15 @@ function runTest($case) $deferred = new Deferred(); $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath, $case) { - $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator($casePath); - $cnRequest = $cn->getRequest(); + $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) { + $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"); @@ -122,7 +122,7 @@ function runTest($case) $rawResponse = substr($rawResponse, 0, $pos + 4); $response = \GuzzleHttp\Psr7\parse_response($rawResponse); - if (!$cn->validateResponse($response)) { + if (!$cn->validateResponse($cnRequest, $response)) { $stream->end(); $deferred->reject(); } else { @@ -155,8 +155,8 @@ function createReport() { $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { $reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true"; - $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator($reportPath); - $cnRequest = $cn->getRequest(); + $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(); + $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $reportPath)); $rawResponse = ""; $response = null; @@ -164,7 +164,7 @@ function createReport() { /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer $ms */ $ms = null; - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context) { + $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"); @@ -173,7 +173,7 @@ function createReport() { $rawResponse = substr($rawResponse, 0, $pos + 4); $response = \GuzzleHttp\Psr7\parse_response($rawResponse); - if (!$cn->validateResponse($response)) { + if (!$cn->validateResponse($cnRequest, $response)) { $stream->end(); $deferred->reject(); } else { From 1e828bf7d4b50eecbd2a152eaf8c52315f03d715 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 8 Feb 2016 07:50:17 -0500 Subject: [PATCH 44/56] Client negotiation cleanup --- src/Handshake/ClientNegotiator.php | 9 ++++++--- src/Handshake/ClientNegotiatorInterface.php | 11 ----------- src/Handshake/ResponseVerifier.php | 14 +++++++------- 3 files changed, 13 insertions(+), 21 deletions(-) delete mode 100644 src/Handshake/ClientNegotiatorInterface.php diff --git a/src/Handshake/ClientNegotiator.php b/src/Handshake/ClientNegotiator.php index c393194..ca93669 100644 --- a/src/Handshake/ClientNegotiator.php +++ b/src/Handshake/ClientNegotiator.php @@ -5,7 +5,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; use GuzzleHttp\Psr7\Request; -class ClientNegotiator implements ClientNegotiatorInterface { +class ClientNegotiator { /** * @var ResponseVerifier */ @@ -22,7 +22,7 @@ class ClientNegotiator implements ClientNegotiatorInterface { $this->defaultHeader = new Request('GET', '', [ 'Connection' => 'Upgrade' , 'Upgrade' => 'websocket' - , 'Sec-WebSocket-Version' => 13 + , 'Sec-WebSocket-Version' => $this->getVersion() , 'User-Agent' => "RatchetRFC/0.0.0" ]); } @@ -48,4 +48,7 @@ class ClientNegotiator implements ClientNegotiatorInterface { return base64_encode($key); } -} \ No newline at end of file + public function getVersion() { + return 13; + } +} \ No newline at end of file diff --git a/src/Handshake/ClientNegotiatorInterface.php b/src/Handshake/ClientNegotiatorInterface.php deleted file mode 100644 index c95c1ac..0000000 --- a/src/Handshake/ClientNegotiatorInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -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') - ); + $response->getHeader('Sec-WebSocket-Accept') + , $request->getHeader('Sec-WebSocket-Accept') + ); - return (4 == $passes); + return (4 === $passes); } public function verifyStatus($status) { - return ($status == 101); + return ((int)$status === 101); } public function verifyUpgrade(array $upgrade) { @@ -34,10 +34,10 @@ class ResponseVerifier { return ( 1 === count($swa) && 1 === count($key) && - $swa[0] == $this->sign($key[0])); + $swa[0] === $this->sign($key[0])); } public function sign($key) { - return base64_encode(sha1($key . Negotiator::GUID, true)); + return base64_encode(sha1($key . NegotiatorInterface::GUID, true)); } } \ No newline at end of file From 4095a7ed6ef5203437f0d05cccf2f3f00efd4693 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 8 Feb 2016 07:51:54 -0500 Subject: [PATCH 45/56] Change scope, defensive --- src/Handshake/ClientNegotiator.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Handshake/ClientNegotiator.php b/src/Handshake/ClientNegotiator.php index ca93669..8726c8f 100644 --- a/src/Handshake/ClientNegotiator.php +++ b/src/Handshake/ClientNegotiator.php @@ -14,7 +14,7 @@ class ClientNegotiator { /** * @var \Psr\Http\Message\RequestInterface */ - public $defaultHeader; + private $defaultHeader; function __construct() { $this->verifier = new ResponseVerifier; @@ -29,7 +29,6 @@ class ClientNegotiator { public function generateRequest(UriInterface $uri) { return $this->defaultHeader->withUri($uri) - ->withoutHeader("Sec-WebSocket-Key") ->withHeader("Sec-WebSocket-Key", $this->generateKey()); } From cd89941a49f7b94959de708226188963f2f06c4d Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 8 Feb 2016 07:55:35 -0500 Subject: [PATCH 46/56] Formatting --- src/Handshake/ResponseVerifier.php | 3 ++- tests/ab/fuzzingclient.json | 23 ++++++++++++----------- tests/ab/fuzzingserver.json | 14 +++++++------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Handshake/ResponseVerifier.php b/src/Handshake/ResponseVerifier.php index 6fa54ae..63909db 100644 --- a/src/Handshake/ResponseVerifier.php +++ b/src/Handshake/ResponseVerifier.php @@ -34,7 +34,8 @@ class ResponseVerifier { return ( 1 === count($swa) && 1 === count($key) && - $swa[0] === $this->sign($key[0])); + $swa[0] === $this->sign($key[0]) + ); } public function sign($key) { diff --git a/tests/ab/fuzzingclient.json b/tests/ab/fuzzingclient.json index 75b1cc9..d2fd0d0 100644 --- a/tests/ab/fuzzingclient.json +++ b/tests/ab/fuzzingclient.json @@ -1,13 +1,14 @@ { - "options": {"failByDrop": false}, - "outdir": "./reports/servers", - - "servers": [{ - "agent": "RatchetRFC/0.1.0", - "url": "ws://localhost:9001", - "options": {"version": 18} - }], - "cases": ["*"], - "exclude-cases": ["12.*","13.*"], - "exclude-agent-cases": {} + "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": {} } diff --git a/tests/ab/fuzzingserver.json b/tests/ab/fuzzingserver.json index 70db183..0422560 100644 --- a/tests/ab/fuzzingserver.json +++ b/tests/ab/fuzzingserver.json @@ -1,10 +1,10 @@ { "url": "ws://127.0.0.1:9001" - , "options": { - "failByDrop": false + , "options": { + "failByDrop": false + } + , "outdir": "./reports/clients" + , "cases": ["*"] + , "exclude-cases": ["6.4.*", "12.*", "13.*"] + , "exclude-agent-cases": {} } - , "outdir": "./reports/clients" - , "cases": ["*"] - , "exclude-cases": ["12.*", "13.*"] - , "exclude-agent-cases": {} -} \ No newline at end of file From 0f4df7fed5d1436e617641f0b477a55b4204db2a Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 8 Feb 2016 08:43:20 -0500 Subject: [PATCH 47/56] Verify proper header --- src/Handshake/ResponseVerifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Handshake/ResponseVerifier.php b/src/Handshake/ResponseVerifier.php index 63909db..f809ff3 100644 --- a/src/Handshake/ResponseVerifier.php +++ b/src/Handshake/ResponseVerifier.php @@ -12,7 +12,7 @@ class ResponseVerifier { $passes += (int)$this->verifyConnection($response->getHeader('Connection')); $passes += (int)$this->verifySecWebSocketAccept( $response->getHeader('Sec-WebSocket-Accept') - , $request->getHeader('Sec-WebSocket-Accept') + , $request->getHeader('Sec-WebSocket-Key') ); return (4 === $passes); From 9f405beccb0c0143c4d0c09bcec8635aa00e4034 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 8 Feb 2016 21:43:17 -0500 Subject: [PATCH 48/56] Added subprotocol check for client, test fixes --- src/Handshake/ResponseVerifier.php | 10 +++++- tests/unit/Handshake/RequestVerifierTest.php | 4 +-- tests/unit/Handshake/ResponseVerifierTest.php | 34 +++++++++++++++++++ tests/unit/Messaging/Protocol/FrameTest.php | 3 +- tests/unit/Messaging/Protocol/MessageTest.php | 4 +-- 5 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 tests/unit/Handshake/ResponseVerifierTest.php diff --git a/src/Handshake/ResponseVerifier.php b/src/Handshake/ResponseVerifier.php index f809ff3..de03f53 100644 --- a/src/Handshake/ResponseVerifier.php +++ b/src/Handshake/ResponseVerifier.php @@ -14,8 +14,12 @@ class ResponseVerifier { $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 (4 === $passes); + return (5 === $passes); } public function verifyStatus($status) { @@ -41,4 +45,8 @@ class ResponseVerifier { 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; + } } \ No newline at end of file diff --git a/tests/unit/Handshake/RequestVerifierTest.php b/tests/unit/Handshake/RequestVerifierTest.php index a7277ff..e0569fd 100644 --- a/tests/unit/Handshake/RequestVerifierTest.php +++ b/tests/unit/Handshake/RequestVerifierTest.php @@ -1,11 +1,9 @@ _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)); + } +} \ No newline at end of file diff --git a/tests/unit/Messaging/Protocol/FrameTest.php b/tests/unit/Messaging/Protocol/FrameTest.php index 7622599..e0a4f61 100644 --- a/tests/unit/Messaging/Protocol/FrameTest.php +++ b/tests/unit/Messaging/Protocol/FrameTest.php @@ -1,10 +1,9 @@ Date: Mon, 8 Feb 2016 22:21:56 -0500 Subject: [PATCH 49/56] Renamed some classes, less depth --- .../{Protocol => }/CloseFrameChecker.php | 4 +-- .../{Protocol => }/DataInterface.php | 2 +- src/Messaging/{Protocol => }/Frame.php | 2 +- .../{Protocol => }/FrameInterface.php | 2 +- src/Messaging/{Protocol => }/Message.php | 2 +- .../MessageStreamer.php => MessageBuffer.php} | 25 +++++++----------- .../{Protocol => }/MessageInterface.php | 2 +- tests/ab/clientRunner.php | 26 +++++++++---------- tests/ab/startServer.php | 10 +++---- tests/unit/Messaging/Protocol/FrameTest.php | 6 ++--- tests/unit/Messaging/Protocol/MessageTest.php | 8 +++--- 11 files changed, 42 insertions(+), 47 deletions(-) rename src/Messaging/{Protocol => }/CloseFrameChecker.php (87%) rename src/Messaging/{Protocol => }/DataInterface.php (93%) rename src/Messaging/{Protocol => }/Frame.php (99%) rename src/Messaging/{Protocol => }/FrameInterface.php (93%) rename src/Messaging/{Protocol => }/Message.php (98%) rename src/Messaging/{Streaming/MessageStreamer.php => MessageBuffer.php} (87%) rename src/Messaging/{Protocol => }/MessageInterface.php (88%) diff --git a/src/Messaging/Protocol/CloseFrameChecker.php b/src/Messaging/CloseFrameChecker.php similarity index 87% rename from src/Messaging/Protocol/CloseFrameChecker.php rename to src/Messaging/CloseFrameChecker.php index 7556b97..3d800e5 100644 --- a/src/Messaging/Protocol/CloseFrameChecker.php +++ b/src/Messaging/CloseFrameChecker.php @@ -1,8 +1,8 @@ validCloseCodes = [ diff --git a/src/Messaging/Protocol/DataInterface.php b/src/Messaging/DataInterface.php similarity index 93% rename from src/Messaging/Protocol/DataInterface.php rename to src/Messaging/DataInterface.php index 2b0c675..18aa2e3 100644 --- a/src/Messaging/Protocol/DataInterface.php +++ b/src/Messaging/DataInterface.php @@ -1,5 +1,5 @@ getRsv1() || @@ -188,7 +183,7 @@ class MessageStreamer { /** * Determine if a message is valid - * @param \Ratchet\RFC6455\Messaging\Protocol\MessageInterface + * @param \Ratchet\RFC6455\Messaging\MessageInterface * @return bool|int true if valid - false if incomplete - int of recommended close code */ public function checkMessage(MessageInterface $message) { @@ -210,7 +205,7 @@ class MessageStreamer { } /** - * @return \Ratchet\RFC6455\Messaging\Protocol\MessageInterface + * @return \Ratchet\RFC6455\Messaging\MessageInterface */ public function newMessage() { return new Message; @@ -220,7 +215,7 @@ class MessageStreamer { * @param string|null $payload * @param bool|null $final * @param int|null $opcode - * @return \Ratchet\RFC6455\Messaging\Protocol\FrameInterface + * @return \Ratchet\RFC6455\Messaging\FrameInterface */ public function newFrame($payload = null, $final = null, $opcode = null) { return new Frame($payload, $final, $opcode, $this->exceptionFactory); diff --git a/src/Messaging/Protocol/MessageInterface.php b/src/Messaging/MessageInterface.php similarity index 88% rename from src/Messaging/Protocol/MessageInterface.php rename to src/Messaging/MessageInterface.php index f153686..fd7212e 100644 --- a/src/Messaging/Protocol/MessageInterface.php +++ b/src/Messaging/MessageInterface.php @@ -1,5 +1,5 @@ maskPayload(); } $conn->write($msg->getContents()); }, - function (\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame) use ($conn) { + 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()); @@ -54,7 +54,7 @@ function getTestCases() { $rawResponse = ""; $response = null; - /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer $ms */ + /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageBuffer $ms */ $ms = null; $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { @@ -70,9 +70,9 @@ function getTestCases() { $stream->end(); $deferred->reject(); } else { - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( - new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, - function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($deferred, $stream) { + $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(); }, @@ -161,7 +161,7 @@ function createReport() { $rawResponse = ""; $response = null; - /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer $ms */ + /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageBuffer $ms */ $ms = null; $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { @@ -177,9 +177,9 @@ function createReport() { $stream->end(); $deferred->reject(); } else { - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( - new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, - function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($deferred, $stream) { + $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(); }, diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 778318d..846b0be 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -1,7 +1,7 @@ on('request', function (\React\Http\Request $request, \React\Http\Respo return; } - $parser = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($closeFrameChecker, function(MessageInterface $message) use ($response) { + $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()) { diff --git a/tests/unit/Messaging/Protocol/FrameTest.php b/tests/unit/Messaging/Protocol/FrameTest.php index e0a4f61..2aa2e1f 100644 --- a/tests/unit/Messaging/Protocol/FrameTest.php +++ b/tests/unit/Messaging/Protocol/FrameTest.php @@ -1,9 +1,9 @@ Date: Wed, 10 Feb 2016 10:24:33 -0500 Subject: [PATCH 50/56] Documentation updates --- README.md | 15 +++++++-------- composer.json | 16 ++++++++-------- tests/ab/clientRunner.php | 2 +- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 95e8b4f..73aac8b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # RFC6455 - The WebSocket Protocol -This library is meant to be a protocol handler for the RFC6455 specification. +[![Build Status](https://travis-ci.org/ratchetphp/RFC6455.svg?branch=master)](https://travis-ci.org/ratchetphp/RFC6455) ---- +This library a protocol handler for the RFC6455 specification. +It contains components for both server and client side handshake and messaging protocol negotation. -### A rough roadmap +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. -* v0.1 is the initial split from Ratchet/v0.3.2 as-is. In this state it currently relies on some of Ratchet's interfaces. -* v0.2 will be more framework agnostic and will not require any interfaces from Ratchet. A dependency on Guzzle (or hopefully PSR-7) may be required. -* v0.3 will look into performance tuning. No more expected exceptions. -* v0.4 extension support -* v1.0 when all the bases are covered +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. diff --git a/composer.json b/composer.json index 7d0f4e7..ffc15da 100644 --- a/composer.json +++ b/composer.json @@ -2,17 +2,17 @@ "name": "ratchet/rfc6455", "type": "library", "description": "RFC6455 protocol handler", - "keywords": ["WebSockets"], + "keywords": ["WebSockets", "websocket", "RFC6455"], "homepage": "http://socketo.me", "license": "MIT", - "authors": [ - { - "name": "Chris Boden", "email": "cboden@gmail.com", "role": "Developer" - } - ], + "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", + , "forum": "https://groups.google.com/forum/#!forum/ratchet-php" + , "issues": "https://github.com/ratchetphp/RFC6455/issues" "irc": "irc://irc.freenode.org/reactphp" }, "autoload": { diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php index 559a1aa..0c5578a 100644 --- a/tests/ab/clientRunner.php +++ b/tests/ab/clientRunner.php @@ -161,7 +161,7 @@ function createReport() { $rawResponse = ""; $response = null; - /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageBuffer $ms */ + /** @var \Ratchet\RFC6455\Messaging\MessageBuffer $ms */ $ms = null; $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { From 84db350a66e1486bc0ba2e737a9ca62d646ff358 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 10 Feb 2016 10:25:54 -0500 Subject: [PATCH 51/56] Fixed invalid json --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index ffc15da..151d3b5 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,9 @@ , "role": "Developer" }], "support": { - , "forum": "https://groups.google.com/forum/#!forum/ratchet-php" + "forum": "https://groups.google.com/forum/#!forum/ratchet-php" , "issues": "https://github.com/ratchetphp/RFC6455/issues" - "irc": "irc://irc.freenode.org/reactphp" + , "irc": "irc://irc.freenode.org/reactphp" }, "autoload": { "psr-4": { From e75c843fc9dc4ea71e65b13998a7b3fe5eaf21c5 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 10 Feb 2016 17:56:28 -0500 Subject: [PATCH 52/56] Rename Negotiator -> ServerNegotiator for consistence --- src/Handshake/{Negotiator.php => ServerNegotiator.php} | 2 +- tests/ab/startServer.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/Handshake/{Negotiator.php => ServerNegotiator.php} (98%) diff --git a/src/Handshake/Negotiator.php b/src/Handshake/ServerNegotiator.php similarity index 98% rename from src/Handshake/Negotiator.php rename to src/Handshake/ServerNegotiator.php index 948993c..e1709e3 100644 --- a/src/Handshake/Negotiator.php +++ b/src/Handshake/ServerNegotiator.php @@ -7,7 +7,7 @@ use GuzzleHttp\Psr7\Response; * The latest version of the WebSocket protocol * @todo Unicode: return mb_convert_encoding(pack("N",$u), mb_internal_encoding(), 'UCS-4BE'); */ -class Negotiator implements NegotiatorInterface { +class ServerNegotiator implements NegotiatorInterface { /** * @var \Ratchet\RFC6455\Handshake\RequestVerifier */ diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 846b0be..ef42de3 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -11,7 +11,7 @@ $socket = new \React\Socket\Server($loop); $server = new \React\Http\Server($socket); $closeFrameChecker = new \Ratchet\RFC6455\Messaging\CloseFrameChecker; -$negotiator = new \Ratchet\RFC6455\Handshake\Negotiator; +$negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator; $uException = new \UnderflowException; From ac4d13cc09e3634eade8f9e648f7992ad70bf1f5 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 15 Feb 2016 17:03:10 -0500 Subject: [PATCH 53/56] Inject RequestVerfier instead of instantiating --- src/Handshake/ServerNegotiator.php | 4 ++-- tests/ab/startServer.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Handshake/ServerNegotiator.php b/src/Handshake/ServerNegotiator.php index e1709e3..86d0411 100644 --- a/src/Handshake/ServerNegotiator.php +++ b/src/Handshake/ServerNegotiator.php @@ -17,8 +17,8 @@ class ServerNegotiator implements NegotiatorInterface { private $_strictSubProtocols = true; - public function __construct() { - $this->verifier = new RequestVerifier; + public function __construct(RequestVerifier $requestVerifier) { + $this->verifier = $requestVerifier; } /** diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index ef42de3..b256ec2 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -11,7 +11,7 @@ $socket = new \React\Socket\Server($loop); $server = new \React\Http\Server($socket); $closeFrameChecker = new \Ratchet\RFC6455\Messaging\CloseFrameChecker; -$negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator; +$negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator(new \Ratchet\RFC6455\Handshake\RequestVerifier); $uException = new \UnderflowException; From b2bd4607ae4de03efa7b6f374eb6252eabbc45cb Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 15 Feb 2016 18:10:53 -0500 Subject: [PATCH 54/56] Moved tests to align namespace with target --- tests/unit/Messaging/{Protocol => }/FrameTest.php | 0 tests/unit/Messaging/{Protocol => }/MessageTest.php | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/unit/Messaging/{Protocol => }/FrameTest.php (100%) rename tests/unit/Messaging/{Protocol => }/MessageTest.php (100%) diff --git a/tests/unit/Messaging/Protocol/FrameTest.php b/tests/unit/Messaging/FrameTest.php similarity index 100% rename from tests/unit/Messaging/Protocol/FrameTest.php rename to tests/unit/Messaging/FrameTest.php diff --git a/tests/unit/Messaging/Protocol/MessageTest.php b/tests/unit/Messaging/MessageTest.php similarity index 100% rename from tests/unit/Messaging/Protocol/MessageTest.php rename to tests/unit/Messaging/MessageTest.php From bbc7818ddb3062db5e6c7919d4fbaca1a72cefbe Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 20 Feb 2016 16:07:27 -0500 Subject: [PATCH 55/56] Strict Sub-Protocol check off by default More of an implementation detail, not specified in spec --- src/Handshake/ServerNegotiator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Handshake/ServerNegotiator.php b/src/Handshake/ServerNegotiator.php index 86d0411..f1e0ae0 100644 --- a/src/Handshake/ServerNegotiator.php +++ b/src/Handshake/ServerNegotiator.php @@ -15,7 +15,7 @@ class ServerNegotiator implements NegotiatorInterface { private $_supportedSubProtocols = []; - private $_strictSubProtocols = true; + private $_strictSubProtocols = false; public function __construct(RequestVerifier $requestVerifier) { $this->verifier = $requestVerifier; From 49cfd1eb50291770796c804857115c0c3b870b90 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 20 Feb 2016 16:16:11 -0500 Subject: [PATCH 56/56] Updated test coverage --- composer.json | 2 +- tests/unit/Handshake/ResponseVerifierTest.php | 4 +- tests/unit/Messaging/FrameTest.php | 94 +++++++++---------- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/composer.json b/composer.json index 151d3b5..77d9fde 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "ratchet/rfc6455", "type": "library", - "description": "RFC6455 protocol handler", + "description": "RFC6455 WebSocket protocol handler", "keywords": ["WebSockets", "websocket", "RFC6455"], "homepage": "http://socketo.me", "license": "MIT", diff --git a/tests/unit/Handshake/ResponseVerifierTest.php b/tests/unit/Handshake/ResponseVerifierTest.php index 52338ae..312930e 100644 --- a/tests/unit/Handshake/ResponseVerifierTest.php +++ b/tests/unit/Handshake/ResponseVerifierTest.php @@ -3,7 +3,7 @@ namespace Ratchet\RFC6455\Test\Unit\Handshake; use Ratchet\RFC6455\Handshake\ResponseVerifier; /** - * @covers Ratchet\WebSocket\Version\RFC6455\ResponseVerifier + * @covers Ratchet\RFC6455\Handshake\ResponseVerifier */ class ResponseVerifierTest extends \PHPUnit_Framework_TestCase { /** @@ -31,4 +31,4 @@ class ResponseVerifierTest extends \PHPUnit_Framework_TestCase { public function testVerifySubProtocol($expected, $response, $request) { $this->assertEquals($expected, $this->_v->verifySubProtocol($response, $request)); } -} \ No newline at end of file +} diff --git a/tests/unit/Messaging/FrameTest.php b/tests/unit/Messaging/FrameTest.php index 2aa2e1f..b73f600 100644 --- a/tests/unit/Messaging/FrameTest.php +++ b/tests/unit/Messaging/FrameTest.php @@ -3,7 +3,7 @@ namespace Ratchet\RFC6455\Test\Unit\Messaging; use Ratchet\RFC6455\Messaging\Frame; /** - * @covers Ratchet\RFC6455\MessagingFrame + * @covers Ratchet\RFC6455\Messaging\Frame * @todo getMaskingKey, getPayloadStartingByte don't have tests yet * @todo Could use some clean up in general, I had to rush to fix a bug for a deadline, sorry. */ @@ -73,15 +73,15 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider underflowProvider * - * covers Ratchet\WebSocket\Version\RFC6455\Frame::isFinal - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv1 - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv2 - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv3 - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getOpcode - * covers Ratchet\WebSocket\Version\RFC6455\Frame::isMasked - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getMaskingKey - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload + * @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'); @@ -110,7 +110,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider firstByteProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::isFinal + * covers Ratchet\RFC6455\Messaging\Frame::isFinal */ public function testFinCodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { $this->_frame->addBuffer(static::encode($bin)); @@ -119,9 +119,9 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider firstByteProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv1 - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv2 - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv3 + * 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)); @@ -132,7 +132,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider firstByteProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getOpcode + * covers Ratchet\RFC6455\Messaging\Frame::getOpcode */ public function testOpcodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { $this->_frame->addBuffer(static::encode($bin)); @@ -141,7 +141,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider UnframeMessageProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::isFinal + * covers Ratchet\RFC6455\Messaging\Frame::isFinal */ public function testFinCodeFromFullMessage($msg, $encoded) { $this->_frame->addBuffer(base64_decode($encoded)); @@ -150,7 +150,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider UnframeMessageProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getOpcode + * covers Ratchet\RFC6455\Messaging\Frame::getOpcode */ public function testOpcodeFromFullMessage($msg, $encoded) { $this->_frame->addBuffer(base64_decode($encoded)); @@ -170,8 +170,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider payloadLengthDescriptionProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::addBuffer - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getFirstPayloadVal + * covers Ratchet\RFC6455\Messaging\Frame::addBuffer + * covers Ratchet\RFC6455\Messaging\Frame::getFirstPayloadVal */ public function testFirstPayloadDesignationValue($bits, $bin) { $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); @@ -183,7 +183,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getFirstPayloadVal + * covers Ratchet\RFC6455\Messaging\Frame::getFirstPayloadVal */ public function testFirstPayloadValUnderflow() { $ref = new \ReflectionClass($this->_frame); @@ -195,7 +195,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider payloadLengthDescriptionProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getNumPayloadBits + * covers Ratchet\RFC6455\Messaging\Frame::getNumPayloadBits */ public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) { $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); @@ -207,7 +207,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getNumPayloadBits + * covers Ratchet\RFC6455\Messaging\Frame::getNumPayloadBits */ public function testgetNumPayloadBitsUnderflow() { $ref = new \ReflectionClass($this->_frame); @@ -226,7 +226,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** * @dataProvider secondByteProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::isMasked + * covers Ratchet\RFC6455\Messaging\Frame::isMasked */ public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) { $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); @@ -236,7 +236,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider UnframeMessageProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::isMasked + * covers Ratchet\RFC6455\Messaging\Frame::isMasked */ public function testIsMaskedFromFullMessage($msg, $encoded) { $this->_frame->addBuffer(base64_decode($encoded)); @@ -245,7 +245,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider secondByteProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength */ public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) { $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); @@ -255,7 +255,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider UnframeMessageProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength * @todo Not yet testing when second additional payload length descriptor */ public function testGetPayloadLengthFromFullMessage($msg, $encoded) { @@ -274,7 +274,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider maskingKeyProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getMaskingKey + * covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey * @todo I I wrote the dataProvider incorrectly, skipping for now */ public function testGetMaskingKey($mask) { @@ -285,7 +285,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getMaskingKey + * covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey */ public function testGetMaskingKeyOnUnmaskedPayload() { $frame = new Frame('Hello World!'); @@ -294,7 +294,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider UnframeMessageProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload + * 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) { @@ -310,7 +310,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider UnframeMessageProvider - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload + * covers Ratchet\RFC6455\Messaging\Frame::getPayload */ public function testCheckPiecingTogetherMessage($msg, $encoded) { $framed = base64_decode($encoded); @@ -321,9 +321,9 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::__construct - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength - * covers Ratchet\WebSocket\Version\RFC6455\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; @@ -337,8 +337,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::__construct - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + * covers Ratchet\RFC6455\Messaging\Frame::__construct + * covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength */ public function testReallyLongCreate() { $len = 65575; @@ -346,8 +346,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($len, $frame->getPayloadLength()); } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::__construct - * covers Ratchet\WebSocket\Version\RFC6455\Frame::extractOverflow + * covers Ratchet\RFC6455\Messaging\Frame::__construct + * covers Ratchet\RFC6455\Messaging\Frame::extractOverflow */ public function testExtractOverflow() { $string1 = $this->generateRandomString(); @@ -365,7 +365,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::extractOverflow + * covers Ratchet\RFC6455\Messaging\Frame::extractOverflow */ public function testEmptyExtractOverflow() { $string = $this->generateRandomString(); @@ -376,7 +376,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getContents + * covers Ratchet\RFC6455\Messaging\Frame::getContents */ public function testGetContents() { $msg = 'The quick brown fox jumps over the lazy dog.'; @@ -388,7 +388,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::maskPayload + * covers Ratchet\RFC6455\Messaging\Frame::maskPayload */ public function testMasking() { $msg = 'The quick brown fox jumps over the lazy dog.'; @@ -399,7 +399,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::unMaskPayload + * covers Ratchet\RFC6455\Messaging\Frame::unMaskPayload */ public function testUnMaskPayload() { $string = $this->generateRandomString(); @@ -410,7 +410,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::generateMaskingKey + * covers Ratchet\RFC6455\Messaging\Frame::generateMaskingKey */ public function testGenerateMaskingKey() { $dupe = false; @@ -427,7 +427,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::maskPayload + * covers Ratchet\RFC6455\Messaging\Frame::maskPayload */ public function testGivenMaskIsValid() { $this->setExpectedException('InvalidArgumentException'); @@ -435,7 +435,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { } /** - * covers Ratchet\WebSocket\Version\RFC6455\Frame::maskPayload + * covers Ratchet\RFC6455\Messaging\Frame::maskPayload */ public function testGivenMaskIsValidAscii() { if (!extension_loaded('mbstring')) { @@ -471,8 +471,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * * This is fixed by setting the defPayLen back to -1 before the underflow exception is thrown. * - * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength - * covers Ratchet\WebSocket\Version\RFC6455\Frame::extractOverflow + * 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 @@ -498,4 +498,4 @@ class FrameTest extends \PHPUnit_Framework_TestCase { // make sure the overflow is good $this->assertEquals($rawOverflow, $frame->extractOverflow()); } -} \ No newline at end of file +}