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