friendship ended with social-app. php is my new best friend
1<?php
2
3namespace React\Http\Message;
4
5use Psr\Http\Message\ServerRequestInterface;
6use Psr\Http\Message\StreamInterface;
7use Psr\Http\Message\UriInterface;
8use React\Http\Io\AbstractRequest;
9use React\Http\Io\BufferedBody;
10use React\Http\Io\HttpBodyStream;
11use React\Stream\ReadableStreamInterface;
12
13/**
14 * Respresents an incoming server request message.
15 *
16 * This class implements the
17 * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface)
18 * which extends the
19 * [PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface)
20 * which in turn extends the
21 * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface).
22 *
23 * This is mostly used internally to represent each incoming request message.
24 * Likewise, you can also use this class in test cases to test how your web
25 * application reacts to certain HTTP requests.
26 *
27 * > Internally, this implementation builds on top of a base class which is
28 * considered an implementation detail that may change in the future.
29 *
30 * @see ServerRequestInterface
31 */
32final class ServerRequest extends AbstractRequest implements ServerRequestInterface
33{
34 private $attributes = array();
35
36 private $serverParams;
37 private $fileParams = array();
38 private $cookies = array();
39 private $queryParams = array();
40 private $parsedBody;
41
42 /**
43 * @param string $method HTTP method for the request.
44 * @param string|UriInterface $url URL for the request.
45 * @param array<string,string|string[]> $headers Headers for the message.
46 * @param string|ReadableStreamInterface|StreamInterface $body Message body.
47 * @param string $version HTTP protocol version.
48 * @param array<string,string> $serverParams server-side parameters
49 * @throws \InvalidArgumentException for an invalid URL or body
50 */
51 public function __construct(
52 $method,
53 $url,
54 array $headers = array(),
55 $body = '',
56 $version = '1.1',
57 $serverParams = array()
58 ) {
59 if (\is_string($body)) {
60 $body = new BufferedBody($body);
61 } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) {
62 $temp = new self($method, '', $headers);
63 $size = (int) $temp->getHeaderLine('Content-Length');
64 if (\strtolower($temp->getHeaderLine('Transfer-Encoding')) === 'chunked') {
65 $size = null;
66 }
67 $body = new HttpBodyStream($body, $size);
68 } elseif (!$body instanceof StreamInterface) {
69 throw new \InvalidArgumentException('Invalid server request body given');
70 }
71
72 parent::__construct($method, $url, $headers, $body, $version);
73
74 $this->serverParams = $serverParams;
75
76 $query = $this->getUri()->getQuery();
77 if ($query !== '') {
78 \parse_str($query, $this->queryParams);
79 }
80
81 // Multiple cookie headers are not allowed according
82 // to https://tools.ietf.org/html/rfc6265#section-5.4
83 $cookieHeaders = $this->getHeader("Cookie");
84
85 if (count($cookieHeaders) === 1) {
86 $this->cookies = $this->parseCookie($cookieHeaders[0]);
87 }
88 }
89
90 public function getServerParams()
91 {
92 return $this->serverParams;
93 }
94
95 public function getCookieParams()
96 {
97 return $this->cookies;
98 }
99
100 public function withCookieParams(array $cookies)
101 {
102 $new = clone $this;
103 $new->cookies = $cookies;
104 return $new;
105 }
106
107 public function getQueryParams()
108 {
109 return $this->queryParams;
110 }
111
112 public function withQueryParams(array $query)
113 {
114 $new = clone $this;
115 $new->queryParams = $query;
116 return $new;
117 }
118
119 public function getUploadedFiles()
120 {
121 return $this->fileParams;
122 }
123
124 public function withUploadedFiles(array $uploadedFiles)
125 {
126 $new = clone $this;
127 $new->fileParams = $uploadedFiles;
128 return $new;
129 }
130
131 public function getParsedBody()
132 {
133 return $this->parsedBody;
134 }
135
136 public function withParsedBody($data)
137 {
138 $new = clone $this;
139 $new->parsedBody = $data;
140 return $new;
141 }
142
143 public function getAttributes()
144 {
145 return $this->attributes;
146 }
147
148 public function getAttribute($name, $default = null)
149 {
150 if (!\array_key_exists($name, $this->attributes)) {
151 return $default;
152 }
153 return $this->attributes[$name];
154 }
155
156 public function withAttribute($name, $value)
157 {
158 $new = clone $this;
159 $new->attributes[$name] = $value;
160 return $new;
161 }
162
163 public function withoutAttribute($name)
164 {
165 $new = clone $this;
166 unset($new->attributes[$name]);
167 return $new;
168 }
169
170 /**
171 * @param string $cookie
172 * @return array
173 */
174 private function parseCookie($cookie)
175 {
176 $cookieArray = \explode(';', $cookie);
177 $result = array();
178
179 foreach ($cookieArray as $pair) {
180 $pair = \trim($pair);
181 $nameValuePair = \explode('=', $pair, 2);
182
183 if (\count($nameValuePair) === 2) {
184 $key = $nameValuePair[0];
185 $value = \urldecode($nameValuePair[1]);
186 $result[$key] = $value;
187 }
188 }
189
190 return $result;
191 }
192
193 /**
194 * [Internal] Parse incoming HTTP protocol message
195 *
196 * @internal
197 * @param string $message
198 * @param array<string,string|int|float> $serverParams
199 * @return self
200 * @throws \InvalidArgumentException if given $message is not a valid HTTP request message
201 */
202 public static function parseMessage($message, array $serverParams)
203 {
204 // parse request line like "GET /path HTTP/1.1"
205 $start = array();
206 if (!\preg_match('#^(?<method>[^ ]+) (?<target>[^ ]+) HTTP/(?<version>\d\.\d)#m', $message, $start)) {
207 throw new \InvalidArgumentException('Unable to parse invalid request-line');
208 }
209
210 // only support HTTP/1.1 and HTTP/1.0 requests
211 if ($start['version'] !== '1.1' && $start['version'] !== '1.0') {
212 throw new \InvalidArgumentException('Received request with invalid protocol version', Response::STATUS_VERSION_NOT_SUPPORTED);
213 }
214
215 // check number of valid header fields matches number of lines + request line
216 $matches = array();
217 $n = \preg_match_all(self::REGEX_HEADERS, $message, $matches, \PREG_SET_ORDER);
218 if (\substr_count($message, "\n") !== $n + 1) {
219 throw new \InvalidArgumentException('Unable to parse invalid request header fields');
220 }
221
222 // format all header fields into associative array
223 $host = null;
224 $headers = array();
225 foreach ($matches as $match) {
226 $headers[$match[1]][] = $match[2];
227
228 // match `Host` request header
229 if ($host === null && \strtolower($match[1]) === 'host') {
230 $host = $match[2];
231 }
232 }
233
234 // scheme is `http` unless TLS is used
235 $scheme = isset($serverParams['HTTPS']) ? 'https://' : 'http://';
236
237 // default host if unset comes from local socket address or defaults to localhost
238 $hasHost = $host !== null;
239 if ($host === null) {
240 $host = isset($serverParams['SERVER_ADDR'], $serverParams['SERVER_PORT']) ? $serverParams['SERVER_ADDR'] . ':' . $serverParams['SERVER_PORT'] : '127.0.0.1';
241 }
242
243 if ($start['method'] === 'OPTIONS' && $start['target'] === '*') {
244 // support asterisk-form for `OPTIONS *` request line only
245 $uri = $scheme . $host;
246 } elseif ($start['method'] === 'CONNECT') {
247 $parts = \parse_url('tcp://' . $start['target']);
248
249 // check this is a valid authority-form request-target (host:port)
250 if (!isset($parts['scheme'], $parts['host'], $parts['port']) || \count($parts) !== 3) {
251 throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target');
252 }
253 $uri = $scheme . $start['target'];
254 } else {
255 // support absolute-form or origin-form for proxy requests
256 if ($start['target'][0] === '/') {
257 $uri = $scheme . $host . $start['target'];
258 } else {
259 // ensure absolute-form request-target contains a valid URI
260 $parts = \parse_url($start['target']);
261
262 // make sure value contains valid host component (IP or hostname), but no fragment
263 if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) {
264 throw new \InvalidArgumentException('Invalid absolute-form request-target');
265 }
266
267 $uri = $start['target'];
268 }
269 }
270
271 $request = new self(
272 $start['method'],
273 $uri,
274 $headers,
275 '',
276 $start['version'],
277 $serverParams
278 );
279
280 // only assign request target if it is not in origin-form (happy path for most normal requests)
281 if ($start['target'][0] !== '/') {
282 $request = $request->withRequestTarget($start['target']);
283 }
284
285 if ($hasHost) {
286 // Optional Host request header value MUST be valid (host and optional port)
287 $parts = \parse_url('http://' . $request->getHeaderLine('Host'));
288
289 // make sure value contains valid host component (IP or hostname)
290 if (!$parts || !isset($parts['scheme'], $parts['host'])) {
291 $parts = false;
292 }
293
294 // make sure value does not contain any other URI component
295 if (\is_array($parts)) {
296 unset($parts['scheme'], $parts['host'], $parts['port']);
297 }
298 if ($parts === false || $parts) {
299 throw new \InvalidArgumentException('Invalid Host header value');
300 }
301 } elseif (!$hasHost && $start['version'] === '1.1' && $start['method'] !== 'CONNECT') {
302 // require Host request header for HTTP/1.1 (except for CONNECT method)
303 throw new \InvalidArgumentException('Missing required Host request header');
304 } elseif (!$hasHost) {
305 // remove default Host request header for HTTP/1.0 when not explicitly given
306 $request = $request->withoutHeader('Host');
307 }
308
309 // ensure message boundaries are valid according to Content-Length and Transfer-Encoding request headers
310 if ($request->hasHeader('Transfer-Encoding')) {
311 if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') {
312 throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', Response::STATUS_NOT_IMPLEMENTED);
313 }
314
315 // Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time
316 // as per https://tools.ietf.org/html/rfc7230#section-3.3.3
317 if ($request->hasHeader('Content-Length')) {
318 throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', Response::STATUS_BAD_REQUEST);
319 }
320 } elseif ($request->hasHeader('Content-Length')) {
321 $string = $request->getHeaderLine('Content-Length');
322
323 if ((string)(int)$string !== $string) {
324 // Content-Length value is not an integer or not a single integer
325 throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST);
326 }
327 }
328
329 return $request;
330 }
331}