From 2ffcc6b0a77d6335f721c842c364a68e5574b6f3 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Tue, 8 May 2012 23:14:28 -0400 Subject: [PATCH] [BCB] Namespace changes Removed the `Component` namespace Removed the `Resource` namespace Renamed components: `IOServerComponent` => `IoServer` `WebSocketComponent` => `WsServer` `SessionComponent` => `SessionProvider` `WAMPServerComponent` => `WampServer` `IpBlackListComponent` => `IpBlackList` `FlashPolicyComponent` => `FlashPolicy` --- Guzzle/Http/Message/RequestFactory.php | 19 ++ Version/FrameInterface.php | 62 +++++++ Version/Hixie76.php | 84 +++++++++ Version/Hixie76/Frame.php | 78 +++++++++ Version/Hixie76/Message.php | 66 +++++++ Version/HyBi10.php | 25 +++ Version/MessageInterface.php | 37 ++++ Version/RFC6455.php | 155 +++++++++++++++++ Version/RFC6455/Frame.php | 226 ++++++++++++++++++++++++ Version/RFC6455/HandshakeVerifier.php | 147 ++++++++++++++++ Version/RFC6455/Message.php | 88 ++++++++++ Version/VersionInterface.php | 47 +++++ WsConnection.php | 32 ++++ WsServer.php | 230 +++++++++++++++++++++++++ WsServerInterface.php | 16 ++ 15 files changed, 1312 insertions(+) create mode 100644 Guzzle/Http/Message/RequestFactory.php create mode 100644 Version/FrameInterface.php create mode 100644 Version/Hixie76.php create mode 100644 Version/Hixie76/Frame.php create mode 100644 Version/Hixie76/Message.php create mode 100644 Version/HyBi10.php create mode 100644 Version/MessageInterface.php create mode 100644 Version/RFC6455.php create mode 100644 Version/RFC6455/Frame.php create mode 100644 Version/RFC6455/HandshakeVerifier.php create mode 100644 Version/RFC6455/Message.php create mode 100644 Version/VersionInterface.php create mode 100644 WsConnection.php create mode 100644 WsServer.php create mode 100644 WsServerInterface.php diff --git a/Guzzle/Http/Message/RequestFactory.php b/Guzzle/Http/Message/RequestFactory.php new file mode 100644 index 0000000..bd7e490 --- /dev/null +++ b/Guzzle/Http/Message/RequestFactory.php @@ -0,0 +1,19 @@ +entityEnclosingRequestClass; + $request = new $c($method, $url, $headers); + if ($body) { + $request->setBody(EntityBody::factory($body)); + } + + return $request; + } +} \ No newline at end of file diff --git a/Version/FrameInterface.php b/Version/FrameInterface.php new file mode 100644 index 0000000..57b27ea --- /dev/null +++ b/Version/FrameInterface.php @@ -0,0 +1,62 @@ +getHeader('Sec-WebSocket-Key2', true)); + } + + /** + * @param Guzzle\Http\Message\RequestInterface + * @return Guzzle\Http\Message\Response + */ + public function handshake(RequestInterface $request) { + $body = $this->sign($request->getHeader('Sec-WebSocket-Key1', true), $request->getHeader('Sec-WebSocket-Key2', true), (string)$request->getBody()); + + $headers = array( + 'Upgrade' => 'WebSocket' + , 'Connection' => 'Upgrade' + , 'Sec-WebSocket-Origin' => $request->getHeader('Origin', true) + , 'Sec-WebSocket-Location' => 'ws://' . $request->getHeader('Host', true) . $request->getPath() + ); + + $response = new Response('101', $headers, $body); + $response->setStatus('101', 'WebSocket Protocol Handshake'); + + return $response; + } + + /** + * @return Hixie76\Message + */ + public function newMessage() { + return new Hixie76\Message; + } + + /** + * @return Hixie76\Frame + */ + public function newFrame() { + return new Hixie76\Frame; + } + + /** + * {@inheritdoc} + */ + public function frame($message, $mask = true) { + return chr(0) . $message . chr(255); + } + + public function generateKeyNumber($key) { + if (0 === substr_count($key, ' ')) { + return ''; + } + + $int = (int)preg_replace('[\D]', '', $key) / substr_count($key, ' '); + + return (is_int($int)) ? $int : ''; + } + + protected function sign($key1, $key2, $code) { + return md5( + pack('N', $this->generateKeyNumber($key1)) + . pack('N', $this->generateKeyNumber($key2)) + . $code + , true); + } +} \ No newline at end of file diff --git a/Version/Hixie76/Frame.php b/Version/Hixie76/Frame.php new file mode 100644 index 0000000..b9af87d --- /dev/null +++ b/Version/Hixie76/Frame.php @@ -0,0 +1,78 @@ +_data[0] == chr(0) && substr($this->_data, -1) == chr(255)); + } + + /** + * {@inheritdoc} + */ + public function addBuffer($buf) { + $this->_data .= (string)$buf; + } + + /** + * {@inheritdoc} + */ + public function isFinal() { + return true; + } + + /** + * {@inheritdoc} + */ + public function isMasked() { + return false; + } + + /** + * {@inheritdoc} + */ + public function getOpcode() { + return 1; + } + + /** + * {@inheritdoc} + */ + public function getPayloadLength() { + if (!$this->isCoalesced()) { + throw new \UnderflowException('Not enough of the message has been buffered to determine the length of the payload'); + } + + return strlen($this->_data) - 2; + } + + /** + * {@inheritdoc} + */ + public function getMaskingKey() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function getPayload() { + if (!$this->isCoalesced()) { + return new \UnderflowException('Not enough data buffered to read payload'); + } + + return substr($this->_data, 1, strlen($this->_data) - 2); + } +} \ No newline at end of file diff --git a/Version/Hixie76/Message.php b/Version/Hixie76/Message.php new file mode 100644 index 0000000..f783e8b --- /dev/null +++ b/Version/Hixie76/Message.php @@ -0,0 +1,66 @@ +getPayload(); + } + + /** + * {@inheritdoc} + */ + public function isCoalesced() { + if (!($this->_frame instanceof FrameInterface)) { + return false; + } + + return $this->_frame->isCoalesced(); + } + + /** + * {@inheritdoc} + */ + public function addFrame(FrameInterface $fragment) { + if (null !== $this->_frame) { + throw new \OverflowException('Hixie76 does not support multiple framing of messages'); + } + + $this->_frame = $fragment; + } + + /** + * {@inheritdoc} + */ + public function getOpcode() { + // Hixie76 only supported text messages + return 1; + } + + /** + * {@inheritdoc} + */ + public function getPayloadLength() { + throw new \DomainException('Please sir, may I have some code? (' . __FUNCTION__ . ')'); + } + + /** + * {@inheritdoc} + */ + public function getPayload() { + if (!$this->isCoalesced()) { + throw new \UnderflowException('Message has not been fully buffered yet'); + } + + return $this->_frame->getPayload(); + } +} \ No newline at end of file diff --git a/Version/HyBi10.php b/Version/HyBi10.php new file mode 100644 index 0000000..734d7e4 --- /dev/null +++ b/Version/HyBi10.php @@ -0,0 +1,25 @@ +getHeader('Sec-WebSocket-Version', -1); + return ($version >= 6 && $version < 13); + } + + /** + * @return HyBi10\Message + * / + public function newMessage() { + return new HyBi10\Message; + } + + /** + * @return HyBi10\Frame + * / + public function newFrame() { + return new HyBi10\Frame; + } + /**/ +} \ No newline at end of file diff --git a/Version/MessageInterface.php b/Version/MessageInterface.php new file mode 100644 index 0000000..90d0179 --- /dev/null +++ b/Version/MessageInterface.php @@ -0,0 +1,37 @@ +_verifier = new HandshakeVerifier; + } + + /** + * {@inheritdoc} + */ + public static function isProtocol(RequestInterface $request) { + $version = (int)$request->getHeader('Sec-WebSocket-Version', -1); + return (13 === $version); + } + + /** + * {@inheritdoc} + * @todo Decide what to do on failure...currently throwing an exception and I think socket connection is closed. Should be sending 40x error - but from where? + */ + public function handshake(RequestInterface $request) { + if (true !== $this->_verifier->verifyAll($request)) { + throw new \InvalidArgumentException('Invalid HTTP header'); + } + + $headers = array( + 'Upgrade' => 'websocket' + , 'Connection' => 'Upgrade' + , 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key')) + ); + + return new Response('101', $headers); + } + + /** + * @return RFC6455\Message + */ + public function newMessage() { + return new RFC6455\Message; + } + + /** + * @return RFC6455\Frame + */ + public function newFrame() { + return new RFC6455\Frame; + } + + /** + * Thanks to @lemmingzshadow for the code on decoding a HyBi-10 frame + * @link https://github.com/lemmingzshadow/php-websocket + * @todo look into what happens when false is returned here + * @todo This is needed when a client is created - needs re-write as missing parts of protocol + * @param string + * @return string + */ + public function frame($message, $mask = true) { + $payload = $message; + $type = 'text'; + $masked = $mask; + + $frameHead = array(); + $frame = ''; + $payloadLength = strlen($payload); + + switch($type) { + case 'text': + // first byte indicates FIN, Text-Frame (10000001): + $frameHead[0] = 129; + break; + + case 'close': + // first byte indicates FIN, Close Frame(10001000): + $frameHead[0] = 136; + break; + + case 'ping': + // first byte indicates FIN, Ping frame (10001001): + $frameHead[0] = 137; + break; + + case 'pong': + // first byte indicates FIN, Pong frame (10001010): + $frameHead[0] = 138; + break; + } + + // set mask and payload length (using 1, 3 or 9 bytes) + if($payloadLength > 65535) { + $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); + $frameHead[1] = ($masked === true) ? 255 : 127; + for($i = 0; $i < 8; $i++) { + $frameHead[$i+2] = bindec($payloadLengthBin[$i]); + } + // most significant bit MUST be 0 (return false if to much data) + if($frameHead[2] > 127) { + return false; + } + } elseif($payloadLength > 125) { + $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); + $frameHead[1] = ($masked === true) ? 254 : 126; + $frameHead[2] = bindec($payloadLengthBin[0]); + $frameHead[3] = bindec($payloadLengthBin[1]); + } else { + $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; + } + + // convert frame-head to string: + foreach(array_keys($frameHead) as $i) { + $frameHead[$i] = chr($frameHead[$i]); + } if($masked === true) { + // generate a random mask: + $mask = array(); + for($i = 0; $i < 4; $i++) + { + $mask[$i] = chr(rand(0, 255)); + } + + $frameHead = array_merge($frameHead, $mask); + } + $frame = implode('', $frameHead); + + // append payload to frame: + $framePayload = array(); + for($i = 0; $i < $payloadLength; $i++) { + $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; + } + + return $frame; + } + + /** + * Used when doing the handshake to encode the key, verifying client/server are speaking the same language + * @param string + * @return string + * @internal + */ + public function sign($key) { + return base64_encode(sha1($key . static::GUID, 1)); + } +} \ No newline at end of file diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php new file mode 100644 index 0000000..60c8a2f --- /dev/null +++ b/Version/RFC6455/Frame.php @@ -0,0 +1,226 @@ +getPayloadLength(); + $payload_start = $this->getPayloadStartingByte(); + } catch (\UnderflowException $e) { + return false; + } + + return $payload_length + $payload_start === $this->_bytes_rec; + } + + /** + * {@inheritdoc} + */ + public function addBuffer($buf) { + $buf = (string)$buf; + + $this->_data .= $buf; + $this->_bytes_rec += strlen($buf); + } + + /** + * {@inheritdoc} + */ + public function isFinal() { + if ($this->_bytes_rec < 1) { + throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message'); + } + + $fbb = sprintf('%08b', ord($this->_data[0])); + return (boolean)(int)$fbb[0]; + } + + /** + * {@inheritdoc} + */ + public function isMasked() { + if ($this->_bytes_rec < 2) { + throw new \UnderflowException("Not enough bytes received ({$this->_bytes_rec}) to determine if mask is set"); + } + + return (boolean)bindec(substr(sprintf('%08b', ord($this->_data[1])), 0, 1)); + } + + /** + * {@inheritdoc} + */ + public function getOpcode() { + if ($this->_bytes_rec < 1) { + throw new \UnderflowException('Not enough bytes received to determine opcode'); + } + + return bindec(substr(sprintf('%08b', ord($this->_data[0])), 4, 4)); + } + + /** + * Gets the decimal value of bits 9 (10th) through 15 inclusive + * @return int + * @throws UnderflowException If the buffer doesn't have enough data to determine this + */ + protected function getFirstPayloadVal() { + if ($this->_bytes_rec < 2) { + throw new \UnderflowException('Not enough bytes received'); + } + + return ord($this->_data[1]) & 127; + } + + /** + * @return int (7|23|71) Number of bits defined for the payload length in the fame + * @throws UnderflowException + */ + protected function getNumPayloadBits() { + if ($this->_bytes_rec < 2) { + throw new \UnderflowException('Not enough bytes received'); + } + + // By default 7 bits are used to describe the payload length + // These are bits 9 (10th) through 15 inclusive + $bits = 7; + + // Get the value of those bits + $check = $this->getFirstPayloadVal(); + + // If the value is 126 the 7 bits plus the next 16 are used to describe the payload length + if ($check >= 126) { + $bits += 16; + } + + // If the value of the initial payload length are is 127 an additional 48 bits are used to describe length + // Note: The documentation specifies the length is to be 63 bits, but I think that's a type and is 64 (16+48) + if ($check === 127) { + $bits += 48; + } + + if (!in_array($bits, array(7, 23, 71))) { + throw new \UnexpectedValueException("Malformed frame, invalid payload length provided"); + } + + return $bits; + } + + /** + * This just returns the number of bytes used in the frame to describe the payload length (as opposed to # of bits) + * @see getNumPayloadBits + */ + protected function getNumPayloadBytes() { + return (1 + $this->getNumPayloadBits()) / 8; + } + + /** + * {@inheritdoc} + */ + public function getPayloadLength() { + if ($this->_pay_len_def !== -1) { + return $this->_pay_len_def; + } + + $length_check = $this->getFirstPayloadVal(); + + if ($length_check <= 125) { + $this->_pay_len_def = $length_check; + return $this->getPayloadLength(); + } + + $byte_length = $this->getNumPayloadBytes(); + if ($this->_bytes_rec < 1 + $byte_length) { + throw new \UnderflowException('Not enough data buffered to determine payload length'); + } + + $strings = array(); + for ($i = 2; $i < $byte_length + 1; $i++) { + $strings[] = ord($this->_data[$i]); + } + + $this->_pay_len_def = bindec(vsprintf(str_repeat('%08b', $byte_length - 1), $strings)); + return $this->getPayloadLength(); + } + + /** + * {@inheritdoc} + */ + public function getMaskingKey() { + if (!$this->isMasked()) { + return ''; + } + + $length = 4; + $start = 1 + $this->getNumPayloadBytes(); + + if ($this->_bytes_rec < $start + $length) { + throw new \UnderflowException('Not enough data buffered to calculate the masking key'); + } + + return substr($this->_data, $start, $length); + } + + /** + * {@inheritdoc} + */ + public function getPayloadStartingByte() { + return 1 + $this->getNumPayloadBytes() + strlen($this->getMaskingKey()); + } + + /** + * {@inheritdoc} + */ + public function getPayload() { + if (!$this->isCoalesced()) { + throw new \UnderflowException('Can not return partial message'); + } + + $payload = ''; + $length = $this->getPayloadLength(); + + if ($this->isMasked()) { + $mask = $this->getMaskingKey(); + $start = $this->getPayloadStartingByte(); + + for ($i = 0; $i < $length; $i++) { + $payload .= $this->_data[$i + $start] ^ $mask[$i % 4]; + } + } else { + $payload = substr($this->_data, $start, $this->getPayloadLength()); + } + + if (strlen($payload) !== $length) { + // Is this possible? isCoalesced() math _should_ ensure if there is mal-formed data, it would return false + throw new \UnexpectedValueException('Payload length does not match expected length'); + } + + return $payload; + } +} \ No newline at end of file diff --git a/Version/RFC6455/HandshakeVerifier.php b/Version/RFC6455/HandshakeVerifier.php new file mode 100644 index 0000000..afad604 --- /dev/null +++ b/Version/RFC6455/HandshakeVerifier.php @@ -0,0 +1,147 @@ +verifyMethod($request->getMethod()); + $passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion()); + $passes += (int)$this->verifyRequestURI($request->getPath()); + $passes += (int)$this->verifyHost($request->getHeader('Host', true)); + $passes += (int)$this->verifyUpgradeRequest($request->getHeader('Upgrade', true)); + $passes += (int)$this->verifyConnection($request->getHeader('Connection', true)); + $passes += (int)$this->verifyKey($request->getHeader('Sec-WebSocket-Key', true)); + //$passes += (int)$this->verifyVersion($headers['Sec-WebSocket-Version']); // Temporarily breaking functionality + + return (7 === $passes); + } + + /** + * Test the HTTP method. MUST be "GET" + * @param string + * @return bool + * @todo Look into STD if "get" is valid (am I supposed to do case conversion?) + */ + public function verifyMethod($val) { + return ('GET' === $val); + } + + /** + * Test the HTTP version passed. MUST be 1.1 or greater + * @param string|int + * @return bool + */ + public function verifyHTTPVersion($val) { + return (1.1 <= (double)$val); + } + + /** + * @param string + * @return bool + * @todo Verify the logic here is correct + */ + public function verifyRequestURI($val) { + if ($val[0] != '/') { + return false; + } + + if (false !== strstr($val, '#')) { + return false; + } + + return mb_check_encoding($val, 'ASCII'); + } + + /** + * @param string|null + * @return bool + * @todo Find out if I can find the master socket, ensure the port is attached to header if not 80 or 443 - not sure if this is possible, as I tried to hide it + * @todo Once I fix HTTP::getHeaders just verify this isn't NULL or empty...or manybe need to verify it's a valid domin??? Or should it equal $_SERVER['HOST'] ? + */ + public function verifyHost($val) { + return (null !== $val); + } + + /** + * Verify the Upgrade request to WebSockets. + * @param string MUST equal "websocket" + * @return bool + */ + public function verifyUpgradeRequest($val) { + return ('websocket' === $val); + } + + /** + * Verify the Connection header + * @param string MUST equal "Upgrade" + * @return bool + */ + public function verifyConnection($val) { + if ('Upgrade' === $val) { + return true; + } + + $vals = explode(',', str_replace(', ', ',', $val)); + return (false !== array_search('Upgrade', $vals)); + } + + /** + * This function verifyies the nonce is valid (64 big encoded, 16 bytes random string) + * @param string|null + * @return bool + * @todo The spec says we don't need to base64_decode - can I just check if the length is 24 and not decode? + */ + public function verifyKey($val) { + return (16 === strlen(base64_decode((string)$val))); + } + + /** + * Verify Origin matches RFC6454 IF it is set + * Origin is an optional field + * @param string|null + * @return bool + * @todo Implement verification functality - see section 4.2.1.7 + */ + public function verifyOrigin($val) { + if (null === $val) { + return true; + } + + // logic here + return true; + } + + /** + * Verify the version passed matches this RFC + * @param string|int MUST equal 13|"13" + * @return bool + * @todo Ran in to a problem here...I'm having HyBi use the RFC files, this breaks it! oops + */ + public function verifyVersion($val) { + return (13 === (int)$val); + } + + /** + * @todo Write logic for this method. See section 4.2.1.8 + */ + public function verifyProtocol($val) { + } + + /** + * @todo Write logic for this method. See section 4.2.1.9 + */ + public function verifyExtensions($val) { + } +} \ No newline at end of file diff --git a/Version/RFC6455/Message.php b/Version/RFC6455/Message.php new file mode 100644 index 0000000..5385475 --- /dev/null +++ b/Version/RFC6455/Message.php @@ -0,0 +1,88 @@ +_frames = new \SplDoublyLinkedList; + } + + /** + * {@inheritdoc} + */ + public function __toString() { + return $this->getPayload(); + } + + /** + * {@inheritdoc} + */ + public function isCoalesced() { + if (count($this->_frames) == 0) { + return false; + } + + $last = $this->_frames->top(); + + return ($last->isCoalesced() && $last->isFinal()); + } + + /** + * {@inheritdoc} + * @todo Should I allow addFrame if the frame is not coalesced yet? I believe I'm assuming this class will only receive fully formed frame messages + * @todo Also, I should perhaps check the type...control frames (ping/pong/close) are not to be considered part of a message + */ + public function addFrame(FrameInterface $fragment) { + $this->_frames->push($fragment); + } + + /** + * {@inheritdoc} + */ + public function getOpcode() { + if (count($this->_frames) == 0) { + throw new \UnderflowException('No frames have been added to this message'); + } + + return $this->_frames->bottom()->getOpcode(); + } + + /** + * {@inheritdoc} + */ + public function getPayloadLength() { + $len = 0; + + foreach ($this->_frames as $frame) { + try { + $len += $frame->getPayloadLength(); + } catch (\UnderflowException $e) { + } + } + + return $len; + } + + /** + * {@inheritdoc} + */ + public function getPayload() { + if (!$this->isCoalesced()) { + throw new \UnderflowMessage('Message has not been put back together yet'); + } + + $buffer = ''; + + foreach ($this->_frames as $frame) { + $buffer .= $frame->getPayload(); + } + + return $buffer; + } +} \ No newline at end of file diff --git a/Version/VersionInterface.php b/Version/VersionInterface.php new file mode 100644 index 0000000..1602641 --- /dev/null +++ b/Version/VersionInterface.php @@ -0,0 +1,47 @@ +WebSocket->version->frame($data, false); + + $this->getConnection()->send($data); + } + + public function close() { + // send close frame + + // ??? + + // profit + + $this->getConnection()->close(); // temporary + } + + public function ping() { + } + + public function pong() { + } +} \ No newline at end of file diff --git a/WsServer.php b/WsServer.php new file mode 100644 index 0000000..c3b1a48 --- /dev/null +++ b/WsServer.php @@ -0,0 +1,230 @@ + null + , 'Hixie76' => null + , 'RFC6455' => null + ); + + protected $_mask_payload = false; + + /** + * For now, array_push accepted subprotocols to this array + * @deprecated + * @temporary + */ + public $accepted_subprotocols = array(); + + public function __construct(MessageComponentInterface $component) { + $this->_decorating = $component; + $this->connections = new \SplObjectStorage; + } + + /** + * {@inheritdoc} + */ + public function onOpen(ConnectionInterface $conn) { + $conn->WebSocket = new \stdClass; + $conn->WebSocket->handshake = false; + $conn->WebSocket->headers = ''; + } + + /** + * Do handshake, frame/unframe messages coming/going in stack + * {@inheritdoc} + */ + public function onMessage(ConnectionInterface $from, $msg) { + if (true !== $from->WebSocket->handshake) { + if (!isset($from->WebSocket->version)) { + $from->WebSocket->headers .= $msg; + if (!$this->isMessageComplete($from->WebSocket->headers)) { + return; + } + + $headers = RequestFactory::getInstance()->fromMessage($from->WebSocket->headers); + $from->WebSocket->version = $this->getVersion($headers); + $from->WebSocket->headers = $headers; + } + + $response = $from->WebSocket->version->handshake($from->WebSocket->headers); + $from->WebSocket->handshake = true; + + // This block is to be moved/changed later + $agreed_protocols = array(); + $requested_protocols = $from->WebSocket->headers->getTokenizedHeader('Sec-WebSocket-Protocol', ','); + if (null !== $requested_protocols) { + foreach ($this->accepted_subprotocols as $sub_protocol) { + if (false !== $requested_protocols->hasValue($sub_protocol)) { + $agreed_protocols[] = $sub_protocol; + } + } + } + if (count($agreed_protocols) > 0) { + $response->setHeader('Sec-WebSocket-Protocol', implode(',', $agreed_protocols)); + } + $response->setHeader('X-Powered-By', \Ratchet\VERSION); + $header = (string)$response; + + $from->send($header); + + $conn = new WsConnection($from); + $this->connections->attach($from, $conn); + + return $this->_decorating->onOpen($conn); + } + + if (!isset($from->WebSocket->message)) { + $from->WebSocket->message = $from->WebSocket->version->newMessage(); + } + + // There is a frame fragment attatched to the connection, add to it + if (!isset($from->WebSocket->frame)) { + $from->WebSocket->frame = $from->WebSocket->version->newFrame(); + } + + $from->WebSocket->frame->addBuffer($msg); + if ($from->WebSocket->frame->isCoalesced()) { + if ($from->WebSocket->frame->getOpcode() > 2) { + $from->end(); + throw new \UnexpectedValueException('Control frame support coming soon!'); + } + // Check frame + // If is control frame, do your thing + // Else, add to message + // Control frames (ping, pong, close) can be sent in between a fragmented message + + $from->WebSocket->message->addFrame($from->WebSocket->frame); + unset($from->WebSocket->frame); + } + + if ($from->WebSocket->message->isCoalesced()) { + $this->_decorating->onMessage($this->connections[$from], (string)$from->WebSocket->message); + unset($from->WebSocket->message); + } + } + + /** + * {@inheritdoc} + */ + public function onClose(ConnectionInterface $conn) { + // WS::onOpen is not called when the socket connects, it's call when the handshake is done + // The socket could close before WS calls onOpen, so we need to check if we've "opened" it for the developer yet + if ($this->connections->contains($conn)) { + $decor = $this->connections[$conn]; + $this->connections->detach($conn); + + $this->_decorating->onClose($decor); + } + } + + /** + * {@inheritdoc} + */ + public function onError(ConnectionInterface $conn, \Exception $e) { + if ($this->connections->contains($conn)) { + $this->_decorating->onError($this->connections[$conn], $e); + } else { + $conn->close(); + } + } + + /** + * Detect the WebSocket protocol version a client is using based on the HTTP header request + * @param string HTTP handshake request + * @return Version\VersionInterface + * @throws UnderFlowException If we think the entire header message hasn't been buffered yet + * @throws InvalidArgumentException If we can't understand protocol version request + * @todo Verify the first line of the HTTP header as per page 16 of RFC 6455 + */ + protected function getVersion(RequestInterface $request) { + foreach ($this->_versions as $name => $instance) { + if (null !== $instance) { + if ($instance::isProtocol($request)) { + return $instance; + } + } else { + $ns = __NAMESPACE__ . "\\Version\\{$name}"; + if ($ns::isProtocol($request)) { + $this->_versions[$name] = new $ns; + return $this->_versions[$name]; + } + } + } + + throw new \InvalidArgumentException('Could not identify WebSocket protocol'); + } + + /** + * @param string + * @return bool + * @todo Abstract, some hard coding done for (stupid) Hixie protocol + */ + protected function isMessageComplete($message) { + static $crlf = "\r\n\r\n"; + + $headers = (boolean)strstr($message, $crlf); + if (!$headers) { + + return false; + } + + if (strstr($message, 'Sec-WebSocket-Key2')) { + if (8 !== strlen(substr($message, strpos($message, $crlf) + strlen($crlf)))) { + return false; + } + } + + return true; + } + + /** + * Disable a version of the WebSocket protocol *cough*Hixie76*cough* + * @param string The name of the version to disable + * @throws InvalidArgumentException If the given version does not exist + */ + public function disableVersion($name) { + if (!array_key_exists($name, $this->_versions)) { + throw new \InvalidArgumentException("Version {$name} not found"); + } + + unset($this->_versions[$name]); + } + + /** + * Set the option to mask the payload upon sending to client + * If WebSocket is used as server, this should be false, client to true + * @param bool + * @todo User shouldn't have to know/set this, need to figure out how to do this automatically + */ + public function setMaskPayload($opt) { + $this->_mask_payload = (boolean)$opt; + } +} \ No newline at end of file diff --git a/WsServerInterface.php b/WsServerInterface.php new file mode 100644 index 0000000..30b9ba4 --- /dev/null +++ b/WsServerInterface.php @@ -0,0 +1,16 @@ +