diff --git a/src/Protocols/Http.php b/src/Protocols/Http.php index 453bd8a3..390c36a1 100644 --- a/src/Protocols/Http.php +++ b/src/Protocols/Http.php @@ -77,6 +77,13 @@ class Http */ protected const HTTP_413 = "HTTP/1.1 413 Payload Too Large\r\nConnection: close\r\n\r\n"; + /** + * Method Not Allowed. + * + * @var string + */ + protected const HTTP_405 = "HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\n\r\n"; + /** * Request Header Fields Too Large. * @@ -90,6 +97,13 @@ class Http protected const MAX_HEADER_LENGTH = 16384; + /** + * Disabled methods. + * + * @var string[] + */ + public static array $disabledMethods = ['TRACE', 'OPTIONS']; + /** * Get or set the request class name. * @@ -137,7 +151,7 @@ public static function input(string $buffer, TcpConnection $connection): int // Validate request line: METHOD SP origin-form SP HTTP/1.x $firstLineEnd = strpos($header, "\r\n"); if (!preg_match( - '~^(?-i:GET|POST|OPTIONS|HEAD|DELETE|PUT|PATCH) /[^\x00-\x20\x7f]* (?-i:HTTP)/1\.(?[0-9])$~', + '~^(?-i:GET|POST|OPTIONS|HEAD|DELETE|PUT|PATCH|TRACE) /[^\x00-\x20\x7f]* (?-i:HTTP)/1\.(?[0-9])$~', substr($header, 0, $firstLineEnd), $matches )) { @@ -145,6 +159,12 @@ public static function input(string $buffer, TcpConnection $connection): int return 0; } + // Check if the method is disabled + if (in_array($matches[0], static::$disabledMethods)) { + $connection->end(static::HTTP_405, true); + return 0; + } + // Parse headers $headers = []; $headerBody = substr($header, $firstLineEnd + 2, $crlfPos - $firstLineEnd - 2); diff --git a/tests/Unit/Protocols/HttpTest.php b/tests/Unit/Protocols/HttpTest.php index ca139f19..32a218ab 100644 --- a/tests/Unit/Protocols/HttpTest.php +++ b/tests/Unit/Protocols/HttpTest.php @@ -213,6 +213,29 @@ ]); }); +describe('Disabled Http methods return 405', function () { + it('rejects method', function (string $buffer) { + testWithConnectionEnd(function (TcpConnection $tcpConnection) use ($buffer) { + expect(Http::input($buffer, $tcpConnection))->toBe(0); + }, '405 Method Not Allowed'); + })->with([ + 'OPTIONS' => ["OPTIONS / HTTP/1.1\r\n\r\n"], + 'TRACE' => ["TRACE / HTTP/1.1\r\n\r\n"], + ]); + + it('rejects manual method', function (string $buffer) { + testWithConnectionEnd(function (TcpConnection $tcpConnection) use ($buffer) { + Http::$disabledMethods[] = 'DELETE'; + Http::$disabledMethods[] = 'POST'; + expect(Http::input($buffer, $tcpConnection))->toBe(0); + }, '405 Method Not Allowed'); + })->with([ + 'DELETE' => ["DELETE / HTTP/1.1\r\n\r\n"], + 'POST' => ["POST / HTTP/1.1\r\n\r\n"], + 'TRACE' => ["TRACE / HTTP/1.1\r\n\r\n"], + ]); +}); + describe('HTTP/1.0', function () { it('accepts minimal GET without Host header', function () { /** @var TcpConnection&\Mockery\MockInterface $tcpConnection */