Fix HTTP spec issues by upgrading uWS version (#14853)

This commit is contained in:
Jarred Sumner
2024-10-27 12:34:45 -07:00
committed by GitHub
parent e93c5ad993
commit f005e8c057
16 changed files with 1295 additions and 712 deletions

View File

@@ -16,8 +16,7 @@
* limitations under the License.
*/
// clang-format off
#ifndef UWS_APP_H
#define UWS_APP_H
#include <string>
#include <charconv>
@@ -619,4 +618,3 @@ typedef TemplatedApp<true> SSLApp;
}
#endif // UWS_APP_H

View File

@@ -16,8 +16,7 @@
* limitations under the License.
*/
#ifndef UWS_HTTPCONTEXT_H
#define UWS_HTTPCONTEXT_H
#pragma once
/* This class defines the main behavior of HTTP and emits various events */
@@ -27,6 +26,8 @@
#include "AsyncSocket.h"
#include "WebSocketData.h"
#include <string>
#include <map>
#include <string_view>
#include <iostream>
#include "MoveOnlyFunction.h"
@@ -171,7 +172,7 @@ private:
#endif
/* The return value is entirely up to us to interpret. The HttpParser only care for whether the returned value is DIFFERENT or not from passed user */
void *returnedSocket = httpResponseData->consumePostPadded(data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
auto [err, returnedSocket] = httpResponseData->consumePostPadded(data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
/* For every request we reset the timeout and hang until user makes action */
/* Warning: if we are in shutdown state, resetting the timer is a security issue! */
us_socket_timeout(SSL, (us_socket_t *) s, 0);
@@ -180,7 +181,9 @@ private:
HttpResponseData<SSL> *httpResponseData = (HttpResponseData<SSL> *) us_socket_ext(SSL, (us_socket_t *) s);
httpResponseData->offset = 0;
/* Are we not ready for another request yet? Terminate the connection. */
/* Are we not ready for another request yet? Terminate the connection.
* Important for denying async pipelining until, if ever, we want to suppot it.
* Otherwise requests can get mixed up on the same connection. We still support sync pipelining. */
if (httpResponseData->state & HttpResponseData<SSL>::HTTP_RESPONSE_PENDING) {
us_socket_close(SSL, (us_socket_t *) s, 0, nullptr);
return nullptr;
@@ -280,10 +283,6 @@ private:
}
}
return user;
}, [](void *user) {
/* Close any socket on HTTP errors */
us_socket_close(SSL, (us_socket_t *) user, 0, nullptr);
return nullptr;
});
/* Mark that we are no longer parsing Http */
@@ -291,6 +290,9 @@ private:
/* If we got fullptr that means the parser wants us to close the socket from error (same as calling the errorHandler) */
if (returnedSocket == FULLPTR) {
/* For errors, we only deliver them "at most once". We don't care if they get halfways delivered or not. */
us_socket_write(SSL, s, httpErrorResponses[err].data(), (int) httpErrorResponses[err].length(), false);
us_socket_shutdown(SSL, s);
/* Close any socket on HTTP errors */
us_socket_close(SSL, s, 0, nullptr);
/* This just makes the following code act as if the socket was closed from error inside the parser. */
@@ -299,9 +301,8 @@ private:
/* We need to uncork in all cases, except for nullptr (closed socket, or upgraded socket) */
if (returnedSocket != nullptr) {
us_socket_t* returnedSocketPtr = (us_socket_t*) returnedSocket;
/* We don't want open sockets to keep the event loop alive between HTTP requests */
us_socket_unref(returnedSocketPtr);
us_socket_unref((us_socket_t *) returnedSocket);
/* Timeout on uncork failure */
auto [written, failed] = ((AsyncSocket<SSL> *) returnedSocket)->uncork();
@@ -321,7 +322,7 @@ private:
}
}
}
return returnedSocketPtr;
return (us_socket_t *) returnedSocket;
}
/* If we upgraded, check here (differ between nullptr close and nullptr upgrade) */
@@ -483,10 +484,27 @@ public:
return;
}
httpContextData->currentRouter->add(methods, pattern, [handler = std::move(handler)](auto *r) mutable {
/* Record this route's parameter offsets */
std::map<std::string, unsigned short, std::less<>> parameterOffsets;
unsigned short offset = 0;
for (unsigned int i = 0; i < pattern.length(); i++) {
if (pattern[i] == ':') {
i++;
unsigned int start = i;
while (i < pattern.length() && pattern[i] != '/') {
i++;
}
parameterOffsets[std::string(pattern.data() + start, i - start)] = offset;
//std::cout << "<" << std::string(pattern.data() + start, i - start) << "> is offset " << offset;
offset++;
}
}
httpContextData->currentRouter->add(methods, pattern, [handler = std::move(handler), parameterOffsets = std::move(parameterOffsets)](auto *r) mutable {
auto user = r->getUserData();
user.httpRequest->setYield(false);
user.httpRequest->setParameters(r->getParameters());
user.httpRequest->setParameterOffsets(&parameterOffsets);
/* Middleware? Automatically respond to expectations */
std::string_view expect = user.httpRequest->getHeader("expect");
@@ -528,4 +546,4 @@ public:
}
#endif // UWS_HTTPCONTEXT_H

View File

@@ -0,0 +1,53 @@
/*
* Authored by Alex Hultman, 2018-2023.
* Intellectual property of third-party.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef UWS_HTTP_ERRORS
#define UWS_HTTP_ERRORS
#include <string_view>
namespace uWS {
/* Possible errors from http parsing */
enum HttpError {
HTTP_ERROR_505_HTTP_VERSION_NOT_SUPPORTED = 1,
HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 2,
HTTP_ERROR_400_BAD_REQUEST = 3
};
#ifndef UWS_HTTPRESPONSE_NO_WRITEMARK
/* Returned parser errors match this LUT. */
static const std::string_view httpErrorResponses[] = {
"", /* Zeroth place is no error so don't use it */
"HTTP/1.1 505 HTTP Version Not Supported\r\nConnection: close\r\n\r\n<h1>HTTP Version Not Supported</h1><p>This server does not support HTTP/1.0.</p><hr><i>uWebSockets/20 Server</i>",
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n<h1>Request Header Fields Too Large</h1><hr><i>uWebSockets/20 Server</i>",
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n<h1>Bad Request</h1><hr><i>uWebSockets/20 Server</i>",
};
#else
/* Anonymized pages */
static const std::string_view httpErrorResponses[] = {
"", /* Zeroth place is no error so don't use it */
"HTTP/1.1 505 HTTP Version Not Supported\r\nConnection: close\r\n\r\n",
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n",
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"
};
#endif
}
#endif

View File

@@ -0,0 +1,42 @@
/*
* Authored by Alex Hultman, 2018-2023.
* Intellectual property of third-party.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <string_view>
namespace uWS {
/* Possible errors from http parsing */
enum HttpError {
HTTP_ERROR_505_HTTP_VERSION_NOT_SUPPORTED = 1,
HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 2,
HTTP_ERROR_400_BAD_REQUEST = 3
};
/* Anonymized pages */
static const std::string_view httpErrorResponses[] = {
"", /* Zeroth place is no error so don't use it */
"HTTP/1.1 505 HTTP Version Not Supported\r\nConnection: close\r\n\r\n",
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n",
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,7 @@
* limitations under the License.
*/
// clang-format off
#ifndef UWS_HTTPRESPONSEDATA_H
#define UWS_HTTPRESPONSEDATA_H
#pragma once
/* This data belongs to the HttpResponse */
@@ -106,4 +105,4 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
}
#endif // UWS_HTTPRESPONSEDATA_H

View File

@@ -15,9 +15,7 @@
* limitations under the License.
*/
#ifndef UWS_TOPICTREE_H
#define UWS_TOPICTREE_H
#pragma once
#include <map>
#include <list>
#include <iostream>
@@ -366,4 +364,4 @@ public:
}
#endif

View File

@@ -307,174 +307,174 @@ const binaryTypes = {
"float32array": Float32Array,
"float64array": Float64Array,
} as const;
expect.extend({
toHaveTestTimedOutAfter(actual: any, expected: number) {
if (typeof actual !== "string") {
return {
pass: false,
message: () => `Expected ${actual} to be a string`,
};
}
const preStartI = actual.indexOf("timed out after ");
if (preStartI === -1) {
return {
pass: false,
message: () => `Expected ${actual} to contain "timed out after "`,
};
}
const startI = preStartI + "timed out after ".length;
const endI = actual.indexOf("ms", startI);
if (endI === -1) {
return {
pass: false,
message: () => `Expected ${actual} to contain "ms" after "timed out after "`,
};
}
const int = parseInt(actual.slice(startI, endI));
if (!Number.isSafeInteger(int)) {
return {
pass: false,
message: () => `Expected ${int} to be a safe integer`,
};
}
return {
pass: int >= expected,
message: () => `Expected ${int} to be >= ${expected}`,
};
},
toBeBinaryType(actual: any, expected: keyof typeof binaryTypes) {
switch (expected) {
case "buffer":
if (expect.extend)
expect.extend({
toHaveTestTimedOutAfter(actual: any, expected: number) {
if (typeof actual !== "string") {
return {
pass: Buffer.isBuffer(actual),
message: () => `Expected ${actual} to be buffer`,
pass: false,
message: () => `Expected ${actual} to be a string`,
};
case "arraybuffer":
}
const preStartI = actual.indexOf("timed out after ");
if (preStartI === -1) {
return {
pass: actual instanceof ArrayBuffer,
message: () => `Expected ${actual} to be ArrayBuffer`,
pass: false,
message: () => `Expected ${actual} to contain "timed out after "`,
};
default: {
const ctor = binaryTypes[expected];
if (!ctor) {
}
const startI = preStartI + "timed out after ".length;
const endI = actual.indexOf("ms", startI);
if (endI === -1) {
return {
pass: false,
message: () => `Expected ${actual} to contain "ms" after "timed out after "`,
};
}
const int = parseInt(actual.slice(startI, endI));
if (!Number.isSafeInteger(int)) {
return {
pass: false,
message: () => `Expected ${int} to be a safe integer`,
};
}
return {
pass: int >= expected,
message: () => `Expected ${int} to be >= ${expected}`,
};
},
toBeBinaryType(actual: any, expected: keyof typeof binaryTypes) {
switch (expected) {
case "buffer":
return {
pass: Buffer.isBuffer(actual),
message: () => `Expected ${actual} to be buffer`,
};
case "arraybuffer":
return {
pass: actual instanceof ArrayBuffer,
message: () => `Expected ${actual} to be ArrayBuffer`,
};
default: {
const ctor = binaryTypes[expected];
if (!ctor) {
return {
pass: false,
message: () => `Expected ${expected} to be a binary type`,
};
}
return {
pass: actual instanceof ctor,
message: () => `Expected ${actual} to be ${expected}`,
};
}
}
},
toRun(cmds: string[], optionalStdout?: string, expectedCode: number = 0) {
const result = Bun.spawnSync({
cmd: [bunExe(), ...cmds],
env: bunEnv,
stdio: ["inherit", "pipe", "inherit"],
});
if (result.exitCode !== expectedCode) {
return {
pass: false,
message: () => `Command ${cmds.join(" ")} failed:` + "\n" + result.stdout.toString("utf-8"),
};
}
if (optionalStdout != null) {
return {
pass: result.stdout.toString("utf-8") === optionalStdout,
message: () =>
`Expected ${cmds.join(" ")} to output ${optionalStdout} but got ${result.stdout.toString("utf-8")}`,
};
}
return {
pass: true,
message: () => `Expected ${cmds.join(" ")} to fail`,
};
},
toThrowWithCode(fn: CallableFunction, cls: CallableFunction, code: string) {
try {
fn();
return {
pass: false,
message: () => `Received function did not throw`,
};
} catch (e) {
// expect(e).toBeInstanceOf(cls);
if (!(e instanceof cls)) {
return {
pass: false,
message: () => `Expected ${expected} to be a binary type`,
message: () => `Expected error to be instanceof ${cls.name}; got ${e.__proto__.constructor.name}`,
};
}
// expect(e).toHaveProperty("code");
if (!("code" in e)) {
return {
pass: false,
message: () => `Expected error to have property 'code'; got ${e}`,
};
}
// expect(e.code).toEqual(code);
if (e.code !== code) {
return {
pass: false,
message: () => `Expected error to have code '${code}'; got ${e.code}`,
};
}
return {
pass: actual instanceof ctor,
message: () => `Expected ${actual} to be ${expected}`,
pass: true,
};
}
}
},
toRun(cmds: string[], optionalStdout?: string, expectedCode: number = 0) {
const result = Bun.spawnSync({
cmd: [bunExe(), ...cmds],
env: bunEnv,
stdio: ["inherit", "pipe", "inherit"],
});
if (result.exitCode !== expectedCode) {
return {
pass: false,
message: () => `Command ${cmds.join(" ")} failed:` + "\n" + result.stdout.toString("utf-8"),
};
}
if (optionalStdout != null) {
return {
pass: result.stdout.toString("utf-8") === optionalStdout,
message: () =>
`Expected ${cmds.join(" ")} to output ${optionalStdout} but got ${result.stdout.toString("utf-8")}`,
};
}
return {
pass: true,
message: () => `Expected ${cmds.join(" ")} to fail`,
};
},
toThrowWithCode(fn: CallableFunction, cls: CallableFunction, code: string) {
try {
fn();
return {
pass: false,
message: () => `Received function did not throw`,
};
} catch (e) {
// expect(e).toBeInstanceOf(cls);
if (!(e instanceof cls)) {
},
async toThrowWithCodeAsync(fn: CallableFunction, cls: CallableFunction, code: string) {
try {
await fn();
return {
pass: false,
message: () => `Expected error to be instanceof ${cls.name}; got ${e.__proto__.constructor.name}`,
message: () => `Received function did not throw`,
};
}
} catch (e) {
// expect(e).toBeInstanceOf(cls);
if (!(e instanceof cls)) {
return {
pass: false,
message: () => `Expected error to be instanceof ${cls.name}; got ${e.__proto__.constructor.name}`,
};
}
// expect(e).toHaveProperty("code");
if (!("code" in e)) {
return {
pass: false,
message: () => `Expected error to have property 'code'; got ${e}`,
};
}
// expect(e.code).toEqual(code);
if (e.code !== code) {
return {
pass: false,
message: () => `Expected error to have code '${code}'; got ${e.code}`,
};
}
// expect(e).toHaveProperty("code");
if (!("code" in e)) {
return {
pass: false,
message: () => `Expected error to have property 'code'; got ${e}`,
pass: true,
};
}
// expect(e.code).toEqual(code);
if (e.code !== code) {
return {
pass: false,
message: () => `Expected error to have code '${code}'; got ${e.code}`,
};
}
return {
pass: true,
};
}
},
async toThrowWithCodeAsync(fn: CallableFunction, cls: CallableFunction, code: string) {
try {
await fn();
return {
pass: false,
message: () => `Received function did not throw`,
};
} catch (e) {
// expect(e).toBeInstanceOf(cls);
if (!(e instanceof cls)) {
return {
pass: false,
message: () => `Expected error to be instanceof ${cls.name}; got ${e.__proto__.constructor.name}`,
};
}
// expect(e).toHaveProperty("code");
if (!("code" in e)) {
return {
pass: false,
message: () => `Expected error to have property 'code'; got ${e}`,
};
}
// expect(e.code).toEqual(code);
if (e.code !== code) {
return {
pass: false,
message: () => `Expected error to have code '${code}'; got ${e.code}`,
};
}
return {
pass: true,
};
}
},
});
},
});
export function ospath(path: string) {
if (isWindows) {
@@ -1115,34 +1115,35 @@ String.prototype.isUTF16 = function () {
return require("bun:internal-for-testing").jscInternals.isUTF16String(this);
};
expect.extend({
toBeLatin1String(actual: unknown) {
if ((actual as string).isLatin1()) {
if (expect.extend)
expect.extend({
toBeLatin1String(actual: unknown) {
if ((actual as string).isLatin1()) {
return {
pass: true,
message: () => `Expected ${actual} to be a Latin1 string`,
};
}
return {
pass: true,
pass: false,
message: () => `Expected ${actual} to be a Latin1 string`,
};
}
},
toBeUTF16String(actual: unknown) {
if ((actual as string).isUTF16()) {
return {
pass: true,
message: () => `Expected ${actual} to be a UTF16 string`,
};
}
return {
pass: false,
message: () => `Expected ${actual} to be a Latin1 string`,
};
},
toBeUTF16String(actual: unknown) {
if ((actual as string).isUTF16()) {
return {
pass: true,
pass: false,
message: () => `Expected ${actual} to be a UTF16 string`,
};
}
return {
pass: false,
message: () => `Expected ${actual} to be a UTF16 string`,
};
},
});
},
});
interface BunHarnessTestMatchers {
toBeLatin1String(): void;

View File

@@ -0,0 +1,7 @@
import { test, expect } from "bun:test";
import { runTests } from "./http-spec.ts";
test("https://github.com/uNetworking/h1spec tests pass", async () => {
const passed = await runTests();
expect(passed).toBe(true);
});

View File

@@ -0,0 +1,344 @@
// https://github.com/uNetworking/h1spec
// https://github.com/oven-sh/bun/issues/14826
// Thanks to Alex Hultman
import net from "net";
// Define test cases
interface TestCase {
request: string;
description: string;
expectedStatus: [number, number][];
expectedTimeout?: boolean;
}
const testCases: TestCase[] = [
{
request: "G",
description: "Fragmented method",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET ",
description: "Fragmented URL 1",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello",
description: "Fragmented URL 2",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello ",
description: "Fragmented URL 3",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP",
description: "Fragmented HTTP version",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP/1.1",
description: "Fragmented request line",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP/1.1\r",
description: "Fragmented request line newline 1",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP/1.1\r\n",
description: "Fragmented request line newline 2",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP/1.1\r\nHos",
description: "Fragmented field name",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP/1.1\r\nHost:",
description: "Fragmented field value 1",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP/1.1\r\nHost: ",
description: "Fragmented field value 2",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP/1.1\r\nHost: localhost",
description: "Fragmented field value 3",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP/1.1\r\nHost: localhost\r",
description: "Fragmented field value 4",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP/1.1\r\nHost: localhost\r\n",
description: "Fragmented request",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET /hello HTTP/1.1\r\nHost: localhost\r\n\r",
description: "Fragmented request termination",
expectedStatus: [[-1, -1]],
expectedTimeout: true,
},
{
request: "GET / \r\n\r\n",
description: "Request without HTTP version",
expectedStatus: [[400, 599]],
},
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\nExpect: 100-continue\r\n\r\n",
description: "Request with Expect header",
expectedStatus: [
[100, 100],
[200, 299],
],
},
{
request: "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Valid GET request",
expectedStatus: [[200, 299]],
},
{
request: "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n",
description: "Valid GET request with HTTP/1.0",
expectedStatus: [[200, 299]],
},
{
request: "GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Valid GET request for a proxy URL",
expectedStatus: [[200, 299]],
},
{
request: "GET https://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Valid GET request for an https proxy URL",
expectedStatus: [[200, 299]],
},
{
request: "GET HTTPS://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Valid GET request for an HTTPS proxy URL",
expectedStatus: [[200, 299]],
},
{
request: "GET HTTPZ://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Invalid GET request for an HTTPS proxy URL",
expectedStatus: [[400, 499]],
},
{
request: "GET H-TTP://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Invalid GET request for an HTTPS proxy URL",
expectedStatus: [[400, 499]],
},
{
request: "GET HTTP://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Valid GET request for an HTTP proxy URL",
expectedStatus: [[200, 299]],
},
{
request: "GET HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Invalid GET request target (space)",
expectedStatus: [[400, 499]],
},
{
request: "GET ^ HTTP/1.1\r\nHost: example.com\r\n\r\n",
description: "Invalid GET request target (caret)",
expectedStatus: [[400, 499]],
},
{
request: "GET / HTTP/1.1\r\nhoSt:\texample.com\r\nempty:\r\n\r\n",
description: "Valid GET request with edge cases",
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]],
},
];
export async function runTestsStandalone(host: string, port: number) {
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.`);
return passedCount === testCases.length;
}
// Run all test cases in parallel
export async function runTests() {
let host, port;
using server = Bun.serve({
port: 0,
fetch(req) {
return new Response("Hello, world!");
},
});
host = server.url.hostname;
port = server.url.port;
return await runTestsStandalone(host, port);
}
// 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 = new Promise((resolve, reject) => {
const client = net.createConnection({ host, port }, () => {
resolve(client);
});
client.on("error", reject);
});
const client: net.Socket = await conn;
// Send the request
client.write(Buffer.from(testCase.request));
// Set up a read timeout promise
const readTimeout = new Promise<boolean>(resolve => {
const timeoutId = setTimeout(() => {
if (testCase.expectedTimeout) {
console.log(`${testCase.description}: Server waited successfully`);
client.destroy(); // Ensure the connection is closed on timeout
resolve(true);
} else {
console.error(`${testCase.description}: Read operation timed out`);
client.destroy(); // Ensure the connection is closed on timeout
resolve(false);
}
}, 500);
client.on("data", data => {
// Clear the timeout if read completes
clearTimeout(timeoutId);
const response = data.toString();
const statusCode = parseStatusCode(response);
const isSuccess = testCase.expectedStatus.some(([min, max]) => statusCode >= min && statusCode <= max);
if (!isSuccess) {
console.log(JSON.stringify(response, null, 2));
}
console.log(
`${isSuccess ? "✅" : "❌"} ${
testCase.description
}: Response Status Code ${statusCode}, Expected ranges: ${JSON.stringify(testCase.expectedStatus)}`,
);
client.destroy();
resolve(isSuccess);
});
client.on("error", error => {
clearTimeout(timeoutId);
console.error(`Error in test "${testCase.description}":`, error);
resolve(false);
});
});
// 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;
}
if (import.meta.main) {
if (process.argv.length > 2) {
await runTestsStandalone(process.argv[2], parseInt(process.argv[3], 10));
} else {
await runTests();
}
}

View File

@@ -2073,3 +2073,75 @@ it("allow custom timeout per request", async () => {
expect(res.status).toBe(200);
expect(res.text()).resolves.toBe("Hello, World!");
}, 20_000);
it("#6462", async () => {
let headers: string[] = [];
using server = Bun.serve({
port: 0,
async fetch(request) {
for (const key of request.headers.keys()) {
headers = headers.concat([[key, request.headers.get(key)]]);
}
return new Response(
JSON.stringify({
"headers": headers,
}),
{ status: 200 },
);
},
});
const bytes = Buffer.from(`GET / HTTP/1.1\r\nConnection: close\r\nHost: ${server.hostname}\r\nTest!: test\r\n\r\n`);
const { promise, resolve } = Promise.withResolvers();
await Bun.connect({
port: server.port,
hostname: server.hostname,
socket: {
open(socket) {
const wrote = socket.write(bytes);
console.log("wrote", wrote);
},
data(socket, data) {
console.log(data.toString("utf8"));
},
close(socket) {
resolve();
},
},
});
await promise;
expect(headers).toStrictEqual([
["connection", "close"],
["host", "localhost"],
["test!", "test"],
]);
});
it("#6583", async () => {
const callback = mock();
using server = Bun.serve({
fetch: callback,
port: 0,
hostname: "localhost",
});
const { promise, resolve } = Promise.withResolvers();
await Bun.connect({
port: server.port,
hostname: server.hostname,
tls: true,
socket: {
open(socket) {
socket.write("GET / HTTP/1.1\r\nConnection: close\r\nHost: localhost\r\n\r\n");
},
data(socket, data) {
console.log(data.toString("utf8"));
},
close(socket) {
resolve();
},
},
});
await promise;
expect(callback).not.toHaveBeenCalled();
});

View File

@@ -1,6 +1,6 @@
import http from "node:http";
if (http.maxHeaderSize !== parseInt(process.env.BUN_HTTP_MAX_HEADER_SIZE, 10)) {
if (http.maxHeaderSize !== parseInt(process.env.BUN_HTTP_MAX_HEADER_SIZE ?? "0", 10)) {
throw new Error("BUN_HTTP_MAX_HEADER_SIZE is not set to the correct value");
}
@@ -18,16 +18,20 @@ await fetch(`${server.url}/`, {
});
try {
await fetch(`${server.url}/`, {
const response = await fetch(`${server.url}/`, {
headers: {
"Huge": Buffer.alloc(http.maxHeaderSize + 1024, "abc").toString(),
},
});
throw new Error("bad");
} catch (e) {
if (e.message.includes("bad")) {
process.exit(1);
if (response.status === 431) {
throw new Error("good!!");
}
process.exit(0);
throw new Error("bad!");
} catch (e) {
if (e instanceof Error && e.message.includes("good!!")) {
process.exit(0);
}
throw e;
}

View File

@@ -18,22 +18,23 @@ test("maxHeaderSize", async () => {
},
});
expect(
async () =>
await fetch(`${server.url}/`, {
headers: {
"Huge": Buffer.alloc(8 * 1024, "abc").toString(),
},
}),
).toThrow();
expect(
async () =>
await fetch(`${server.url}/`, {
headers: {
"Huge": Buffer.alloc(512, "abc").toString(),
},
}),
).not.toThrow();
{
const response = await fetch(`${server.url}/`, {
headers: {
"Huge": Buffer.alloc(8 * 1024, "abc").toString(),
},
});
expect(response.status).toBe(431);
}
{
const response = await fetch(`${server.url}/`, {
headers: {
"Huge": Buffer.alloc(15 * 1024, "abc").toString(),
},
});
expect(response.status).toBe(431);
}
}
http.maxHeaderSize = 16 * 1024;
{
@@ -45,22 +46,23 @@ test("maxHeaderSize", async () => {
},
});
expect(
async () =>
await fetch(`${server.url}/`, {
headers: {
"Huge": Buffer.alloc(15 * 1024, "abc").toString(),
},
}),
).not.toThrow();
expect(
async () =>
await fetch(`${server.url}/`, {
headers: {
"Huge": Buffer.alloc(17 * 1024, "abc").toString(),
},
}),
).toThrow();
{
const response = await fetch(`${server.url}/`, {
headers: {
"Huge": Buffer.alloc(15 * 1024, "abc").toString(),
},
});
expect(response.status).toBe(200);
}
{
const response = await fetch(`${server.url}/`, {
headers: {
"Huge": Buffer.alloc(17 * 1024, "abc").toString(),
},
});
expect(response.status).toBe(431);
}
}
http.maxHeaderSize = originalMaxHeaderSize;

View File

@@ -0,0 +1,79 @@
import assert from "node:assert";
import { createServer, request } from "node:http";
import url from "node:url";
export async function run() {
const { promise, resolve, reject } = Promise.withResolvers();
const proxyServer = createServer(function (req, res) {
// Use URL object instead of deprecated url.parse
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
const options = {
protocol: parsedUrl.protocol,
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname + parsedUrl.search,
method: req.method,
headers: req.headers,
};
const proxyRequest = request(options, function (proxyResponse) {
res.writeHead(proxyResponse.statusCode, proxyResponse.headers);
proxyResponse.pipe(res); // Use pipe instead of manual data handling
});
proxyRequest.on("error", error => {
console.error("Proxy Request Error:", error);
res.writeHead(500);
res.end("Proxy Error");
});
req.pipe(proxyRequest); // Use pipe instead of manual data handling
});
proxyServer.listen(0, "localhost", async () => {
const address = proxyServer.address();
const options = {
protocol: "http:",
hostname: "localhost",
port: address.port,
path: "/", // Change path to /
headers: {
Host: "example.com",
"accept-encoding": "identity",
},
};
const req = request(options, res => {
let data = "";
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
try {
assert.strictEqual(res.statusCode, 200);
assert(data.length > 0);
assert(data.includes("This domain is for use in illustrative examples in documents"));
resolve();
} catch (err) {
reject(err);
}
});
});
req.on("error", err => {
reject(err);
});
req.end();
});
await promise;
proxyServer.close();
}
if (import.meta.main) {
run().catch(console.error);
}

View File

@@ -1,6 +1,11 @@
// @ts-nocheck
import { bunExe } from "bun:harness";
import { bunEnv, randomPort } from "harness";
/**
* All new tests in this file should also run in Node.js.
*
* Do not add any tests that only run in Bun.
*
* A handful of older tests do not run in Node in this file. These tests should be updated to run in Node, or deleted.
*/
import { bunEnv, randomPort, bunExe } from "harness";
import { createTest } from "node-harness";
import { spawnSync } from "node:child_process";
import { EventEmitter, once } from "node:events";
@@ -23,10 +28,9 @@ import { tmpdir } from "node:os";
import * as path from "node:path";
import * as stream from "node:stream";
import { PassThrough } from "node:stream";
import url from "node:url";
import * as zlib from "node:zlib";
import { run as runHTTPProxyTest } from "./node-http-proxy.js";
const { describe, expect, it, beforeAll, afterAll, createDoneDotAll, mock, test } = createTest(import.meta.path);
function listen(server: Server, protocol: string = "http"): Promise<URL> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject("Timed out"), 5000).unref();
@@ -772,62 +776,8 @@ describe("node:http", () => {
});
});
it("request via http proxy, issue#4295", done => {
const proxyServer = createServer(function (req, res) {
let option = url.parse(req.url);
option.host = req.headers.host;
option.headers = req.headers;
const proxyRequest = request(option, function (proxyResponse) {
res.writeHead(proxyResponse.statusCode, proxyResponse.headers);
proxyResponse.on("data", function (chunk) {
res.write(chunk, "binary");
});
proxyResponse.on("end", function () {
res.end();
});
});
req.on("data", function (chunk) {
proxyRequest.write(chunk, "binary");
});
req.on("end", function () {
proxyRequest.end();
});
});
proxyServer.listen({ port: 0 }, async (_err, hostname, port) => {
const options = {
protocol: "http:",
hostname: hostname,
port: port,
path: "http://example.com",
headers: {
Host: "example.com",
"accept-encoding": "identity",
},
};
const req = request(options, res => {
let data = "";
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
try {
expect(res.statusCode).toBe(200);
expect(data.length).toBeGreaterThan(0);
expect(data).toContain("This domain is for use in illustrative examples in documents");
done();
} catch (err) {
done(err);
}
});
});
req.on("error", err => {
done(err);
});
req.end();
});
it("request via http proxy, issue#4295", async () => {
await runHTTPProxyTest();
});
it("should correctly stream a multi-chunk response #5320", async done => {

View File

@@ -16,4 +16,4 @@ for (let key in harness.bunEnv) {
process.env[key] = harness.bunEnv[key] + "";
}
Bun.$.env(process.env);
if (Bun.$?.env) Bun.$.env(process.env);