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'));
}
}