friendship ended with social-app. php is my new best friend
at main 12 kB view raw
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}