PSR-7 + RFC
Http components and APIs now use PSR-7 interfaces No longer using deprecated Guzzle dependency Use RFC6455 repo for WebSocket message handling Remove Hixie76 (refs #201)
This commit is contained in:
parent
6b247c0525
commit
a744aea1f0
3
Makefile
3
Makefile
@ -13,16 +13,19 @@ abtests:
|
||||
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver-noutf8.php 8003 StreamSelect &
|
||||
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8004 LibEv &
|
||||
wstest -m testeeserver -w ws://localhost:8000 &
|
||||
sleep 1
|
||||
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-all.json
|
||||
killall php wstest
|
||||
|
||||
abtest:
|
||||
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8000 StreamSelect &
|
||||
sleep 1
|
||||
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-quick.json
|
||||
killall php
|
||||
|
||||
profile:
|
||||
php -d 'xdebug.profiler_enable=1' tests/autobahn/bin/fuzzingserver.php 8000 LibEvent &
|
||||
sleep 1
|
||||
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-profile.json
|
||||
killall php
|
||||
|
||||
|
@ -25,7 +25,8 @@
|
||||
, "require": {
|
||||
"php": ">=5.3.9"
|
||||
, "react/socket": "^0.3 || ^0.4"
|
||||
, "guzzle/http": "^3.6"
|
||||
, "guzzlehttp/psr7": "^1.0"
|
||||
, "ratchet/rfc6455": "dev-psr7"
|
||||
, "symfony/http-foundation": "^2.2"
|
||||
, "symfony/routing": "^2.2"
|
||||
}
|
||||
|
22
src/Ratchet/Http/CloseResponseTrait.php
Normal file
22
src/Ratchet/Http/CloseResponseTrait.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace Ratchet\Http;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use GuzzleHttp\Psr7 as gPsr;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
|
||||
trait CloseResponseTrait {
|
||||
/**
|
||||
* Close a connection with an HTTP response
|
||||
* @param \Ratchet\ConnectionInterface $conn
|
||||
* @param int $code HTTP status code
|
||||
* @return null
|
||||
*/
|
||||
private function close(ConnectionInterface $conn, $code = 400, array $additional_headers = []) {
|
||||
$response = new Response($code, array_merge([
|
||||
'X-Powered-By' => \Ratchet\VERSION
|
||||
], $additional_headers));
|
||||
|
||||
$conn->send(gPsr\str($response));
|
||||
$conn->close();
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\Http\Guzzle\Http\Message;
|
||||
use Guzzle\Http\Message\RequestFactory as GuzzleRequestFactory;
|
||||
use Guzzle\Http\EntityBody;
|
||||
|
||||
class RequestFactory extends GuzzleRequestFactory {
|
||||
|
||||
protected static $ratchetInstance;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
if (!static::$ratchetInstance) {
|
||||
static::$ratchetInstance = new static();
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
return static::$ratchetInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function create($method, $url, $headers = null, $body = '', array $options = array()) {
|
||||
$c = $this->entityEnclosingRequestClass;
|
||||
$request = new $c($method, $url, $headers);
|
||||
$request->setBody(EntityBody::factory($body));
|
||||
|
||||
return $request;
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
namespace Ratchet\Http;
|
||||
use Ratchet\MessageInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\Http\Guzzle\Http\Message\RequestFactory;
|
||||
use GuzzleHttp\Psr7 as g7;
|
||||
|
||||
/**
|
||||
* This class receives streaming data from a client request
|
||||
@ -22,7 +22,7 @@ class HttpRequestParser implements MessageInterface {
|
||||
/**
|
||||
* @param \Ratchet\ConnectionInterface $context
|
||||
* @param string $data Data stream to buffer
|
||||
* @return \Guzzle\Http\Message\RequestInterface|null
|
||||
* @return \Psr\Http\Message\RequestInterface
|
||||
* @throws \OverflowException If the message buffer has become too large
|
||||
*/
|
||||
public function onMessage(ConnectionInterface $context, $data) {
|
||||
@ -37,7 +37,7 @@ class HttpRequestParser implements MessageInterface {
|
||||
}
|
||||
|
||||
if ($this->isEom($context->httpBuffer)) {
|
||||
$request = RequestFactory::getInstance()->fromMessage($context->httpBuffer);
|
||||
$request = g7\parse_request($context->httpBuffer);
|
||||
|
||||
unset($context->httpBuffer);
|
||||
|
||||
|
@ -2,9 +2,10 @@
|
||||
namespace Ratchet\Http;
|
||||
use Ratchet\MessageComponentInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Guzzle\Http\Message\Response;
|
||||
|
||||
class HttpServer implements MessageComponentInterface {
|
||||
use CloseResponseTrait;
|
||||
|
||||
/**
|
||||
* Buffers incoming HTTP requests returning a Guzzle Request when coalesced
|
||||
* @var HttpRequestParser
|
||||
@ -72,19 +73,4 @@ class HttpServer implements MessageComponentInterface {
|
||||
$this->close($conn, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a connection with an HTTP response
|
||||
* @param \Ratchet\ConnectionInterface $conn
|
||||
* @param int $code HTTP status code
|
||||
* @return null
|
||||
*/
|
||||
protected function close(ConnectionInterface $conn, $code = 400) {
|
||||
$response = new Response($code, array(
|
||||
'X-Powered-By' => \Ratchet\VERSION
|
||||
));
|
||||
|
||||
$conn->send((string)$response);
|
||||
$conn->close();
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,12 @@
|
||||
namespace Ratchet\Http;
|
||||
use Ratchet\MessageComponentInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
interface HttpServerInterface extends MessageComponentInterface {
|
||||
/**
|
||||
* @param \Ratchet\ConnectionInterface $conn
|
||||
* @param \Guzzle\Http\Message\RequestInterface $request null is default because PHP won't let me overload; don't pass null!!!
|
||||
* @param \Psr\Http\Message\RequestInterface $request null is default because PHP won't let me overload; don't pass null!!!
|
||||
* @throws \UnexpectedValueException if a RequestInterface is not passed
|
||||
*/
|
||||
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null);
|
||||
|
@ -1,9 +1,8 @@
|
||||
<?php
|
||||
namespace Ratchet\Http;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\MessageComponentInterface;
|
||||
use Guzzle\Http\Message\Response;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* A middleware to ensure JavaScript clients connecting are from the expected domain.
|
||||
@ -11,18 +10,20 @@ use Guzzle\Http\Message\Response;
|
||||
* Note: This can be spoofed from non-web browser clients
|
||||
*/
|
||||
class OriginCheck implements HttpServerInterface {
|
||||
use CloseResponseTrait;
|
||||
|
||||
/**
|
||||
* @var \Ratchet\MessageComponentInterface
|
||||
*/
|
||||
protected $_component;
|
||||
|
||||
public $allowedOrigins = array();
|
||||
public $allowedOrigins = [];
|
||||
|
||||
/**
|
||||
* @param MessageComponentInterface $component Component/Application to decorate
|
||||
* @param array $allowed An array of allowed domains that are allowed to connect from
|
||||
*/
|
||||
public function __construct(MessageComponentInterface $component, array $allowed = array()) {
|
||||
public function __construct(MessageComponentInterface $component, array $allowed = []) {
|
||||
$this->_component = $component;
|
||||
$this->allowedOrigins += $allowed;
|
||||
}
|
||||
@ -31,7 +32,7 @@ class OriginCheck implements HttpServerInterface {
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {
|
||||
$header = (string)$request->getHeader('Origin');
|
||||
$header = (string)$request->getHeader('Origin')[0];
|
||||
$origin = parse_url($header, PHP_URL_HOST) ?: $header;
|
||||
|
||||
if (!in_array($origin, $this->allowedOrigins)) {
|
||||
@ -61,19 +62,4 @@ class OriginCheck implements HttpServerInterface {
|
||||
function onError(ConnectionInterface $conn, \Exception $e) {
|
||||
return $this->_component->onError($conn, $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a connection with an HTTP response
|
||||
* @param \Ratchet\ConnectionInterface $conn
|
||||
* @param int $code HTTP status code
|
||||
* @return null
|
||||
*/
|
||||
protected function close(ConnectionInterface $conn, $code = 400) {
|
||||
$response = new Response($code, array(
|
||||
'X-Powered-By' => \Ratchet\VERSION
|
||||
));
|
||||
|
||||
$conn->send((string)$response);
|
||||
$conn->close();
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
<?php
|
||||
namespace Ratchet\Http;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
use Guzzle\Http\Message\Response;
|
||||
use Guzzle\Http\Url;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
|
||||
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
|
||||
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
|
||||
|
||||
class Router implements HttpServerInterface {
|
||||
use CloseResponseTrait;
|
||||
|
||||
/**
|
||||
* @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
|
||||
*/
|
||||
@ -27,12 +27,14 @@ class Router implements HttpServerInterface {
|
||||
throw new \UnexpectedValueException('$request can not be null');
|
||||
}
|
||||
|
||||
$uri = $request->getUri();
|
||||
|
||||
$context = $this->_matcher->getContext();
|
||||
$context->setMethod($request->getMethod());
|
||||
$context->setHost($request->getHost());
|
||||
$context->setHost($uri->getHost());
|
||||
|
||||
try {
|
||||
$route = $this->_matcher->match($request->getPath());
|
||||
$route = $this->_matcher->match($uri->getPath());
|
||||
} catch (MethodNotAllowedException $nae) {
|
||||
return $this->close($conn, 403);
|
||||
} catch (ResourceNotFoundException $nfe) {
|
||||
@ -47,17 +49,18 @@ class Router implements HttpServerInterface {
|
||||
throw new \UnexpectedValueException('All routes must implement Ratchet\Http\HttpServerInterface');
|
||||
}
|
||||
|
||||
$parameters = array();
|
||||
foreach($route as $key => $value) {
|
||||
if ((is_string($key)) && ('_' !== substr($key, 0, 1))) {
|
||||
$parameters[$key] = $value;
|
||||
}
|
||||
}
|
||||
$parameters = array_merge($parameters, $request->getQuery()->getAll());
|
||||
|
||||
$url = Url::factory($request->getPath());
|
||||
$url->setQuery($parameters);
|
||||
$request->setUrl($url);
|
||||
// TODO: Apply Symfony default params to request
|
||||
// $parameters = [];
|
||||
// foreach($route as $key => $value) {
|
||||
// if ((is_string($key)) && ('_' !== substr($key, 0, 1))) {
|
||||
// $parameters[$key] = $value;
|
||||
// }
|
||||
// }
|
||||
// $parameters = array_merge($parameters, gPsr\parse_query($uri->getQuery()));
|
||||
//
|
||||
// $url = Url::factory($request->getPath());
|
||||
// $url->setQuery($parameters);
|
||||
// $request->setUrl($url);
|
||||
|
||||
$conn->controller = $route['_controller'];
|
||||
$conn->controller->onOpen($conn, $request);
|
||||
@ -87,19 +90,4 @@ class Router implements HttpServerInterface {
|
||||
$conn->controller->onError($conn, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a connection with an HTTP response
|
||||
* @param \Ratchet\ConnectionInterface $conn
|
||||
* @param int $code HTTP status code
|
||||
* @return null
|
||||
*/
|
||||
protected function close(ConnectionInterface $conn, $code = 400) {
|
||||
$response = new Response($code, array(
|
||||
'X-Powered-By' => \Ratchet\VERSION
|
||||
));
|
||||
|
||||
$conn->send((string)$response);
|
||||
$conn->close();
|
||||
}
|
||||
}
|
79
src/Ratchet/WebSocket/ConnectionContext.php
Normal file
79
src/Ratchet/WebSocket/ConnectionContext.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\MessageComponentInterface;
|
||||
use Ratchet\RFC6455\Messaging\Protocol\FrameInterface;
|
||||
use Ratchet\RFC6455\Messaging\Protocol\MessageInterface;
|
||||
use Ratchet\RFC6455\Messaging\Streaming\ContextInterface;
|
||||
|
||||
class ConnectionContext implements ContextInterface {
|
||||
private $message;
|
||||
private $frame;
|
||||
|
||||
private $conn;
|
||||
private $component;
|
||||
|
||||
public function __construct(ConnectionInterface $conn, MessageComponentInterface $component) {
|
||||
$this->conn = $conn;
|
||||
$this->component = $component;
|
||||
}
|
||||
|
||||
public function detach() {
|
||||
$conn = $this->conn;
|
||||
|
||||
$this->frame = null;
|
||||
$this->message = null;
|
||||
|
||||
$this->component = null;
|
||||
$this->conn = null;
|
||||
|
||||
return $conn;
|
||||
}
|
||||
|
||||
public function onError(\Exception $e) {
|
||||
$this->component->onError($this->conn, $e);
|
||||
}
|
||||
|
||||
public function setFrame(FrameInterface $frame = null) {
|
||||
$this->frame = $frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Ratchet\RFC6455\Messaging\Protocol\FrameInterface
|
||||
*/
|
||||
public function getFrame() {
|
||||
return $this->frame;
|
||||
}
|
||||
|
||||
public function setMessage(MessageInterface $message = null) {
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Ratchet\RFC6455\Messaging\Protocol\MessageInterface
|
||||
*/
|
||||
public function getMessage() {
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function onMessage(MessageInterface $msg) {
|
||||
$this->component->onMessage($this->conn, $msg->getPayload());
|
||||
}
|
||||
|
||||
public function onPing(FrameInterface $frame) {
|
||||
$pong = new \Ratchet\RFC6455\Messaging\Protocol\Frame($frame->getPayload(), true, $frame::OP_PONG);
|
||||
|
||||
$this->conn->send($pong);
|
||||
}
|
||||
|
||||
public function onPong(FrameInterface $frame) {
|
||||
// TODO: Implement onPong() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $code int
|
||||
*/
|
||||
public function onClose($code) {
|
||||
$this->conn->close($code);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Encoding;
|
||||
|
||||
class ToggleableValidator implements ValidatorInterface {
|
||||
/**
|
||||
* Toggle if checkEncoding checks the encoding or not
|
||||
* @var bool
|
||||
*/
|
||||
public $on;
|
||||
|
||||
/**
|
||||
* @var Validator
|
||||
*/
|
||||
private $validator;
|
||||
|
||||
public function __construct($on = true) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Encoding;
|
||||
|
||||
/**
|
||||
* This class handled encoding validation
|
||||
*/
|
||||
class Validator {
|
||||
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/).
|
||||
*/
|
||||
protected 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
|
||||
);
|
||||
|
||||
/**
|
||||
* Lookup if mbstring is available
|
||||
* @var bool
|
||||
*/
|
||||
private $hasMbString = false;
|
||||
|
||||
/**
|
||||
* Lookup if iconv is available
|
||||
* @var bool
|
||||
*/
|
||||
private $hasIconv = false;
|
||||
|
||||
public function __construct() {
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Encoding;
|
||||
|
||||
interface ValidatorInterface {
|
||||
/**
|
||||
* Verify a string matches the encoding type
|
||||
* @param string $str The string to check
|
||||
* @param string $encoding The encoding type to check against
|
||||
* @return bool
|
||||
*/
|
||||
function checkEncoding($str, $encoding);
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version;
|
||||
|
||||
interface DataInterface {
|
||||
/**
|
||||
* Determine if the message is complete or still fragmented
|
||||
* @return bool
|
||||
*/
|
||||
function isCoalesced();
|
||||
|
||||
/**
|
||||
* Get the number of bytes the payload is set to be
|
||||
* @return int
|
||||
*/
|
||||
function getPayloadLength();
|
||||
|
||||
/**
|
||||
* Get the payload (message) sent from peer
|
||||
* @return string
|
||||
*/
|
||||
function getPayload();
|
||||
|
||||
/**
|
||||
* Get raw contents of the message
|
||||
* @return string
|
||||
*/
|
||||
function getContents();
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version;
|
||||
|
||||
interface FrameInterface extends DataInterface {
|
||||
/**
|
||||
* Add incoming data to the frame from peer
|
||||
* @param string
|
||||
*/
|
||||
function addBuffer($buf);
|
||||
|
||||
/**
|
||||
* Is this the final frame in a fragmented message?
|
||||
* @return bool
|
||||
*/
|
||||
function isFinal();
|
||||
|
||||
/**
|
||||
* Is the payload masked?
|
||||
* @return bool
|
||||
*/
|
||||
function isMasked();
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
function getOpcode();
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
//function getReceivedPayloadLength();
|
||||
|
||||
/**
|
||||
* 32-big string
|
||||
* @return string
|
||||
*/
|
||||
function getMaskingKey();
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\MessageInterface;
|
||||
use Ratchet\WebSocket\Version\Hixie76\Connection;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
use Guzzle\Http\Message\Response;
|
||||
use Ratchet\WebSocket\Version\Hixie76\Frame;
|
||||
|
||||
/**
|
||||
* FOR THE LOVE OF BEER, PLEASE PLEASE PLEASE DON'T allow the use of this in your application!
|
||||
* Hixie76 is bad for 2 (there's more) reasons:
|
||||
* 1) The handshake is done in HTTP, which includes a key for signing in the body...
|
||||
* BUT there is no Length defined in the header (as per HTTP spec) so the TCP buffer can't tell when the message is done!
|
||||
* 2) By nature it's insecure. Google did a test study where they were able to do a
|
||||
* man-in-the-middle attack on 10%-15% of the people who saw their ad who had a browser (currently only Safari) supporting the Hixie76 protocol.
|
||||
* This was exploited by taking advantage of proxy servers in front of the user who ignored some HTTP headers in the handshake
|
||||
* The Hixie76 is currently implemented by Safari
|
||||
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
||||
*/
|
||||
class Hixie76 implements VersionInterface {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isProtocol(RequestInterface $request) {
|
||||
return !(null === $request->getHeader('Sec-WebSocket-Key2'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getVersionNumber() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Guzzle\Http\Message\RequestInterface $request
|
||||
* @return \Guzzle\Http\Message\Response
|
||||
* @throws \UnderflowException If there hasn't been enough data received
|
||||
*/
|
||||
public function handshake(RequestInterface $request) {
|
||||
$body = substr($request->getBody(), 0, 8);
|
||||
if (8 !== strlen($body)) {
|
||||
throw new \UnderflowException("Not enough data received to issue challenge response");
|
||||
}
|
||||
|
||||
$challenge = $this->sign((string)$request->getHeader('Sec-WebSocket-Key1'), (string)$request->getHeader('Sec-WebSocket-Key2'), $body);
|
||||
|
||||
$headers = array(
|
||||
'Upgrade' => 'WebSocket'
|
||||
, 'Connection' => 'Upgrade'
|
||||
, 'Sec-WebSocket-Origin' => (string)$request->getHeader('Origin')
|
||||
, 'Sec-WebSocket-Location' => 'ws://' . (string)$request->getHeader('Host') . $request->getPath()
|
||||
);
|
||||
|
||||
$response = new Response(101, $headers, $challenge);
|
||||
$response->setStatus(101, 'WebSocket Protocol Handshake');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
public function onMessage(ConnectionInterface $from, $data) {
|
||||
$overflow = '';
|
||||
|
||||
if (!isset($from->WebSocket->frame)) {
|
||||
$from->WebSocket->frame = $this->newFrame();
|
||||
}
|
||||
|
||||
$from->WebSocket->frame->addBuffer($data);
|
||||
if ($from->WebSocket->frame->isCoalesced()) {
|
||||
$overflow = $from->WebSocket->frame->extractOverflow();
|
||||
|
||||
$parsed = $from->WebSocket->frame->getPayload();
|
||||
unset($from->WebSocket->frame);
|
||||
|
||||
$from->WebSocket->coalescedCallback->onMessage($from, $parsed);
|
||||
|
||||
unset($from->WebSocket->frame);
|
||||
}
|
||||
|
||||
if (strlen($overflow) > 0) {
|
||||
$this->onMessage($from, $overflow);
|
||||
}
|
||||
}
|
||||
|
||||
public function newFrame() {
|
||||
return new Frame;
|
||||
}
|
||||
|
||||
public function generateKeyNumber($key) {
|
||||
if (0 === substr_count($key, ' ')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return preg_replace('[\D]', '', $key) / substr_count($key, ' ');
|
||||
}
|
||||
|
||||
protected function sign($key1, $key2, $code) {
|
||||
return md5(
|
||||
pack('N', $this->generateKeyNumber($key1))
|
||||
. pack('N', $this->generateKeyNumber($key2))
|
||||
. $code
|
||||
, true);
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version\Hixie76;
|
||||
use Ratchet\AbstractConnectionDecorator;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @property \StdClass $WebSocket
|
||||
*/
|
||||
class Connection extends AbstractConnectionDecorator {
|
||||
public function send($msg) {
|
||||
if (!$this->WebSocket->closing) {
|
||||
$this->getConnection()->send(chr(0) . $msg . chr(255));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function close() {
|
||||
if (!$this->WebSocket->closing) {
|
||||
$this->getConnection()->send(chr(255));
|
||||
$this->getConnection()->close();
|
||||
|
||||
$this->WebSocket->closing = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version\Hixie76;
|
||||
use Ratchet\WebSocket\Version\FrameInterface;
|
||||
|
||||
/**
|
||||
* This does not entirely follow the protocol to spec, but (mostly) works
|
||||
* Hixie76 probably should not even be supported
|
||||
*/
|
||||
class Frame implements FrameInterface {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
protected $_data = '';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCoalesced() {
|
||||
return (boolean)($this->_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);
|
||||
}
|
||||
|
||||
public function getContents() {
|
||||
return $this->_data;
|
||||
}
|
||||
|
||||
public function extractOverflow() {
|
||||
return '';
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
|
||||
class HyBi10 extends RFC6455 {
|
||||
public function isProtocol(RequestInterface $request) {
|
||||
$version = (int)(string)$request->getHeader('Sec-WebSocket-Version');
|
||||
|
||||
return ($version >= 6 && $version < 13);
|
||||
}
|
||||
|
||||
public function getVersionNumber() {
|
||||
return 6;
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version;
|
||||
|
||||
interface MessageInterface extends DataInterface {
|
||||
/**
|
||||
* @param FrameInterface $fragment
|
||||
* @return MessageInterface
|
||||
*/
|
||||
function addFrame(FrameInterface $fragment);
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
function getOpcode();
|
||||
}
|
@ -1,271 +0,0 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* The latest version of the WebSocket protocol
|
||||
* @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)(string)$request->getHeader('Sec-WebSocket-Version');
|
||||
|
||||
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((string)$request->getHeader('Sec-WebSocket-Key'))
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Ratchet\WebSocket\Version\RFC6455\Connection $from
|
||||
* @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 (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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when doing the handshake to encode the key, verifying client/server are speaking the same language
|
||||
* @param string $key
|
||||
* @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;
|
||||
}
|
||||
}
|
@ -1,451 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version\RFC6455;
|
||||
use Ratchet\WebSocket\Version\FrameInterface;
|
||||
|
||||
class Frame implements FrameInterface {
|
||||
const OP_CONTINUE = 0;
|
||||
const OP_TEXT = 1;
|
||||
const OP_BINARY = 2;
|
||||
const OP_CLOSE = 8;
|
||||
const OP_PING = 9;
|
||||
const OP_PONG = 10;
|
||||
|
||||
const CLOSE_NORMAL = 1000;
|
||||
const CLOSE_GOING_AWAY = 1001;
|
||||
const CLOSE_PROTOCOL = 1002;
|
||||
const CLOSE_BAD_DATA = 1003;
|
||||
const CLOSE_NO_STATUS = 1005;
|
||||
const CLOSE_ABNORMAL = 1006;
|
||||
const CLOSE_BAD_PAYLOAD = 1007;
|
||||
const CLOSE_POLICY = 1008;
|
||||
const CLOSE_TOO_BIG = 1009;
|
||||
const CLOSE_MAND_EXT = 1010;
|
||||
const CLOSE_SRV_ERR = 1011;
|
||||
const CLOSE_TLS = 1015;
|
||||
|
||||
const MASK_LENGTH = 4;
|
||||
|
||||
/**
|
||||
* The contents of the frame
|
||||
* @var string
|
||||
*/
|
||||
protected $data = '';
|
||||
|
||||
/**
|
||||
* Number of bytes received from the frame
|
||||
* @var int
|
||||
*/
|
||||
public $bytesRecvd = 0;
|
||||
|
||||
/**
|
||||
* Number of bytes in the payload (as per framing protocol)
|
||||
* @var int
|
||||
*/
|
||||
protected $defPayLen = -1;
|
||||
|
||||
/**
|
||||
* If the frame is coalesced this is true
|
||||
* This is to prevent doing math every time ::isCoalesced is called
|
||||
* @var boolean
|
||||
*/
|
||||
private $isCoalesced = false;
|
||||
|
||||
/**
|
||||
* The unpacked first byte of the frame
|
||||
* @var int
|
||||
*/
|
||||
protected $firstByte = -1;
|
||||
|
||||
/**
|
||||
* The unpacked second byte of the frame
|
||||
* @var int
|
||||
*/
|
||||
protected $secondByte = -1;
|
||||
|
||||
|
||||
/**
|
||||
* @param string|null $payload
|
||||
* @param bool $final
|
||||
* @param int $opcode
|
||||
*/
|
||||
public function __construct($payload = null, $final = true, $opcode = 1) {
|
||||
if (null === $payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->defPayLen = strlen($payload);
|
||||
$this->firstByte = ($final ? 128 : 0) + $opcode;
|
||||
$this->secondByte = $this->defPayLen;
|
||||
$this->isCoalesced = true;
|
||||
|
||||
$ext = '';
|
||||
if ($this->defPayLen > 65535) {
|
||||
$ext = pack('NN', 0, $this->defPayLen);
|
||||
$this->secondByte = 127;
|
||||
} elseif ($this->defPayLen > 125) {
|
||||
$ext = pack('n', $this->defPayLen);
|
||||
$this->secondByte = 126;
|
||||
}
|
||||
|
||||
$this->data = chr($this->firstByte) . chr($this->secondByte) . $ext . $payload;
|
||||
$this->bytesRecvd = 2 + strlen($ext) + $this->defPayLen;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCoalesced() {
|
||||
if (true === $this->isCoalesced) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$payload_length = $this->getPayloadLength();
|
||||
$payload_start = $this->getPayloadStartingByte();
|
||||
} catch (\UnderflowException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->isCoalesced = $this->bytesRecvd >= $payload_length + $payload_start;
|
||||
|
||||
return $this->isCoalesced;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addBuffer($buf) {
|
||||
$len = strlen($buf);
|
||||
|
||||
$this->data .= $buf;
|
||||
$this->bytesRecvd += $len;
|
||||
|
||||
if ($this->firstByte === -1 && $this->bytesRecvd !== 0) {
|
||||
$this->firstByte = ord($this->data[0]);
|
||||
}
|
||||
|
||||
if ($this->secondByte === -1 && $this->bytesRecvd >= 2) {
|
||||
$this->secondByte = ord($this->data[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isFinal() {
|
||||
if (-1 === $this->firstByte) {
|
||||
throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message');
|
||||
}
|
||||
|
||||
return 128 === ($this->firstByte & 128);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
* @throws \UnderflowException
|
||||
*/
|
||||
public function getRsv1() {
|
||||
if (-1 === $this->firstByte) {
|
||||
throw new \UnderflowException('Not enough bytes received to determine reserved bit');
|
||||
}
|
||||
|
||||
return 64 === ($this->firstByte & 64);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
* @throws \UnderflowException
|
||||
*/
|
||||
public function getRsv2() {
|
||||
if (-1 === $this->firstByte) {
|
||||
throw new \UnderflowException('Not enough bytes received to determine reserved bit');
|
||||
}
|
||||
|
||||
return 32 === ($this->firstByte & 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
* @throws \UnderflowException
|
||||
*/
|
||||
public function getRsv3() {
|
||||
if (-1 === $this->firstByte) {
|
||||
throw new \UnderflowException('Not enough bytes received to determine reserved bit');
|
||||
}
|
||||
|
||||
return 16 == ($this->firstByte & 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isMasked() {
|
||||
if (-1 === $this->secondByte) {
|
||||
throw new \UnderflowException("Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set");
|
||||
}
|
||||
|
||||
return 128 === ($this->secondByte & 128);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMaskingKey() {
|
||||
if (!$this->isMasked()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$start = 1 + $this->getNumPayloadBytes();
|
||||
|
||||
if ($this->bytesRecvd < $start + static::MASK_LENGTH) {
|
||||
throw new \UnderflowException('Not enough data buffered to calculate the masking key');
|
||||
}
|
||||
|
||||
return substr($this->data, $start, static::MASK_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a 4 byte masking key
|
||||
* @return string
|
||||
*/
|
||||
public function generateMaskingKey() {
|
||||
$mask = '';
|
||||
|
||||
for ($i = 1; $i <= static::MASK_LENGTH; $i++) {
|
||||
$mask .= chr(rand(32, 126));
|
||||
}
|
||||
|
||||
return $mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a mask to the payload
|
||||
* @param string|null If NULL is passed a masking key will be generated
|
||||
* @throws \OutOfBoundsException
|
||||
* @throws \InvalidArgumentException If there is an issue with the given masking key
|
||||
* @return Frame
|
||||
*/
|
||||
public function maskPayload($maskingKey = null) {
|
||||
if (null === $maskingKey) {
|
||||
$maskingKey = $this->generateMaskingKey();
|
||||
}
|
||||
|
||||
if (static::MASK_LENGTH !== strlen($maskingKey)) {
|
||||
throw new \InvalidArgumentException("Masking key must be " . static::MASK_LENGTH ." characters");
|
||||
}
|
||||
|
||||
if (extension_loaded('mbstring') && true !== mb_check_encoding($maskingKey, 'US-ASCII')) {
|
||||
throw new \OutOfBoundsException("Masking key MUST be ASCII");
|
||||
}
|
||||
|
||||
$this->unMaskPayload();
|
||||
|
||||
$this->secondByte = $this->secondByte | 128;
|
||||
$this->data[1] = chr($this->secondByte);
|
||||
|
||||
$this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0);
|
||||
|
||||
$this->bytesRecvd += static::MASK_LENGTH;
|
||||
$this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a mask from the payload
|
||||
* @throws \UnderFlowException If the frame is not coalesced
|
||||
* @return Frame
|
||||
*/
|
||||
public function unMaskPayload() {
|
||||
if (!$this->isCoalesced()) {
|
||||
throw new \UnderflowException('Frame must be coalesced before applying mask');
|
||||
}
|
||||
|
||||
if (!$this->isMasked()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$maskingKey = $this->getMaskingKey();
|
||||
|
||||
$this->secondByte = $this->secondByte & ~128;
|
||||
$this->data[1] = chr($this->secondByte);
|
||||
|
||||
$this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH);
|
||||
|
||||
$this->bytesRecvd -= static::MASK_LENGTH;
|
||||
$this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a mask to a string or the payload of the instance
|
||||
* @param string $maskingKey The 4 character masking key to be applied
|
||||
* @param string|null $payload A string to mask or null to use the payload
|
||||
* @throws \UnderflowException If using the payload but enough hasn't been buffered
|
||||
* @return string The masked string
|
||||
*/
|
||||
public function applyMask($maskingKey, $payload = null) {
|
||||
if (null === $payload) {
|
||||
if (!$this->isCoalesced()) {
|
||||
throw new \UnderflowException('Frame must be coalesced to apply a mask');
|
||||
}
|
||||
|
||||
$payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
|
||||
}
|
||||
|
||||
$applied = '';
|
||||
for ($i = 0, $len = strlen($payload); $i < $len; $i++) {
|
||||
$applied .= $payload[$i] ^ $maskingKey[$i % static::MASK_LENGTH];
|
||||
}
|
||||
|
||||
return $applied;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOpcode() {
|
||||
if (-1 === $this->firstByte) {
|
||||
throw new \UnderflowException('Not enough bytes received to determine opcode');
|
||||
}
|
||||
|
||||
return ($this->firstByte & ~240);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (-1 === $this->secondByte) {
|
||||
throw new \UnderflowException('Not enough bytes received');
|
||||
}
|
||||
|
||||
return $this->secondByte & 127;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int (7|23|71) Number of bits defined for the payload length in the fame
|
||||
* @throws \UnderflowException
|
||||
*/
|
||||
protected function getNumPayloadBits() {
|
||||
if (-1 === $this->secondByte) {
|
||||
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 typo and is 64 (16+48)
|
||||
if ($check === 127) {
|
||||
$bits += 48;
|
||||
}
|
||||
|
||||
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->defPayLen !== -1) {
|
||||
return $this->defPayLen;
|
||||
}
|
||||
|
||||
$this->defPayLen = $this->getFirstPayloadVal();
|
||||
if ($this->defPayLen <= 125) {
|
||||
return $this->getPayloadLength();
|
||||
}
|
||||
|
||||
$byte_length = $this->getNumPayloadBytes();
|
||||
if ($this->bytesRecvd < 1 + $byte_length) {
|
||||
$this->defPayLen = -1;
|
||||
throw new \UnderflowException('Not enough data buffered to determine payload length');
|
||||
}
|
||||
|
||||
$len = 0;
|
||||
for ($i = 2; $i <= $byte_length; $i++) {
|
||||
$len <<= 8;
|
||||
$len += ord($this->data[$i]);
|
||||
}
|
||||
|
||||
$this->defPayLen = $len;
|
||||
|
||||
return $this->getPayloadLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPayloadStartingByte() {
|
||||
return 1 + $this->getNumPayloadBytes() + ($this->isMasked() ? static::MASK_LENGTH : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @todo Consider not checking mask, always returning the payload, masked or not
|
||||
*/
|
||||
public function getPayload() {
|
||||
if (!$this->isCoalesced()) {
|
||||
throw new \UnderflowException('Can not return partial message');
|
||||
}
|
||||
|
||||
$payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
|
||||
|
||||
if ($this->isMasked()) {
|
||||
$payload = $this->applyMask($this->getMaskingKey(), $payload);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw contents of the frame
|
||||
* @todo This is untested, make sure the substr is right - trying to return the frame w/o the overflow
|
||||
*/
|
||||
public function getContents() {
|
||||
return substr($this->data, 0, $this->getPayloadStartingByte() + $this->getPayloadLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
if ($this->isCoalesced()) {
|
||||
$endPoint = $this->getPayloadLength();
|
||||
$endPoint += $this->getPayloadStartingByte();
|
||||
|
||||
if ($this->bytesRecvd > $endPoint) {
|
||||
$overflow = substr($this->data, $endPoint);
|
||||
$this->data = substr($this->data, 0, $endPoint);
|
||||
|
||||
return $overflow;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version\RFC6455;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* These are checks to ensure the client requested handshake are valid
|
||||
* Verification rules come from section 4.2.1 of the RFC6455 document
|
||||
* @todo Currently just returning invalid - should consider returning appropriate HTTP status code error #s
|
||||
*/
|
||||
class HandshakeVerifier {
|
||||
/**
|
||||
* Given an array of the headers this method will run through all verification methods
|
||||
* @param \Guzzle\Http\Message\RequestInterface $request
|
||||
* @return bool TRUE if all headers are valid, FALSE if 1 or more were invalid
|
||||
*/
|
||||
public function verifyAll(RequestInterface $request) {
|
||||
$passes = 0;
|
||||
|
||||
$passes += (int)$this->verifyMethod($request->getMethod());
|
||||
$passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion());
|
||||
$passes += (int)$this->verifyRequestURI($request->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']); // Temporarily breaking functionality
|
||||
|
||||
return (7 === $passes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the HTTP method. MUST be "GET"
|
||||
* @param string
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyMethod($val) {
|
||||
return ('get' === strtolower($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
|
||||
*/
|
||||
public function verifyRequestURI($val) {
|
||||
if ($val[0] != '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false !== strstr($val, '#')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!extension_loaded('mbstring')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return mb_check_encoding($val, 'US-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 maybe need to verify it's a valid domain??? Or should it equal $_SERVER['HOST'] ?
|
||||
*/
|
||||
public function verifyHost($val) {
|
||||
return (null !== $val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the Upgrade request to WebSockets.
|
||||
* @param string $val MUST equal "websocket"
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyUpgradeRequest($val) {
|
||||
return ('websocket' === strtolower($val));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the Connection header
|
||||
* @param string $val MUST equal "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));
|
||||
}
|
||||
|
||||
/**
|
||||
* This function verifies 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?
|
||||
* @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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version\RFC6455;
|
||||
use Ratchet\WebSocket\Version\MessageInterface;
|
||||
use Ratchet\WebSocket\Version\FrameInterface;
|
||||
|
||||
class Message implements MessageInterface, \Countable {
|
||||
/**
|
||||
* @var \SplDoublyLinkedList
|
||||
*/
|
||||
protected $_frames;
|
||||
|
||||
public function __construct() {
|
||||
$this->_frames = new \SplDoublyLinkedList;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count() {
|
||||
return count($this->_frames);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCoalesced() {
|
||||
if (count($this->_frames) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$last = $this->_frames->top();
|
||||
|
||||
return ($last->isCoalesced() && $last->isFinal());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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) {
|
||||
$this->_frames->push($fragment);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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) {
|
||||
// Not an error, want the current amount buffered
|
||||
}
|
||||
}
|
||||
|
||||
return $len;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPayload() {
|
||||
if (!$this->isCoalesced()) {
|
||||
throw new \UnderflowException('Message has not been put back together yet');
|
||||
}
|
||||
|
||||
$buffer = '';
|
||||
|
||||
foreach ($this->_frames as $frame) {
|
||||
$buffer .= $frame->getPayload();
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContents() {
|
||||
if (!$this->isCoalesced()) {
|
||||
throw new \UnderflowException("Message has not been put back together yet");
|
||||
}
|
||||
|
||||
$buffer = '';
|
||||
|
||||
foreach ($this->_frames as $frame) {
|
||||
$buffer .= $frame->getContents();
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version;
|
||||
use Ratchet\MessageInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* A standard interface for interacting with the various version of the WebSocket protocol
|
||||
*/
|
||||
interface VersionInterface extends MessageInterface {
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Although the version has a name associated with it the integer returned is the proper identification
|
||||
* @return int
|
||||
*/
|
||||
function getVersionNumber();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* @param \Ratchet\ConnectionInterface $conn
|
||||
* @param \Ratchet\MessageInterface $coalescedCallback
|
||||
* @return \Ratchet\ConnectionInterface
|
||||
*/
|
||||
function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback);
|
||||
|
||||
/**
|
||||
* @return MessageInterface
|
||||
*/
|
||||
//function newMessage();
|
||||
|
||||
/**
|
||||
* @return FrameInterface
|
||||
*/
|
||||
//function newFrame();
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @param bool
|
||||
* @return string
|
||||
* @todo Change to use other classes, this will be removed eventually
|
||||
*/
|
||||
//function frame($message, $mask = true);
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket;
|
||||
use Ratchet\WebSocket\Version\VersionInterface;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* Manage the various versions of the WebSocket protocol
|
||||
* This accepts interfaces of versions to enable/disable
|
||||
*/
|
||||
class VersionManager {
|
||||
/**
|
||||
* The header string to let clients know which versions are supported
|
||||
* @var string
|
||||
*/
|
||||
private $versionString = '';
|
||||
|
||||
/**
|
||||
* Storage of each version enabled
|
||||
* @var array
|
||||
*/
|
||||
protected $versions = array();
|
||||
|
||||
/**
|
||||
* Get the protocol negotiator for the request, if supported
|
||||
* @param \Guzzle\Http\Message\RequestInterface $request
|
||||
* @throws \InvalidArgumentException
|
||||
* @return \Ratchet\WebSocket\Version\VersionInterface
|
||||
*/
|
||||
public function getVersion(RequestInterface $request) {
|
||||
foreach ($this->versions as $version) {
|
||||
if ($version->isProtocol($request)) {
|
||||
return $version;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException("Version not found");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Guzzle\Http\Message\RequestInterface
|
||||
* @return bool
|
||||
*/
|
||||
public function isVersionEnabled(RequestInterface $request) {
|
||||
foreach ($this->versions as $version) {
|
||||
if ($version->isProtocol($request)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable support for a specific version of the WebSocket protocol
|
||||
* @param \Ratchet\WebSocket\Version\VersionInterface $version
|
||||
* @return VersionManager
|
||||
*/
|
||||
public function enableVersion(VersionInterface $version) {
|
||||
$this->versions[$version->getVersionNumber()] = $version;
|
||||
|
||||
if (empty($this->versionString)) {
|
||||
$this->versionString = (string)$version->getVersionNumber();
|
||||
} else {
|
||||
$this->versionString .= ", {$version->getVersionNumber()}";
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable support for a specific WebSocket protocol version
|
||||
* @param int $versionId The version ID to un-support
|
||||
* @return VersionManager
|
||||
*/
|
||||
public function disableVersion($versionId) {
|
||||
unset($this->versions[$versionId]);
|
||||
|
||||
$this->versionString = implode(',', array_keys($this->versions));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string of version numbers supported (comma delimited)
|
||||
* @return string
|
||||
*/
|
||||
public function getSupportedVersionString() {
|
||||
return $this->versionString;
|
||||
}
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version\RFC6455;
|
||||
namespace Ratchet\WebSocket;
|
||||
use Ratchet\AbstractConnectionDecorator;
|
||||
use Ratchet\WebSocket\Version\DataInterface;
|
||||
use Ratchet\RFC6455\Messaging\Protocol\DataInterface;
|
||||
use Ratchet\RFC6455\Messaging\Protocol\Frame;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @property \StdClass $WebSocket
|
||||
*/
|
||||
class Connection extends AbstractConnectionDecorator {
|
||||
class WsConnection extends AbstractConnectionDecorator {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
@ -3,10 +3,9 @@ namespace Ratchet\WebSocket;
|
||||
use Ratchet\MessageComponentInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\Http\HttpServerInterface;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
use Guzzle\Http\Message\Response;
|
||||
use Ratchet\WebSocket\Version;
|
||||
use Ratchet\WebSocket\Encoding\ToggleableValidator;
|
||||
use Ratchet\Http\CloseResponseTrait;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use GuzzleHttp\Psr7 as gPsr;
|
||||
|
||||
/**
|
||||
* The adapter to handle WebSocket requests/responses
|
||||
@ -15,12 +14,7 @@ use Ratchet\WebSocket\Encoding\ToggleableValidator;
|
||||
* @link http://dev.w3.org/html5/websockets/
|
||||
*/
|
||||
class WsServer implements HttpServerInterface {
|
||||
/**
|
||||
* Manage the various WebSocket versions to support
|
||||
* @var VersionManager
|
||||
* @note May not expose this in the future, may do through facade methods
|
||||
*/
|
||||
public $versioner;
|
||||
use CloseResponseTrait;
|
||||
|
||||
/**
|
||||
* Decorated component
|
||||
@ -36,13 +30,7 @@ class WsServer implements HttpServerInterface {
|
||||
/**
|
||||
* Holder of accepted protocols, implement through WampServerInterface
|
||||
*/
|
||||
protected $acceptedSubProtocols = array();
|
||||
|
||||
/**
|
||||
* UTF-8 validator
|
||||
* @var \Ratchet\WebSocket\Encoding\ValidatorInterface
|
||||
*/
|
||||
protected $validator;
|
||||
protected $acceptedSubProtocols = [];
|
||||
|
||||
/**
|
||||
* Flag if we have checked the decorated component for sub-protocols
|
||||
@ -50,22 +38,20 @@ class WsServer implements HttpServerInterface {
|
||||
*/
|
||||
private $isSpGenerated = false;
|
||||
|
||||
private $handshakeNegotiator;
|
||||
private $messageStreamer;
|
||||
|
||||
/**
|
||||
* @param \Ratchet\MessageComponentInterface $component Your application to run with WebSockets
|
||||
* If you want to enable sub-protocols have your component implement WsServerInterface as well
|
||||
*/
|
||||
public function __construct(MessageComponentInterface $component) {
|
||||
$this->versioner = new VersionManager;
|
||||
$this->validator = new ToggleableValidator;
|
||||
|
||||
$this->versioner
|
||||
->enableVersion(new Version\RFC6455($this->validator))
|
||||
->enableVersion(new Version\HyBi10($this->validator))
|
||||
->enableVersion(new Version\Hixie76)
|
||||
;
|
||||
|
||||
$this->component = $component;
|
||||
$this->connections = new \SplObjectStorage;
|
||||
|
||||
$encodingValidator = new \Ratchet\RFC6455\Encoding\Validator;
|
||||
$this->handshakeNegotiator = new \Ratchet\RFC6455\Handshake\Negotiator($encodingValidator);
|
||||
$this->messageStreamer = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer($encodingValidator);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,12 +62,33 @@ class WsServer implements HttpServerInterface {
|
||||
throw new \UnexpectedValueException('$request can not be null');
|
||||
}
|
||||
|
||||
$conn->WebSocket = new \StdClass;
|
||||
$conn->WebSocket->request = $request;
|
||||
$conn->WebSocket->established = false;
|
||||
$conn->WebSocket->closing = false;
|
||||
$conn->httpRequest = $request; // This will replace ->WebSocket->request
|
||||
|
||||
$this->attemptUpgrade($conn);
|
||||
$conn->WebSocket = new \StdClass;
|
||||
$conn->WebSocket->closing = false;
|
||||
$conn->WebSocket->request = $request; // deprecated
|
||||
|
||||
$response = $this->handshakeNegotiator->handshake($request)->withHeader('X-Powered-By', \Ratchet\VERSION);
|
||||
|
||||
// Probably moved to RFC lib
|
||||
// $subHeader = $conn->WebSocket->request->getHeader('Sec-WebSocket-Protocol');
|
||||
// if (count($subHeader) > 0) {
|
||||
// if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($subHeader))) {
|
||||
// $response = $response->withHeader('Sec-WebSocket-Protocol', $agreedSubProtocols);
|
||||
// }
|
||||
// }
|
||||
|
||||
$conn->send(gPsr\str($response));
|
||||
|
||||
if (101 != $response->getStatusCode()) {
|
||||
return $conn->close();
|
||||
}
|
||||
|
||||
$wsConn = new WsConnection($conn);
|
||||
$context = new ConnectionContext($wsConn, $this->component);
|
||||
$this->connections->attach($conn, $context);
|
||||
|
||||
return $this->component->onOpen($wsConn);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,50 +99,9 @@ class WsServer implements HttpServerInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
if (true === $from->WebSocket->established) {
|
||||
return $from->WebSocket->version->onMessage($this->connections[$from], $msg);
|
||||
}
|
||||
$context = $this->connections[$from];
|
||||
|
||||
$this->attemptUpgrade($from, $msg);
|
||||
}
|
||||
|
||||
protected function attemptUpgrade(ConnectionInterface $conn, $data = '') {
|
||||
if ('' !== $data) {
|
||||
$conn->WebSocket->request->getBody()->write($data);
|
||||
} else {
|
||||
if (!$this->versioner->isVersionEnabled($conn->WebSocket->request)) {
|
||||
return $this->close($conn);
|
||||
}
|
||||
|
||||
$conn->WebSocket->version = $this->versioner->getVersion($conn->WebSocket->request);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $conn->WebSocket->version->handshake($conn->WebSocket->request);
|
||||
} catch (\UnderflowException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== ($subHeader = $conn->WebSocket->request->getHeader('Sec-WebSocket-Protocol'))) {
|
||||
if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($subHeader->normalize()))) {
|
||||
$response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols);
|
||||
}
|
||||
}
|
||||
|
||||
$response->setHeader('X-Powered-By', \Ratchet\VERSION);
|
||||
$conn->send((string)$response);
|
||||
|
||||
if (101 != $response->getStatusCode()) {
|
||||
return $conn->close();
|
||||
}
|
||||
|
||||
$upgraded = $conn->WebSocket->version->upgradeConnection($conn, $this->component);
|
||||
|
||||
$this->connections->attach($conn, $upgraded);
|
||||
|
||||
$upgraded->WebSocket->established = true;
|
||||
|
||||
return $this->component->onOpen($upgraded);
|
||||
$this->messageStreamer->onData($msg, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -146,7 +112,9 @@ class WsServer implements HttpServerInterface {
|
||||
$decor = $this->connections[$conn];
|
||||
$this->connections->detach($conn);
|
||||
|
||||
$this->component->onClose($decor);
|
||||
$conn = $decor->detach();
|
||||
|
||||
$this->component->onClose($conn);
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,31 +122,21 @@ class WsServer implements HttpServerInterface {
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onError(ConnectionInterface $conn, \Exception $e) {
|
||||
if ($conn->WebSocket->established && $this->connections->contains($conn)) {
|
||||
$this->component->onError($this->connections[$conn], $e);
|
||||
if ($this->connections->contains($conn)) {
|
||||
$context = $this->connections[$conn];
|
||||
$context->onError($e);
|
||||
} else {
|
||||
$conn->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a specific version of the WebSocket protocol
|
||||
* @param int $versionId Version ID to disable
|
||||
* @return WsServer
|
||||
*/
|
||||
public function disableVersion($versionId) {
|
||||
$this->versioner->disableVersion($versionId);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle weather to check encoding of incoming messages
|
||||
* @param bool
|
||||
* @return WsServer
|
||||
*/
|
||||
public function setEncodingChecks($opt) {
|
||||
$this->validator->on = (boolean)$opt;
|
||||
// $this->validator->on = (boolean)$opt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -214,19 +172,4 @@ class WsServer implements HttpServerInterface {
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a connection with an HTTP response
|
||||
* @param \Ratchet\ConnectionInterface $conn
|
||||
* @param int $code HTTP status code
|
||||
*/
|
||||
protected function close(ConnectionInterface $conn, $code = 400) {
|
||||
$response = new Response($code, array(
|
||||
'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString()
|
||||
, 'X-Powered-By' => \Ratchet\VERSION
|
||||
));
|
||||
|
||||
$conn->send((string)$response);
|
||||
$conn->close();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user