1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
|
<?php
namespace React\Http;
use Evenement\EventEmitter;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Http\Io\IniUtil;
use React\Http\Io\MiddlewareRunner;
use React\Http\Io\StreamingServer;
use React\Http\Middleware\LimitConcurrentRequestsMiddleware;
use React\Http\Middleware\StreamingRequestMiddleware;
use React\Http\Middleware\RequestBodyBufferMiddleware;
use React\Http\Middleware\RequestBodyParserMiddleware;
use React\Socket\ServerInterface;
/**
* The `React\Http\HttpServer` class is responsible for handling incoming connections and then
* processing each incoming HTTP request.
*
* When a complete HTTP request has been received, it will invoke the given
* request handler function. This request handler function needs to be passed to
* the constructor and will be invoked with the respective [request](#server-request)
* object and expects a [response](#server-response) object in return:
*
* ```php
* $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
* return new React\Http\Message\Response(
* React\Http\Message\Response::STATUS_OK,
* array(
* 'Content-Type' => 'text/plain'
* ),
* "Hello World!\n"
* );
* });
* ```
*
* Each incoming HTTP request message is always represented by the
* [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface),
* see also following [request](#server-request) chapter for more details.
*
* Each outgoing HTTP response message is always represented by the
* [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface),
* see also following [response](#server-response) chapter for more details.
*
* This class takes an optional `LoopInterface|null $loop` parameter that can be used to
* pass the event loop instance to use for this object. You can use a `null` value
* here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
* given event loop instance.
*
* In order to start listening for any incoming connections, the `HttpServer` needs
* to be attached to an instance of
* [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface)
* through the [`listen()`](#listen) method as described in the following
* chapter. In its most simple form, you can attach this to a
* [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver)
* in order to start a plaintext HTTP server like this:
*
* ```php
* $http = new React\Http\HttpServer($handler);
*
* $socket = new React\Socket\SocketServer('0.0.0.0:8080');
* $http->listen($socket);
* ```
*
* See also the [`listen()`](#listen) method and
* [hello world server example](../examples/51-server-hello-world.php)
* for more details.
*
* By default, the `HttpServer` buffers and parses the complete incoming HTTP
* request in memory. It will invoke the given request handler function when the
* complete request headers and request body has been received. This means the
* [request](#server-request) object passed to your request handler function will be
* fully compatible with PSR-7 (http-message). This provides sane defaults for
* 80% of the use cases and is the recommended way to use this library unless
* you're sure you know what you're doing.
*
* On the other hand, buffering complete HTTP requests in memory until they can
* be processed by your request handler function means that this class has to
* employ a number of limits to avoid consuming too much memory. In order to
* take the more advanced configuration out your hand, it respects setting from
* your [`php.ini`](https://www.php.net/manual/en/ini.core.php) to apply its
* default settings. This is a list of PHP settings this class respects with
* their respective default values:
*
* ```
* memory_limit 128M
* post_max_size 8M // capped at 64K
*
* enable_post_data_reading 1
* max_input_nesting_level 64
* max_input_vars 1000
*
* file_uploads 1
* upload_max_filesize 2M
* max_file_uploads 20
* ```
*
* In particular, the `post_max_size` setting limits how much memory a single
* HTTP request is allowed to consume while buffering its request body. This
* needs to be limited because the server can process a large number of requests
* concurrently, so the server may potentially consume a large amount of memory
* otherwise. To support higher concurrency by default, this value is capped
* at `64K`. If you assign a higher value, it will only allow `64K` by default.
* If a request exceeds this limit, its request body will be ignored and it will
* be processed like a request with no request body at all. See below for
* explicit configuration to override this setting.
*
* By default, this class will try to avoid consuming more than half of your
* `memory_limit` for buffering multiple concurrent HTTP requests. As such, with
* the above default settings of `128M` max, it will try to consume no more than
* `64M` for buffering multiple concurrent HTTP requests. As a consequence, it
* will limit the concurrency to `1024` HTTP requests with the above defaults.
*
* It is imperative that you assign reasonable values to your PHP ini settings.
* It is usually recommended to not support buffering incoming HTTP requests
* with a large HTTP request body (e.g. large file uploads). If you want to
* increase this buffer size, you will have to also increase the total memory
* limit to allow for more concurrent requests (set `memory_limit 512M` or more)
* or explicitly limit concurrency.
*
* In order to override the above buffering defaults, you can configure the
* `HttpServer` explicitly. You can use the
* [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and
* [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below)
* to explicitly configure the total number of requests that can be handled at
* once like this:
*
* ```php
* $http = new React\Http\HttpServer(
* new React\Http\Middleware\StreamingRequestMiddleware(),
* new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
* new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
* new React\Http\Middleware\RequestBodyParserMiddleware(),
* $handler
* ));
* ```
*
* In this example, we allow processing up to 100 concurrent requests at once
* and each request can buffer up to `2M`. This means you may have to keep a
* maximum of `200M` of memory for incoming request body buffers. Accordingly,
* you need to adjust the `memory_limit` ini setting to allow for these buffers
* plus your actual application logic memory requirements (think `512M` or more).
*
* > Internally, this class automatically assigns these middleware handlers
* automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware)
* is given. Accordingly, you can use this example to override all default
* settings to implement custom limits.
*
* As an alternative to buffering the complete request body in memory, you can
* also use a streaming approach where only small chunks of data have to be kept
* in memory:
*
* ```php
* $http = new React\Http\HttpServer(
* new React\Http\Middleware\StreamingRequestMiddleware(),
* $handler
* );
* ```
*
* In this case, it will invoke the request handler function once the HTTP
* request headers have been received, i.e. before receiving the potentially
* much larger HTTP request body. This means the [request](#server-request) passed to
* your request handler function may not be fully compatible with PSR-7. This is
* specifically designed to help with more advanced use cases where you want to
* have full control over consuming the incoming HTTP request body and
* concurrency settings. See also [streaming incoming request](#streaming-incoming-request)
* below for more details.
*
* > Changelog v1.5.0: This class has been renamed to `HttpServer` from the
* previous `Server` class in order to avoid any ambiguities.
* The previous name has been deprecated and should not be used anymore.
*/
final class HttpServer extends EventEmitter
{
/**
* The maximum buffer size used for each request.
*
* This needs to be limited because the server can process a large number of
* requests concurrently, so the server may potentially consume a large
* amount of memory otherwise.
*
* See `RequestBodyBufferMiddleware` to override this setting.
*
* @internal
*/
const MAXIMUM_BUFFER_SIZE = 65536; // 64 KiB
/**
* @var StreamingServer
*/
private $streamingServer;
/**
* Creates an HTTP server that invokes the given callback for each incoming HTTP request
*
* In order to process any connections, the server needs to be attached to an
* instance of `React\Socket\ServerInterface` which emits underlying streaming
* connections in order to then parse incoming data as HTTP.
* See also [listen()](#listen) for more details.
*
* @param callable|LoopInterface $requestHandlerOrLoop
* @param callable[] ...$requestHandler
* @see self::listen()
*/
public function __construct($requestHandlerOrLoop)
{
$requestHandlers = \func_get_args();
if (reset($requestHandlers) instanceof LoopInterface) {
$loop = \array_shift($requestHandlers);
} else {
$loop = Loop::get();
}
$requestHandlersCount = \count($requestHandlers);
if ($requestHandlersCount === 0 || \count(\array_filter($requestHandlers, 'is_callable')) < $requestHandlersCount) {
throw new \InvalidArgumentException('Invalid request handler given');
}
$streaming = false;
foreach ((array) $requestHandlers as $handler) {
if ($handler instanceof StreamingRequestMiddleware) {
$streaming = true;
break;
}
}
$middleware = array();
if (!$streaming) {
$maxSize = $this->getMaxRequestSize();
$concurrency = $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), $maxSize);
if ($concurrency !== null) {
$middleware[] = new LimitConcurrentRequestsMiddleware($concurrency);
}
$middleware[] = new RequestBodyBufferMiddleware($maxSize);
// 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 = \array_merge($middleware, $requestHandlers);
/**
* Filter out any configuration middleware, no need to run requests through something that isn't
* doing anything with the request.
*/
$middleware = \array_filter($middleware, function ($handler) {
return !($handler instanceof StreamingRequestMiddleware);
});
$this->streamingServer = new StreamingServer($loop, new MiddlewareRunner($middleware));
$that = $this;
$this->streamingServer->on('error', function ($error) use ($that) {
$that->emit('error', array($error));
});
}
/**
* Starts listening for HTTP requests on the given socket server instance
*
* The given [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface)
* is responsible for emitting the underlying streaming connections. This
* HTTP server needs to be attached to it in order to process any
* connections and pase incoming streaming data as incoming HTTP request
* messages. In its most common form, you can attach this to a
* [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver)
* in order to start a plaintext HTTP server like this:
*
* ```php
* $http = new React\Http\HttpServer($handler);
*
* $socket = new React\Socket\SocketServer('0.0.0.0:8080');
* $http->listen($socket);
* ```
*
* See also [hello world server example](../examples/51-server-hello-world.php)
* for more details.
*
* This example will start listening for HTTP requests on the alternative
* HTTP port `8080` on all interfaces (publicly). As an alternative, it is
* very common to use a reverse proxy and let this HTTP server listen on the
* localhost (loopback) interface only by using the listen address
* `127.0.0.1:8080` instead. This way, you host your application(s) on the
* default HTTP port `80` and only route specific requests to this HTTP
* server.
*
* Likewise, it's usually recommended to use a reverse proxy setup to accept
* secure HTTPS requests on default HTTPS port `443` (TLS termination) and
* only route plaintext requests to this HTTP server. As an alternative, you
* can also accept secure HTTPS requests with this HTTP server by attaching
* this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver)
* using a secure TLS listen address, a certificate file and optional
* `passphrase` like this:
*
* ```php
* $http = new React\Http\HttpServer($handler);
*
* $socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array(
* 'tls' => array(
* 'local_cert' => __DIR__ . '/localhost.pem'
* )
* ));
* $http->listen($socket);
* ```
*
* See also [hello world HTTPS example](../examples/61-server-hello-world-https.php)
* for more details.
*
* @param ServerInterface $socket
*/
public function listen(ServerInterface $socket)
{
$this->streamingServer->listen($socket);
}
/**
* @param string $memory_limit
* @param string $post_max_size
* @return ?int
*/
private function getConcurrentRequestsLimit($memory_limit, $post_max_size)
{
if ($memory_limit == -1) {
return null;
}
$availableMemory = IniUtil::iniSizeToBytes($memory_limit) / 2;
$concurrentRequests = (int) \ceil($availableMemory / IniUtil::iniSizeToBytes($post_max_size));
return $concurrentRequests;
}
/**
* @param ?string $post_max_size
* @return int
*/
private function getMaxRequestSize($post_max_size = null)
{
$maxSize = IniUtil::iniSizeToBytes($post_max_size === null ? \ini_get('post_max_size') : $post_max_size);
return ($maxSize === 0 || $maxSize >= self::MAXIMUM_BUFFER_SIZE) ? self::MAXIMUM_BUFFER_SIZE : $maxSize;
}
}
|