parser = new HeaderParser(); } public function testParseRawHeadersEmpty(): void { $result = $this->parser->parseRawHeaders(''); $this->assertSame([], $result->toArray()); } public function testParseRawHeadersSimple(): void { $rawHeaders = "Content-Type: application/json\r\nContent-Length: 123\r\n"; $result = $this->parser->parseRawHeaders($rawHeaders); $this->assertSame('application/json', $result->getFirst('Content-Type')); $this->assertSame('123', $result->getFirst('Content-Length')); } public function testParseRawHeadersMultipleValues(): void { $rawHeaders = "Set-Cookie: session=abc\r\nSet-Cookie: user=xyz\r\n"; $result = $this->parser->parseRawHeaders($rawHeaders); $cookies = $result->get('Set-Cookie'); $this->assertIsArray($cookies); $this->assertSame(['session=abc', 'user=xyz'], $cookies); } public function testParseRawHeadersSkipsRequestLine(): void { $rawHeaders = "GET /test HTTP/1.1\r\nHost: example.com\r\n"; $result = $this->parser->parseRawHeaders($rawHeaders); $this->assertSame('example.com', $result->getFirst('Host')); $this->assertNull($result->getFirst('GET')); } public function testParseRawHeadersStopsAtEmptyLine(): void { $rawHeaders = "Host: example.com\r\n\r\nBody content here"; $result = $this->parser->parseRawHeaders($rawHeaders); $this->assertSame('example.com', $result->getFirst('Host')); $this->assertNull($result->getFirst('Body')); } public function testParseFromServerArrayStandard(): void { $server = [ 'HTTP_HOST' => 'example.com', 'HTTP_USER_AGENT' => 'TestAgent/1.0', 'HTTP_ACCEPT' => 'application/json', 'HTTP_X_FORWARDED_FOR' => '192.168.1.1', 'REQUEST_METHOD' => 'GET', // Should be ignored 'SERVER_NAME' => 'example.com', // Should be ignored ]; $result = $this->parser->parseFromServerArray($server); $this->assertSame('example.com', $result->getFirst('Host')); $this->assertSame('TestAgent/1.0', $result->getFirst('User-Agent')); $this->assertSame('application/json', $result->getFirst('Accept')); $this->assertSame('192.168.1.1', $result->getFirst('X-Forwarded-For')); $this->assertNull($result->getFirst('Request-Method')); } public function testParseFromServerArraySpecialHeaders(): void { $server = [ 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => '1234', 'CONTENT_MD5' => 'abc123', ]; $result = $this->parser->parseFromServerArray($server); $this->assertSame('application/json', $result->getFirst('Content-Type')); $this->assertSame('1234', $result->getFirst('Content-Length')); $this->assertSame('abc123', $result->getFirst('Content-Md5')); } public function testParseFromServerArrayBasicAuth(): void { $server = [ 'PHP_AUTH_USER' => 'testuser', 'PHP_AUTH_PW' => 'testpass', ]; $result = $this->parser->parseFromServerArray($server); $expected = 'Basic ' . base64_encode('testuser:testpass'); $this->assertSame($expected, $result->getFirst('Authorization')); } public function testParseFromServerArrayDigestAuth(): void { $server = [ 'PHP_AUTH_DIGEST' => 'username="test", realm="api"', ]; $result = $this->parser->parseFromServerArray($server); $this->assertSame('Digest username="test", realm="api"', $result->getFirst('Authorization')); } public function testParseFromServerArrayIgnoresNonStringValues(): void { $server = [ 'HTTP_HOST' => 'example.com', 'HTTP_PORT' => 8080, // Integer should be ignored 'HTTP_ARRAY' => ['value1', 'value2'], // Array should be ignored 'HTTP_NULL' => null, // Null should be ignored ]; $result = $this->parser->parseFromServerArray($server); $this->assertSame('example.com', $result->getFirst('Host')); $this->assertNull($result->getFirst('Port')); $this->assertNull($result->getFirst('Array')); $this->assertNull($result->getFirst('Null')); } public function testParseContentTypeSimple(): void { $result = $this->parser->parseContentType('application/json'); $this->assertSame(['type' => 'application/json'], $result); } public function testParseContentTypeWithCharset(): void { $result = $this->parser->parseContentType('text/html; charset=utf-8'); $this->assertSame([ 'type' => 'text/html', 'charset' => 'utf-8', ], $result); } public function testParseContentTypeWithBoundary(): void { $result = $this->parser->parseContentType('multipart/form-data; boundary=----FormBoundary123'); $this->assertSame([ 'type' => 'multipart/form-data', 'boundary' => '----FormBoundary123', ], $result); } public function testParseContentTypeWithMultipleParameters(): void { $result = $this->parser->parseContentType('text/html; charset=utf-8; boundary=test; other=ignored'); $this->assertSame([ 'type' => 'text/html', 'charset' => 'utf-8', 'boundary' => 'test', ], $result); } public function testParseContentTypeWithQuotedValues(): void { $result = $this->parser->parseContentType('multipart/form-data; boundary="----FormBoundary123"'); $this->assertSame([ 'type' => 'multipart/form-data', 'boundary' => '----FormBoundary123', ], $result); } public function testParseContentTypeWithSpaces(): void { $result = $this->parser->parseContentType(' text/html ; charset = utf-8 ; boundary = test '); $this->assertSame([ 'type' => 'text/html', 'charset' => 'utf-8', 'boundary' => 'test', ], $result); } public function testNormalizeHeaderNameFromServer(): void { $server = [ 'HTTP_CONTENT_TYPE' => 'application/json', 'HTTP_X_FORWARDED_FOR' => '192.168.1.1', 'HTTP_ACCEPT_ENCODING' => 'gzip', 'HTTP_USER_AGENT' => 'TestAgent', ]; $result = $this->parser->parseFromServerArray($server); $this->assertSame('application/json', $result->getFirst('Content-Type')); $this->assertSame('192.168.1.1', $result->getFirst('X-Forwarded-For')); $this->assertSame('gzip', $result->getFirst('Accept-Encoding')); $this->assertSame('TestAgent', $result->getFirst('User-Agent')); } // Security Tests public function testHeaderCountLimitExceeded(): void { $config = new ParserConfig(maxHeaderCount: 2); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Header count exceeded: 3 headers > 2 maximum'); $rawHeaders = "Header1: value1\r\nHeader2: value2\r\nHeader3: value3\r\n"; $parser->parseRawHeaders($rawHeaders); } public function testHeaderSizeExceeded(): void { $config = new ParserConfig(maxTotalHeaderSize: new \App\Framework\Core\ValueObjects\Byte(50)); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Total header size exceeded'); $longValue = str_repeat('x', 100); $rawHeaders = "LongHeader: {$longValue}\r\n"; $parser->parseRawHeaders($rawHeaders); } public function testHeaderNameTooLong(): void { $config = new ParserConfig(maxHeaderNameLength: 10); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Header name too long'); $rawHeaders = "VeryLongHeaderName: value\r\n"; $parser->parseRawHeaders($rawHeaders); } public function testHeaderValueTooLong(): void { $config = new ParserConfig(maxHeaderValueLength: 10); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Header value too long'); $rawHeaders = "Header: verylongheadervaluethatexceedslimit\r\n"; $parser->parseRawHeaders($rawHeaders); } public function testMaliciousScriptInjection(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Suspicious content detected'); $rawHeaders = "XSS: \r\n"; $parser->parseRawHeaders($rawHeaders); } public function testMaliciousJavaScriptUrl(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Suspicious content detected'); $rawHeaders = "Redirect: javascript:alert('xss')\r\n"; $parser->parseRawHeaders($rawHeaders); } public function testControlCharactersDetected(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Control characters detected'); $rawHeaders = "Header: value\x00nullbyte\r\n"; $parser->parseRawHeaders($rawHeaders); } public function testCrlfInjectionDetected(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('CRLF injection detected'); // CRLF injection within a single header value $rawHeaders = "Header: value-with\r-crlf\r\n"; $parser->parseRawHeaders($rawHeaders); } public function testSuspiciousSecurityHeaderValue(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Potentially dangerous security header value'); $rawHeaders = "X-XSS-Protection: none\r\n"; $parser->parseRawHeaders($rawHeaders); } public function testSuspiciousBase64Value(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Suspicious base64 encoded value'); // Create a proper base64 string that's over 1000 characters $longBase64 = str_repeat('A', 1001); // Simple base64-like string $rawHeaders = "Data: {$longBase64}\r\n"; $parser->parseRawHeaders($rawHeaders); } // Server Array Security Tests public function testServerArrayHeaderCountLimit(): void { $config = new ParserConfig(maxHeaderCount: 2); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Header count exceeded'); $server = [ 'HTTP_HEADER1' => 'value1', 'HTTP_HEADER2' => 'value2', 'HTTP_HEADER3' => 'value3', ]; $parser->parseFromServerArray($server); } public function testServerArrayHeaderNameTooLong(): void { $config = new ParserConfig(maxHeaderNameLength: 10); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Header name too long'); $server = [ 'HTTP_VERY_LONG_HEADER_NAME' => 'value', ]; $parser->parseFromServerArray($server); } public function testServerArrayHeaderValueTooLong(): void { $config = new ParserConfig(maxHeaderValueLength: 10); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Header value too long'); $server = [ 'HTTP_HEADER' => 'verylongheadervaluethatexceedslimit', ]; $parser->parseFromServerArray($server); } public function testServerArrayMaliciousContent(): void { $config = new ParserConfig(scanForMaliciousContent: true); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Suspicious content detected'); $server = [ 'HTTP_XSS' => '', ]; $parser->parseFromServerArray($server); } public function testAuthHeaderValueTooLong(): void { $config = new ParserConfig(maxHeaderValueLength: 20); $parser = new HeaderParser($config); $this->expectException(ParserSecurityException::class); $this->expectExceptionMessage('Header value too long'); $server = [ 'PHP_AUTH_USER' => 'verylongusernamethatexceedslimit', 'PHP_AUTH_PW' => 'verylongpasswordthatexceedslimit', ]; $parser->parseFromServerArray($server); } // Security Configuration Tests public function testSecurityDisabled(): void { $config = new ParserConfig( scanForMaliciousContent: false, maxHeaderCount: 1000, maxHeaderNameLength: 1000, maxHeaderValueLength: 1000 ); $parser = new HeaderParser($config); // Should not throw exception when security is disabled $rawHeaders = "XSS: \r\n"; $result = $parser->parseRawHeaders($rawHeaders); $this->assertSame('', $result->getFirst('XSS')); } public function testWithinSecurityLimits(): void { $config = new ParserConfig( maxHeaderCount: 5, maxHeaderNameLength: 20, maxHeaderValueLength: 50, scanForMaliciousContent: true ); $parser = new HeaderParser($config); // Should work fine within limits $rawHeaders = "Host: example.com\r\nUser-Agent: TestAgent/1.0\r\nAccept: application/json\r\n"; $result = $parser->parseRawHeaders($rawHeaders); $this->assertSame('example.com', $result->getFirst('Host')); $this->assertSame('TestAgent/1.0', $result->getFirst('User-Agent')); $this->assertSame('application/json', $result->getFirst('Accept')); } }