Merge branch 'master' into permessage-deflate

# Conflicts:
#	src/Messaging/MessageBuffer.php
#	tests/ab/clientRunner.php
#	tests/ab/run_ab_tests.sh
#	tests/ab/startServer.php
This commit is contained in:
Matt Bonneau 2020-01-22 00:36:05 -05:00
commit 1af7e998b3
8 changed files with 545 additions and 117 deletions

View File

@ -1,12 +1,12 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- 7.3
- 7.4
before_install:
- export PATH=$HOME/.local/bin:$PATH

View File

@ -25,8 +25,15 @@
"guzzlehttp/psr7": "^1.0"
},
"require-dev": {
"react/http": "^0.4.1",
"react/socket-client": "^0.4.3",
"phpunit/phpunit": "4.8.*"
"phpunit/phpunit": "4.8.*",
"react/socket": "^1.3"
},
"scripts": {
"abtests": "sh tests/ab/run_ab_tests.sh",
"phpunit": "phpunit --colors=always",
"test": [
"@abtests",
"@phpunit"
]
}
}

View File

@ -7,8 +7,14 @@ class Message implements \IteratorAggregate, MessageInterface {
*/
private $_frames;
/**
* @var int
*/
private $len;
public function __construct() {
$this->_frames = new \SplDoublyLinkedList;
$this->len = 0;
}
public function getIterator() {
@ -39,6 +45,7 @@ class Message implements \IteratorAggregate, MessageInterface {
* {@inheritdoc}
*/
public function addFrame(FrameInterface $fragment) {
$this->len += $fragment->getPayloadLength();
$this->_frames->push($fragment);
return $this;
@ -59,17 +66,7 @@ class Message implements \IteratorAggregate, MessageInterface {
* {@inheritdoc}
*/
public function getPayloadLength() {
$len = 0;
foreach ($this->_frames as $frame) {
try {
$len += $frame->getPayloadLength();
} catch (\UnderflowException $e) {
// Not an error, want the current amount buffered
}
}
return $len;
return $this->len;
}
/**

View File

@ -44,6 +44,11 @@ class MessageBuffer {
*/
private $sender;
/**
* @var string
*/
private $leftovers;
/**
* @var int
*/
@ -59,6 +64,20 @@ class MessageBuffer {
*/
private $deflate = false;
/**
* @var int
*/
private $maxMessagePayloadSize;
/**
* @var int
*/
private $maxFramePayloadSize;
/**
* @var bool
*/
private $compressedMessage;
function __construct(
CloseFrameChecker $frameChecker,
@ -66,6 +85,8 @@ class MessageBuffer {
callable $onControl = null,
$expectMask = true,
$exceptionFactory = null,
$maxMessagePayloadSize = null, // null for default - zero for no limit
$maxFramePayloadSize = null, // null for default - zero for no limit
callable $sender = null,
PermessageDeflateOptions $permessageDeflateOptions = null
) {
@ -90,12 +111,91 @@ class MessageBuffer {
}
$this->compressedMessage = false;
$this->leftovers = '';
$memory_limit_bytes = static::getMemoryLimit();
if ($maxMessagePayloadSize === null) {
$maxMessagePayloadSize = $memory_limit_bytes / 4;
}
if ($maxFramePayloadSize === null) {
$maxFramePayloadSize = $memory_limit_bytes / 4;
}
if (!is_int($maxFramePayloadSize) || $maxFramePayloadSize > 0x7FFFFFFFFFFFFFFF || $maxFramePayloadSize < 0) { // this should be interesting on non-64 bit systems
throw new \InvalidArgumentException($maxFramePayloadSize . ' is not a valid maxFramePayloadSize');
}
$this->maxFramePayloadSize = $maxFramePayloadSize;
if (!is_int($maxMessagePayloadSize) || $maxMessagePayloadSize > 0x7FFFFFFFFFFFFFFF || $maxMessagePayloadSize < 0) {
throw new \InvalidArgumentException($maxMessagePayloadSize . 'is not a valid maxMessagePayloadSize');
}
$this->maxMessagePayloadSize = $maxMessagePayloadSize;
}
public function onData($data) {
while (strlen($data) > 0) {
$data = $this->processData($data);
$data = $this->leftovers . $data;
$dataLen = strlen($data);
if ($dataLen < 2) {
$this->leftovers = $data;
return;
}
$frameStart = 0;
while ($frameStart + 2 <= $dataLen) {
$headerSize = 2;
$payload_length = unpack('C', $data[$frameStart + 1] & "\x7f")[1];
$isMasked = ($data[$frameStart + 1] & "\x80") === "\x80";
$headerSize += $isMasked ? 4 : 0;
if ($payload_length > 125 && ($dataLen - $frameStart < $headerSize + 125)) {
// no point of checking - this frame is going to be bigger than the buffer is right now
break;
}
if ($payload_length > 125) {
$payloadLenBytes = $payload_length === 126 ? 2 : 8;
$headerSize += $payloadLenBytes;
$bytesToUpack = substr($data, $frameStart + 2, $payloadLenBytes);
$payload_length = $payload_length === 126
? unpack('n', $bytesToUpack)[1]
: unpack('J', $bytesToUpack)[1];
}
$closeFrame = null;
if ($payload_length < 0) {
// this can happen when unpacking in php
$closeFrame = $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Invalid frame length');
}
if (!$closeFrame && $this->maxFramePayloadSize > 1 && $payload_length > $this->maxFramePayloadSize) {
$closeFrame = $this->newCloseFrame(Frame::CLOSE_TOO_BIG, 'Maximum frame size exceeded');
}
if (!$closeFrame && $this->maxMessagePayloadSize > 0
&& $payload_length + ($this->messageBuffer ? $this->messageBuffer->getPayloadLength() : 0) > $this->maxMessagePayloadSize) {
$closeFrame = $this->newCloseFrame(Frame::CLOSE_TOO_BIG, 'Maximum message size exceeded');
}
if ($closeFrame !== null) {
$onControl = $this->onControl;
$onControl($closeFrame);
$this->leftovers = '';
return;
}
$isCoalesced = $dataLen - $frameStart >= $payload_length + $headerSize;
if (!$isCoalesced) {
break;
}
$this->processData(substr($data, $frameStart, $payload_length + $headerSize));
$frameStart = $frameStart + $payload_length + $headerSize;
}
$this->leftovers = substr($data, $frameStart);
}
/**
@ -107,16 +207,12 @@ class MessageBuffer {
$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();
@ -159,8 +255,6 @@ class MessageBuffer {
$this->inflator = null;
}
}
return $overflow;
}
/**
@ -282,7 +376,6 @@ class MessageBuffer {
return $this->newFrame(pack('n', $code) . $reason, true, Frame::OP_CLOSE);
}
public function sendFrame(Frame $frame) {
if ($this->sender === null) {
throw new \Exception('To send frames using the MessageBuffer, sender must be set.');
@ -372,9 +465,11 @@ class MessageBuffer {
$frame->getOpcode()
);
}
private $deflator;
private function deflateFrame(Frame $frame) {
private function deflateFrame(Frame $frame)
{
if ($frame->getRsv1()) {
return $frame; // frame is already deflated
}
@ -413,11 +508,11 @@ class MessageBuffer {
// $payload .= deflate_add($this->deflator, $part, $flags);
// }
// } else {
$payload = deflate_add(
$this->deflator,
$frame->getPayload(),
ZLIB_SYNC_FLUSH
);
$payload = deflate_add(
$this->deflator,
$frame->getPayload(),
ZLIB_SYNC_FLUSH
);
// }
$deflatedFrame = new Frame(
@ -432,4 +527,24 @@ class MessageBuffer {
return $deflatedFrame;
}
/**
* This is a separate function for testing purposes
* $memory_limit is only used for testing
*
* @param null|string $memory_limit
* @return int
*/
private static function getMemoryLimit($memory_limit = null) {
$memory_limit = $memory_limit === null ? \trim(\ini_get('memory_limit')) : $memory_limit;
$memory_limit_bytes = 0;
if ($memory_limit !== '') {
$shifty = ['k' => 0, 'm' => 10, 'g' => 20];
$multiplier = strlen($memory_limit) > 1 ? substr(strtolower($memory_limit), -1) : '';
$memory_limit = (int)$memory_limit;
$memory_limit_bytes = in_array($multiplier, array_keys($shifty), true) ? $memory_limit * 1024 << $shifty[$multiplier] : $memory_limit;
}
return $memory_limit_bytes < 0 ? 0 : $memory_limit_bytes;
}
}

View File

@ -1,12 +1,15 @@
<?php
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\RequestInterface;
use Ratchet\RFC6455\Handshake\InvalidPermessageDeflateOptionsException;
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
use Ratchet\RFC6455\Handshake\ResponseVerifier;
use Ratchet\RFC6455\Messaging\MessageBuffer;
use Ratchet\RFC6455\Handshake\ClientNegotiator;
use Ratchet\RFC6455\Messaging\CloseFrameChecker;
use Ratchet\RFC6455\Messaging\MessageInterface;
use React\Promise\Deferred;
use Ratchet\RFC6455\Messaging\Frame;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
require __DIR__ . '/../bootstrap.php';
@ -16,10 +19,7 @@ $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);
$connector = new Connector($loop);
function echoStreamerFactory($conn, $permessageDeflateOptions = null)
{
@ -42,19 +42,21 @@ function echoStreamerFactory($conn, $permessageDeflateOptions = null)
},
false,
null,
null,
null,
[$conn, 'write'],
$permessageDeflateOptions
);
}
function getTestCases() {
global $factory;
global $testServer;
global $connector;
$deferred = new Deferred();
$factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) {
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator();
$connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred) {
$cn = new ClientNegotiator();
$cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001/getCaseCount'));
$rawResponse = "";
@ -63,7 +65,7 @@ function getTestCases() {
/** @var MessageBuffer $ms */
$ms = null;
$stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
$connection->on('data', function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
if ($response === null) {
$rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n");
@ -73,18 +75,20 @@ function getTestCases() {
$response = \GuzzleHttp\Psr7\parse_response($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) {
$stream->end();
$connection->end();
$deferred->reject();
} else {
$ms = new \Ratchet\RFC6455\Messaging\MessageBuffer(
new \Ratchet\RFC6455\Messaging\CloseFrameChecker,
function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) {
$ms = new MessageBuffer(
new CloseFrameChecker,
function (MessageInterface $msg) use ($deferred, $connection) {
$deferred->resolve($msg->getPayload());
$stream->close();
$connection->close();
},
null,
false,
null,
null,
null,
function () {}
);
}
@ -97,7 +101,7 @@ function getTestCases() {
}
});
$stream->write(\GuzzleHttp\Psr7\str($cnRequest));
$connection->write(\GuzzleHttp\Psr7\str($cnRequest));
});
return $deferred->promise();
@ -107,7 +111,7 @@ $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(PermessageDeflateOptions::
function runTest($case)
{
global $factory;
global $connector;
global $testServer;
global $cn;
@ -115,8 +119,8 @@ function runTest($case)
$deferred = new Deferred();
$factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($cn, $deferred, $casePath, $case) {
/** @var RequestInterface $cnRequest */
$connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred, $casePath, $case) {
$cn = new ClientNegotiator(PermessageDeflateOptions::createDefault());
$cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $casePath));
$rawResponse = "";
@ -124,7 +128,7 @@ function runTest($case)
$ms = null;
$stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
$connection->on('data', function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
if ($response === null) {
$rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n");
@ -135,19 +139,18 @@ function runTest($case)
if (!$cn->validateResponse($cnRequest, $response)) {
echo "Invalid response.\n";
$stream->end();
$connection->end();
$deferred->reject();
} else {
try {
$permessageDeflateOptions = PermessageDeflateOptions::fromRequestOrResponse($response)[0];
$ms = echoStreamerFactory(
$connection,
$permessageDeflateOptions
);
} catch (InvalidPermessageDeflateOptionsException $e) {
$stream->end();
$connection->end();
}
$ms = echoStreamerFactory(
$stream,
$permessageDeflateOptions
);
}
}
}
@ -158,34 +161,34 @@ function runTest($case)
}
});
$stream->on('close', function () use ($deferred) {
$connection->on('close', function () use ($deferred) {
$deferred->resolve();
});
$stream->write(\GuzzleHttp\Psr7\str($cnRequest));
$connection->write(\GuzzleHttp\Psr7\str($cnRequest));
});
return $deferred->promise();
}
function createReport() {
global $factory;
global $connector;
global $testServer;
$deferred = new Deferred();
$factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) {
$connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred) {
$reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true";
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator();
$cn = new ClientNegotiator();
$cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $reportPath));
$rawResponse = "";
$response = null;
/** @var \Ratchet\RFC6455\Messaging\MessageBuffer $ms */
/** @var MessageBuffer $ms */
$ms = null;
$stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
$connection->on('data', function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
if ($response === null) {
$rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n");
@ -195,18 +198,20 @@ function createReport() {
$response = \GuzzleHttp\Psr7\parse_response($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) {
$stream->end();
$connection->end();
$deferred->reject();
} else {
$ms = new \Ratchet\RFC6455\Messaging\MessageBuffer(
new \Ratchet\RFC6455\Messaging\CloseFrameChecker,
function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) {
$ms = new MessageBuffer(
new CloseFrameChecker,
function (MessageInterface $msg) use ($deferred, $connection) {
$deferred->resolve($msg->getPayload());
$stream->close();
$connection->close();
},
null,
false,
null,
null,
null,
function () {}
);
}
@ -219,7 +224,7 @@ function createReport() {
}
});
$stream->write(\GuzzleHttp\Psr7\str($cnRequest));
$connection->write(\GuzzleHttp\Psr7\str($cnRequest));
});
return $deferred->promise();

View File

@ -16,8 +16,9 @@ php clientRunner.php
sleep 2
php startServer.php &
PHP_SERVER_PID=$!
sleep 3
wstest -m fuzzingclient -s fuzzingclient$SKIP_DEFLATE.json
kill $PHP_SERVER_PID
wstest -m fuzzingclient -s fuzzingclient$SKIP_DEFLATE.json
sleep 1
kill $(ps aux | grep 'php startServer.php' | awk '{print $2}' | head -n 1)

View File

@ -9,61 +9,69 @@ require_once __DIR__ . "/../bootstrap.php";
$loop = \React\EventLoop\Factory::create();
$socket = new \React\Socket\Server($loop);
$server = new \React\Http\Server($socket);
$socket = new \React\Socket\Server('127.0.0.1:9001', $loop);
$closeFrameChecker = new \Ratchet\RFC6455\Messaging\CloseFrameChecker;
$negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator(new \Ratchet\RFC6455\Handshake\RequestVerifier, true);
$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);
$socket->on('connection', function (React\Socket\ConnectionInterface $connection) use ($negotiator, $closeFrameChecker, $uException) {
$headerComplete = false;
$buffer = '';
$parser = null;
$connection->on('data', function ($data) use ($connection, &$parser, &$headerComplete, &$buffer, $negotiator, $closeFrameChecker, $uException) {
if ($headerComplete) {
$parser->onData($data);
return;
}
$response->writeHead(
$negotiatorResponse->getStatusCode(),
array_merge(
$negotiatorResponse->getHeaders(),
["Content-Length" => "0"]
)
);
$buffer .= $data;
$parts = explode("\r\n\r\n", $buffer);
if (count($parts) < 2) {
return;
}
$headerComplete = true;
$psrRequest = \GuzzleHttp\Psr7\parse_request($parts[0] . "\r\n\r\n");
$negotiatorResponse = $negotiator->handshake($psrRequest);
if ($negotiatorResponse->getStatusCode() !== 101) {
$response->end();
return;
}
$negotiatorResponse = $negotiatorResponse->withAddedHeader("Content-Length", "0");
// there is no need to look through the client requests
// we support any valid permessage deflate
$deflateOptions = PermessageDeflateOptions::fromRequestOrResponse($psrRequest)[0];
$connection->write(\GuzzleHttp\Psr7\str($negotiatorResponse));
$parser = new \Ratchet\RFC6455\Messaging\MessageBuffer(
$closeFrameChecker,
function (MessageInterface $message, MessageBuffer $messageBuffer) use ($response) {
$messageBuffer->sendMessage($message->getPayload(), true, $message->isBinary());
},
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;
},
[$response, 'write'],
$deflateOptions
);
if ($negotiatorResponse->getStatusCode() !== 101) {
$connection->end();
return;
}
$request->on('data', [$parser, 'onData']);
// there is no need to look through the client requests
// we support any valid permessage deflate
$deflateOptions = PermessageDeflateOptions::fromRequestOrResponse($psrRequest)[0];
$parser = new \Ratchet\RFC6455\Messaging\MessageBuffer($closeFrameChecker,
function (MessageInterface $message, MessageBuffer $messageBuffer) {
$messageBuffer->sendMessage($message->getPayload(), true, $message->isBinary());
}, function (FrameInterface $frame) use ($connection, &$parser) {
switch ($frame->getOpCode()) {
case Frame::OP_CLOSE:
$connection->end($frame->getContents());
break;
case Frame::OP_PING:
$connection->write($parser->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents());
break;
}
}, true, function () use ($uException) {
return $uException;
},
null,
null,
[$connection, 'write'],
$deflateOptions);
array_shift($parts);
$parser->onData(implode("\r\n\r\n", $parts));
});
});
$socket->listen(9001, '0.0.0.0');
$loop->run();

View File

@ -69,4 +69,299 @@ class MessageBufferTest extends \PHPUnit_Framework_TestCase
$this->assertTrue($bReceived);
}
public function testInvalidFrameLength() {
$frame = new Frame(str_repeat('a', 200), true, Frame::OP_TEXT);
$frameRaw = $frame->getContents();
$frameRaw[1] = "\x7f"; // 127 in the first spot
$frameRaw[2] = "\xff"; // this will unpack to -1
$frameRaw[3] = "\xff";
$frameRaw[4] = "\xff";
$frameRaw[5] = "\xff";
$frameRaw[6] = "\xff";
$frameRaw[7] = "\xff";
$frameRaw[8] = "\xff";
$frameRaw[9] = "\xff";
/** @var Frame $controlFrame */
$controlFrame = null;
$messageCount = 0;
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) use (&$messageCount) {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame) {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
0,
10
);
$messageBuffer->onData($frameRaw);
$this->assertEquals(0, $messageCount);
$this->assertTrue($controlFrame instanceof Frame);
$this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode());
$this->assertEquals([Frame::CLOSE_PROTOCOL], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2))));
}
public function testFrameLengthTooBig() {
$frame = new Frame(str_repeat('a', 200), true, Frame::OP_TEXT);
$frameRaw = $frame->getContents();
$frameRaw[1] = "\x7f"; // 127 in the first spot
$frameRaw[2] = "\x7f"; // this will unpack to -1
$frameRaw[3] = "\xff";
$frameRaw[4] = "\xff";
$frameRaw[5] = "\xff";
$frameRaw[6] = "\xff";
$frameRaw[7] = "\xff";
$frameRaw[8] = "\xff";
$frameRaw[9] = "\xff";
/** @var Frame $controlFrame */
$controlFrame = null;
$messageCount = 0;
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) use (&$messageCount) {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame) {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
0,
10
);
$messageBuffer->onData($frameRaw);
$this->assertEquals(0, $messageCount);
$this->assertTrue($controlFrame instanceof Frame);
$this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode());
$this->assertEquals([Frame::CLOSE_TOO_BIG], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2))));
}
public function testFrameLengthBiggerThanMaxMessagePayload() {
$frame = new Frame(str_repeat('a', 200), true, Frame::OP_TEXT);
$frameRaw = $frame->getContents();
/** @var Frame $controlFrame */
$controlFrame = null;
$messageCount = 0;
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) use (&$messageCount) {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame) {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
100,
0
);
$messageBuffer->onData($frameRaw);
$this->assertEquals(0, $messageCount);
$this->assertTrue($controlFrame instanceof Frame);
$this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode());
$this->assertEquals([Frame::CLOSE_TOO_BIG], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2))));
}
public function testSecondFrameLengthPushesPastMaxMessagePayload() {
$frame = new Frame(str_repeat('a', 200), false, Frame::OP_TEXT);
$firstFrameRaw = $frame->getContents();
$frame = new Frame(str_repeat('b', 200), true, Frame::OP_TEXT);
$secondFrameRaw = $frame->getContents();
/** @var Frame $controlFrame */
$controlFrame = null;
$messageCount = 0;
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) use (&$messageCount) {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame) {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
300,
0
);
$messageBuffer->onData($firstFrameRaw);
// only put part of the second frame in to watch it fail fast
$messageBuffer->onData(substr($secondFrameRaw, 0, 150));
$this->assertEquals(0, $messageCount);
$this->assertTrue($controlFrame instanceof Frame);
$this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode());
$this->assertEquals([Frame::CLOSE_TOO_BIG], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2))));
}
/**
* Some test cases from memory limit inspired by https://github.com/BrandEmbassy/php-memory
*
* Here is the license for that project:
* MIT License
*
* Copyright (c) 2018 Brand Embassy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* @dataProvider phpConfigurationProvider
*
* @param string $phpConfigurationValue
* @param int $expectedLimit
*/
public function testMemoryLimits($phpConfigurationValue, $expectedLimit) {
$method = new \ReflectionMethod('Ratchet\RFC6455\Messaging\MessageBuffer', 'getMemoryLimit');
$method->setAccessible(true);
$actualLimit = $method->invoke(null, $phpConfigurationValue);
$this->assertSame($expectedLimit, $actualLimit);
}
public function phpConfigurationProvider() {
return [
'without unit type, just bytes' => ['500', 500],
'1 GB with big "G"' => ['1G', 1 * 1024 * 1024 * 1024],
'128 MB with big "M"' => ['128M', 128 * 1024 * 1024],
'128 MB with small "m"' => ['128m', 128 * 1024 * 1024],
'24 kB with small "k"' => ['24k', 24 * 1024],
'2 GB with small "g"' => ['2g', 2 * 1024 * 1024 * 1024],
'unlimited memory' => ['-1', 0],
'invalid float value' => ['2.5M', 2 * 1024 * 1024],
'empty value' => ['', 0],
'invalid ini setting' => ['whatever it takes', 0]
];
}
/**
* @expectedException \InvalidArgumentException
*/
public function testInvalidMaxFramePayloadSizes() {
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) {},
function (Frame $frame) {},
false,
null,
0,
0x8000000000000000
);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testInvalidMaxMessagePayloadSizes() {
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) {},
function (Frame $frame) {},
false,
null,
0x8000000000000000,
0
);
}
/**
* @dataProvider phpConfigurationProvider
*
* @param string $phpConfigurationValue
* @param int $expectedLimit
*
* @runInSeparateProcess
* @requires PHP 7.0
*/
public function testIniSizes($phpConfigurationValue, $expectedLimit) {
ini_set('memory_limit', $phpConfigurationValue);
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) {},
function (Frame $frame) {},
false,
null
);
if ($expectedLimit === -1) {
$expectedLimit = 0;
}
$prop = new \ReflectionProperty($messageBuffer, 'maxMessagePayloadSize');
$prop->setAccessible(true);
$this->assertEquals($expectedLimit / 4, $prop->getValue($messageBuffer));
$prop = new \ReflectionProperty($messageBuffer, 'maxFramePayloadSize');
$prop->setAccessible(true);
$this->assertEquals($expectedLimit / 4, $prop->getValue($messageBuffer));
}
/**
* @runInSeparateProcess
* @requires PHP 7.0
*/
public function testInvalidIniSize() {
ini_set('memory_limit', 'lots of memory');
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) {},
function (Frame $frame) {},
false,
null
);
$prop = new \ReflectionProperty($messageBuffer, 'maxMessagePayloadSize');
$prop->setAccessible(true);
$this->assertEquals(0, $prop->getValue($messageBuffer));
$prop = new \ReflectionProperty($messageBuffer, 'maxFramePayloadSize');
$prop->setAccessible(true);
$this->assertEquals(0, $prop->getValue($messageBuffer));
}
}