Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht

* [Quickstart example](#quickstart-example)
* [Usage](#usage)
* [Server](#server)
* [StreamingServer](#streamingserver)
* [Request](#request)
* [Response](#response)
Expand All @@ -28,7 +29,7 @@ This is an HTTP server which responds with `Hello World` to every request.
```php
$loop = React\EventLoop\Factory::create();

$server = new StreamingServer(function (ServerRequestInterface $request) {
$server = new Server(function (ServerRequestInterface $request) {
return new Response(
200,
array('Content-Type' => 'text/plain'),
Expand All @@ -46,6 +47,19 @@ See also the [examples](examples).

## Usage

### Server

For most users a server that buffers and parses a requests before handling it over as a
PSR-7 request is what they want. The `Server` facade takes care of that, and takes the more
advanced configuration out of hand. Under the hood it uses [StreamingServer](#streamingserver)
with the the three stock middleware using default settings from `php.ini`.

The [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) requires a limit,
as such the `Server` facade uses the `memory_limit` and `post_max_size` ini settings to
calculate a sensible limit. It assumes a maximum of a quarter of the `memory_limit` for
buffering and the other three quarter for parsing and handling the requests. The limit is
division of half of `memory_limit` by `memory_limit` rounded up.

### StreamingServer

The `StreamingServer` class is responsible for handling incoming connections and then
Expand Down
95 changes: 95 additions & 0 deletions src/Server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace React\Http;

use React\Http\Io\IniUtil;
use React\Http\Middleware\LimitConcurrentRequestsMiddleware;
use React\Http\Middleware\RequestBodyBufferMiddleware;
use React\Http\Middleware\RequestBodyParserMiddleware;
use React\Socket\ServerInterface;

/**
* Facade around StreamingServer with sane defaults for 80% of the use cases.
* The requests passed to your callable are fully compatible with PSR-7 because
* the body of the requests are fully buffered and parsed, unlike StreamingServer
* where the body is a raw ReactPHP stream.
*
* Wraps StreamingServer with the following middleware:
* - LimitConcurrentRequestsMiddleware
* - RequestBodyBufferMiddleware
* - RequestBodyParserMiddleware (only when enable_post_data_reading is true (default))
* - The callable you in passed as first constructor argument
*
* All middleware use their default configuration, which can be controlled with
* the the following configuration directives from php.ini:
* - upload_max_filesize
* - post_max_size
* - max_input_nesting_level
* - max_input_vars
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also respects file_uploads and max_file_uploads ini settings as documented for RequestBodyParserMiddleware.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah my bad, added them 👍

* - file_uploads
* - max_file_uploads
* - enable_post_data_reading
*/
final class Server
{
/**
* @internal
*/
const MAXIMUM_CONCURRENT_REQUESTS = 100;

/**
* @var StreamingServer
*/
private $streamingServer;

/**
* @see StreamingServer::__construct()
*/
public function __construct($callback)
{
if (!is_callable($callback)) {
throw new \InvalidArgumentException();
}

$middleware = array();
$middleware[] = new LimitConcurrentRequestsMiddleware($this->getConcurrentRequestsLimit());
$middleware[] = new RequestBodyBufferMiddleware();
// Checking for an empty string because that is what a boolean
// false is returned as by ini_get depending on the PHP version.
// @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading
// @link http://php.net/manual/en/function.ini-get.php#refsect1-function.ini-get-notes
// @link https://3v4l.org/qJtsa
$enablePostDataReading = ini_get('enable_post_data_reading');
if ($enablePostDataReading !== '') {
$middleware[] = new RequestBodyParserMiddleware();
}
$middleware[] = $callback;

$this->streamingServer = new StreamingServer(new MiddlewareRunner($middleware));
}

/**
* @see StreamingServer::listen()
*/
public function listen(ServerInterface $server)
{
$this->streamingServer->listen($server);
}

private function getConcurrentRequestsLimit()
{
if (ini_get('memory_limit') == -1) {
return self::MAXIMUM_CONCURRENT_REQUESTS;
}

$availableMemory = IniUtil::iniSizeToBytes(ini_get('memory_limit')) / 4;
$concurrentRequests = $availableMemory / IniUtil::iniSizeToBytes(ini_get('post_max_size'));
$concurrentRequests = ceil($concurrentRequests);

if ($concurrentRequests >= self::MAXIMUM_CONCURRENT_REQUESTS) {
return self::MAXIMUM_CONCURRENT_REQUESTS;
}

return $concurrentRequests;
}
}
103 changes: 103 additions & 0 deletions tests/ServerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

namespace React\Tests\Http;

use React\EventLoop\Factory;
use React\EventLoop\Timer\TimerInterface;
use React\Http\MiddlewareRunner;
use React\Http\Server;
use React\Http\StreamingServer;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Response;
use React\Promise\Deferred;
use Clue\React\Block;
use React\Stream\ThroughStream;
use React\Promise\Promise;

final class ServerTest extends TestCase
{
private $connection;
private $socket;

public function setUp()
{
$this->connection = $this->getMockBuilder('React\Socket\Connection')
->disableOriginalConstructor()
->setMethods(
array(
'write',
'end',
'close',
'pause',
'resume',
'isReadable',
'isWritable',
'getRemoteAddress',
'getLocalAddress',
'pipe'
)
)
->getMock();

$this->connection->method('isWritable')->willReturn(true);
$this->connection->method('isReadable')->willReturn(true);

$this->socket = new SocketServerStub();
}

public function testPostFileUpload()
{
$loop = Factory::create();
$deferred = new Deferred();
$server = new Server(function (ServerRequestInterface $request) use ($deferred) {
$deferred->resolve($request);
});

$server->listen($this->socket);
$this->socket->emit('connection', array($this->connection));

$connection = $this->connection;
$data = $this->createPostFileUploadRequest();
$loop->addPeriodicTimer(0.01, function (TimerInterface $timer) use ($loop, &$data, $connection) {
$line = array_shift($data);
$connection->emit('data', array($line));

if (count($data) === 0) {
$loop->cancelTimer($timer);
}
});

$parsedRequest = Block\await($deferred->promise(), $loop);
$this->assertNotEmpty($parsedRequest->getUploadedFiles());
$this->assertEmpty($parsedRequest->getParsedBody());

$files = $parsedRequest->getUploadedFiles();

$this->assertTrue(isset($files['file']));
$this->assertCount(1, $files);

$this->assertSame('hello.txt', $files['file']->getClientFilename());
$this->assertSame('text/plain', $files['file']->getClientMediaType());
$this->assertSame("hello\r\n", (string)$files['file']->getStream());
}

private function createPostFileUploadRequest()
{
$boundary = "---------------------------5844729766471062541057622570";

$data = array();
$data[] = "POST / HTTP/1.1\r\n";
$data[] = "Content-Type: multipart/form-data; boundary=" . $boundary . "\r\n";
$data[] = "Content-Length: 220\r\n";
$data[] = "\r\n";
$data[] = "--$boundary\r\n";
$data[] = "Content-Disposition: form-data; name=\"file\"; filename=\"hello.txt\"\r\n";
$data[] = "Content-type: text/plain\r\n";
$data[] = "\r\n";
$data[] = "hello\r\n";
$data[] = "\r\n";
$data[] = "--$boundary--\r\n";

return $data;
}
}