diff --git a/src/Handshake/ClientNegotiator.php b/src/Handshake/ClientNegotiator.php new file mode 100644 index 0000000..b4a4e16 --- /dev/null +++ b/src/Handshake/ClientNegotiator.php @@ -0,0 +1,84 @@ + 'Upgrade' + , 'Cache-Control' => 'no-cache' + , 'Pragma' => 'no-cache' + , 'Upgrade' => 'websocket' + , 'Sec-WebSocket-Version' => 13 + , 'User-Agent' => "RatchetRFC/0.0.0" + ]; + + /** @var Request */ + public $request; + + /** @var Response */ + public $response; + + /** @var ResponseVerifier */ + public $verifier; + + private $websocketKey = ''; + + function __construct($path = null) + { + if (!is_string($path)) $path = "/"; + $request = new Request("GET", $path); + + $request = $request->withUri(new Uri("ws://127.0.0.1:9001" . $path)); + + $this->request = $request; + + $this->verifier = new ResponseVerifier(); + + $this->websocketKey = $this->generateKey(); + } + + public function addRequiredHeaders() { + foreach ($this->defaultHeaders as $k => $v) { + // remove any header that is there now + $this->request = $this->request->withoutHeader($k); + $this->request = $this->request->withHeader($k, $v); + } + $this->request = $this->request->withoutHeader("Sec-WebSocket-Key"); + $this->request = $this->request->withHeader("Sec-WebSocket-Key", $this->websocketKey); + $this->request = $this->request->withoutHeader("Host") + ->withHeader("Host", $this->request->getUri()->getHost() . ":" . $this->request->getUri()->getPort()); + } + + public function getRequest() { + $this->addRequiredHeaders(); + return $this->request; + } + + public function getResponse() { + return $this->response; + } + + public function validateResponse(Response $response) { + $this->response = $response; + + return $this->verifier->verifyAll($this->getRequest(), $response); + } + + protected 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); + } + +} \ No newline at end of file diff --git a/src/Handshake/ClientNegotiatorInterface.php b/src/Handshake/ClientNegotiatorInterface.php new file mode 100644 index 0000000..c95c1ac --- /dev/null +++ b/src/Handshake/ClientNegotiatorInterface.php @@ -0,0 +1,11 @@ +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') + ); + + return (4 == $passes); + } + + public function verifyStatus($status) { + return ($status == 101); + } + + public function verifyUpgrade($upgrade) { + return (strtolower($upgrade) == "websocket"); + } + + public function verifyConnection($connection) { + return (strtolower($connection) == "upgrade"); + } + + public function verifySecWebSocketAccept($swa, $key) { + return ($swa == $this->sign($key)); + } + + public function sign($key) { + return base64_encode(sha1($key . Negotiator::GUID, true)); + } +} \ No newline at end of file diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php new file mode 100644 index 0000000..ade81d0 --- /dev/null +++ b/tests/ab/clientRunner.php @@ -0,0 +1,227 @@ +createCached('8.8.8.8', $loop); + +$factory = new \React\SocketClient\Connector($loop, $dnsResolver); + +function getTestCases() { + global $factory; + + $deferred = new Deferred(); + + $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { + $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator("/getCaseCount"); + $cnRequest = $cn->getRequest(); + + $rawResponse = ""; + $response = null; + + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + + $ms->on('message', function (Message $msg) use ($stream, $deferred) { + $deferred->resolve($msg->getPayload()); + + $closeFrame = new Frame(pack('n', Frame::CLOSE_NORMAL), true, Frame::OP_CLOSE); + $closeFrame->maskPayload(); + $stream->end($closeFrame->getContents()); + }); + + $ms->on('close', function ($code) use ($stream) { + if ($code === null) { + $stream->close(); + return; + } + $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); + $frame->maskPayload(); + $stream->end($frame->getContents()); + }); + + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred) { + 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($response)) { + $stream->end(); + $deferred->reject(); + } + } + } + + // feed the message streamer + if ($response) { + $ms->onData($data); + } + }); + + $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); + }); + + return $deferred->promise(); +} + +function runTest($case) +{ + global $factory; + + $casePath = "/runCase?case={$case}&agent=" . AGENT; + + $deferred = new Deferred(); + + $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath) { + $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator($casePath); + $cnRequest = $cn->getRequest(); + + $rawResponse = ""; + $response = null; + + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + + $ms->on('message', function (Message $msg) use ($stream, $deferred) { + $opcode = $msg->isBinary() ? Frame::OP_BINARY : Frame::OP_TEXT; + $frame = new Frame($msg->getPayload(), true, $opcode); + $frame->maskPayload(); + + $stream->write($frame->getContents()); + }); + + $ms->on('ping', function (Frame $frame) use ($stream) { + $response = new Frame($frame->getPayload(), true, Frame::OP_PONG); + $response->maskPayload(); + $stream->write($response->getContents()); + }); + + $ms->on('close', function ($code) use ($stream, $deferred) { + if ($code === null) { + $stream->close(); + return; + } + $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); + $frame->maskPayload(); + $stream->end($frame->getContents()); + }); + + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred) { + 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($response)) { + $stream->end(); + $deferred->reject(); + } + } + } + + // feed the message streamer + if ($response) { + $ms->onData($data); + } + }); + + $stream->on('close', function () use ($deferred) { + $deferred->resolve(); + }); + + $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); + }); + + return $deferred->promise(); +} + + + +function createReport() { + global $factory; + + $deferred = new Deferred(); + + $factory->create('127.0.0.1', 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { + $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator('/updateReports?agent=' . AGENT); + $cnRequest = $cn->getRequest(); + + $rawResponse = ""; + $response = null; + + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(); + + $ms->on('message', function (Message $msg) use ($stream, $deferred) { + $deferred->resolve($msg->getPayload()); + + $closeFrame = new Frame(pack('n', Frame::CLOSE_NORMAL), true, Frame::OP_CLOSE); + $closeFrame->maskPayload(); + $stream->end($closeFrame->getContents()); + }); + + $ms->on('close', function ($code) use ($stream) { + if ($code === null) { + $stream->close(); + return; + } + $frame = new Frame(pack('n', $code), true, Frame::OP_CLOSE); + $frame->maskPayload(); + $stream->end($frame->getContents()); + }); + + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred) { + 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($response)) { + $stream->end(); + $deferred->reject(); + } + } + } + + // feed the message streamer + if ($response) { + $ms->onData($data); + } + }); + + $stream->write(\GuzzleHttp\Psr7\str($cnRequest)); + }); + + return $deferred->promise(); +} + + +$testPromises = []; + +getTestCases()->then(function ($count) { + echo "Running " . $count . " test cases.\n"; + + for ($i = 0; $i < $count; $i++) { + $testPromises[] = runTest($i + 1); + } + + \React\Promise\all($testPromises)->then(function () { + createReport(); + }); +}); + +$loop->run(); diff --git a/tests/ab/fuzzingserver.json b/tests/ab/fuzzingserver.json new file mode 100644 index 0000000..4193e62 --- /dev/null +++ b/tests/ab/fuzzingserver.json @@ -0,0 +1,12 @@ +{ + "url": "ws://127.0.0.1:9001" + , "options": { + "failByDrop": false +} + , "outdir": "./reports/clients" + , "casesy": ["*"] + , "cases": ["1.*", "2.*", "3.*", "4.*", "5.*"] + , "casesx": ["1.1.8"] + , "exclude-cases": ["9.8.6"] + , "exclude-agent-cases": {} +} \ No newline at end of file