
Added the ability to turn UTF-8 encoding checks off to increase performance Separated encoding checks into its own set of classes Encoding checks now use mbstring or iconv depending on availability
266 lines
8.1 KiB
PHP
266 lines
8.1 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 Ratchet\WebSocket\Encoding\ValidatorInterface;
|
|
use Ratchet\WebSocket\Encoding\Validator;
|
|
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';
|
|
|
|
/**
|
|
* @var RFC6455\HandshakeVerifier
|
|
*/
|
|
protected $_verifier;
|
|
|
|
/**
|
|
* A lookup of the valid close codes that can be sent in a frame
|
|
* @var array
|
|
*/
|
|
private $closeCodes = array();
|
|
|
|
/**
|
|
* @var Ratchet\WebSocket\Encoding\ValidatorInterface
|
|
*/
|
|
protected $validator;
|
|
|
|
public function __construct(ValidatorInterface $validator = null) {
|
|
$this->_verifier = new HandshakeVerifier;
|
|
$this->setCloseCodes();
|
|
|
|
if (null === $validator) {
|
|
$validator = new Validator;
|
|
}
|
|
|
|
$this->validator = $validator;
|
|
}
|
|
|
|
/**
|
|
* {@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->validator->checkEncoding(substr($bin, 2), 'UTF-8')) {
|
|
return $from->close($frame::CLOSE_BAD_PAYLOAD);
|
|
}
|
|
|
|
return $from->close($frame);
|
|
break;
|
|
case $frame::OP_PING:
|
|
$from->send($this->newFrame($frame->getPayload(), true, $frame::OP_PONG));
|
|
break;
|
|
case $frame::OP_PONG:
|
|
break;
|
|
default:
|
|
return $from->close($frame::CLOSE_PROTOCOL);
|
|
break;
|
|
}
|
|
|
|
$overflow = $from->WebSocket->frame->extractOverflow();
|
|
|
|
unset($from->WebSocket->frame, $frame, $opcode);
|
|
|
|
if (strlen($overflow) > 0) {
|
|
$this->onMessage($from, $overflow);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$overflow = $from->WebSocket->frame->extractOverflow();
|
|
|
|
if ($frame::OP_CONTINUE == $frame->getOpcode() && 0 == count($from->WebSocket->message)) {
|
|
return $from->close($frame::CLOSE_PROTOCOL);
|
|
}
|
|
|
|
if (count($from->WebSocket->message) > 0 && $frame::OP_CONTINUE != $frame->getOpcode()) {
|
|
return $from->close($frame::CLOSE_PROTOCOL);
|
|
}
|
|
|
|
$from->WebSocket->message->addFrame($from->WebSocket->frame);
|
|
unset($from->WebSocket->frame);
|
|
}
|
|
|
|
if ($from->WebSocket->message->isCoalesced()) {
|
|
$parsed = $from->WebSocket->message->getPayload();
|
|
unset($from->WebSocket->message);
|
|
|
|
if (!$this->validator->checkEncoding($parsed, 'UTF-8')) {
|
|
return $from->close(Frame::CLOSE_BAD_PAYLOAD);
|
|
}
|
|
|
|
$from->WebSocket->coalescedCallback->onMessage($from, $parsed);
|
|
}
|
|
|
|
if (strlen($overflow) > 0) {
|
|
$this->onMessage($from, $overflow);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return RFC6455\Message
|
|
*/
|
|
public function newMessage() {
|
|
return new Message;
|
|
}
|
|
|
|
/**
|
|
* @return RFC6455\Frame
|
|
*/
|
|
public function newFrame($payload = null, $final = null, $opcode = null) {
|
|
return new Frame($payload, $final, $opcode);
|
|
}
|
|
|
|
/**
|
|
* Used when doing the handshake to encode the key, verifying client/server are speaking the same language
|
|
* @param string
|
|
* @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;
|
|
}
|
|
} |