diff --git a/CHANGELOG.md b/CHANGELOG.md index 96132cd..3096b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # CHANGELOG +## 1.9.2 (Unreleased) + +* Reject handshake request lines with a bare line feed ending or a non-literal HTTP version +* Match `server_ssl_` socket option keys to the true end of input +* Pass explicit trim characters ahead of the PHP 8.6 trim default change + + ## 1.9.1 (2026-07-06) * Fix `waitForData` crash when the socket is disconnected diff --git a/src/Protocol/Protocol.php b/src/Protocol/Protocol.php index 1d4cef2..8070bce 100644 --- a/src/Protocol/Protocol.php +++ b/src/Protocol/Protocol.php @@ -120,7 +120,7 @@ abstract class Protocol /** * Used for parsing requested path. preg_* compatible. */ - public const REQUEST_LINE_REGEX = '/^GET (\S+) HTTP\/1.1$/'; + public const REQUEST_LINE_REGEX = '/^GET (\S+) HTTP\/1\.1$/D'; /** * printf compatible. @@ -459,9 +459,9 @@ public function validateResponseHandshake(string $response, string $key): bool $statusCode = $this->getStatusCode($response); if (self::HTTP_SWITCHING_PROTOCOLS !== $statusCode) { - $errorMessage = \explode("\n", \trim($this->getBody($response)), 2)[0]; + $errorMessage = \explode("\n", \trim($this->getBody($response), " \n\r\t\0\x0B"), 2)[0]; - throw new HandshakeException(\trim(\sprintf('Expected handshake response status code %d, but received %d. %s', self::HTTP_SWITCHING_PROTOCOLS, $statusCode, $errorMessage))); + throw new HandshakeException(\trim(\sprintf('Expected handshake response status code %d, but received %d. %s', self::HTTP_SWITCHING_PROTOCOLS, $statusCode, $errorMessage), " \n\r\t\0\x0B")); } $acceptHeaderValue = $this->getHeaders($response)[self::HEADER_ACCEPT] ?? ''; @@ -510,12 +510,12 @@ protected function getHeaders(string $response): array if (2 == \count($parts)) { [$name, $value] = $parts; if (!isset($return[$name])) { - $return[$name] = \trim($value); + $return[$name] = \trim($value, " \n\r\t\0\x0B"); } else { if (\is_array($return[$name])) { - $return[$name][] = \trim($value); + $return[$name][] = \trim($value, " \n\r\t\0\x0B"); } else { - $return[$name] = [$return[$name], \trim($value)]; + $return[$name] = [$return[$name], \trim($value, " \n\r\t\0\x0B")]; } } } @@ -608,7 +608,7 @@ public function validateRequestHandshake(string $request): array throw new BadRequestException('No key header received'); } - $key = \trim($headers[self::HEADER_KEY]); + $key = \trim($headers[self::HEADER_KEY], " \n\r\t\0\x0B"); if (!$key) { throw new BadRequestException('Invalid key'); diff --git a/src/Socket/ServerSocket.php b/src/Socket/ServerSocket.php index 5ba559c..dcb31f5 100644 --- a/src/Socket/ServerSocket.php +++ b/src/Socket/ServerSocket.php @@ -113,7 +113,7 @@ protected function getSslStreamContextOptions(): array // Otherwise map any options through foreach ($this->options as $option => $value) { - if (\preg_match('/^server_ssl_(.*)$/', $option, $matches)) { + if (\preg_match('/^server_ssl_(.*)$/D', $option, $matches)) { $options[$matches[1]] = $value; } } diff --git a/tests/Protocol/ProtocolBaseTest.php b/tests/Protocol/ProtocolBaseTest.php index a2c31f0..9b293b7 100644 --- a/tests/Protocol/ProtocolBaseTest.php +++ b/tests/Protocol/ProtocolBaseTest.php @@ -4,6 +4,7 @@ use Exception; use InvalidArgumentException; +use Wrench\Exception\BadRequestException; use Wrench\Test\BaseTest; abstract class ProtocolBaseTest extends BaseTest @@ -35,6 +36,16 @@ public function testValidatHandshakeRequestValid(string $request): void } } + /** + * @dataProvider getInvalidHandshakeRequestLines + */ + public function testValidateHandshakeRequestLineInvalid(string $request): void + { + $this->expectException(BadRequestException::class); + + self::getInstance()->validateRequestHandshake($request); + } + /** * @dataProvider getValidHandshakeResponses */ @@ -160,6 +171,22 @@ public static function getValidHandshakeRequests(): array return $cases; } + public static function getInvalidHandshakeRequestLines(): array + { + $headers = "Host: server.example.com\r +Upgrade: websocket\r +Connection: Upgrade\r +Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r +Origin: http://example.com\r +Sec-WebSocket-Version: 13\r +\r\n"; + + return [ + 'bare line feed ending the request line' => ["GET /chat HTTP/1.1\n\r\n".$headers], + 'non-literal HTTP version' => ["GET /chat HTTP/1x1\r\n".$headers], + ]; + } + public static function getValidHandshakeResponses(): array { $cases = [];