Merge branch 'psr7-multi-streamer'

This commit is contained in:
Chris Boden 2016-02-20 16:26:08 -05:00
commit cbd376e1b3
35 changed files with 1864 additions and 618 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
composer.lock
vendor
tests/ab/reports
reports

20
.travis.yml Normal file
View File

@ -0,0 +1,20 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7
- hhvm
before_install:
- export PATH=$HOME/.local/bin:$PATH
- pip install autobahntestsuite --user `whoami`
- pip list autobahntestsuite --user `whoami`
before_script:
- composer install
- sh tests/ab/run_ab_tests.sh
script:
- phpunit

View File

@ -1,4 +1,4 @@
Copyright (c) 2011-2014 Chris Boden
Copyright (c) 2011-2016 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

View File

@ -1,13 +1,12 @@
# RFC6455 - The WebSocket Protocol
This library is meant to be a protocol handler for the RFC6455 specification.
[![Build Status](https://travis-ci.org/ratchetphp/RFC6455.svg?branch=master)](https://travis-ci.org/ratchetphp/RFC6455)
---
This library a protocol handler for the RFC6455 specification.
It contains components for both server and client side handshake and messaging protocol negotation.
### A rough roadmap
Aspects that are left open to interpertation in the specification are also left open in this library.
It is up to the implementation to determine how those interpertations are to be dealt with.
* v0.1 is the initial split from Ratchet/v0.3.2 as-is. In this state it currently relies on some of Ratchet's interfaces.
* v0.2 will be more framework agnostic and will not require any interfaces from Ratchet. A dependency on Guzzle (or hopefully PSR-7) may be required.
* v0.3 will look into performance tuning. No more expected exceptions.
* v0.4 extension support
* v1.0 when all the bases are covered
This library is independent, framework agnostic, and does not deal with any I/O.
HTTP upgrade negotiation integration points are handled with PSR-7 interfaces.

View File

@ -1,29 +1,31 @@
{
"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"
"name": "ratchet/rfc6455",
"type": "library",
"description": "RFC6455 WebSocket protocol handler",
"keywords": ["WebSockets", "websocket", "RFC6455"],
"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/RFC6455/issues"
, "irc": "irc://irc.freenode.org/reactphp"
}
, "autoload": {
, "irc": "irc://irc.freenode.org/reactphp"
},
"autoload": {
"psr-4": {
"Ratchet\\WebSocket\\": "src"
"Ratchet\\RFC6455\\": "src"
}
}
, "require": {
"php": ">=5.3.9"
, "guzzle/http": "~3.6"
},
"require": {
"php": ">=5.4.2",
"guzzlehttp/psr7": "^1.0"
},
"require-dev": {
"react/http": "^0.4.1",
"react/socket-client": "^0.4.3"
}
}

27
phpunit.xml.dist Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
forceCoversAnnotation="true"
mapTestClassNameToCoveredClassName="true"
bootstrap="tests/bootstrap.php"
colors="true"
backupGlobals="false"
backupStaticAttributes="false"
syntaxCheck="false"
stopOnError="false"
>
<testsuites>
<testsuite name="tests">
<directory>tests</directory>
<exclude>
<directory>test/ab</directory>
</exclude>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>

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 implements ValidatorInterface {
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

@ -0,0 +1,53 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use GuzzleHttp\Psr7\Request;
class ClientNegotiator {
/**
* @var ResponseVerifier
*/
private $verifier;
/**
* @var \Psr\Http\Message\RequestInterface
*/
private $defaultHeader;
function __construct() {
$this->verifier = new ResponseVerifier;
$this->defaultHeader = new Request('GET', '', [
'Connection' => 'Upgrade'
, 'Upgrade' => 'websocket'
, 'Sec-WebSocket-Version' => $this->getVersion()
, 'User-Agent' => "RatchetRFC/0.0.0"
]);
}
public function generateRequest(UriInterface $uri) {
return $this->defaultHeader->withUri($uri)
->withHeader("Sec-WebSocket-Key", $this->generateKey());
}
public function validateResponse(RequestInterface $request, ResponseInterface $response) {
return $this->verifier->verifyAll($request, $response);
}
public function generateKey() {
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwzyz1234567890+/=';
$charRange = strlen($chars) - 1;
$key = '';
for ($i = 0; $i < 16; $i++) {
$key .= $chars[mt_rand(0, $charRange)];
}
return base64_encode($key);
}
public function getVersion() {
return 13;
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\RequestInterface;
/**
* A standard interface for interacting with the various version of the WebSocket protocol
* @todo Look in to extension support
*/
interface NegotiatorInterface {
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
/**
* Given an HTTP header, determine if this version should handle the protocol
* @param RequestInterface $request
* @return bool
*/
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 RequestInterface $request
* @return \Psr\Http\Message\ResponseInterface
*/
function handshake(RequestInterface $request);
/**
* Add supported protocols. If the request has any matching the response will include one
* @param array $protocols
*/
function setSupportedSubProtocols(array $protocols);
/**
* If enabled and support for a subprotocol has been added handshake
* will not upgrade if a match between request and supported subprotocols
* @param boolean $enable
* @todo Consider extending this interface and moving this there.
* The spec does says the server can fail for this reason, but
it is not a requirement. This is an implementation detail.
*/
function setStrictSubProtocolCheck($enable);
}

View File

@ -1,16 +1,18 @@
<?php
namespace Ratchet\WebSocket\Version\RFC6455;
use Guzzle\Http\Message\RequestInterface;
namespace Ratchet\RFC6455\Handshake;
use Psr\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 {
class RequestVerifier {
const VERSION = 13;
/**
* Given an array of the headers this method will run through all verification methods
* @param \Guzzle\Http\Message\RequestInterface $request
* @param RequestInterface $request
* @return bool TRUE if all headers are valid, FALSE if 1 or more were invalid
*/
public function verifyAll(RequestInterface $request) {
@ -18,14 +20,14 @@ class HandshakeVerifier {
$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
$passes += (int)$this->verifyRequestURI($request->getUri()->getPath());
$passes += (int)$this->verifyHost($request->getHeader('Host'));
$passes += (int)$this->verifyUpgradeRequest($request->getHeader('Upgrade'));
$passes += (int)$this->verifyConnection($request->getHeader('Connection'));
$passes += (int)$this->verifyKey($request->getHeader('Sec-WebSocket-Key'));
$passes += (int)$this->verifyVersion($request->getHeader('Sec-WebSocket-Version'));
return (7 === $passes);
return (8 === $passes);
}
/**
@ -51,7 +53,7 @@ class HandshakeVerifier {
* @return bool
*/
public function verifyRequestURI($val) {
if ($val[0] != '/') {
if ($val[0] !== '/') {
return false;
}
@ -67,60 +69,52 @@ class HandshakeVerifier {
}
/**
* @param string|null
* @param array $hostHeader
* @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);
public function verifyHost(array $hostHeader) {
return (1 === count($hostHeader));
}
/**
* Verify the Upgrade request to WebSockets.
* @param string $val MUST equal "websocket"
* @param array $upgradeHeader MUST equal "websocket"
* @return bool
*/
public function verifyUpgradeRequest($val) {
return ('websocket' === strtolower($val));
public function verifyUpgradeRequest(array $upgradeHeader) {
return (1 === count($upgradeHeader) && 'websocket' === strtolower($upgradeHeader[0]));
}
/**
* Verify the Connection header
* @param string $val MUST equal "Upgrade"
* @param array $connectionHeader MUST include "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));
public function verifyConnection(array $connectionHeader) {
return count(array_filter($connectionHeader, function ($x) {
return 'upgrade' === strtolower($x);
})) > 0;
}
/**
* This function verifies the nonce is valid (64 big encoded, 16 bytes random string)
* @param string|null
* @param array $keyHeader
* @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)));
public function verifyKey(array $keyHeader) {
return (1 === count($keyHeader) && 16 === strlen(base64_decode($keyHeader[0])));
}
/**
* Verify the version passed matches this RFC
* @param string|int MUST equal 13|"13"
* @param string|int $versionHeader 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);
public function verifyVersion($versionHeader) {
return (1 === count($versionHeader) && static::VERSION === (int)$versionHeader[0]);
}
/**

View File

@ -0,0 +1,52 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class ResponseVerifier {
public function verifyAll(RequestInterface $request, ResponseInterface $response) {
$passes = 0;
$passes += (int)$this->verifyStatus($response->getStatusCode());
$passes += (int)$this->verifyUpgrade($response->getHeader('Upgrade'));
$passes += (int)$this->verifyConnection($response->getHeader('Connection'));
$passes += (int)$this->verifySecWebSocketAccept(
$response->getHeader('Sec-WebSocket-Accept')
, $request->getHeader('Sec-WebSocket-Key')
);
$passes += (int)$this->verifySubProtocol(
$request->getHeader('Sec-WebSocket-Protocol')
, $response->getHeader('Sec-WebSocket-Protocol')
);
return (5 === $passes);
}
public function verifyStatus($status) {
return ((int)$status === 101);
}
public function verifyUpgrade(array $upgrade) {
return (in_array('websocket', array_map('strtolower', $upgrade)));
}
public function verifyConnection(array $connection) {
return (in_array('upgrade', array_map('strtolower', $connection)));
}
public function verifySecWebSocketAccept($swa, $key) {
return (
1 === count($swa) &&
1 === count($key) &&
$swa[0] === $this->sign($key[0])
);
}
public function sign($key) {
return base64_encode(sha1($key . NegotiatorInterface::GUID, true));
}
public function verifySubProtocol(array $requestHeader, array $responseHeader) {
return 0 === count($responseHeader) || count(array_intersect($responseHeader, $requestHeader)) > 0;
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\RequestInterface;
use GuzzleHttp\Psr7\Response;
/**
* The latest version of the WebSocket protocol
* @todo Unicode: return mb_convert_encoding(pack("N",$u), mb_internal_encoding(), 'UCS-4BE');
*/
class ServerNegotiator implements NegotiatorInterface {
/**
* @var \Ratchet\RFC6455\Handshake\RequestVerifier
*/
private $verifier;
private $_supportedSubProtocols = [];
private $_strictSubProtocols = false;
public function __construct(RequestVerifier $requestVerifier) {
$this->verifier = $requestVerifier;
}
/**
* {@inheritdoc}
*/
public function isProtocol(RequestInterface $request) {
return $this->verifier->verifyVersion($request->getHeader('Sec-WebSocket-Version'));
}
/**
* {@inheritdoc}
*/
public function getVersionNumber() {
return RequestVerifier::VERSION;
}
/**
* {@inheritdoc}
*/
public function handshake(RequestInterface $request) {
if (true !== $this->verifier->verifyMethod($request->getMethod())) {
return new Response(405);
}
if (true !== $this->verifier->verifyHTTPVersion($request->getProtocolVersion())) {
return new Response(505);
}
if (true !== $this->verifier->verifyRequestURI($request->getUri()->getPath())) {
return new Response(400);
}
if (true !== $this->verifier->verifyHost($request->getHeader('Host'))) {
return new Response(400);
}
if (true !== $this->verifier->verifyUpgradeRequest($request->getHeader('Upgrade'))) {
return new Response(400, [], '1.1', null, 'Upgrade header MUST be provided');
}
if (true !== $this->verifier->verifyConnection($request->getHeader('Connection'))) {
return new Response(400, [], '1.1', null, 'Connection header MUST be provided');
}
if (true !== $this->verifier->verifyKey($request->getHeader('Sec-WebSocket-Key'))) {
return new Response(400, [], '1.1', null, 'Invalid Sec-WebSocket-Key');
}
if (true !== $this->verifier->verifyVersion($request->getHeader('Sec-WebSocket-Version'))) {
return new Response(426, ['Sec-WebSocket-Version' => $this->getVersionNumber()]);
}
$headers = [];
$subProtocols = $request->getHeader('Sec-WebSocket-Protocol');
if (count($subProtocols) > 0 || (count($this->_supportedSubProtocols) > 0 && $this->_strictSubProtocols)) {
$subProtocols = array_map('trim', explode(',', implode(',', $subProtocols)));
$match = array_reduce($subProtocols, function($accumulator, $protocol) {
return $accumulator ?: (isset($this->_supportedSubProtocols[$protocol]) ? $protocol : null);
}, null);
if ($this->_strictSubProtocols && null === $match) {
return new Response(400, [], '1.1', null ,'No Sec-WebSocket-Protocols requested supported');
}
if (null !== $match) {
$headers['Sec-WebSocket-Protocol'] = $match;
}
}
return new Response(101, array_merge($headers, [
'Upgrade' => 'websocket'
, 'Connection' => 'Upgrade'
, 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0])
, 'X-Powered-By' => 'Ratchet'
]));
}
/**
* 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));
}
function setSupportedSubProtocols(array $protocols) {
$this->_supportedSubProtocols = array_flip($protocols);
}
/**
* If enabled and support for a subprotocol has been added handshake
* will not upgrade if a match between request and supported subprotocols
* @param boolean $enable
* @todo Consider extending this interface and moving this there.
* The spec does says the server can fail for this reason, but
* it is not a requirement. This is an implementation detail.
*/
function setStrictSubProtocolCheck($enable) {
$this->_strictSubProtocols = (boolean)$enable;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Ratchet\RFC6455\Messaging;
class CloseFrameChecker {
private $validCloseCodes = [];
public function __construct() {
$this->validCloseCodes = [
Frame::CLOSE_NORMAL,
Frame::CLOSE_GOING_AWAY,
Frame::CLOSE_PROTOCOL,
Frame::CLOSE_BAD_DATA,
Frame::CLOSE_BAD_PAYLOAD,
Frame::CLOSE_POLICY,
Frame::CLOSE_TOO_BIG,
Frame::CLOSE_MAND_EXT,
Frame::CLOSE_SRV_ERR,
];
}
public function __invoke($val) {
return ($val >= 3000 && $val <= 4999) || in_array($val, $this->validCloseCodes);
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace Ratchet\WebSocket\Version;
namespace Ratchet\RFC6455\Messaging;
interface DataInterface {
/**
@ -25,4 +25,10 @@ interface DataInterface {
* @return string
*/
function getContents();
/**
* Should return the unmasked payload received from peer
* @return string
*/
function __toString();
}

View File

@ -1,6 +1,5 @@
<?php
namespace Ratchet\WebSocket\Version\RFC6455;
use Ratchet\WebSocket\Version\FrameInterface;
namespace Ratchet\RFC6455\Messaging;
class Frame implements FrameInterface {
const OP_CONTINUE = 0;
@ -62,13 +61,23 @@ class Frame implements FrameInterface {
*/
protected $secondByte = -1;
/**
* @var callable
* @returns \UnderflowException
*/
private $ufeg;
/**
* @param string|null $payload
* @param bool $final
* @param int $opcode
* @param callable<\UnderflowException> $ufExceptionFactory
*/
public function __construct($payload = null, $final = true, $opcode = 1) {
public function __construct($payload = null, $final = true, $opcode = 1, callable $ufExceptionFactory = null) {
$this->ufeg = $ufExceptionFactory ?: function($msg = '') {
return new \UnderflowException($msg);
};
if (null === $payload) {
return;
}
@ -134,7 +143,7 @@ class Frame implements FrameInterface {
*/
public function isFinal() {
if (-1 === $this->firstByte) {
throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message');
throw call_user_func($this->ufeg, 'Not enough bytes received to determine if this is the final frame in message');
}
return 128 === ($this->firstByte & 128);
@ -146,7 +155,7 @@ class Frame implements FrameInterface {
*/
public function getRsv1() {
if (-1 === $this->firstByte) {
throw new \UnderflowException('Not enough bytes received to determine reserved bit');
throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit');
}
return 64 === ($this->firstByte & 64);
@ -158,7 +167,7 @@ class Frame implements FrameInterface {
*/
public function getRsv2() {
if (-1 === $this->firstByte) {
throw new \UnderflowException('Not enough bytes received to determine reserved bit');
throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit');
}
return 32 === ($this->firstByte & 32);
@ -170,7 +179,7 @@ class Frame implements FrameInterface {
*/
public function getRsv3() {
if (-1 === $this->firstByte) {
throw new \UnderflowException('Not enough bytes received to determine reserved bit');
throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit');
}
return 16 == ($this->firstByte & 16);
@ -181,7 +190,7 @@ class Frame implements FrameInterface {
*/
public function isMasked() {
if (-1 === $this->secondByte) {
throw new \UnderflowException("Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set");
throw call_user_func($this->ufeg, "Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set");
}
return 128 === ($this->secondByte & 128);
@ -198,7 +207,7 @@ class Frame implements FrameInterface {
$start = 1 + $this->getNumPayloadBytes();
if ($this->bytesRecvd < $start + static::MASK_LENGTH) {
throw new \UnderflowException('Not enough data buffered to calculate the masking key');
throw call_user_func($this->ufeg, 'Not enough data buffered to calculate the masking key');
}
return substr($this->data, $start, static::MASK_LENGTH);
@ -258,7 +267,7 @@ class Frame implements FrameInterface {
*/
public function unMaskPayload() {
if (!$this->isCoalesced()) {
throw new \UnderflowException('Frame must be coalesced before applying mask');
throw call_user_func($this->ufeg, 'Frame must be coalesced before applying mask');
}
if (!$this->isMasked()) {
@ -288,12 +297,22 @@ class Frame implements FrameInterface {
public function applyMask($maskingKey, $payload = null) {
if (null === $payload) {
if (!$this->isCoalesced()) {
throw new \UnderflowException('Frame must be coalesced to apply a mask');
throw call_user_func($this->ufeg, 'Frame must be coalesced to apply a mask');
}
$payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
}
$len = strlen($payload);
if (0 === $len) {
return '';
}
return $payload ^ str_pad('', $len, $maskingKey, STR_PAD_RIGHT);
// TODO: Remove this before publish - keeping methods here to compare performance (above is faster but need control against v0.3.3)
$applied = '';
for ($i = 0, $len = strlen($payload); $i < $len; $i++) {
$applied .= $payload[$i] ^ $maskingKey[$i % static::MASK_LENGTH];
@ -307,7 +326,7 @@ class Frame implements FrameInterface {
*/
public function getOpcode() {
if (-1 === $this->firstByte) {
throw new \UnderflowException('Not enough bytes received to determine opcode');
throw call_user_func($this->ufeg, 'Not enough bytes received to determine opcode');
}
return ($this->firstByte & ~240);
@ -320,7 +339,7 @@ class Frame implements FrameInterface {
*/
protected function getFirstPayloadVal() {
if (-1 === $this->secondByte) {
throw new \UnderflowException('Not enough bytes received');
throw call_user_func($this->ufeg, 'Not enough bytes received');
}
return $this->secondByte & 127;
@ -332,7 +351,7 @@ class Frame implements FrameInterface {
*/
protected function getNumPayloadBits() {
if (-1 === $this->secondByte) {
throw new \UnderflowException('Not enough bytes received');
throw call_user_func($this->ufeg, 'Not enough bytes received');
}
// By default 7 bits are used to describe the payload length
@ -379,7 +398,8 @@ class Frame implements FrameInterface {
$byte_length = $this->getNumPayloadBytes();
if ($this->bytesRecvd < 1 + $byte_length) {
throw new \UnderflowException('Not enough data buffered to determine payload length');
$this->defPayLen = -1;
throw call_user_func($this->ufeg, 'Not enough data buffered to determine payload length');
}
$len = 0;
@ -406,16 +426,10 @@ class Frame implements FrameInterface {
*/
public function getPayload() {
if (!$this->isCoalesced()) {
throw new \UnderflowException('Can not return partial message');
throw call_user_func($this->ufeg, 'Can not return partial message');
}
$payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
if ($this->isMasked()) {
$payload = $this->applyMask($this->getMaskingKey(), $payload);
}
return $payload;
return $this->__toString();
}
/**
@ -426,10 +440,19 @@ class Frame implements FrameInterface {
return substr($this->data, 0, $this->getPayloadStartingByte() + $this->getPayloadLength());
}
public function __toString() {
$payload = (string)substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
if ($this->isMasked()) {
$payload = $this->applyMask($this->getMaskingKey(), $payload);
}
return $payload;
}
/**
* 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() {

View File

@ -1,5 +1,5 @@
<?php
namespace Ratchet\WebSocket\Version;
namespace Ratchet\RFC6455\Messaging;
interface FrameInterface extends DataInterface {
/**

View File

@ -1,18 +1,20 @@
<?php
namespace Ratchet\WebSocket\Version\RFC6455;
use Ratchet\WebSocket\Version\MessageInterface;
use Ratchet\WebSocket\Version\FrameInterface;
namespace Ratchet\RFC6455\Messaging;
class Message implements MessageInterface, \Countable {
class Message implements \IteratorAggregate, MessageInterface {
/**
* @var \SplDoublyLinkedList
*/
protected $_frames;
private $_frames;
public function __construct() {
$this->_frames = new \SplDoublyLinkedList;
}
public function getIterator() {
return $this->_frames;
}
/**
* {@inheritdoc}
*/
@ -35,7 +37,6 @@ class Message implements MessageInterface, \Countable {
/**
* {@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);
@ -79,13 +80,7 @@ class Message implements MessageInterface, \Countable {
throw new \UnderflowException('Message has not been put back together yet');
}
$buffer = '';
foreach ($this->_frames as $frame) {
$buffer .= $frame->getPayload();
}
return $buffer;
return $this->__toString();
}
/**
@ -104,4 +99,25 @@ class Message implements MessageInterface, \Countable {
return $buffer;
}
public function __toString() {
$buffer = '';
foreach ($this->_frames as $frame) {
$buffer .= $frame->getPayload();
}
return $buffer;
}
/**
* @return boolean
*/
public function isBinary() {
if ($this->_frames->isEmpty()) {
throw new \UnderflowException('Not enough data has been received to determine if message is binary');
}
return Frame::OP_BINARY === $this->_frames->bottom()->getOpcode();
}
}

View File

@ -0,0 +1,227 @@
<?php
namespace Ratchet\RFC6455\Messaging;
class MessageBuffer {
/**
* @var \Ratchet\RFC6455\Messaging\CloseFrameChecker
*/
private $closeFrameChecker;
/**
* @var callable
*/
private $exceptionFactory;
/**
* @var \Ratchet\RFC6455\Messaging\Message
*/
private $messageBuffer;
/**
* @var \Ratchet\RFC6455\Messaging\Frame
*/
private $frameBuffer;
/**
* @var callable
*/
private $onMessage;
/**
* @var callable
*/
private $onControl;
/**
* @var bool
*/
private $checkForMask;
function __construct(
CloseFrameChecker $frameChecker,
callable $onMessage,
callable $onControl = null,
$expectMask = true,
$exceptionFactory = null
) {
$this->closeFrameChecker = $frameChecker;
$this->checkForMask = (bool)$expectMask;
$this->exceptionFactory ?: $this->exceptionFactory = function($msg) {
return new \UnderflowException($msg);
};
$this->onMessage = $onMessage;
$this->onControl = $onControl ?: function() {};
}
/**
* @param string $data
* @return null
*/
public function onData($data) {
$this->messageBuffer ?: $this->messageBuffer = $this->newMessage();
$this->frameBuffer ?: $this->frameBuffer = $this->newFrame();
$this->frameBuffer->addBuffer($data);
if (!$this->frameBuffer->isCoalesced()) {
return;
}
$onMessage = $this->onMessage;
$onControl = $this->onControl;
$this->frameBuffer = $this->frameCheck($this->frameBuffer);
$overflow = $this->frameBuffer->extractOverflow();
$this->frameBuffer->unMaskPayload();
$opcode = $this->frameBuffer->getOpcode();
if ($opcode > 2) {
$onControl($this->frameBuffer);
if (Frame::OP_CLOSE === $opcode) {
return;
}
} else {
$this->messageBuffer->addFrame($this->frameBuffer);
}
$this->frameBuffer = null;
if ($this->messageBuffer->isCoalesced()) {
$msgCheck = $this->checkMessage($this->messageBuffer);
if (true !== $msgCheck) {
$onControl($this->newCloseFrame($msgCheck));
} else {
$onMessage($this->messageBuffer);
}
$this->messageBuffer = null;
}
if (strlen($overflow) > 0) {
$this->onData($overflow); // PHP doesn't do tail recursion :(
}
}
/**
* Check a frame to be added to the current message buffer
* @param \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface $frame
* @return \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface
*/
public function frameCheck(FrameInterface $frame) {
if (false !== $frame->getRsv1() ||
false !== $frame->getRsv2() ||
false !== $frame->getRsv3()
) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
if ($this->checkForMask && !$frame->isMasked()) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
$opcode = $frame->getOpcode();
if ($opcode > 2) {
if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
switch ($opcode) {
case Frame::OP_CLOSE:
$closeCode = 0;
$bin = $frame->getPayload();
if (empty($bin)) {
return $this->newCloseFrame(Frame::CLOSE_NORMAL);
}
if (strlen($bin) == 1) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
if (strlen($bin) >= 2) {
list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2)));
}
$checker = $this->closeFrameChecker;
if (!$checker($closeCode)) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
if (!$this->checkUtf8(substr($bin, 2))) {
return $this->newCloseFrame(Frame::CLOSE_BAD_PAYLOAD);
}
return $this->newCloseFrame(Frame::CLOSE_NORMAL);
break;
case Frame::OP_PING:
case Frame::OP_PONG:
break;
default:
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
break;
}
return $frame;
}
if (Frame::OP_CONTINUE == $frame->getOpcode() && 0 == count($this->messageBuffer)) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
if (count($this->messageBuffer) > 0 && Frame::OP_CONTINUE != $frame->getOpcode()) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL);
}
return $frame;
}
/**
* Determine if a message is valid
* @param \Ratchet\RFC6455\Messaging\MessageInterface
* @return bool|int true if valid - false if incomplete - int of recommended close code
*/
public function checkMessage(MessageInterface $message) {
if (!$message->isBinary()) {
if (!$this->checkUtf8($message->getPayload())) {
return Frame::CLOSE_BAD_PAYLOAD;
}
}
return true;
}
private function checkUtf8($string) {
if (extension_loaded('mbstring')) {
return mb_check_encoding($string, 'UTF-8');
}
return preg_match('//u', $string);
}
/**
* @return \Ratchet\RFC6455\Messaging\MessageInterface
*/
public function newMessage() {
return new Message;
}
/**
* @param string|null $payload
* @param bool|null $final
* @param int|null $opcode
* @return \Ratchet\RFC6455\Messaging\FrameInterface
*/
public function newFrame($payload = null, $final = null, $opcode = null) {
return new Frame($payload, $final, $opcode, $this->exceptionFactory);
}
public function newCloseFrame($code) {
return $this->newFrame(pack('n', $code), true, Frame::OP_CLOSE);
}
}

View File

@ -1,7 +1,7 @@
<?php
namespace Ratchet\WebSocket\Version;
namespace Ratchet\RFC6455\Messaging;
interface MessageInterface extends DataInterface {
interface MessageInterface extends DataInterface, \Traversable, \Countable {
/**
* @param FrameInterface $fragment
* @return MessageInterface
@ -12,4 +12,9 @@ interface MessageInterface extends DataInterface {
* @return int
*/
function getOpcode();
/**
* @return bool
*/
function isBinary();
}

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,44 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version\RFC6455;
use Ratchet\AbstractConnectionDecorator;
use Ratchet\WebSocket\Version\DataInterface;
/**
* {@inheritdoc}
* @property \StdClass $WebSocket
*/
class Connection extends AbstractConnectionDecorator {
/**
* {@inheritdoc}
*/
public function send($msg) {
if (!$this->WebSocket->closing) {
if (!($msg instanceof DataInterface)) {
$msg = new Frame($msg);
}
$this->getConnection()->send($msg->getContents());
}
return $this;
}
/**
* {@inheritdoc}
*/
public function close($code = 1000) {
if ($this->WebSocket->closing) {
return;
}
if ($code instanceof DataInterface) {
$this->send($code);
} else {
$this->send(new Frame(pack('n', $code), true, Frame::OP_CLOSE));
}
$this->getConnection()->close();
$this->WebSocket->closing = true;
}
}

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

30
tests/AbResultsTest.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace Ratchet\RFC6455\Test;
class AbResultsTest extends \PHPUnit_Framework_TestCase {
private function verifyAutobahnResults($fileName) {
if (!file_exists($fileName)) {
return $this->markTestSkipped('Autobahn TestSuite results not found');
}
$resultsJson = file_get_contents($fileName);
$results = json_decode($resultsJson);
$agentName = array_keys(get_object_vars($results))[0];
foreach ($results->$agentName as $name => $result) {
if ($result->behavior === "INFORMATIONAL") {
continue;
}
$this->assertTrue(in_array($result->behavior, ["OK", "NON-STRICT"]), "Autobahn test case " . $name . " in " . $fileName);
}
}
public function testAutobahnClientResults() {
$this->verifyAutobahnResults(__DIR__ . '/ab/reports/clients/index.json');
}
public function testAutobahnServerResults() {
$this->verifyAutobahnResults(__DIR__ . '/ab/reports/servers/index.json');
}
}

228
tests/ab/clientRunner.php Normal file
View File

@ -0,0 +1,228 @@
<?php
use GuzzleHttp\Psr7\Uri;
use React\Promise\Deferred;
use Ratchet\RFC6455\Messaging\Frame;
require __DIR__ . '/../bootstrap.php';
define('AGENT', 'RatchetRFC/0.0.0');
$testServer = "127.0.0.1";
$loop = React\EventLoop\Factory::create();
$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dnsResolver = $dnsResolverFactory->createCached('8.8.8.8', $loop);
$factory = new \React\SocketClient\Connector($loop, $dnsResolver);
function echoStreamerFactory($conn)
{
return new \Ratchet\RFC6455\Messaging\MessageBuffer(
new \Ratchet\RFC6455\Messaging\CloseFrameChecker,
function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($conn) {
/** @var Frame $frame */
foreach ($msg as $frame) {
$frame->maskPayload();
}
$conn->write($msg->getContents());
},
function (\Ratchet\RFC6455\Messaging\FrameInterface $frame) use ($conn) {
switch ($frame->getOpcode()) {
case Frame::OP_PING:
return $conn->write((new Frame($frame->getPayload(), true, Frame::OP_PONG))->maskPayload()->getContents());
break;
case Frame::OP_CLOSE:
return $conn->end((new Frame($frame->getPayload(), true, Frame::OP_CLOSE))->maskPayload()->getContents());
break;
}
},
false
);
}
function getTestCases() {
global $factory;
global $testServer;
$deferred = new Deferred();
$factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) {
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator();
$cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001/getCaseCount'));
$rawResponse = "";
$response = null;
/** @var \Ratchet\RFC6455\Messaging\Streaming\MessageBuffer $ms */
$ms = null;
$stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
if ($response === null) {
$rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n");
if ($pos) {
$data = substr($rawResponse, $pos + 4);
$rawResponse = substr($rawResponse, 0, $pos + 4);
$response = \GuzzleHttp\Psr7\parse_response($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) {
$stream->end();
$deferred->reject();
} else {
$ms = new \Ratchet\RFC6455\Messaging\MessageBuffer(
new \Ratchet\RFC6455\Messaging\CloseFrameChecker,
function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) {
$deferred->resolve($msg->getPayload());
$stream->close();
},
null,
false
);
}
}
}
// feed the message streamer
if ($ms) {
$ms->onData($data);
}
});
$stream->write(\GuzzleHttp\Psr7\str($cnRequest));
});
return $deferred->promise();
}
function runTest($case)
{
global $factory;
global $testServer;
$casePath = "/runCase?case={$case}&agent=" . AGENT;
$deferred = new Deferred();
$factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath, $case) {
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator();
$cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $casePath));
$rawResponse = "";
$response = null;
$ms = null;
$stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
if ($response === null) {
$rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n");
if ($pos) {
$data = substr($rawResponse, $pos + 4);
$rawResponse = substr($rawResponse, 0, $pos + 4);
$response = \GuzzleHttp\Psr7\parse_response($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) {
$stream->end();
$deferred->reject();
} else {
$ms = echoStreamerFactory($stream);
}
}
}
// feed the message streamer
if ($ms) {
$ms->onData($data);
}
});
$stream->on('close', function () use ($deferred) {
$deferred->resolve();
});
$stream->write(\GuzzleHttp\Psr7\str($cnRequest));
});
return $deferred->promise();
}
function createReport() {
global $factory;
global $testServer;
$deferred = new Deferred();
$factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) {
$reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true";
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator();
$cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $reportPath));
$rawResponse = "";
$response = null;
/** @var \Ratchet\RFC6455\Messaging\MessageBuffer $ms */
$ms = null;
$stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
if ($response === null) {
$rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n");
if ($pos) {
$data = substr($rawResponse, $pos + 4);
$rawResponse = substr($rawResponse, 0, $pos + 4);
$response = \GuzzleHttp\Psr7\parse_response($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) {
$stream->end();
$deferred->reject();
} else {
$ms = new \Ratchet\RFC6455\Messaging\MessageBuffer(
new \Ratchet\RFC6455\Messaging\CloseFrameChecker,
function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) {
$deferred->resolve($msg->getPayload());
$stream->close();
},
null,
false
);
}
}
}
// feed the message streamer
if ($ms) {
$ms->onData($data);
}
});
$stream->write(\GuzzleHttp\Psr7\str($cnRequest));
});
return $deferred->promise();
}
$testPromises = [];
getTestCases()->then(function ($count) use ($loop) {
$allDeferred = new Deferred();
$runNextCase = function () use (&$i, &$runNextCase, $count, $allDeferred) {
$i++;
if ($i > $count) {
$allDeferred->resolve();
return;
}
runTest($i)->then($runNextCase);
};
$i = 0;
$runNextCase();
$allDeferred->promise()->then(function () {
createReport();
});
});
$loop->run();

View File

@ -0,0 +1,14 @@
{
"options": {
"failByDrop": false
}
, "outdir": "./reports/servers"
, "servers": [{
"agent": "RatchetRFC/0.1.0"
, "url": "ws://localhost:9001"
, "options": {"version": 18}
}]
, "cases": ["*"]
, "exclude-cases": ["6.4.*", "12.*","13.*"]
, "exclude-agent-cases": {}
}

View File

@ -0,0 +1,10 @@
{
"url": "ws://127.0.0.1:9001"
, "options": {
"failByDrop": false
}
, "outdir": "./reports/clients"
, "cases": ["*"]
, "exclude-cases": ["6.4.*", "12.*", "13.*"]
, "exclude-agent-cases": {}
}

11
tests/ab/run_ab_tests.sh Normal file
View File

@ -0,0 +1,11 @@
cd tests/ab
wstest -m fuzzingserver -s fuzzingserver.json &
sleep 5
php clientRunner.php
sleep 2
php startServer.php &
sleep 3
wstest -m fuzzingclient -s fuzzingclient.json

55
tests/ab/startServer.php Normal file
View File

@ -0,0 +1,55 @@
<?php
use Ratchet\RFC6455\Messaging\MessageInterface;
use Ratchet\RFC6455\Messaging\FrameInterface;
use Ratchet\RFC6455\Messaging\Frame;
require_once __DIR__ . "/../bootstrap.php";
$loop = \React\EventLoop\Factory::create();
$socket = new \React\Socket\Server($loop);
$server = new \React\Http\Server($socket);
$closeFrameChecker = new \Ratchet\RFC6455\Messaging\CloseFrameChecker;
$negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator(new \Ratchet\RFC6455\Handshake\RequestVerifier);
$uException = new \UnderflowException;
$server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $closeFrameChecker, $uException) {
$psrRequest = new \GuzzleHttp\Psr7\Request($request->getMethod(), $request->getPath(), $request->getHeaders());
$negotiatorResponse = $negotiator->handshake($psrRequest);
$response->writeHead(
$negotiatorResponse->getStatusCode(),
array_merge(
$negotiatorResponse->getHeaders(),
["Content-Length" => "0"]
)
);
if ($negotiatorResponse->getStatusCode() !== 101) {
$response->end();
return;
}
$parser = new \Ratchet\RFC6455\Messaging\MessageBuffer($closeFrameChecker, function(MessageInterface $message) use ($response) {
$response->write($message->getContents());
}, function(FrameInterface $frame) use ($response, &$parser) {
switch ($frame->getOpCode()) {
case Frame::OP_CLOSE:
$response->end($frame->getContents());
break;
case Frame::OP_PING:
$response->write($parser->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents());
break;
}
}, true, function() use ($uException) {
return $uException;
});
$request->on('data', [$parser, 'onData']);
});
$socket->listen(9001, '0.0.0.0');
$loop->run();

19
tests/bootstrap.php Normal file
View File

@ -0,0 +1,19 @@
<?php
/**
* Find the auto loader file
*/
$files = [
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../vendor/autoload.php',
__DIR__ . '/../../../vendor/autoload.php',
__DIR__ . '/../../../../vendor/autoload.php',
];
foreach ($files as $file) {
if (file_exists($file)) {
$loader = require $file;
$loader->addPsr4('Ratchet\\RFC6455\\Test\\', __DIR__);
break;
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Handshake;
use Ratchet\RFC6455\Handshake\RequestVerifier;
/**
* @covers Ratchet\RFC6455\Handshake\RequestVerifier
*/
class RequestVerifierTest extends \PHPUnit_Framework_TestCase {
/**
* @var RequestVerifier
*/
protected $_v;
public function setUp() {
$this->_v = new RequestVerifier();
}
public static function methodProvider() {
return array(
array(true, 'GET'),
array(true, 'get'),
array(true, 'Get'),
array(false, 'POST'),
array(false, 'DELETE'),
array(false, 'PUT'),
array(false, 'PATCH')
);
}
/**
* @dataProvider methodProvider
*/
public function testMethodMustBeGet($result, $in) {
$this->assertEquals($result, $this->_v->verifyMethod($in));
}
public static function httpVersionProvider() {
return array(
array(true, 1.1),
array(true, '1.1'),
array(true, 1.2),
array(true, '1.2'),
array(true, 2),
array(true, '2'),
array(true, '2.0'),
array(false, '1.0'),
array(false, 1),
array(false, '0.9'),
array(false, ''),
array(false, 'hello')
);
}
/**
* @dataProvider httpVersionProvider
*/
public function testHttpVersionIsAtLeast1Point1($expected, $in) {
$this->assertEquals($expected, $this->_v->verifyHTTPVersion($in));
}
public static function uRIProvider() {
return array(
array(true, '/chat'),
array(true, '/hello/world?key=val'),
array(false, '/chat#bad'),
array(false, 'nope'),
array(false, '/ ಠ_ಠ '),
array(false, '/✖')
);
}
/**
* @dataProvider URIProvider
*/
public function testRequestUri($expected, $in) {
$this->assertEquals($expected, $this->_v->verifyRequestURI($in));
}
public static function hostProvider() {
return array(
array(true, ['server.example.com']),
array(false, [])
);
}
/**
* @dataProvider HostProvider
*/
public function testVerifyHostIsSet($expected, $in) {
$this->assertEquals($expected, $this->_v->verifyHost($in));
}
public static function upgradeProvider() {
return array(
array(true, ['websocket']),
array(true, ['Websocket']),
array(true, ['webSocket']),
array(false, []),
array(false, [''])
);
}
/**
* @dataProvider upgradeProvider
*/
public function testVerifyUpgradeIsWebSocket($expected, $val) {
$this->assertEquals($expected, $this->_v->verifyUpgradeRequest($val));
}
public static function connectionProvider() {
return array(
array(true, ['Upgrade']),
array(true, ['upgrade']),
array(true, ['keep-alive', 'Upgrade']),
array(true, ['Upgrade', 'keep-alive']),
array(true, ['keep-alive', 'Upgrade', 'something']),
array(false, ['']),
array(false, [])
);
}
/**
* @dataProvider connectionProvider
*/
public function testConnectionHeaderVerification($expected, $val) {
$this->assertEquals($expected, $this->_v->verifyConnection($val));
}
public static function keyProvider() {
return array(
array(true, ['hkfa1L7uwN6DCo4IS3iWAw==']),
array(true, ['765vVoQpKSGJwPzJIMM2GA==']),
array(true, ['AQIDBAUGBwgJCgsMDQ4PEC==']),
array(true, ['axa2B/Yz2CdpfQAY2Q5P7w==']),
array(false, [0]),
array(false, ['Hello World']),
array(false, ['1234567890123456']),
array(false, ['123456789012345678901234']),
array(true, [base64_encode('UTF8allthngs+✓')]),
array(true, ['dGhlIHNhbXBsZSBub25jZQ==']),
array(false, []),
array(false, ['dGhlIHNhbXBsZSBub25jZQ==', 'Some other value']),
array(false, ['Some other value', 'dGhlIHNhbXBsZSBub25jZQ=='])
);
}
/**
* @dataProvider keyProvider
*/
public function testKeyIsBase64Encoded16BitNonce($expected, $val) {
$this->assertEquals($expected, $this->_v->verifyKey($val));
}
public static function versionProvider() {
return array(
array(true, [13]),
array(true, ['13']),
array(false, [12]),
array(false, [14]),
array(false, ['14']),
array(false, ['hi']),
array(false, ['']),
array(false, [])
);
}
/**
* @dataProvider versionProvider
*/
public function testVersionEquals13($expected, $in) {
$this->assertEquals($expected, $this->_v->verifyVersion($in));
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Handshake;
use Ratchet\RFC6455\Handshake\ResponseVerifier;
/**
* @covers Ratchet\RFC6455\Handshake\ResponseVerifier
*/
class ResponseVerifierTest extends \PHPUnit_Framework_TestCase {
/**
* @var ResponseVerifier
*/
protected $_v;
public function setUp() {
$this->_v = new ResponseVerifier;
}
public static function subProtocolsProvider() {
return [
[true, ['a'], ['a']]
, [true, ['b', 'a'], ['c', 'd', 'a']]
, [false, ['a', 'b', 'c'], ['d']]
, [true, [], []]
, [true, ['a', 'b'], []]
];
}
/**
* @dataProvider subProtocolsProvider
*/
public function testVerifySubProtocol($expected, $response, $request) {
$this->assertEquals($expected, $this->_v->verifySubProtocol($response, $request));
}
}

View File

@ -0,0 +1,501 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Messaging;
use Ratchet\RFC6455\Messaging\Frame;
/**
* @covers Ratchet\RFC6455\Messaging\Frame
* @todo getMaskingKey, getPayloadStartingByte don't have tests yet
* @todo Could use some clean up in general, I had to rush to fix a bug for a deadline, sorry.
*/
class FrameTest extends \PHPUnit_Framework_TestCase {
protected $_firstByteFinText = '10000001';
protected $_secondByteMaskedSPL = '11111101';
/** @var Frame */
protected $_frame;
protected $_packer;
public function setUp() {
$this->_frame = new Frame;
}
/**
* Encode the fake binary string to send over the wire
* @param string of 1's and 0's
* @return string
*/
public static function encode($in) {
if (strlen($in) > 8) {
$out = '';
while (strlen($in) >= 8) {
$out .= static::encode(substr($in, 0, 8));
$in = substr($in, 8);
}
return $out;
}
return chr(bindec($in));
}
/**
* This is a data provider
* param string The UTF8 message
* param string The WebSocket framed message, then base64_encoded
*/
public static function UnframeMessageProvider() {
return array(
array('Hello World!', 'gYydAIfa1WXrtvIg0LXvbOP7'),
array('!@#$%^&*()-=_+[]{}\|/.,<>`~', 'gZv+h96r38f9j9vZ+IHWrvOWoayF9oX6gtfRqfKXwOeg'),
array('ಠ_ಠ', 'gYfnSpu5B/g75gf4Ow=='),
array(
"The quick brown fox jumps over the lazy dog. All work and no play makes Chris a dull boy. I'm trying to get past 128 characters for a unit test here...",
'gf4Amahb14P8M7Kj2S6+4MN7tfHHLLmjzjSvo8IuuvPbe7j1zSn398A+9+/JIa6jzDSwrYh7lu/Ee6Ds2jD34sY/9+3He6fvySL37skwsvCIGL/xwSj34og/ou/Ee7Xs0XX3o+F8uqPcKa7qxjz398d7sObce6fi2y/3sppj9+DAOqXiyy+y8dt7sezae7aj3TW+94gvsvDce7/m2j75rYY='
)
);
}
public static function underflowProvider() {
return array(
array('isFinal', ''),
array('getRsv1', ''),
array('getRsv2', ''),
array('getRsv3', ''),
array('getOpcode', ''),
array('isMasked', '10000001'),
array('getPayloadLength', '10000001'),
array('getPayloadLength', '1000000111111110'),
array('getMaskingKey', '1000000110000111'),
array('getPayload', '100000011000000100011100101010101001100111110100')
);
}
/**
* @dataProvider underflowProvider
*
* @covers Ratchet\RFC6455\Messaging\Frame::isFinal
* @covers Ratchet\RFC6455\Messaging\Frame::getRsv1
* @covers Ratchet\RFC6455\Messaging\Frame::getRsv2
* @covers Ratchet\RFC6455\Messaging\Frame::getRsv3
* @covers Ratchet\RFC6455\Messaging\Frame::getOpcode
* @covers Ratchet\RFC6455\Messaging\Frame::isMasked
* @covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength
* @covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey
* @covers Ratchet\RFC6455\Messaging\Frame::getPayload
*/
public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering($method, $bin) {
$this->setExpectedException('\UnderflowException');
if (!empty($bin)) {
$this->_frame->addBuffer(static::encode($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
* param int Given, what is the expected opcode
* param string of 0|1 Each character represents a bit in the byte
*/
public static function firstByteProvider() {
return array(
array(false, false, false, true, 8, '00011000'),
array(true, false, true, false, 10, '10101010'),
array(false, false, false, false, 15, '00001111'),
array(true, false, false, false, 1, '10000001'),
array(true, true, true, true, 15, '11111111'),
array(true, true, false, false, 7, '11000111')
);
}
/**
* @dataProvider firstByteProvider
* covers Ratchet\RFC6455\Messaging\Frame::isFinal
*/
public function testFinCodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) {
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($fin, $this->_frame->isFinal());
}
/**
* @dataProvider firstByteProvider
* covers Ratchet\RFC6455\Messaging\Frame::getRsv1
* covers Ratchet\RFC6455\Messaging\Frame::getRsv2
* covers Ratchet\RFC6455\Messaging\Frame::getRsv3
*/
public function testGetRsvFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) {
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($rsv1, $this->_frame->getRsv1());
$this->assertEquals($rsv2, $this->_frame->getRsv2());
$this->assertEquals($rsv3, $this->_frame->getRsv3());
}
/**
* @dataProvider firstByteProvider
* covers Ratchet\RFC6455\Messaging\Frame::getOpcode
*/
public function testOpcodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) {
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($opcode, $this->_frame->getOpcode());
}
/**
* @dataProvider UnframeMessageProvider
* covers Ratchet\RFC6455\Messaging\Frame::isFinal
*/
public function testFinCodeFromFullMessage($msg, $encoded) {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertTrue($this->_frame->isFinal());
}
/**
* @dataProvider UnframeMessageProvider
* covers Ratchet\RFC6455\Messaging\Frame::getOpcode
*/
public function testOpcodeFromFullMessage($msg, $encoded) {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertEquals(1, $this->_frame->getOpcode());
}
public static function payloadLengthDescriptionProvider() {
return array(
array(7, '01110101'),
array(7, '01111101'),
array(23, '01111110'),
array(71, '01111111'),
array(7, '00000000'), // Should this throw an exception? Can a payload be empty?
array(7, '00000001')
);
}
/**
* @dataProvider payloadLengthDescriptionProvider
* covers Ratchet\RFC6455\Messaging\Frame::addBuffer
* covers Ratchet\RFC6455\Messaging\Frame::getFirstPayloadVal
*/
public function testFirstPayloadDesignationValue($bits, $bin) {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getFirstPayloadVal');
$cb->setAccessible(true);
$this->assertEquals(bindec($bin), $cb->invoke($this->_frame));
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::getFirstPayloadVal
*/
public function testFirstPayloadValUnderflow() {
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getFirstPayloadVal');
$cb->setAccessible(true);
$this->setExpectedException('UnderflowException');
$cb->invoke($this->_frame);
}
/**
* @dataProvider payloadLengthDescriptionProvider
* covers Ratchet\RFC6455\Messaging\Frame::getNumPayloadBits
*/
public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getNumPayloadBits');
$cb->setAccessible(true);
$this->assertEquals($expected_bits, $cb->invoke($this->_frame));
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::getNumPayloadBits
*/
public function testgetNumPayloadBitsUnderflow() {
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getNumPayloadBits');
$cb->setAccessible(true);
$this->setExpectedException('UnderflowException');
$cb->invoke($this->_frame);
}
public function secondByteProvider() {
return array(
array(true, 1, '10000001'),
array(false, 1, '00000001'),
array(true, 125, $this->_secondByteMaskedSPL)
);
}
/**
* @dataProvider secondByteProvider
* covers Ratchet\RFC6455\Messaging\Frame::isMasked
*/
public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($masked, $this->_frame->isMasked());
}
/**
* @dataProvider UnframeMessageProvider
* covers Ratchet\RFC6455\Messaging\Frame::isMasked
*/
public function testIsMaskedFromFullMessage($msg, $encoded) {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertTrue($this->_frame->isMasked());
}
/**
* @dataProvider secondByteProvider
* covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength
*/
public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($payload_length, $this->_frame->getPayloadLength());
}
/**
* @dataProvider UnframeMessageProvider
* covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength
* @todo Not yet testing when second additional payload length descriptor
*/
public function testGetPayloadLengthFromFullMessage($msg, $encoded) {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertEquals(strlen($msg), $this->_frame->getPayloadLength());
}
public function maskingKeyProvider() {
$frame = new Frame;
return array(
array($frame->generateMaskingKey()),
array($frame->generateMaskingKey()),
array($frame->generateMaskingKey())
);
}
/**
* @dataProvider maskingKeyProvider
* covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey
* @todo I I wrote the dataProvider incorrectly, skipping for now
*/
public function testGetMaskingKey($mask) {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($this->_secondByteMaskedSPL));
$this->_frame->addBuffer($mask);
$this->assertEquals($mask, $this->_frame->getMaskingKey());
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::getMaskingKey
*/
public function testGetMaskingKeyOnUnmaskedPayload() {
$frame = new Frame('Hello World!');
$this->assertEquals('', $frame->getMaskingKey());
}
/**
* @dataProvider UnframeMessageProvider
* covers Ratchet\RFC6455\Messaging\Frame::getPayload
* @todo Move this test to bottom as it requires all methods of the class
*/
public function testUnframeFullMessage($unframed, $base_framed) {
$this->_frame->addBuffer(base64_decode($base_framed));
$this->assertEquals($unframed, $this->_frame->getPayload());
}
public static function messageFragmentProvider() {
return array(
array(false, '', '', '', '', '')
);
}
/**
* @dataProvider UnframeMessageProvider
* covers Ratchet\RFC6455\Messaging\Frame::getPayload
*/
public function testCheckPiecingTogetherMessage($msg, $encoded) {
$framed = base64_decode($encoded);
for ($i = 0, $len = strlen($framed);$i < $len; $i++) {
$this->_frame->addBuffer(substr($framed, $i, 1));
}
$this->assertEquals($msg, $this->_frame->getPayload());
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::__construct
* covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength
* covers Ratchet\RFC6455\Messaging\Frame::getPayload
*/
public function testLongCreate() {
$len = 65525;
$pl = $this->generateRandomString($len);
$frame = new Frame($pl, true, Frame::OP_PING);
$this->assertTrue($frame->isFinal());
$this->assertEquals(Frame::OP_PING, $frame->getOpcode());
$this->assertFalse($frame->isMasked());
$this->assertEquals($len, $frame->getPayloadLength());
$this->assertEquals($pl, $frame->getPayload());
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::__construct
* covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength
*/
public function testReallyLongCreate() {
$len = 65575;
$frame = new Frame($this->generateRandomString($len));
$this->assertEquals($len, $frame->getPayloadLength());
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::__construct
* covers Ratchet\RFC6455\Messaging\Frame::extractOverflow
*/
public function testExtractOverflow() {
$string1 = $this->generateRandomString();
$frame1 = new Frame($string1);
$string2 = $this->generateRandomString();
$frame2 = new Frame($string2);
$cat = new Frame;
$cat->addBuffer($frame1->getContents() . $frame2->getContents());
$this->assertEquals($frame1->getContents(), $cat->getContents());
$this->assertEquals($string1, $cat->getPayload());
$uncat = new Frame;
$uncat->addBuffer($cat->extractOverflow());
$this->assertEquals($string1, $cat->getPayload());
$this->assertEquals($string2, $uncat->getPayload());
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::extractOverflow
*/
public function testEmptyExtractOverflow() {
$string = $this->generateRandomString();
$frame = new Frame($string);
$this->assertEquals($string, $frame->getPayload());
$this->assertEquals('', $frame->extractOverflow());
$this->assertEquals($string, $frame->getPayload());
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::getContents
*/
public function testGetContents() {
$msg = 'The quick brown fox jumps over the lazy dog.';
$frame1 = new Frame($msg);
$frame2 = new Frame($msg);
$frame2->maskPayload();
$this->assertNotEquals($frame1->getContents(), $frame2->getContents());
$this->assertEquals(strlen($frame1->getContents()) + 4, strlen($frame2->getContents()));
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::maskPayload
*/
public function testMasking() {
$msg = 'The quick brown fox jumps over the lazy dog.';
$frame = new Frame($msg);
$frame->maskPayload();
$this->assertTrue($frame->isMasked());
$this->assertEquals($msg, $frame->getPayload());
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::unMaskPayload
*/
public function testUnMaskPayload() {
$string = $this->generateRandomString();
$frame = new Frame($string);
$frame->maskPayload()->unMaskPayload();
$this->assertFalse($frame->isMasked());
$this->assertEquals($string, $frame->getPayload());
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::generateMaskingKey
*/
public function testGenerateMaskingKey() {
$dupe = false;
$done = array();
for ($i = 0; $i < 10; $i++) {
$new = $this->_frame->generateMaskingKey();
if (in_array($new, $done)) {
$dupe = true;
}
$done[] = $new;
}
$this->assertEquals(4, strlen($new));
$this->assertFalse($dupe);
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::maskPayload
*/
public function testGivenMaskIsValid() {
$this->setExpectedException('InvalidArgumentException');
$this->_frame->maskPayload('hello world');
}
/**
* covers Ratchet\RFC6455\Messaging\Frame::maskPayload
*/
public function testGivenMaskIsValidAscii() {
if (!extension_loaded('mbstring')) {
$this->markTestSkipped("mbstring required for this test");
return;
}
$this->setExpectedException('OutOfBoundsException');
$this->_frame->maskPayload('x✖');
}
protected function generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) {
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&/()=[]{}'; // ยง
$useChars = array();
for($i = 0; $i < $length; $i++) {
$useChars[] = $characters[mt_rand(0, strlen($characters) - 1)];
}
if($addSpaces === true) {
array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' ');
}
if($addNumbers === true) {
array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9));
}
shuffle($useChars);
$randomString = trim(implode('', $useChars));
$randomString = substr($randomString, 0, $length);
return $randomString;
}
/**
* There was a frame boundary issue when the first 3 bytes of a frame with a payload greater than
* 126 was added to the frame buffer and then Frame::getPayloadLength was called. It would cause the frame
* to set the payload length to 126 and then not recalculate it once the full length information was available.
*
* This is fixed by setting the defPayLen back to -1 before the underflow exception is thrown.
*
* covers Ratchet\RFC6455\Messaging\Frame::getPayloadLength
* covers Ratchet\RFC6455\Messaging\Frame::extractOverflow
*/
public function testFrameDeliveredOneByteAtATime() {
$startHeader = "\x01\x7e\x01\x00"; // header for a text frame of 256 - non-final
$framePayload = str_repeat("*", 256);
$rawOverflow = "xyz";
$rawFrame = $startHeader . $framePayload . $rawOverflow;
$frame = new Frame();
$payloadLen = 256;
for ($i = 0; $i < strlen($rawFrame); $i++) {
$frame->addBuffer($rawFrame[$i]);
try {
// payloadLen will
$payloadLen = $frame->getPayloadLength();
} catch (\UnderflowException $e) {
if ($i > 2) { // we should get an underflow on 0,1,2
$this->fail("Underflow exception when the frame length should be available");
}
}
if ($payloadLen !== 256) {
$this->fail("Payload length of " . $payloadLen . " should have been 256.");
}
}
// make sure the overflow is good
$this->assertEquals($rawOverflow, $frame->extractOverflow());
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Messaging;
use Ratchet\RFC6455\Messaging\Frame;
use Ratchet\RFC6455\Messaging\Message;
/**
* @covers Ratchet\RFC6455\Messaging\Message
*/
class MessageTest extends \PHPUnit_Framework_TestCase {
/** @var Message */
protected $message;
public function setUp() {
$this->message = new Message;
}
public function testNoFrames() {
$this->assertFalse($this->message->isCoalesced());
}
public function testNoFramesOpCode() {
$this->setExpectedException('UnderflowException');
$this->message->getOpCode();
}
public function testFragmentationPayload() {
$a = 'Hello ';
$b = 'World!';
$f1 = new Frame($a, false);
$f2 = new Frame($b, true, Frame::OP_CONTINUE);
$this->message->addFrame($f1)->addFrame($f2);
$this->assertEquals(strlen($a . $b), $this->message->getPayloadLength());
$this->assertEquals($a . $b, $this->message->getPayload());
}
public function testUnbufferedFragment() {
$this->message->addFrame(new Frame('The quick brow', false));
$this->setExpectedException('UnderflowException');
$this->message->getPayload();
}
public function testGetOpCode() {
$this->message
->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT))
->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE))
->addFrame(new Frame('er the lazy dog', true, Frame::OP_CONTINUE))
;
$this->assertEquals(Frame::OP_TEXT, $this->message->getOpCode());
}
public function testGetUnBufferedPayloadLength() {
$this->message
->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT))
->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE))
;
$this->assertEquals(28, $this->message->getPayloadLength());
}
}