Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 7 additions & 7 deletions src/Protocol/Protocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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] ?? '';
Expand Down Expand Up @@ -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")];
}
}
}
Expand Down Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion src/Socket/ServerSocket.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
27 changes: 27 additions & 0 deletions tests/Protocol/ProtocolBaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Exception;
use InvalidArgumentException;
use Wrench\Exception\BadRequestException;
use Wrench\Test\BaseTest;

abstract class ProtocolBaseTest extends BaseTest
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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 = [];
Expand Down