friendship ended with social-app. php is my new best friend
1<?php
2
3namespace React\Http\Io;
4
5use Evenement\EventEmitter;
6use Psr\Http\Message\ServerRequestInterface;
7use React\Http\Message\Response;
8use React\Http\Message\ServerRequest;
9use React\Socket\ConnectionInterface;
10use Exception;
11
12/**
13 * [Internal] Parses an incoming request header from an input stream
14 *
15 * This is used internally to parse the request header from the connection and
16 * then process the remaining connection as the request body.
17 *
18 * @event headers
19 * @event error
20 *
21 * @internal
22 */
23class RequestHeaderParser extends EventEmitter
24{
25 private $maxSize = 8192;
26
27 /** @var Clock */
28 private $clock;
29
30 /** @var array<string|int,array<string,string>> */
31 private $connectionParams = array();
32
33 public function __construct(Clock $clock)
34 {
35 $this->clock = $clock;
36 }
37
38 public function handle(ConnectionInterface $conn)
39 {
40 $buffer = '';
41 $maxSize = $this->maxSize;
42 $that = $this;
43 $conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn, $maxSize, $that) {
44 // append chunk of data to buffer and look for end of request headers
45 $buffer .= $data;
46 $endOfHeader = \strpos($buffer, "\r\n\r\n");
47
48 // reject request if buffer size is exceeded
49 if ($endOfHeader > $maxSize || ($endOfHeader === false && isset($buffer[$maxSize]))) {
50 $conn->removeListener('data', $fn);
51 $fn = null;
52
53 $that->emit('error', array(
54 new \OverflowException("Maximum header size of {$maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE),
55 $conn
56 ));
57 return;
58 }
59
60 // ignore incomplete requests
61 if ($endOfHeader === false) {
62 return;
63 }
64
65 // request headers received => try to parse request
66 $conn->removeListener('data', $fn);
67 $fn = null;
68
69 try {
70 $request = $that->parseRequest(
71 (string)\substr($buffer, 0, $endOfHeader + 2),
72 $conn
73 );
74 } catch (Exception $exception) {
75 $buffer = '';
76 $that->emit('error', array(
77 $exception,
78 $conn
79 ));
80 return;
81 }
82
83 $contentLength = 0;
84 if ($request->hasHeader('Transfer-Encoding')) {
85 $contentLength = null;
86 } elseif ($request->hasHeader('Content-Length')) {
87 $contentLength = (int)$request->getHeaderLine('Content-Length');
88 }
89
90 if ($contentLength === 0) {
91 // happy path: request body is known to be empty
92 $stream = new EmptyBodyStream();
93 $request = $request->withBody($stream);
94 } else {
95 // otherwise body is present => delimit using Content-Length or ChunkedDecoder
96 $stream = new CloseProtectionStream($conn);
97 if ($contentLength !== null) {
98 $stream = new LengthLimitedStream($stream, $contentLength);
99 } else {
100 $stream = new ChunkedDecoder($stream);
101 }
102
103 $request = $request->withBody(new HttpBodyStream($stream, $contentLength));
104 }
105
106 $bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : '';
107 $buffer = '';
108 $that->emit('headers', array($request, $conn));
109
110 if ($bodyBuffer !== '') {
111 $conn->emit('data', array($bodyBuffer));
112 }
113
114 // happy path: request body is known to be empty => immediately end stream
115 if ($contentLength === 0) {
116 $stream->emit('end');
117 $stream->close();
118 }
119 });
120 }
121
122 /**
123 * @param string $headers buffer string containing request headers only
124 * @param ConnectionInterface $connection
125 * @return ServerRequestInterface
126 * @throws \InvalidArgumentException
127 * @internal
128 */
129 public function parseRequest($headers, ConnectionInterface $connection)
130 {
131 // reuse same connection params for all server params for this connection
132 $cid = \PHP_VERSION_ID < 70200 ? \spl_object_hash($connection) : \spl_object_id($connection);
133 if (isset($this->connectionParams[$cid])) {
134 $serverParams = $this->connectionParams[$cid];
135 } else {
136 // assign new server params for new connection
137 $serverParams = array();
138
139 // scheme is `http` unless TLS is used
140 $localSocketUri = $connection->getLocalAddress();
141 $localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri);
142 if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') {
143 $serverParams['HTTPS'] = 'on';
144 }
145
146 // apply SERVER_ADDR and SERVER_PORT if server address is known
147 // address should always be known, even for Unix domain sockets (UDS)
148 // but skip UDS as it doesn't have a concept of host/port.
149 if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) {
150 $serverParams['SERVER_ADDR'] = $localParts['host'];
151 $serverParams['SERVER_PORT'] = $localParts['port'];
152 }
153
154 // apply REMOTE_ADDR and REMOTE_PORT if source address is known
155 // address should always be known, unless this is over Unix domain sockets (UDS)
156 $remoteSocketUri = $connection->getRemoteAddress();
157 if ($remoteSocketUri !== null) {
158 $remoteAddress = \parse_url($remoteSocketUri);
159 $serverParams['REMOTE_ADDR'] = $remoteAddress['host'];
160 $serverParams['REMOTE_PORT'] = $remoteAddress['port'];
161 }
162
163 // remember server params for all requests from this connection, reset on connection close
164 $this->connectionParams[$cid] = $serverParams;
165 $params =& $this->connectionParams;
166 $connection->on('close', function () use (&$params, $cid) {
167 assert(\is_array($params));
168 unset($params[$cid]);
169 });
170 }
171
172 // create new obj implementing ServerRequestInterface by preserving all
173 // previous properties and restoring original request-target
174 $serverParams['REQUEST_TIME'] = (int) ($now = $this->clock->now());
175 $serverParams['REQUEST_TIME_FLOAT'] = $now;
176
177 return ServerRequest::parseMessage($headers, $serverParams);
178 }
179}