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:
Chris Boden 2015-05-30 10:19:30 -04:00
parent 6b247c0525
commit a744aea1f0
28 changed files with 194 additions and 1796 deletions

View File

@ -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-noutf8.php 8003 StreamSelect &
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8004 LibEv & ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8004 LibEv &
wstest -m testeeserver -w ws://localhost:8000 & wstest -m testeeserver -w ws://localhost:8000 &
sleep 1
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-all.json wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-all.json
killall php wstest killall php wstest
abtest: abtest:
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8000 StreamSelect & ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8000 StreamSelect &
sleep 1
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-quick.json wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-quick.json
killall php killall php
profile: profile:
php -d 'xdebug.profiler_enable=1' tests/autobahn/bin/fuzzingserver.php 8000 LibEvent & php -d 'xdebug.profiler_enable=1' tests/autobahn/bin/fuzzingserver.php 8000 LibEvent &
sleep 1
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-profile.json wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-profile.json
killall php killall php

View File

@ -25,7 +25,8 @@
, "require": { , "require": {
"php": ">=5.3.9" "php": ">=5.3.9"
, "react/socket": "^0.3 || ^0.4" , "react/socket": "^0.3 || ^0.4"
, "guzzle/http": "^3.6" , "guzzlehttp/psr7": "^1.0"
, "ratchet/rfc6455": "dev-psr7"
, "symfony/http-foundation": "^2.2" , "symfony/http-foundation": "^2.2"
, "symfony/routing": "^2.2" , "symfony/routing": "^2.2"
} }

View 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();
}
}

View File

@ -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;
}
}

View File

@ -2,7 +2,7 @@
namespace Ratchet\Http; namespace Ratchet\Http;
use Ratchet\MessageInterface; use Ratchet\MessageInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\Http\Guzzle\Http\Message\RequestFactory; use GuzzleHttp\Psr7 as g7;
/** /**
* This class receives streaming data from a client request * This class receives streaming data from a client request
@ -22,7 +22,7 @@ class HttpRequestParser implements MessageInterface {
/** /**
* @param \Ratchet\ConnectionInterface $context * @param \Ratchet\ConnectionInterface $context
* @param string $data Data stream to buffer * @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 * @throws \OverflowException If the message buffer has become too large
*/ */
public function onMessage(ConnectionInterface $context, $data) { public function onMessage(ConnectionInterface $context, $data) {
@ -37,7 +37,7 @@ class HttpRequestParser implements MessageInterface {
} }
if ($this->isEom($context->httpBuffer)) { if ($this->isEom($context->httpBuffer)) {
$request = RequestFactory::getInstance()->fromMessage($context->httpBuffer); $request = g7\parse_request($context->httpBuffer);
unset($context->httpBuffer); unset($context->httpBuffer);

View File

@ -2,9 +2,10 @@
namespace Ratchet\Http; namespace Ratchet\Http;
use Ratchet\MessageComponentInterface; use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\Response;
class HttpServer implements MessageComponentInterface { class HttpServer implements MessageComponentInterface {
use CloseResponseTrait;
/** /**
* Buffers incoming HTTP requests returning a Guzzle Request when coalesced * Buffers incoming HTTP requests returning a Guzzle Request when coalesced
* @var HttpRequestParser * @var HttpRequestParser
@ -72,19 +73,4 @@ class HttpServer implements MessageComponentInterface {
$this->close($conn, 500); $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();
}
} }

View File

@ -2,12 +2,12 @@
namespace Ratchet\Http; namespace Ratchet\Http;
use Ratchet\MessageComponentInterface; use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
interface HttpServerInterface extends MessageComponentInterface { interface HttpServerInterface extends MessageComponentInterface {
/** /**
* @param \Ratchet\ConnectionInterface $conn * @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 * @throws \UnexpectedValueException if a RequestInterface is not passed
*/ */
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null); public function onOpen(ConnectionInterface $conn, RequestInterface $request = null);

View File

@ -1,9 +1,8 @@
<?php <?php
namespace Ratchet\Http; namespace Ratchet\Http;
use Guzzle\Http\Message\RequestInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface; 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. * 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 * Note: This can be spoofed from non-web browser clients
*/ */
class OriginCheck implements HttpServerInterface { class OriginCheck implements HttpServerInterface {
use CloseResponseTrait;
/** /**
* @var \Ratchet\MessageComponentInterface * @var \Ratchet\MessageComponentInterface
*/ */
protected $_component; protected $_component;
public $allowedOrigins = array(); public $allowedOrigins = [];
/** /**
* @param MessageComponentInterface $component Component/Application to decorate * @param MessageComponentInterface $component Component/Application to decorate
* @param array $allowed An array of allowed domains that are allowed to connect from * @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->_component = $component;
$this->allowedOrigins += $allowed; $this->allowedOrigins += $allowed;
} }
@ -31,7 +32,7 @@ class OriginCheck implements HttpServerInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { 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; $origin = parse_url($header, PHP_URL_HOST) ?: $header;
if (!in_array($origin, $this->allowedOrigins)) { if (!in_array($origin, $this->allowedOrigins)) {
@ -61,19 +62,4 @@ class OriginCheck implements HttpServerInterface {
function onError(ConnectionInterface $conn, \Exception $e) { function onError(ConnectionInterface $conn, \Exception $e) {
return $this->_component->onError($conn, $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();
}
}

View File

@ -1,14 +1,14 @@
<?php <?php
namespace Ratchet\Http; namespace Ratchet\Http;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Guzzle\Http\Url;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Exception\ResourceNotFoundException;
class Router implements HttpServerInterface { class Router implements HttpServerInterface {
use CloseResponseTrait;
/** /**
* @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface * @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
*/ */
@ -27,12 +27,14 @@ class Router implements HttpServerInterface {
throw new \UnexpectedValueException('$request can not be null'); throw new \UnexpectedValueException('$request can not be null');
} }
$uri = $request->getUri();
$context = $this->_matcher->getContext(); $context = $this->_matcher->getContext();
$context->setMethod($request->getMethod()); $context->setMethod($request->getMethod());
$context->setHost($request->getHost()); $context->setHost($uri->getHost());
try { try {
$route = $this->_matcher->match($request->getPath()); $route = $this->_matcher->match($uri->getPath());
} catch (MethodNotAllowedException $nae) { } catch (MethodNotAllowedException $nae) {
return $this->close($conn, 403); return $this->close($conn, 403);
} catch (ResourceNotFoundException $nfe) { } catch (ResourceNotFoundException $nfe) {
@ -47,17 +49,18 @@ class Router implements HttpServerInterface {
throw new \UnexpectedValueException('All routes must implement Ratchet\Http\HttpServerInterface'); throw new \UnexpectedValueException('All routes must implement Ratchet\Http\HttpServerInterface');
} }
$parameters = array(); // TODO: Apply Symfony default params to request
foreach($route as $key => $value) { // $parameters = [];
if ((is_string($key)) && ('_' !== substr($key, 0, 1))) { // foreach($route as $key => $value) {
$parameters[$key] = $value; // if ((is_string($key)) && ('_' !== substr($key, 0, 1))) {
} // $parameters[$key] = $value;
} // }
$parameters = array_merge($parameters, $request->getQuery()->getAll()); // }
// $parameters = array_merge($parameters, gPsr\parse_query($uri->getQuery()));
$url = Url::factory($request->getPath()); //
$url->setQuery($parameters); // $url = Url::factory($request->getPath());
$request->setUrl($url); // $url->setQuery($parameters);
// $request->setUrl($url);
$conn->controller = $route['_controller']; $conn->controller = $route['_controller'];
$conn->controller->onOpen($conn, $request); $conn->controller->onOpen($conn, $request);
@ -87,19 +90,4 @@ class Router implements HttpServerInterface {
$conn->controller->onError($conn, $e); $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();
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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 '';
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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 '';
}
}

View File

@ -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) {
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -1,13 +1,14 @@
<?php <?php
namespace Ratchet\WebSocket\Version\RFC6455; namespace Ratchet\WebSocket;
use Ratchet\AbstractConnectionDecorator; use Ratchet\AbstractConnectionDecorator;
use Ratchet\WebSocket\Version\DataInterface; use Ratchet\RFC6455\Messaging\Protocol\DataInterface;
use Ratchet\RFC6455\Messaging\Protocol\Frame;
/** /**
* {@inheritdoc} * {@inheritdoc}
* @property \StdClass $WebSocket * @property \StdClass $WebSocket
*/ */
class Connection extends AbstractConnectionDecorator { class WsConnection extends AbstractConnectionDecorator {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@ -3,10 +3,9 @@ namespace Ratchet\WebSocket;
use Ratchet\MessageComponentInterface; use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServerInterface; use Ratchet\Http\HttpServerInterface;
use Guzzle\Http\Message\RequestInterface; use Ratchet\Http\CloseResponseTrait;
use Guzzle\Http\Message\Response; use Psr\Http\Message\RequestInterface;
use Ratchet\WebSocket\Version; use GuzzleHttp\Psr7 as gPsr;
use Ratchet\WebSocket\Encoding\ToggleableValidator;
/** /**
* The adapter to handle WebSocket requests/responses * The adapter to handle WebSocket requests/responses
@ -15,12 +14,7 @@ use Ratchet\WebSocket\Encoding\ToggleableValidator;
* @link http://dev.w3.org/html5/websockets/ * @link http://dev.w3.org/html5/websockets/
*/ */
class WsServer implements HttpServerInterface { class WsServer implements HttpServerInterface {
/** use CloseResponseTrait;
* Manage the various WebSocket versions to support
* @var VersionManager
* @note May not expose this in the future, may do through facade methods
*/
public $versioner;
/** /**
* Decorated component * Decorated component
@ -36,13 +30,7 @@ class WsServer implements HttpServerInterface {
/** /**
* Holder of accepted protocols, implement through WampServerInterface * Holder of accepted protocols, implement through WampServerInterface
*/ */
protected $acceptedSubProtocols = array(); protected $acceptedSubProtocols = [];
/**
* UTF-8 validator
* @var \Ratchet\WebSocket\Encoding\ValidatorInterface
*/
protected $validator;
/** /**
* Flag if we have checked the decorated component for sub-protocols * Flag if we have checked the decorated component for sub-protocols
@ -50,22 +38,20 @@ class WsServer implements HttpServerInterface {
*/ */
private $isSpGenerated = false; private $isSpGenerated = false;
private $handshakeNegotiator;
private $messageStreamer;
/** /**
* @param \Ratchet\MessageComponentInterface $component Your application to run with WebSockets * @param \Ratchet\MessageComponentInterface $component Your application to run with WebSockets
* If you want to enable sub-protocols have your component implement WsServerInterface as well * If you want to enable sub-protocols have your component implement WsServerInterface as well
*/ */
public function __construct(MessageComponentInterface $component) { 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->component = $component;
$this->connections = new \SplObjectStorage; $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'); throw new \UnexpectedValueException('$request can not be null');
} }
$conn->WebSocket = new \StdClass; $conn->httpRequest = $request; // This will replace ->WebSocket->request
$conn->WebSocket->request = $request;
$conn->WebSocket->established = false;
$conn->WebSocket->closing = false;
$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; return;
} }
if (true === $from->WebSocket->established) { $context = $this->connections[$from];
return $from->WebSocket->version->onMessage($this->connections[$from], $msg);
}
$this->attemptUpgrade($from, $msg); $this->messageStreamer->onData($msg, $context);
}
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);
} }
/** /**
@ -146,7 +112,9 @@ class WsServer implements HttpServerInterface {
$decor = $this->connections[$conn]; $decor = $this->connections[$conn];
$this->connections->detach($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} * {@inheritdoc}
*/ */
public function onError(ConnectionInterface $conn, \Exception $e) { public function onError(ConnectionInterface $conn, \Exception $e) {
if ($conn->WebSocket->established && $this->connections->contains($conn)) { if ($this->connections->contains($conn)) {
$this->component->onError($this->connections[$conn], $e); $context = $this->connections[$conn];
$context->onError($e);
} else { } else {
$conn->close(); $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 * Toggle weather to check encoding of incoming messages
* @param bool * @param bool
* @return WsServer * @return WsServer
*/ */
public function setEncodingChecks($opt) { public function setEncodingChecks($opt) {
$this->validator->on = (boolean)$opt; // $this->validator->on = (boolean)$opt;
return $this; return $this;
} }
@ -214,19 +172,4 @@ class WsServer implements HttpServerInterface {
return ''; 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();
}
}