Refactor just RFC6455, Interfaces, Valication

This commit is contained in:
Chris Boden 2014-08-30 08:07:33 -04:00
parent 80124ec05e
commit aa6bb1b40e
22 changed files with 49 additions and 583 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
composer.lock
vendor

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2011-2014 Chris Boden
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

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,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,232 +0,0 @@
<?php
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;
/**
* The adapter to handle WebSocket requests/responses
* This is a mediator between the Server and your application to handle real-time messaging through a web browser
* @link http://ca.php.net/manual/en/ref.http.php
* @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;
/**
* Decorated component
* @var \Ratchet\MessageComponentInterface
*/
public $component;
/**
* @var \SplObjectStorage
*/
protected $connections;
/**
* Holder of accepted protocols, implement through WampServerInterface
*/
protected $acceptedSubProtocols = array();
/**
* UTF-8 validator
* @var \Ratchet\WebSocket\Encoding\ValidatorInterface
*/
protected $validator;
/**
* Flag if we have checked the decorated component for sub-protocols
* @var boolean
*/
private $isSpGenerated = false;
/**
* @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;
}
/**
* {@inheritdoc}
*/
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {
if (null === $request) {
throw new \UnexpectedValueException('$request can not be null');
}
$conn->WebSocket = new \StdClass;
$conn->WebSocket->request = $request;
$conn->WebSocket->established = false;
$conn->WebSocket->closing = false;
$this->attemptUpgrade($conn);
}
/**
* {@inheritdoc}
*/
public function onMessage(ConnectionInterface $from, $msg) {
if ($from->WebSocket->closing) {
return;
}
if (true === $from->WebSocket->established) {
return $from->WebSocket->version->onMessage($this->connections[$from], $msg);
}
$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);
}
/**
* {@inheritdoc}
*/
public function onClose(ConnectionInterface $conn) {
if ($this->connections->contains($conn)) {
$decor = $this->connections[$conn];
$this->connections->detach($conn);
$this->component->onClose($decor);
}
}
/**
* {@inheritdoc}
*/
public function onError(ConnectionInterface $conn, \Exception $e) {
if ($conn->WebSocket->established && $this->connections->contains($conn)) {
$this->component->onError($this->connections[$conn], $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;
return $this;
}
/**
* @param string
* @return boolean
*/
public function isSubProtocolSupported($name) {
if (!$this->isSpGenerated) {
if ($this->component instanceof WsServerInterface) {
$this->acceptedSubProtocols = array_flip($this->component->getSubProtocols());
}
$this->isSpGenerated = true;
}
return array_key_exists($name, $this->acceptedSubProtocols);
}
/**
* @param \Traversable|null $requested
* @return string
*/
protected function getSubProtocolString(\Traversable $requested = null) {
if (null !== $requested) {
foreach ($requested as $sub) {
if ($this->isSubProtocolSupported($sub)) {
return $sub;
}
}
}
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();
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace Ratchet\WebSocket;
/**
* WebSocket Server Interface
*/
interface WsServerInterface {
/**
* If any component in a stack supports a WebSocket sub-protocol return each supported in an array
* @return array
* @temporary This method may be removed in future version (note that will not break code, just make some code obsolete)
*/
function getSubProtocols();
}

28
composer.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "ratchet/rfc6455"
, "type": "library"
, "description": "RFC6455 protocol handler"
, "keywords": ["WebSockets"]
, "homepage": "http://socketo.me"
, "license": "MIT"
, "authors": [
{
"name": "Chris Boden"
, "email": "cboden@gmail.com"
, "role": "Developer"
}
]
, "support": {
"forum": "https://groups.google.com/forum/#!forum/ratchet-php"
, "issues": "https://github.com/ratchetphp/Ratchet/issues"
, "irc": "irc://irc.freenode.org/reactphp"
}
, "autoload": {
"psr-4": {
"Ratchet\\WebSocket\\": "src"
}
}
, "require": {
"php": ">=5.4.2"
}
}