Skip to content

Commit

Permalink
Add (and pass) HTTP spec check
Browse files Browse the repository at this point in the history
  • Loading branch information
uNetworkingAB committed Oct 25, 2024
1 parent 5b6d685 commit 5ee9ac5
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/HttpParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ struct HttpParser {
/* We should not accept whitespace between key and colon, so colon must foloow immediately */
if (postPaddedBuffer[0] != ':') {
/* Error: invalid chars in field name */
err = HTTP_ERROR_400_BAD_REQUEST;
return 0;
}
postPaddedBuffer++;
Expand All @@ -406,6 +407,7 @@ struct HttpParser {
continue;
}
/* Error - invalid chars in field value */
err = HTTP_ERROR_400_BAD_REQUEST;
return 0;
}
break;
Expand Down Expand Up @@ -437,6 +439,9 @@ struct HttpParser {
return (unsigned int) ((postPaddedBuffer + 2) - start);
} else {
/* \r\n\r plus non-\n letter is malformed request, or simply out of search space */
if (postPaddedBuffer != end) {
err = HTTP_ERROR_400_BAD_REQUEST;
}
return 0;
}
}
Expand Down
154 changes: 154 additions & 0 deletions tests/http_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
const encoder = new TextEncoder();
const decoder = new TextDecoder();

// Define test cases
interface TestCase {
request: string;
description: string;
expectedStatus: [number, number][];
}

const testCases: TestCase[] = [
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Valid GET request",
expectedStatus: [[200, 299]],
},
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\nX-Invalid[]: test\r\n\r\n",
description: "Invalid header characters",
expectedStatus: [[400, 499]],
},
{
request: "GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\n",
description: "Missing Host header",
expectedStatus: [[400, 499]],
},
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: -123456789123456789123456789\r\n\r\n",
description: "Overflowing negative Content-Length header",
expectedStatus: [[400, 499]],
},
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: -1234\r\n\r\n",
description: "Negative Content-Length header",
expectedStatus: [[400, 499]],
},
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: abc\r\n\r\n",
description: "Non-numeric Content-Length header",
expectedStatus: [[400, 499]],
},
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\nX-Empty-Header: \r\n\r\n",
description: "Empty header value",
expectedStatus: [[200, 299]],
},
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\nX-Bad-Control-Char: test\x07\r\n\r\n",
description: "Header containing invalid control character",
expectedStatus: [[400, 499]],
},
{
request: "GET / HTTP/9.9\r\nHost: example.com\r\n\r\n",
description: "Invalid HTTP version",
expectedStatus: [[400, 499], [500, 599]],
},
{
request: "Extra lineGET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Invalid prefix of request",
expectedStatus: [[400, 499], [500, 599]],
},
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\n\rSome-Header: Test\r\n\r\n",
description: "Invalid line ending",
expectedStatus: [[400, 499]],
},
{
request: "POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello",
description: "Valid POST request with body",
expectedStatus: [[200, 299], [404, 404]],
},
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n",
description: "Conflicting Transfer-Encoding and Content-Length",
expectedStatus: [[400, 499]],
},
];

// Get host and port from command-line arguments
const [host, port] = Deno.args;
if (!host || !port) {
console.error("Usage: deno run --allow-net tcp_http_test.ts <host> <port>");
Deno.exit(1);
}

// Run all test cases in parallel
async function runTests() {
const results = await Promise.all(
testCases.map((testCase) => runTestCase(testCase, host, parseInt(port, 10)))
);

const passedCount = results.filter((result) => result).length;
console.log(`\n${passedCount} out of ${testCases.length} tests passed.`);
}

// Run a single test case with a 3-second timeout on reading
async function runTestCase(testCase: TestCase, host: string, port: number): Promise<boolean> {
try {
const conn = await Deno.connect({ hostname: host, port });

// Send the request
await conn.write(encoder.encode(testCase.request));

// Set up a read timeout promise
const readTimeout = new Promise<boolean>((resolve) => {
const timeoutId = setTimeout(() => {
console.error(`❌ ${testCase.description}: Read operation timed out`);
conn.close(); // Ensure the connection is closed on timeout
resolve(false);
}, 500);

const readPromise = (async () => {
const buffer = new Uint8Array(1024);
try {
const bytesRead = await conn.read(buffer);

// Clear the timeout if read completes
clearTimeout(timeoutId);
const response = decoder.decode(buffer.subarray(0, bytesRead || 0));
const statusCode = parseStatusCode(response);

const isSuccess = testCase.expectedStatus.some(
([min, max]) => statusCode >= min && statusCode <= max
);

console.log(
`${isSuccess ? "✅" : "❌"} ${testCase.description}: Response Status Code ${statusCode}, Expected ranges: ${JSON.stringify(testCase.expectedStatus)}`
);
return resolve(isSuccess);
} catch {

}
})();
});

// Wait for the read operation or timeout
return await readTimeout;

} catch (error) {
console.error(`Error in test "${testCase.description}":`, error);
return false;
}
}


// Parse the HTTP status code from the response
function parseStatusCode(response: string): number {
const statusLine = response.split("\r\n")[0];
const match = statusLine.match(/HTTP\/1\.\d (\d{3})/);
return match ? parseInt(match[1], 10) : 0;
}

// Run all tests
runTests();

0 comments on commit 5ee9ac5

Please sign in to comment.