Skip to content

Commit 5ee9ac5

Browse files
committed
Add (and pass) HTTP spec check
1 parent 5b6d685 commit 5ee9ac5

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

src/HttpParser.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ struct HttpParser {
390390
/* We should not accept whitespace between key and colon, so colon must foloow immediately */
391391
if (postPaddedBuffer[0] != ':') {
392392
/* Error: invalid chars in field name */
393+
err = HTTP_ERROR_400_BAD_REQUEST;
393394
return 0;
394395
}
395396
postPaddedBuffer++;
@@ -406,6 +407,7 @@ struct HttpParser {
406407
continue;
407408
}
408409
/* Error - invalid chars in field value */
410+
err = HTTP_ERROR_400_BAD_REQUEST;
409411
return 0;
410412
}
411413
break;
@@ -437,6 +439,9 @@ struct HttpParser {
437439
return (unsigned int) ((postPaddedBuffer + 2) - start);
438440
} else {
439441
/* \r\n\r plus non-\n letter is malformed request, or simply out of search space */
442+
if (postPaddedBuffer != end) {
443+
err = HTTP_ERROR_400_BAD_REQUEST;
444+
}
440445
return 0;
441446
}
442447
}

tests/http_test.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
const encoder = new TextEncoder();
2+
const decoder = new TextDecoder();
3+
4+
// Define test cases
5+
interface TestCase {
6+
request: string;
7+
description: string;
8+
expectedStatus: [number, number][];
9+
}
10+
11+
const testCases: TestCase[] = [
12+
{
13+
request: "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
14+
description: "Valid GET request",
15+
expectedStatus: [[200, 299]],
16+
},
17+
{
18+
request: "GET / HTTP/1.1\r\nHost: example.com\r\nX-Invalid[]: test\r\n\r\n",
19+
description: "Invalid header characters",
20+
expectedStatus: [[400, 499]],
21+
},
22+
{
23+
request: "GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\n",
24+
description: "Missing Host header",
25+
expectedStatus: [[400, 499]],
26+
},
27+
{
28+
request: "GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: -123456789123456789123456789\r\n\r\n",
29+
description: "Overflowing negative Content-Length header",
30+
expectedStatus: [[400, 499]],
31+
},
32+
{
33+
request: "GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: -1234\r\n\r\n",
34+
description: "Negative Content-Length header",
35+
expectedStatus: [[400, 499]],
36+
},
37+
{
38+
request: "GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: abc\r\n\r\n",
39+
description: "Non-numeric Content-Length header",
40+
expectedStatus: [[400, 499]],
41+
},
42+
{
43+
request: "GET / HTTP/1.1\r\nHost: example.com\r\nX-Empty-Header: \r\n\r\n",
44+
description: "Empty header value",
45+
expectedStatus: [[200, 299]],
46+
},
47+
{
48+
request: "GET / HTTP/1.1\r\nHost: example.com\r\nX-Bad-Control-Char: test\x07\r\n\r\n",
49+
description: "Header containing invalid control character",
50+
expectedStatus: [[400, 499]],
51+
},
52+
{
53+
request: "GET / HTTP/9.9\r\nHost: example.com\r\n\r\n",
54+
description: "Invalid HTTP version",
55+
expectedStatus: [[400, 499], [500, 599]],
56+
},
57+
{
58+
request: "Extra lineGET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
59+
description: "Invalid prefix of request",
60+
expectedStatus: [[400, 499], [500, 599]],
61+
},
62+
{
63+
request: "GET / HTTP/1.1\r\nHost: example.com\r\n\rSome-Header: Test\r\n\r\n",
64+
description: "Invalid line ending",
65+
expectedStatus: [[400, 499]],
66+
},
67+
{
68+
request: "POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello",
69+
description: "Valid POST request with body",
70+
expectedStatus: [[200, 299], [404, 404]],
71+
},
72+
{
73+
request: "GET / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n",
74+
description: "Conflicting Transfer-Encoding and Content-Length",
75+
expectedStatus: [[400, 499]],
76+
},
77+
];
78+
79+
// Get host and port from command-line arguments
80+
const [host, port] = Deno.args;
81+
if (!host || !port) {
82+
console.error("Usage: deno run --allow-net tcp_http_test.ts <host> <port>");
83+
Deno.exit(1);
84+
}
85+
86+
// Run all test cases in parallel
87+
async function runTests() {
88+
const results = await Promise.all(
89+
testCases.map((testCase) => runTestCase(testCase, host, parseInt(port, 10)))
90+
);
91+
92+
const passedCount = results.filter((result) => result).length;
93+
console.log(`\n${passedCount} out of ${testCases.length} tests passed.`);
94+
}
95+
96+
// Run a single test case with a 3-second timeout on reading
97+
async function runTestCase(testCase: TestCase, host: string, port: number): Promise<boolean> {
98+
try {
99+
const conn = await Deno.connect({ hostname: host, port });
100+
101+
// Send the request
102+
await conn.write(encoder.encode(testCase.request));
103+
104+
// Set up a read timeout promise
105+
const readTimeout = new Promise<boolean>((resolve) => {
106+
const timeoutId = setTimeout(() => {
107+
console.error(`❌ ${testCase.description}: Read operation timed out`);
108+
conn.close(); // Ensure the connection is closed on timeout
109+
resolve(false);
110+
}, 500);
111+
112+
const readPromise = (async () => {
113+
const buffer = new Uint8Array(1024);
114+
try {
115+
const bytesRead = await conn.read(buffer);
116+
117+
// Clear the timeout if read completes
118+
clearTimeout(timeoutId);
119+
const response = decoder.decode(buffer.subarray(0, bytesRead || 0));
120+
const statusCode = parseStatusCode(response);
121+
122+
const isSuccess = testCase.expectedStatus.some(
123+
([min, max]) => statusCode >= min && statusCode <= max
124+
);
125+
126+
console.log(
127+
`${isSuccess ? "✅" : "❌"} ${testCase.description}: Response Status Code ${statusCode}, Expected ranges: ${JSON.stringify(testCase.expectedStatus)}`
128+
);
129+
return resolve(isSuccess);
130+
} catch {
131+
132+
}
133+
})();
134+
});
135+
136+
// Wait for the read operation or timeout
137+
return await readTimeout;
138+
139+
} catch (error) {
140+
console.error(`Error in test "${testCase.description}":`, error);
141+
return false;
142+
}
143+
}
144+
145+
146+
// Parse the HTTP status code from the response
147+
function parseStatusCode(response: string): number {
148+
const statusLine = response.split("\r\n")[0];
149+
const match = statusLine.match(/HTTP\/1\.\d (\d{3})/);
150+
return match ? parseInt(match[1], 10) : 0;
151+
}
152+
153+
// Run all tests
154+
runTests();

0 commit comments

Comments
 (0)