327 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			327 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace Ratchet\WebSocket\Version;
 | |
| use Ratchet\ConnectionInterface;
 | |
| use Ratchet\MessageInterface;
 | |
| use Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier;
 | |
| use Ratchet\WebSocket\Version\RFC6455\Message;
 | |
| use Ratchet\WebSocket\Version\RFC6455\Frame;
 | |
| use Ratchet\WebSocket\Version\RFC6455\Connection;
 | |
| use Guzzle\Http\Message\RequestInterface;
 | |
| use Guzzle\Http\Message\Response;
 | |
| 
 | |
| /**
 | |
|  * @link http://tools.ietf.org/html/rfc6455
 | |
|  * @todo Unicode: return mb_convert_encoding(pack("N",$u), mb_internal_encoding(), 'UCS-4BE');
 | |
|  */
 | |
| class RFC6455 implements VersionInterface {
 | |
|     const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
 | |
| 
 | |
|     const UTF8_ACCEPT = 0;
 | |
|     const UTF8_REJECT = 1;
 | |
| 
 | |
|     /**
 | |
|      * Incremental UTF-8 validator with constant memory consumption (minimal state).
 | |
|      *
 | |
|      * Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
 | |
|      * Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
 | |
|      */
 | |
|     public static $dfa = array(
 | |
|         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 00..1f
 | |
|         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 20..3f
 | |
|         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 40..5f
 | |
|         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 60..7f
 | |
|         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, # 80..9f
 | |
|         7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, # a0..bf
 | |
|         8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, # c0..df
 | |
|         0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, # e0..ef
 | |
|         0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, # f0..ff
 | |
|         0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, # s0..s0
 | |
|         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, # s1..s2
 | |
|         1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, # s3..s4
 | |
|         1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, # s5..s6
 | |
|         1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, # s7..s8
 | |
|     );
 | |
| 
 | |
|     /**
 | |
|      * @var RFC6455\HandshakeVerifier
 | |
|      */
 | |
|     protected $_verifier;
 | |
| 
 | |
|     /**
 | |
|      * A lookup of the valid close codes that can be sent in a frame
 | |
|      * @var array
 | |
|      */
 | |
|     private $closeCodes = array();
 | |
| 
 | |
|     /**
 | |
|      * Lookup if mbstring is available
 | |
|      * @var bool
 | |
|      */
 | |
|      private $hasMbString = false;
 | |
| 
 | |
|     public function __construct() {
 | |
|         $this->_verifier = new HandshakeVerifier;
 | |
|         $this->setCloseCodes();
 | |
| 
 | |
|         $this->hasMbString = extension_loaded('mbstring');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * {@inheritdoc}
 | |
|      */
 | |
|     public function isProtocol(RequestInterface $request) {
 | |
|         $version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
 | |
| 
 | |
|         return ($this->getVersionNumber() === $version);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * {@inheritdoc}
 | |
|      */
 | |
|     public function getVersionNumber() {
 | |
|         return 13;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * {@inheritdoc}
 | |
|      */
 | |
|     public function handshake(RequestInterface $request) {
 | |
|         if (true !== $this->_verifier->verifyAll($request)) {
 | |
|             return new Response(400);
 | |
|         }
 | |
| 
 | |
|         return new Response(101, array(
 | |
|             'Upgrade'              => 'websocket'
 | |
|           , 'Connection'           => 'Upgrade'
 | |
|           , 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key'))
 | |
|         ));
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param Ratchet\ConnectionInterface
 | |
|      * @return Ratchet\WebSocket\Version\RFC6455\Connection
 | |
|      */
 | |
|     public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) {
 | |
|         $upgraded = new Connection($conn);
 | |
| 
 | |
|         if (!isset($upgraded->WebSocket)) {
 | |
|             $upgraded->WebSocket = new \StdClass;
 | |
|         }
 | |
| 
 | |
|         $upgraded->WebSocket->coalescedCallback = $coalescedCallback;
 | |
| 
 | |
|         return $upgraded;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param Ratchet\WebSocket\Version\RFC6455\Connection
 | |
|      * @param string
 | |
|      */
 | |
|     public function onMessage(ConnectionInterface $from, $data) {
 | |
|         $overflow = '';
 | |
| 
 | |
|         if (!isset($from->WebSocket->message)) {
 | |
|             $from->WebSocket->message = $this->newMessage();
 | |
|         }
 | |
| 
 | |
|         // There is a frame fragment attatched to the connection, add to it
 | |
|         if (!isset($from->WebSocket->frame)) {
 | |
|             $from->WebSocket->frame = $this->newFrame();
 | |
|         }
 | |
| 
 | |
|         $from->WebSocket->frame->addBuffer($data);
 | |
|         if ($from->WebSocket->frame->isCoalesced()) {
 | |
|             $frame = $from->WebSocket->frame;
 | |
| 
 | |
|             if (false !== $frame->getRsv1() ||
 | |
|                 false !== $frame->getRsv2() ||
 | |
|                 false !== $frame->getRsv3()
 | |
|             ) {
 | |
|                 return $from->close($frame::CLOSE_PROTOCOL);
 | |
|             }
 | |
| 
 | |
|             if (!$frame->isMasked()) {
 | |
|                 return $from->close($frame::CLOSE_PROTOCOL);
 | |
|             }
 | |
| 
 | |
|             $opcode = $frame->getOpcode();
 | |
| 
 | |
|             if ($opcode > 2) {
 | |
|                 if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) {
 | |
|                     return $from->close($frame::CLOSE_PROTOCOL);
 | |
|                 }
 | |
| 
 | |
|                 switch ($opcode) {
 | |
|                     case $frame::OP_CLOSE:
 | |
|                         $closeCode = 0;
 | |
| 
 | |
|                         $bin = $frame->getPayload();
 | |
| 
 | |
|                         if (empty($bin)) {
 | |
|                             return $from->close();
 | |
|                         }
 | |
| 
 | |
|                         if (strlen($bin) >= 2) {
 | |
|                             list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2)));
 | |
|                         }
 | |
| 
 | |
|                         if (!$this->isValidCloseCode($closeCode)) {
 | |
|                             return $from->close($frame::CLOSE_PROTOCOL);
 | |
|                         }
 | |
| 
 | |
|                         if (!$this->isUtf8(substr($bin, 2))) {
 | |
|                             return $from->close($frame::CLOSE_BAD_PAYLOAD);
 | |
|                         }
 | |
| 
 | |
|                         return $from->close($frame);
 | |
|                     break;
 | |
|                     case $frame::OP_PING:
 | |
|                         $from->send($this->newFrame($frame->getPayload(), true, $frame::OP_PONG));
 | |
|                     break;
 | |
|                     case $frame::OP_PONG:
 | |
|                     break;
 | |
|                     default:
 | |
|                         return $from->close($frame::CLOSE_PROTOCOL);
 | |
|                     break;
 | |
|                 }
 | |
| 
 | |
|                 $overflow = $from->WebSocket->frame->extractOverflow();
 | |
| 
 | |
|                 unset($from->WebSocket->frame, $frame, $opcode);
 | |
| 
 | |
|                 if (strlen($overflow) > 0) {
 | |
|                     $this->onMessage($from, $overflow);
 | |
|                 }
 | |
| 
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             $overflow = $from->WebSocket->frame->extractOverflow();
 | |
| 
 | |
|             if ($frame::OP_CONTINUE == $frame->getOpcode() && 0 == count($from->WebSocket->message)) {
 | |
|                 return $from->close($frame::CLOSE_PROTOCOL);
 | |
|             }
 | |
| 
 | |
|             if (count($from->WebSocket->message) > 0 && $frame::OP_CONTINUE != $frame->getOpcode()) {
 | |
|                 return $from->close($frame::CLOSE_PROTOCOL);
 | |
|             }
 | |
| 
 | |
|             $from->WebSocket->message->addFrame($from->WebSocket->frame);
 | |
|             unset($from->WebSocket->frame);
 | |
|         }
 | |
| 
 | |
|         if ($from->WebSocket->message->isCoalesced()) {
 | |
|             $parsed = $from->WebSocket->message->getPayload();
 | |
|             unset($from->WebSocket->message);
 | |
| 
 | |
|             if (!$this->isUtf8($parsed)) {
 | |
|                 return $from->close(Frame::CLOSE_BAD_PAYLOAD);
 | |
|             }
 | |
| 
 | |
|             $from->WebSocket->coalescedCallback->onMessage($from, $parsed);
 | |
|         }
 | |
| 
 | |
|         if (strlen($overflow) > 0) {
 | |
|             $this->onMessage($from, $overflow);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @return RFC6455\Message
 | |
|      */
 | |
|     public function newMessage() {
 | |
|         return new Message;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @return RFC6455\Frame
 | |
|      */
 | |
|     public function newFrame($payload = null, $final = true, $opcode = 1) {
 | |
|         return new Frame($payload, $final, $opcode);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @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) {
 | |
|         return $this->newFrame($message)->getContents();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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, true));
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Determine if a close code is valid
 | |
|      * @param int|string
 | |
|      * @return bool
 | |
|      */
 | |
|     public function isValidCloseCode($val) {
 | |
|         if (array_key_exists($val, $this->closeCodes)) {
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         if ($val >= 3000 && $val <= 4999) {
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Creates a private lookup of valid, private close codes
 | |
|      */
 | |
|     protected function setCloseCodes() {
 | |
|         $this->closeCodes[Frame::CLOSE_NORMAL]      = true;
 | |
|         $this->closeCodes[Frame::CLOSE_GOING_AWAY]  = true;
 | |
|         $this->closeCodes[Frame::CLOSE_PROTOCOL]    = true;
 | |
|         $this->closeCodes[Frame::CLOSE_BAD_DATA]    = true;
 | |
|         //$this->closeCodes[Frame::CLOSE_NO_STATUS]   = true;
 | |
|         //$this->closeCodes[Frame::CLOSE_ABNORMAL]    = true;
 | |
|         $this->closeCodes[Frame::CLOSE_BAD_PAYLOAD] = true;
 | |
|         $this->closeCodes[Frame::CLOSE_POLICY]      = true;
 | |
|         $this->closeCodes[Frame::CLOSE_TOO_BIG]     = true;
 | |
|         $this->closeCodes[Frame::CLOSE_MAND_EXT]    = true;
 | |
|         $this->closeCodes[Frame::CLOSE_SRV_ERR]     = true;
 | |
|         //$this->closeCodes[Frame::CLOSE_TLS]         = true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Determine if a string is a valid UTF-8 string
 | |
|      * @param string
 | |
|      * @return bool
 | |
|      */
 | |
|     function isUtf8($str) {
 | |
|         if ($this->hasMbString && false === mb_check_encoding($str, 'UTF-8')) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         $len = strlen($str);
 | |
| 
 | |
|         // The secondary method of checking is painfully slow
 | |
|         // If the message is more than 10kb, skip UTF-8 checks
 | |
|         if ($len > 10000) {
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         $state = static::UTF8_ACCEPT;
 | |
| 
 | |
|         for ($i = 0; $i < $len; $i++) {
 | |
|             $state = static::$dfa[256 + ($state << 4) + static::$dfa[ord($str[$i])]];
 | |
| 
 | |
|             if (static::UTF8_REJECT === $state) {
 | |
|                 return false;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| } | 
