| @@ -15,9 +15,9 @@ | ||
| 15 | 15 | */ |
| 16 | 16 | public static function toString(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message) : string |
| 17 | 17 | { |
| 18 | 18 | if ($message instanceof \YoastSEO_Vendor\Psr\Http\Message\RequestInterface) { |
| 19 | - $msg = \trim($message->getMethod() . ' ' . $message->getRequestTarget()) . ' HTTP/' . $message->getProtocolVersion(); | |
| 19 | + $msg = \trim($message->getMethod() . ' ' . $message->getRequestTarget(), " \n\r\t\x00\v") . ' HTTP/' . $message->getProtocolVersion(); | |
| 20 | 20 | if (!$message->hasHeader('host')) { |
| 21 | 21 | $msg .= "\r\nHost: " . $message->getUri()->getHost(); |
| 22 | 22 | } |
| 23 | 23 | } elseif ($message instanceof \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface) { |
| @@ -25,9 +25,9 @@ | ||
| 25 | 25 | } else { |
| 26 | 26 | throw new \InvalidArgumentException('Unknown message type'); |
| 27 | 27 | } |
| 28 | 28 | foreach ($message->getHeaders() as $name => $values) { |
| 29 | - if (\is_string($name) && \strtolower($name) === 'set-cookie') { | |
| 29 | + if (\is_string($name) && \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($name) === 'set-cookie') { | |
| 30 | 30 | foreach ($values as $value) { |
| 31 | 31 | $msg .= "\r\n{$name}: " . $value; |
| 32 | 32 | } |
| 33 | 33 | } else { |
| @@ -142,9 +142,12 @@ | ||
| 142 | 142 | throw new \InvalidArgumentException('Invalid message'); |
| 143 | 143 | } |
| 144 | 144 | $message = \ltrim($message, "\r\n"); |
| 145 | 145 | $messageParts = \preg_split("/\r?\n\r?\n/", $message, 2); |
| 146 | - if ($messageParts === \false || \count($messageParts) !== 2) { | |
| 146 | + if ($messageParts === \false) { | |
| 147 | + throw new \RuntimeException('Unable to split HTTP message: ' . \preg_last_error_msg()); | |
| 148 | + } | |
| 149 | + if (\count($messageParts) !== 2) { | |
| 147 | 150 | throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); |
| 148 | 151 | } |
| 149 | 152 | [$rawHeaders, $body] = $messageParts; |
| 150 | 153 | $rawHeaders .= "\r\n"; |
| @@ -149,22 +152,39 @@ | ||
| 149 | 152 | [$rawHeaders, $body] = $messageParts; |
| 150 | 153 | $rawHeaders .= "\r\n"; |
| 151 | 154 | // Put back the delimiter we split previously |
| 152 | 155 | $headerParts = \preg_split("/\r?\n/", $rawHeaders, 2); |
| 153 | - if ($headerParts === \false || \count($headerParts) !== 2) { | |
| 156 | + if ($headerParts === \false) { | |
| 157 | + throw new \RuntimeException('Unable to split HTTP message headers: ' . \preg_last_error_msg()); | |
| 158 | + } | |
| 159 | + if (\count($headerParts) !== 2) { | |
| 154 | 160 | throw new \InvalidArgumentException('Invalid message: Missing status line'); |
| 155 | 161 | } |
| 156 | 162 | [$startLine, $rawHeaders] = $headerParts; |
| 157 | - if (\preg_match("/(?:^HTTP\\/|^[A-Z]+ \\S+ HTTP\\/)(\\d+(?:\\.\\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { | |
| 163 | + $versionMatch = \preg_match("/(?:^HTTP\\/|^[A-Z]+ \\S+ HTTP\\/)(\\d+(?:\\.\\d+)?)/i", $startLine, $matches); | |
| 164 | + if ($versionMatch === \false) { | |
| 165 | + throw new \RuntimeException('Unable to parse HTTP start line: ' . \preg_last_error_msg()); | |
| 166 | + } | |
| 167 | + if ($versionMatch === 1 && $matches[1] === '1.0') { | |
| 158 | 168 | // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 |
| 159 | 169 | $rawHeaders = \preg_replace(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); |
| 170 | + if ($rawHeaders === null) { | |
| 171 | + throw new \RuntimeException('Unable to unfold HTTP headers: ' . \preg_last_error_msg()); | |
| 172 | + } | |
| 160 | 173 | } |
| 161 | 174 | /** @var array[] $headerLines */ |
| 162 | 175 | $count = \preg_match_all(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, \PREG_SET_ORDER); |
| 176 | + if ($count === \false) { | |
| 177 | + throw new \RuntimeException('Unable to parse HTTP headers: ' . \preg_last_error_msg()); | |
| 178 | + } | |
| 163 | 179 | // If these aren't the same, then one line didn't match and there's an invalid header. |
| 164 | 180 | if ($count !== \substr_count($rawHeaders, "\n")) { |
| 165 | 181 | // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 |
| 166 | - if (\preg_match(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { | |
| 182 | + $hasFoldedHeader = \preg_match(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_FOLD_REGEX, $rawHeaders); | |
| 183 | + if ($hasFoldedHeader === \false) { | |
| 184 | + throw new \RuntimeException('Unable to inspect HTTP header folding: ' . \preg_last_error_msg()); | |
| 185 | + } | |
| 186 | + if ($hasFoldedHeader === 1) { | |
| 167 | 187 | throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); |
| 168 | 188 | } |
| 169 | 189 | throw new \InvalidArgumentException('Invalid header syntax'); |
| 170 | 190 | } |
| @@ -183,14 +203,23 @@ | ||
| 183 | 203 | public static function parseRequestUri(string $path, array $headers) : string |
| 184 | 204 | { |
| 185 | 205 | $host = self::getHostFromHeaders($headers); |
| 186 | 206 | // If no host is found, then a full URI cannot be constructed. |
| 207 | + // Collapse leading slashes so an origin-form target cannot be | |
| 208 | + // parsed as a network-path reference with its own authority. | |
| 187 | 209 | if ($host === null) { |
| 188 | - return $path; | |
| 210 | + return self::normalizePathForOriginForm($path); | |
| 189 | 211 | } |
| 190 | 212 | $scheme = \substr($host, -4) === ':443' ? 'https' : 'http'; |
| 191 | 213 | return $scheme . '://' . $host . '/' . \ltrim($path, '/'); |
| 192 | 214 | } |
| 215 | + private static function normalizePathForOriginForm(string $path) : string | |
| 216 | + { | |
| 217 | + if (0 === \strpos($path, '//')) { | |
| 218 | + return '/' . \ltrim($path, '/'); | |
| 219 | + } | |
| 220 | + return $path; | |
| 221 | + } | |
| 193 | 222 | /** |
| 194 | 223 | * @param array $headers Array of headers (each value an array). |
| 195 | 224 | */ |
| 196 | 225 | private static function getHostFromHeaders(array $headers) : ?string |
| @@ -197,9 +226,9 @@ | ||
| 197 | 226 | { |
| 198 | 227 | $hostKey = \array_filter(\array_keys($headers), function ($k) { |
| 199 | 228 | // Numeric array keys are converted to int by PHP. |
| 200 | 229 | $k = (string) $k; |
| 201 | - return \strtolower($k) === 'host'; | |
| 230 | + return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($k) === 'host'; | |
| 202 | 231 | }); |
| 203 | 232 | if (!$hostKey) { |
| 204 | 233 | return null; |
| 205 | 234 | } |
| @@ -220,9 +249,13 @@ | ||
| 220 | 249 | if (\strpbrk($data['start-line'], "\r\n") !== \false) { |
| 221 | 250 | throw new \InvalidArgumentException('Invalid request string'); |
| 222 | 251 | } |
| 223 | 252 | $matches = []; |
| 224 | - if (!\preg_match('/^[\\S]+\\s+([a-zA-Z]+:\\/\\/|\\/).*/', $data['start-line'], $matches)) { | |
| 253 | + $requestStartLineMatch = \preg_match('/^[\\S]+\\s+([a-zA-Z]+:\\/\\/|\\/).*/', $data['start-line'], $matches); | |
| 254 | + if ($requestStartLineMatch === \false) { | |
| 255 | + throw new \RuntimeException('Unable to parse request start line: ' . \preg_last_error_msg()); | |
| 256 | + } | |
| 257 | + if ($requestStartLineMatch === 0) { | |
| 225 | 258 | throw new \InvalidArgumentException('Invalid request string'); |
| 226 | 259 | } |
| 227 | 260 | $parts = \explode(' ', $data['start-line'], 3); |
| 228 | 261 | $version = isset($parts[2]) ? \explode('/', $parts[2])[1] : '1.1'; |
| @@ -242,9 +275,13 @@ | ||
| 242 | 275 | } |
| 243 | 276 | // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 |
| 244 | 277 | // the space between status-code and reason-phrase is required. But |
| 245 | 278 | // browsers accept responses without space and reason as well. |
| 246 | - if (!\preg_match('/^HTTP\\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { | |
| 279 | + $responseStartLineMatch = \preg_match('/^HTTP\\/.* [0-9]{3}( .*|$)/D', $data['start-line']); | |
| 280 | + if ($responseStartLineMatch === \false) { | |
| 281 | + throw new \RuntimeException('Unable to parse response start line: ' . \preg_last_error_msg()); | |
| 282 | + } | |
| 283 | + if ($responseStartLineMatch === 0) { | |
| 247 | 284 | throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']); |
| 248 | 285 | } |
| 249 | 286 | $parts = \explode(' ', $data['start-line'], 3); |
| 250 | 287 | return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response((int) $parts[1], $data['headers'], $data['body'], \explode('/', $parts[0])[1], $parts[2] ?? null); |