Merge branch 'refs/heads/wamp'
This commit is contained in:
commit
3d41152b41
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "vendor/guzzle"]
|
||||
path = vendor/guzzle
|
||||
url = git://github.com/guzzle/guzzle.git
|
@ -7,12 +7,6 @@ use Ratchet\Resource\Connection;
|
||||
* It impelemtns the decorator and command pattern to build an application stack
|
||||
*/
|
||||
interface ApplicationInterface {
|
||||
/**
|
||||
* Decorator pattern
|
||||
* @param Ratchet\ApplicationInterface Application to wrap in protocol
|
||||
*/
|
||||
public function __construct(ApplicationInterface $app = null);
|
||||
|
||||
/**
|
||||
* When a new connection is opened it will be passed to this method
|
||||
* @param Ratchet\Resource\Connection The socket/connection that just connected to your application
|
||||
|
@ -41,11 +41,7 @@ class App implements ApplicationInterface {
|
||||
*/
|
||||
protected $_run = true;
|
||||
|
||||
public function __construct(ApplicationInterface $application = null) {
|
||||
if (null === $application) {
|
||||
throw new \UnexpectedValueException("Server requires an application to run off of");
|
||||
}
|
||||
|
||||
public function __construct(ApplicationInterface $application) {
|
||||
$this->_app = $application;
|
||||
}
|
||||
|
||||
@ -84,6 +80,7 @@ class App implements ApplicationInterface {
|
||||
|
||||
declare(ticks = 1);
|
||||
|
||||
$host->set_option(SOL_SOCKET, SO_SNDBUF, $this->_buffer_size);
|
||||
$host->set_nonblock()->bind($address, (int)$port)->listen();
|
||||
|
||||
do {
|
||||
@ -109,7 +106,7 @@ class App implements ApplicationInterface {
|
||||
$res = $this->onOpen($conn);
|
||||
} else {
|
||||
$data = $buf = '';
|
||||
$bytes = $conn->getSocket()->recv($buf, $this->_buffer_size, 0);
|
||||
$bytes = $conn->getSocket()->recv($buf, $this->_buffer_size, MSG_DONTWAIT);
|
||||
if ($bytes > 0) {
|
||||
$data = $buf;
|
||||
|
||||
|
167
lib/Ratchet/Application/WAMP/App.php
Normal file
167
lib/Ratchet/Application/WAMP/App.php
Normal file
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WAMP;
|
||||
use Ratchet\Application\ApplicationInterface;
|
||||
use Ratchet\Application\WebSocket\WebSocketAppInterface;
|
||||
use Ratchet\Resource\Connection;
|
||||
use Ratchet\Resource\Command\Composite;
|
||||
use Ratchet\Resource\Command\CommandInterface;
|
||||
use Ratchet\Application\WAMP\Command\Action\Prefix;
|
||||
|
||||
/**
|
||||
* WebSocket Application Messaging Protocol
|
||||
*
|
||||
* +--------------+----+------------------+
|
||||
* | Message Type | ID | DIRECTION |
|
||||
* |--------------+----+------------------+
|
||||
* | PREFIX | 1 | Bi-Directional |
|
||||
* | CALL | 2 | Client-to-Server |
|
||||
* | CALL RESULT | 3 | Server-to-Client |
|
||||
* | CALL ERROR | 4 | Server-to-Client |
|
||||
* | SUBSCRIBE | 5 | Client-to-Server |
|
||||
* | UNSUBSCRIBE | 6 | Client-to-Server |
|
||||
* | PUBLISH | 7 | Client-to-Server |
|
||||
* | EVENT | 8 | Server-to-Client |
|
||||
* +--------------+----+------------------+
|
||||
* @link http://www.tavendo.de/autobahn/protocol.html
|
||||
* @link https://raw.github.com/oberstet/Autobahn/master/lib/javascript/autobahn.js
|
||||
*/
|
||||
class App implements WebSocketAppInterface {
|
||||
const MSG_WELCOME = 0;
|
||||
const MSG_PREFIX = 1;
|
||||
const MSG_CALL = 2;
|
||||
const MSG_CALL_RESULT = 3;
|
||||
const MSG_CALL_ERROR = 4;
|
||||
const MSG_SUBSCRIBE = 5;
|
||||
const MSG_UNSUBSCRIBE = 6;
|
||||
const MSG_PUBLISH = 7;
|
||||
const MSG_EVENT = 8;
|
||||
|
||||
protected $_app;
|
||||
|
||||
/**
|
||||
* Any server to client prefixes are stored here
|
||||
* They're taxied along with the next outgoing message
|
||||
* @var Ratchet\Resource\Command\Composite
|
||||
*/
|
||||
protected $_msg_buffer = null;
|
||||
|
||||
public function getSubProtocol() {
|
||||
return 'wamp';
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo WAMP spec does not say what to do when there is an error with PREFIX...
|
||||
*/
|
||||
public function addPrefix(Connection $conn, $curie, $uri, $from_server = false) {
|
||||
// validate uri
|
||||
// validate curie
|
||||
|
||||
// make sure the curie is shorter than the uri
|
||||
|
||||
$conn->WAMP->prefixes[$curie] = $uri;
|
||||
|
||||
if ($from_server) {
|
||||
$prefix = new Prefix($conn);
|
||||
$prefix->setPrefix($curie, $uri);
|
||||
|
||||
$this->_msg_buffer->enqueue($prefix);
|
||||
}
|
||||
}
|
||||
|
||||
public function onOpen(Connection $conn) {
|
||||
$conn->WAMP = new \StdClass;
|
||||
$conn->WAMP->prefixes = array();
|
||||
$conn->WAMP->subscriptions = array();
|
||||
|
||||
$wamp = $this;
|
||||
$conn->WAMP->addPrefix = function($curie, $uri) use ($wamp, $conn) {
|
||||
$wamp->addPrefix($conn, $curie, $uri, true);
|
||||
};
|
||||
|
||||
return $this->_app->onOpen($conn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @{inherit}
|
||||
* @throws Exception
|
||||
* @throws JSONException
|
||||
*/
|
||||
public function onMessage(Connection $from, $msg) {
|
||||
if (null === ($json = @json_decode($msg, true))) {
|
||||
throw new JSONException;
|
||||
}
|
||||
|
||||
switch ($json[0]) {
|
||||
case static::MSG_PREFIX:
|
||||
$ret = $this->addPrefix($from, $json[1], $json[2]);
|
||||
break;
|
||||
|
||||
case static::MSG_CALL:
|
||||
array_shift($json);
|
||||
$callID = array_shift($json);
|
||||
$procURI = array_shift($json);
|
||||
|
||||
if (count($json) == 1 && is_array($json[0])) {
|
||||
$json = $json[0];
|
||||
}
|
||||
|
||||
$ret = $this->_app->onCall($from, $callID, $procURI, $json);
|
||||
break;
|
||||
|
||||
case static::MSG_SUBSCRIBE:
|
||||
$ret = $this->_app->onSubscribe($from, $this->getUri($from, $json[1]));
|
||||
break;
|
||||
|
||||
case static::MSG_UNSUBSCRIBE:
|
||||
$ret = $this->_app->onUnSubscribe($from, $this->getUri($from, $json[1]));
|
||||
break;
|
||||
|
||||
case static::MSG_PUBLISH:
|
||||
$ret = $this->_app->onPublish($from, $this->getUri($from, $json[1]), $json[2]);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('Invalid message type');
|
||||
}
|
||||
|
||||
return $this->attachStack($ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full request URI from the connection object if a prefix has been established for it
|
||||
* @param Ratchet\Resource\Connection
|
||||
* @param ...
|
||||
* @return string
|
||||
*/
|
||||
protected function getUri(Connection $conn, $uri) {
|
||||
return (isset($conn->WAMP->prefixes[$uri]) ? $conn->WAMP->prefixes[$uri] : $uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the developer's application as set some server-to-client prefixes to be set,
|
||||
* this method ensures those are taxied to the next outgoing message
|
||||
* @param Ratchet\Resource\Command\CommandInterface|NULL
|
||||
* @return Ratchet\Resource\Command\Composite
|
||||
*/
|
||||
protected function attachStack(CommandInterface $command = null) {
|
||||
$stack = $this->_msg_buffer;
|
||||
$stack->enqueue($command);
|
||||
|
||||
$this->_msg_buffer = new Composite;
|
||||
|
||||
return $stack;
|
||||
}
|
||||
|
||||
public function __construct(ServerInterface $app) {
|
||||
$this->_app = $app;
|
||||
$this->_msg_buffer = new Composite;
|
||||
}
|
||||
|
||||
public function onClose(Connection $conn) {
|
||||
return $this->_app->onClose($conn);
|
||||
}
|
||||
|
||||
public function onError(Connection $conn, \Exception $e) {
|
||||
return $this->_app->onError($conn, $e);
|
||||
}
|
||||
}
|
32
lib/Ratchet/Application/WAMP/Command/Action/CallError.php
Normal file
32
lib/Ratchet/Application/WAMP/Command/Action/CallError.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WAMP\Command\Action;
|
||||
use Ratchet\Resource\Command\Action\SendMessage;
|
||||
use Ratchet\Application\WAMP\App as WAMP;
|
||||
|
||||
class CallError extends SendMessage {
|
||||
protected $_id;
|
||||
|
||||
protected $_uri;
|
||||
|
||||
protected $_desc;
|
||||
|
||||
public function setError($callId, $uri, $desc) {
|
||||
$this->_id = $callId;
|
||||
$this->_uri = $uri;
|
||||
$this->_desc = $desc;
|
||||
|
||||
return $this->setMessage(json_encode(array(WAMP::MSG_CALL_ERROR, $callId, $uri, $desc)));
|
||||
}
|
||||
|
||||
public function getId() {
|
||||
return $this->_id;
|
||||
}
|
||||
|
||||
public function getUri() {
|
||||
return $this->_uri;
|
||||
}
|
||||
|
||||
public function getDescription() {
|
||||
return $this->_desc;
|
||||
}
|
||||
}
|
27
lib/Ratchet/Application/WAMP/Command/Action/CallResult.php
Normal file
27
lib/Ratchet/Application/WAMP/Command/Action/CallResult.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WAMP\Command\Action;
|
||||
use Ratchet\Resource\Command\Action\SendMessage;
|
||||
use Ratchet\Application\WAMP\App as WAMP;
|
||||
|
||||
/**
|
||||
*/
|
||||
class CallResult extends SendMessage {
|
||||
protected $_id;
|
||||
|
||||
protected $_data;
|
||||
|
||||
public function setResult($callId, array $data = array()) {
|
||||
$this->_id = $callId;
|
||||
$this->_data = $data;
|
||||
|
||||
return $this->setMessage(json_encode(array(WAMP::MSG_CALL_RESULT, $callId, $data)));
|
||||
}
|
||||
|
||||
public function getId() {
|
||||
return $this->_id;
|
||||
}
|
||||
|
||||
public function getData() {
|
||||
return $this->_data;
|
||||
}
|
||||
}
|
19
lib/Ratchet/Application/WAMP/Command/Action/Event.php
Normal file
19
lib/Ratchet/Application/WAMP/Command/Action/Event.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WAMP\Command\Action;
|
||||
use Ratchet\Resource\Command\Action\SendMessage;
|
||||
use Ratchet\Application\WAMP\App as WAMP;
|
||||
|
||||
/**
|
||||
* This is an event in the context of a topicURI
|
||||
* This event (message) is to be sent to all subscribers of $uri
|
||||
*/
|
||||
class Event extends SendMessage {
|
||||
/**
|
||||
* @param ...
|
||||
* @param string
|
||||
* @return Event
|
||||
*/
|
||||
public function setEvent($uri, $msg) {
|
||||
return $this->setMessage(json_encode(array(WAMP::MSG_EVENT, $uri, (string)$msg)));
|
||||
}
|
||||
}
|
40
lib/Ratchet/Application/WAMP/Command/Action/Prefix.php
Normal file
40
lib/Ratchet/Application/WAMP/Command/Action/Prefix.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WAMP\Command\Action;
|
||||
use Ratchet\Resource\Command\Action\SendMessage;
|
||||
use Ratchet\Application\WAMP\App as WAMP;
|
||||
|
||||
/**
|
||||
* Send a curie to uri mapping to the client
|
||||
* Both sides will agree to send the curie, representing the uri,
|
||||
* resulting in less data transfered
|
||||
*/
|
||||
class Prefix extends SendMessage {
|
||||
protected $_curie;
|
||||
protected $_uri;
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @param string
|
||||
* @return Prefix
|
||||
*/
|
||||
public function setPrefix($curie, $uri) {
|
||||
$this->_curie = $curie;
|
||||
$this->_uri = $uri;
|
||||
|
||||
return $this->setMessage(json_encode(array(WAMP::MSG_PREFIX, $curie, $uri)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCurie() {
|
||||
return $this->_curie;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUri() {
|
||||
return $this->_uri;
|
||||
}
|
||||
}
|
5
lib/Ratchet/Application/WAMP/Exception.php
Normal file
5
lib/Ratchet/Application/WAMP/Exception.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WAMP;
|
||||
|
||||
class Exception extends \Exception {
|
||||
}
|
31
lib/Ratchet/Application/WAMP/JSONException.php
Normal file
31
lib/Ratchet/Application/WAMP/JSONException.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WAMP;
|
||||
|
||||
class JSONException extends Exception {
|
||||
public function __construct() {
|
||||
$code = json_last_error();
|
||||
|
||||
switch ($code) {
|
||||
case JSON_ERROR_DEPTH:
|
||||
$msg = 'Maximum stack depth exceeded';
|
||||
break;
|
||||
case JSON_ERROR_STATE_MISMATCH:
|
||||
$msg = 'Underflow or the modes mismatch';
|
||||
break;
|
||||
case JSON_ERROR_CTRL_CHAR:
|
||||
$msg = 'Unexpected control character found';
|
||||
break;
|
||||
case JSON_ERROR_SYNTAX:
|
||||
$msg = 'Syntax error, malformed JSON';
|
||||
break;
|
||||
case JSON_ERROR_UTF8:
|
||||
$msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
|
||||
break;
|
||||
default:
|
||||
$msg = 'Unknown error';
|
||||
break;
|
||||
}
|
||||
|
||||
parent::__construct($msg, $code);
|
||||
}
|
||||
}
|
65
lib/Ratchet/Application/WAMP/ServerInterface.php
Normal file
65
lib/Ratchet/Application/WAMP/ServerInterface.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WAMP;
|
||||
use Ratchet\Resource\Connection;
|
||||
|
||||
/**
|
||||
* A (not literal) extension of Ratchet\Application\ApplicationInterface
|
||||
* onMessage is replaced by various types of messages for this protocol (pub/sub or rpc)
|
||||
* @todo Thought: URI as class. Class has short and long version stored (if as prefix)
|
||||
*/
|
||||
interface ServerInterface {
|
||||
/**
|
||||
* When a new connection is opened it will be passed to this method
|
||||
* @param Ratchet\Resource\Connection
|
||||
*/
|
||||
function onOpen(Connection $conn);
|
||||
|
||||
/**
|
||||
* The user closed their connection
|
||||
* @param Ratchet\Resource\Connection
|
||||
* @return Ratchet\Resource\Command\CommandInterface|null
|
||||
*/
|
||||
function onClose(Connection $conn);
|
||||
|
||||
/**
|
||||
* @param Ratchet\Resource\Connection
|
||||
* @param \Exception
|
||||
* @return Ratchet\Resource\Command\CommandInterface|null
|
||||
*/
|
||||
function onError(Connection $conn, \Exception $e);
|
||||
|
||||
/**
|
||||
* An RPC call has been received
|
||||
* @param Ratchet\Resource\Connection
|
||||
* @param string
|
||||
* @param ...
|
||||
* @param array Call parameters received from the client
|
||||
* @return Ratchet\Resource\Command\CommandInterface|null
|
||||
*/
|
||||
function onCall(Connection $conn, $id, $procURI, array $params);
|
||||
|
||||
/**
|
||||
* A request to subscribe to a URI has been made
|
||||
* @param Ratchet\Resource\Connection
|
||||
* @param ...
|
||||
* @return Ratchet\Resource\Command\CommandInterface|null
|
||||
*/
|
||||
function onSubscribe(Connection $conn, $uri);
|
||||
|
||||
/**
|
||||
* A request to unsubscribe from a URI has been made
|
||||
* @param Ratchet\Resource\Connection
|
||||
* @param ...
|
||||
* @return Ratchet\Resource\Command\CommandInterface|null
|
||||
*/
|
||||
function onUnSubscribe(Connection $conn, $uri);
|
||||
|
||||
/**
|
||||
* A client is attempting to publish content to a subscribed connections on a URI
|
||||
* @param Ratchet\Resource\Connection
|
||||
* @param ...
|
||||
* @param string
|
||||
* @return Ratchet\Resource\Command\CommandInterface|null
|
||||
*/
|
||||
function onPublish(Connection $conn, $uri, $event);
|
||||
}
|
@ -6,7 +6,8 @@ use Ratchet\Resource\Connection;
|
||||
use Ratchet\Resource\Command\Factory;
|
||||
use Ratchet\Resource\Command\CommandInterface;
|
||||
use Ratchet\Resource\Command\Action\SendMessage;
|
||||
use Ratchet\Application\WebSocket\Util\HTTP;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
use Ratchet\Application\WebSocket\Guzzle\Http\Message\RequestFactory;
|
||||
|
||||
/**
|
||||
* The adapter to handle WebSocket requests/responses
|
||||
@ -41,11 +42,13 @@ class App implements ApplicationInterface, ConfiguratorInterface {
|
||||
|
||||
protected $_mask_payload = false;
|
||||
|
||||
public function __construct(ApplicationInterface $app = null) {
|
||||
if (null === $app) {
|
||||
throw new \UnexpectedValueException("WebSocket requires an application to run");
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
* @temporary
|
||||
*/
|
||||
public $accepted_subprotocols = array();
|
||||
|
||||
public function __construct(ApplicationInterface $app) {
|
||||
$this->_app = $app;
|
||||
$this->_factory = new Factory;
|
||||
}
|
||||
@ -80,18 +83,32 @@ class App implements ApplicationInterface, ConfiguratorInterface {
|
||||
public function onMessage(Connection $from, $msg) {
|
||||
if (true !== $from->WebSocket->handshake) {
|
||||
if (!isset($from->WebSocket->version)) {
|
||||
try {
|
||||
$from->WebSocket->headers .= $msg;
|
||||
$from->WebSocket->version = $this->getVersion($from->WebSocket->headers);
|
||||
} catch (\UnderflowException $e) {
|
||||
$from->WebSocket->headers .= $msg;
|
||||
if (!$this->isMessageComplete($from->WebSocket->headers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = RequestFactory::fromRequest($from->WebSocket->headers);
|
||||
$from->WebSocket->version = $this->getVersion($headers);
|
||||
$from->WebSocket->headers = $headers;
|
||||
}
|
||||
|
||||
$response = $from->WebSocket->version->handshake($from->WebSocket->headers);
|
||||
$from->WebSocket->handshake = true;
|
||||
|
||||
if (is_array($response)) {
|
||||
// This block is to be moved/changed later
|
||||
$agreed_protocols = array();
|
||||
$requested_protocols = $from->WebSocket->headers->getTokenizedHeader('Sec-WebSocket-Protocol', ',');
|
||||
foreach ($this->accepted_subprotocols as $sub_protocol) {
|
||||
if (false !== $requested_protocols->hasValue($sub_protocol)) {
|
||||
$agreed_protocols[] = $sub_protocol;
|
||||
}
|
||||
}
|
||||
if (count($agreed_protocols) > 0) {
|
||||
$response['Sec-WebSocket-Protocol'] = implode(',', $agreed_protocols);
|
||||
}
|
||||
|
||||
$header = '';
|
||||
foreach ($response as $key => $val) {
|
||||
if (!empty($key)) {
|
||||
@ -154,14 +171,6 @@ class App implements ApplicationInterface, ConfiguratorInterface {
|
||||
return $this->_app->onError($conn, $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Incomplete, WebSocket protocol allows client to ask to use a sub-protocol, I'm thinking/wanting to somehow implement this in an application decorated class
|
||||
* @param string
|
||||
* @todo Implement or delete...
|
||||
*/
|
||||
public function setSubProtocol($name) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a return Command from your application is a message, if so encode it/them
|
||||
* @param Ratchet\Resource\Command\CommandInterface|NULL
|
||||
@ -212,21 +221,15 @@ class App implements ApplicationInterface, ConfiguratorInterface {
|
||||
* @throws InvalidArgumentException If we can't understand protocol version request
|
||||
* @todo Verify the first line of the HTTP header as per page 16 of RFC 6455
|
||||
*/
|
||||
protected function getVersion($message) {
|
||||
if (false === strstr($message, "\r\n\r\n")) { // This CAN fail with Hixie, depending on the TCP buffer in between
|
||||
throw new \UnderflowException;
|
||||
}
|
||||
|
||||
$headers = HTTP::getHeaders($message);
|
||||
|
||||
protected function getVersion(RequestInterface $request) {
|
||||
foreach ($this->_versions as $name => $instance) {
|
||||
if (null !== $instance) {
|
||||
if ($instance::isProtocol($headers)) {
|
||||
if ($instance::isProtocol($request)) {
|
||||
return $instance;
|
||||
}
|
||||
} else {
|
||||
$ns = __NAMESPACE__ . "\\Version\\{$name}";
|
||||
if ($ns::isProtocol($headers)) {
|
||||
if ($ns::isProtocol($request)) {
|
||||
$this->_versions[$name] = new $ns;
|
||||
return $this->_versions[$name];
|
||||
}
|
||||
@ -236,6 +239,29 @@ class App implements ApplicationInterface, ConfiguratorInterface {
|
||||
throw new \InvalidArgumentException('Could not identify WebSocket protocol');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @return bool
|
||||
* @todo Abstract, some hard coding done for (stupid) Hixie protocol
|
||||
*/
|
||||
protected function isMessageComplete($message) {
|
||||
static $crlf = "\r\n\r\n";
|
||||
|
||||
$headers = (boolean)strstr($message, $crlf);
|
||||
if (!$headers) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strstr($message, 'Sec-WebSocket-Key2')) {
|
||||
if (8 !== strlen(substr($message, strpos($message, $crlf) + strlen($crlf)))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a version of the WebSocket protocol *cough*Hixie76*cough*
|
||||
* @param string The name of the version to disable
|
||||
|
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WebSocket\Guzzle\Http\Message;
|
||||
use Guzzle\Http\Message\RequestFactory as gReqFac;
|
||||
use Guzzle\Http\Url;
|
||||
|
||||
/**
|
||||
* Just slighly changing the Guzzle fromMessage() method to always return an EntityEnclosingRequest instance instead of Request
|
||||
*/
|
||||
class RequestFactory extends gReqFac {
|
||||
/**
|
||||
* @param string
|
||||
* @return Guzzle\Http\Message\RequestInterface
|
||||
*/
|
||||
public static function fromRequest($message) {
|
||||
$parsed = static::parseMessage($message);
|
||||
|
||||
if (!$parsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::fromRequestParts(
|
||||
$parsed['method'],
|
||||
$parsed['parts'],
|
||||
$parsed['headers'],
|
||||
$parsed['body'],
|
||||
$parsed['protocol'],
|
||||
$parsed['protocol_version']
|
||||
);
|
||||
}
|
||||
|
||||
protected static function fromRequestParts($method, array $parts, $headers = null, $body = null, $protocol = 'HTTP', $protocolVersion = '1.1') {
|
||||
return self::requestCreate($method, Url::buildUrl($parts, true), $headers, $body)
|
||||
->setProtocolVersion($protocolVersion);
|
||||
}
|
||||
|
||||
protected static function requestCreate($method, $url, $headers = null, $body = null) {
|
||||
$c = static::$entityEnclosingRequestClass;
|
||||
$request = new $c($method, $url, $headers);
|
||||
$request->setBody($body);
|
||||
|
||||
return $request;
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WebSocket\Util;
|
||||
|
||||
/**
|
||||
* A helper class for handling HTTP requests
|
||||
* @todo Needs re-write...http_parse_headers is a PECL extension that changes the case to unexpected values
|
||||
* @todo Again, RE-WRITE - I want all the expected headers to at least be set in the returned, even if not there, set as null - having to do too much work in HandshaekVerifier
|
||||
*/
|
||||
class HTTP {
|
||||
/**
|
||||
* @todo Probably should iterate through the array, strtolower all the things, then return it
|
||||
* @param string
|
||||
* @return array
|
||||
*/
|
||||
public static function getHeaders($http_message) {
|
||||
$header_array = function_exists('http_parse_headers') ? http_parse_headers($http_message) : self::http_parse_headers($http_message);
|
||||
|
||||
return $header_array + array(
|
||||
'Host' => null
|
||||
, 'Upgrade' => null
|
||||
, 'Connection' => null
|
||||
, 'Sec-Websocket-Key' => null
|
||||
, 'Origin' => null
|
||||
, 'Sec-Websocket-Protocol' => null
|
||||
, 'Sec-Websocket-Version' => null
|
||||
, 'Sec-Websocket-Origin' => null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @return array
|
||||
* This is a fallback method for http_parse_headers as not all php installs have the HTTP module present
|
||||
* @internal
|
||||
*/
|
||||
protected static function http_parse_headers($http_message) {
|
||||
$retVal = array();
|
||||
$fields = explode("br", preg_replace("%(<|/\>|>)%", "", nl2br($http_message)));
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (preg_match('%^(GET|POST|PUT|DELETE|PATCH)(\s)(.*)%', $field, $matchReq)) {
|
||||
$retVal["Request Method"] = $matchReq[1];
|
||||
$retVal["Request Url"] = $matchReq[3];
|
||||
} elseif (preg_match('/([^:]+): (.+)/m', $field, $match) ) {
|
||||
$match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
|
||||
if (isset($retVal[$match[1]])) {
|
||||
$retVal[$match[1]] = array($retVal[$match[1]], $match[2]);
|
||||
} else {
|
||||
$retVal[$match[1]] = trim($match[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WebSocket\Version;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* FOR THE LOVE OF BEER, PLEASE PLEASE PLEASE DON'T allow the use of this in your application!
|
||||
@ -14,16 +15,17 @@ namespace Ratchet\Application\WebSocket\Version;
|
||||
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
||||
*/
|
||||
class Hixie76 implements VersionInterface {
|
||||
public static function isProtocol(array $headers) {
|
||||
return isset($headers['Sec-Websocket-Key2']);
|
||||
public static function isProtocol(RequestInterface $request) {
|
||||
return !(null === $request->getHeader('Sec-WebSocket-Key2'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string
|
||||
* @return string
|
||||
* @todo Unhack this mess...or wait for Hixie to die (HURRY UP APPLE)
|
||||
*/
|
||||
public function handshake($message) {
|
||||
$buffer = $message;
|
||||
public function handshake(RequestInterface $request) {
|
||||
$buffer = $request->getRawHeaders() . "\r\n\r\n" . $request->getBody();
|
||||
$resource = $host = $origin = $key1 = $key2 = $protocol = $code = $handshake = null;
|
||||
|
||||
preg_match('#GET (.*?) HTTP#', $buffer, $match) && $resource = $match[1];
|
||||
|
@ -1,18 +1,14 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WebSocket\Version;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @todo Note: Even though this is the "legacy" HyBi version, it's using the RFC Message and Frame classes - change if needed
|
||||
*/
|
||||
class HyBi10 extends RFC6455 {
|
||||
public static function isProtocol(array $headers) {
|
||||
if (isset($headers['Sec-Websocket-Version'])) {
|
||||
if ((int)$headers['Sec-Websocket-Version'] >= 6 && (int)$headers['Sec-Websocket-Version'] < 13) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
public static function isProtocol(RequestInterface $request) {
|
||||
$version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
|
||||
return ($version >= 6 && $version < 13);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,10 +1,10 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WebSocket\Version;
|
||||
use Ratchet\Application\WebSocket\Version\RFC6455\HandshakeVerifier;
|
||||
use Ratchet\Application\WebSocket\Util\HTTP;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @link http://www.rfc-editor.org/authors/rfc6455.txt
|
||||
* @link http://tools.ietf.org/html/rfc6455
|
||||
*/
|
||||
class RFC6455 implements VersionInterface {
|
||||
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
@ -18,14 +18,12 @@ class RFC6455 implements VersionInterface {
|
||||
$this->_verifier = new HandshakeVerifier;
|
||||
}
|
||||
|
||||
public static function isProtocol(array $headers) {
|
||||
if (isset($headers['Sec-Websocket-Version'])) {
|
||||
if ((int)$headers['Sec-Websocket-Version'] == 13) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* @todo Change the request to be a Guzzle RequestInterface
|
||||
*/
|
||||
public static function isProtocol(RequestInterface $request) {
|
||||
$version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
|
||||
return (13 === $version);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,11 +31,8 @@ class RFC6455 implements VersionInterface {
|
||||
* I kept this as an array and combined in App for future considerations...easier to add a subprotol as a key value than edit a string
|
||||
* @todo Decide what to do on failure...currently throwing an exception and I think socket connection is closed. Should be sending 40x error - but from where?
|
||||
*/
|
||||
public function handshake($message) {
|
||||
$headers = HTTP::getHeaders($message);
|
||||
$key = $this->sign($headers['Sec-Websocket-Key']);
|
||||
|
||||
if (true !== $this->_verifier->verifyAll($headers)) {
|
||||
public function handshake(RequestInterface $request) {
|
||||
if (true !== $this->_verifier->verifyAll($request)) {
|
||||
throw new \InvalidArgumentException('Invalid HTTP header');
|
||||
}
|
||||
|
||||
@ -45,7 +40,7 @@ class RFC6455 implements VersionInterface {
|
||||
'' => 'HTTP/1.1 101 Switching Protocols'
|
||||
, 'Upgrade' => 'websocket'
|
||||
, 'Connection' => 'Upgrade'
|
||||
, 'Sec-WebSocket-Accept' => $this->sign($headers['Sec-Websocket-Key'])
|
||||
, 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key'))
|
||||
// , 'Sec-WebSocket-Protocol' => ''
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WebSocket\Version\RFC6455;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* These are checks to ensure the client requested handshake are valid
|
||||
@ -9,22 +10,24 @@ namespace Ratchet\Application\WebSocket\Version\RFC6455;
|
||||
class HandshakeVerifier {
|
||||
/**
|
||||
* Given an array of the headers this method will run through all verification methods
|
||||
* @param array
|
||||
* @param Guzzle\Http\Message\RequestInterface
|
||||
* @return bool TRUE if all headers are valid, FALSE if 1 or more were invalid
|
||||
*/
|
||||
public function verifyAll(array $headers) {
|
||||
public function verifyAll(RequestInterface $request) {
|
||||
$headers = $request->getHeaders();
|
||||
|
||||
$passes = 0;
|
||||
|
||||
$passes += (int)$this->verifyMethod($headers['Request Method']);
|
||||
//$passes += (int)$this->verifyHTTPVersion($headers['???']); // This isn't in the array!
|
||||
$passes += (int)$this->verifyRequestURI($headers['Request Url']);
|
||||
$passes += (int)$this->verifyMethod($request->getMethod());
|
||||
$passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion());
|
||||
$passes += (int)$this->verifyRequestURI($request->getPath());
|
||||
$passes += (int)$this->verifyHost($headers['Host']);
|
||||
$passes += (int)$this->verifyUpgradeRequest($headers['Upgrade']);
|
||||
$passes += (int)$this->verifyConnection($headers['Connection']);
|
||||
$passes += (int)$this->verifyKey($headers['Sec-Websocket-Key']);
|
||||
//$passes += (int)$this->verifyVersion($headers['Sec-Websocket-Version']); // Temporarily breaking functionality
|
||||
$passes += (int)$this->verifyKey($headers['Sec-WebSocket-Key']);
|
||||
//$passes += (int)$this->verifyVersion($headers['Sec-WebSocket-Version']); // Temporarily breaking functionality
|
||||
|
||||
return (6 === $passes);
|
||||
return (7 === $passes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
namespace Ratchet\Application\WebSocket\Version;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* Despite the version iterations of WebInterface the actions they go through are similar
|
||||
@ -10,20 +11,21 @@ namespace Ratchet\Application\WebSocket\Version;
|
||||
interface VersionInterface {
|
||||
/**
|
||||
* Given an HTTP header, determine if this version should handle the protocol
|
||||
* @param array
|
||||
* @param Guzzle\Http\Message\RequestInterface
|
||||
* @return bool
|
||||
* @throws UnderflowException If the protocol thinks the headers are still fragmented
|
||||
*/
|
||||
static function isProtocol(array $headers);
|
||||
static function isProtocol(RequestInterface $request);
|
||||
|
||||
/**
|
||||
* Perform the handshake and return the response headers
|
||||
* @param string
|
||||
* @param Guzzle\Http\Message\RequestInterface
|
||||
* @return array|string
|
||||
* @throws InvalidArgumentException If the HTTP handshake is mal-formed
|
||||
* @throws UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version)
|
||||
* @todo Change param to accept a Guzzle RequestInterface object
|
||||
*/
|
||||
function handshake($message);
|
||||
function handshake(RequestInterface $request);
|
||||
|
||||
/**
|
||||
* @return MessageInterface
|
||||
|
@ -12,7 +12,7 @@ interface WebSocketAppInterface extends ApplicationInterface {
|
||||
* Currently instead of this, I'm setting header in the Connection object passed around...not sure which I like more
|
||||
* @param string
|
||||
*/
|
||||
function setHeaders($headers);
|
||||
//function setHeaders($headers);
|
||||
|
||||
/**
|
||||
* @return string
|
||||
|
@ -8,6 +8,8 @@ use Ratchet\Resource\Connection;
|
||||
class Factory {
|
||||
protected $_paths = array();
|
||||
|
||||
protected $_mapped_commands = array();
|
||||
|
||||
public function __construct() {
|
||||
$this->addActionPath(__NAMESPACE__ . '\\Action');
|
||||
}
|
||||
@ -33,19 +35,19 @@ class Factory {
|
||||
* @throws UnexpectedValueException
|
||||
*/
|
||||
public function newCommand($name, Connection $conn) {
|
||||
$cmd = null;
|
||||
if (isset($this->_mapped_commands[$name])) {
|
||||
$cmd = $this->_mapped_commands[$name];
|
||||
return new $cmd($conn);
|
||||
}
|
||||
|
||||
foreach ($this->_paths as $path) {
|
||||
if (class_exists($path . $name)) {
|
||||
$cmd = $path . $name;
|
||||
break;
|
||||
$this->_mapped_commands[$name] = $path . $name;
|
||||
return $this->newCommand($name, $conn);
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $cmd) {
|
||||
throw new \UnexepctedValueException("Command {$name} not found");
|
||||
}
|
||||
|
||||
return new $cmd($conn);
|
||||
throw new \UnexepctedValueException("Command {$name} not found");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -19,6 +19,7 @@
|
||||
<filter>
|
||||
<blacklist>
|
||||
<directory>./tests/</directory>
|
||||
<directory>./vendor/</directory>
|
||||
</blacklist>
|
||||
</filter>
|
||||
</phpunit>
|
@ -36,11 +36,6 @@ class AppTest extends \PHPUnit_Framework_TestCase {
|
||||
return array_pop($connections);
|
||||
}
|
||||
|
||||
public function testDoNotAllowStacklessServer() {
|
||||
$this->setExpectedException('UnexpectedValueException');
|
||||
new ServerApp;
|
||||
}
|
||||
|
||||
public function testOnOpenPassesClonedSocket() {
|
||||
$this->_server->run($this->_catalyst);
|
||||
$master = $this->getMasterConnection();
|
||||
|
@ -20,6 +20,17 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
|
||||
}
|
||||
|
||||
protected static function convert($in) {
|
||||
if (strlen($in) > 8) {
|
||||
$out = '';
|
||||
|
||||
while (strlen($in) > 8) {
|
||||
$out .= static::convert(substr($in, 0, 8));
|
||||
$in = substr($in, 8);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
return pack('C', bindec($in));
|
||||
}
|
||||
|
||||
@ -37,6 +48,31 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
|
||||
);
|
||||
}
|
||||
|
||||
public static function underflowProvider() {
|
||||
return array(
|
||||
array('isFinal', '')
|
||||
, array('getOpcode', '')
|
||||
, array('isMasked', '10000001')
|
||||
, array('getPayloadLength', '10000001')
|
||||
, array('getPayloadLength', '1000000111111110')
|
||||
, array('getMaskingKey', '1000000110000111')
|
||||
, array('getPayload', '100000011000000100011100101010101001100111110100')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider underflowProvider
|
||||
*/
|
||||
public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering($method, $bin) {
|
||||
$this->setExpectedException('\UnderflowException');
|
||||
|
||||
if (!empty($bin)) {
|
||||
$this->_frame->addBuffer(static::convert($bin));
|
||||
}
|
||||
|
||||
call_user_func(array($this->_frame, $method));
|
||||
}
|
||||
|
||||
/**
|
||||
* A data provider for testing the first byte of a WebSocket frame
|
||||
* @param bool Given, is the byte indicate this is the final frame
|
||||
@ -53,12 +89,6 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
|
||||
);
|
||||
}
|
||||
|
||||
public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering() {
|
||||
return $this->markTestIncomplete();
|
||||
|
||||
$this->expectException('\UnderflowException');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider firstByteProvider
|
||||
*/
|
||||
|
@ -2,6 +2,7 @@
|
||||
namespace Ratchet\Tests\Application\WebSocket\Version;
|
||||
use Ratchet\Application\WebSocket\Version\RFC6455;
|
||||
use Ratchet\Application\WebSocket\Version\RFC6455\Frame;
|
||||
use Guzzle\Http\Message\RequestFactory;
|
||||
|
||||
/**
|
||||
* @covers Ratchet\Application\WebSocket\Version\RFC6455
|
||||
@ -103,32 +104,25 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase {
|
||||
public static function headerHandshakeProvider() {
|
||||
return array(
|
||||
array(false, "GET /test HTTP/1.0\r\n" . static::getAndSpliceHeader())
|
||||
, array(true, static::$good_rest . "\r\n" . static::getAndSpliceHeader())
|
||||
, array(false, "POST / HTTP:/1.1\r\n" . static::getAndSpliceHeader())
|
||||
, array(false, static::$good_rest . "\r\n" . static::getAndSpliceHeader('Upgrade', 'useless'))
|
||||
, array(false, "GET /ಠ_ಠ HTTP/1.1\r\n" . static::getAndSpliceHeader())
|
||||
, array(true, static::$good_rest . "\r\n" . static::getAndSpliceHeader('Connection', 'Herp, Upgrade, Derp'))
|
||||
);
|
||||
}
|
||||
|
||||
/* RFC example of a good header
|
||||
GET /chat HTTP/1.1
|
||||
Host: server.example.com
|
||||
Upgrade: websocket
|
||||
Connection: Upgrade
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
|
||||
Origin: http://example.com
|
||||
Sec-WebSocket-Protocol: chat, superchat
|
||||
Sec-WebSocket-Version: 13
|
||||
*/
|
||||
|
||||
/**
|
||||
* @dataProvider headerHandshakeProvider
|
||||
* @todo Can't finish this test until I rewrite headers
|
||||
*/
|
||||
public function testVariousHeadersToCheckHandshakeTolerance($pass, $header) {
|
||||
return $this->markTestIncomplete();
|
||||
$request = RequestFactory::fromMessage($header);
|
||||
|
||||
if ($pass) {
|
||||
$this->assertTrue(is_array($this->_version->handshake($header)));
|
||||
$this->assertTrue(is_array($this->_version->handshake($request)));
|
||||
} else {
|
||||
$this->setExpectedException('InvalidArgumentException');
|
||||
$this->_version->handshake($header);
|
||||
$this->_version->handshake($request);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,4 +5,7 @@
|
||||
$app->register();
|
||||
|
||||
$app = new SplClassLoader('Ratchet', dirname(__DIR__) . DIRECTORY_SEPARATOR . 'lib');
|
||||
$app->register();
|
||||
|
||||
$app = new SplClassLoader('Guzzle', dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'guzzle' . DIRECTORY_SEPARATOR . 'src');
|
||||
$app->register();
|
5
vendor/README.md
vendored
Normal file
5
vendor/README.md
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
## External Libraries
|
||||
|
||||
### Guzzle
|
||||
|
||||
Used to parse the incoming HTTP handshake request. A Guzzle Request object is then passed around the application for a consistent API.
|
1
vendor/guzzle
vendored
Submodule
1
vendor/guzzle
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit ac64abc2c05b921efc4623379c1674a282475ae5
|
Loading…
Reference in New Issue
Block a user