mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
2 Commits
claude/fix
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
785976d25c | ||
|
|
66b96948a4 |
@@ -666,6 +666,11 @@ public:
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
TemplatedApp &&setMaxHTTPHeadersCount(uint32_t maxHeadersCount) {
|
||||
httpContext->getSocketContextData()->maxHeadersCount = maxHeadersCount;
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
typedef TemplatedApp<false> App;
|
||||
|
||||
@@ -243,7 +243,7 @@ private:
|
||||
|
||||
/* The return value is entirely up to us to interpret. The HttpParser cares only for whether the returned value is DIFFERENT from passed user */
|
||||
|
||||
auto result = httpResponseData->consumePostPadded(httpContextData->maxHeaderSize, httpResponseData->isConnectRequest, httpContextData->flags.requireHostHeader,httpContextData->flags.useStrictMethodValidation, data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
|
||||
auto result = httpResponseData->consumePostPadded(httpContextData->maxHeaderSize, httpContextData->maxHeadersCount, httpResponseData->isConnectRequest, httpContextData->flags.requireHostHeader,httpContextData->flags.useStrictMethodValidation, 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 */
|
||||
|
||||
@@ -70,6 +70,7 @@ private:
|
||||
OnClientErrorCallback onClientError = nullptr;
|
||||
|
||||
uint64_t maxHeaderSize = 0; // 0 means no limit
|
||||
uint32_t maxHeadersCount = 0; // 0 means use default (UWS_HTTP_MAX_HEADERS_COUNT)
|
||||
|
||||
// TODO: SNI
|
||||
void clearRoutes() {
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#endif
|
||||
|
||||
extern "C" size_t BUN_DEFAULT_MAX_HTTP_HEADER_SIZE;
|
||||
extern "C" uint32_t BUN_DEFAULT_MAX_HTTP_HEADERS_COUNT;
|
||||
extern "C" int16_t Bun__HTTPMethod__from(const char *str, size_t len);
|
||||
|
||||
namespace uWS
|
||||
@@ -148,11 +149,22 @@ namespace uWS
|
||||
|
||||
friend struct HttpParser;
|
||||
|
||||
private:
|
||||
public:
|
||||
struct Header
|
||||
{
|
||||
std::string_view key, value;
|
||||
} headers[UWS_HTTP_MAX_HEADERS_COUNT];
|
||||
};
|
||||
|
||||
private:
|
||||
/* Stack-allocated headers for the common case (fast path) */
|
||||
Header stackHeaders[UWS_HTTP_MAX_HEADERS_COUNT];
|
||||
/* Heap-allocated headers when maxHeadersCount > UWS_HTTP_MAX_HEADERS_COUNT */
|
||||
std::vector<Header> dynamicHeaders;
|
||||
/* Points to either stackHeaders or dynamicHeaders.data() */
|
||||
Header *headers = stackHeaders;
|
||||
/* Current max headers count limit */
|
||||
uint32_t maxHeadersCount = UWS_HTTP_MAX_HEADERS_COUNT;
|
||||
|
||||
bool ancientHttp;
|
||||
bool didYield;
|
||||
unsigned int querySeparator;
|
||||
@@ -161,6 +173,23 @@ namespace uWS
|
||||
std::map<std::string, unsigned short, std::less<>> *currentParameterOffsets = nullptr;
|
||||
|
||||
public:
|
||||
/* Configure max headers count - must be called before parsing */
|
||||
void setMaxHeadersCount(uint32_t count) {
|
||||
if (count == 0) {
|
||||
count = BUN_DEFAULT_MAX_HTTP_HEADERS_COUNT;
|
||||
}
|
||||
maxHeadersCount = count;
|
||||
if (count > UWS_HTTP_MAX_HEADERS_COUNT) {
|
||||
dynamicHeaders.resize(count);
|
||||
headers = dynamicHeaders.data();
|
||||
} else {
|
||||
headers = stackHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t getMaxHeadersCount() const {
|
||||
return maxHeadersCount;
|
||||
}
|
||||
/* Any data pipelined after the HTTP headers (before response).
|
||||
* Used for Node.js compatibility: 'connect' and 'upgrade' events
|
||||
* pass this as the 'head' Buffer parameter.
|
||||
@@ -651,7 +680,7 @@ namespace uWS
|
||||
}
|
||||
|
||||
/* End is only used for the proxy parser. The HTTP parser recognizes "\ra" as invalid "\r\n" scan and breaks. */
|
||||
static HttpParserResult getHeaders(char *postPaddedBuffer, char *end, struct HttpRequest::Header *headers, void *reserved, bool &isAncientHTTP, bool &isConnectRequest, bool useStrictMethodValidation, uint64_t maxHeaderSize) {
|
||||
static HttpParserResult getHeaders(char *postPaddedBuffer, char *end, struct HttpRequest::Header *headers, void *reserved, bool &isAncientHTTP, bool &isConnectRequest, bool useStrictMethodValidation, uint64_t maxHeaderSize, uint32_t maxHeadersCount) {
|
||||
char *preliminaryKey, *preliminaryValue, *start = postPaddedBuffer;
|
||||
#ifdef UWS_WITH_PROXY
|
||||
/* ProxyParser is passed as reserved parameter */
|
||||
@@ -725,7 +754,7 @@ namespace uWS
|
||||
|
||||
headers++;
|
||||
|
||||
for (unsigned int i = 1; i < UWS_HTTP_MAX_HEADERS_COUNT - 1; i++) {
|
||||
for (unsigned int i = 1; i < maxHeadersCount - 1; i++) {
|
||||
/* Lower case and consume the field name */
|
||||
preliminaryKey = postPaddedBuffer;
|
||||
postPaddedBuffer = consumeFieldName(postPaddedBuffer);
|
||||
@@ -828,7 +857,7 @@ namespace uWS
|
||||
data[length + 1] = 'a'; /* Anything that is not \n, to trigger "invalid request" */
|
||||
req->ancientHttp = false;
|
||||
for (;length;) {
|
||||
auto result = getHeaders(data, data + length, req->headers, reserved, req->ancientHttp, isConnectRequest, useStrictMethodValidation, maxHeaderSize);
|
||||
auto result = getHeaders(data, data + length, req->headers, reserved, req->ancientHttp, isConnectRequest, useStrictMethodValidation, maxHeaderSize, req->getMaxHeadersCount());
|
||||
if(result.isError()) {
|
||||
return result;
|
||||
}
|
||||
@@ -960,10 +989,12 @@ namespace uWS
|
||||
}
|
||||
|
||||
public:
|
||||
HttpParserResult consumePostPadded(uint64_t maxHeaderSize, bool& isConnectRequest, bool requireHostHeader, bool useStrictMethodValidation, char *data, unsigned int length, void *user, void *reserved, MoveOnlyFunction<void *(void *, HttpRequest *)> &&requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &&dataHandler) {
|
||||
HttpParserResult consumePostPadded(uint64_t maxHeaderSize, uint32_t maxHeadersCount, bool& isConnectRequest, bool requireHostHeader, bool useStrictMethodValidation, char *data, unsigned int length, void *user, void *reserved, MoveOnlyFunction<void *(void *, HttpRequest *)> &&requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &&dataHandler) {
|
||||
/* This resets BloomFilter by construction, but later we also reset it again.
|
||||
* Optimize this to skip resetting twice (req could be made global) */
|
||||
HttpRequest req;
|
||||
/* Configure max headers count - enables dynamic allocation when > UWS_HTTP_MAX_HEADERS_COUNT */
|
||||
req.setMaxHeadersCount(maxHeadersCount);
|
||||
if (remainingStreamingBytes) {
|
||||
if (isConnectRequest) {
|
||||
dataHandler(user, std::string_view(data, length), false);
|
||||
|
||||
@@ -40,5 +40,31 @@ pub fn setMaxHTTPHeaderSize(globalThis: *jsc.JSGlobalObject, callframe: *jsc.Cal
|
||||
return jsc.JSValue.jsNumber(bun.http.max_http_header_size);
|
||||
}
|
||||
|
||||
pub fn getMaxHTTPHeadersCount(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
||||
_ = globalThis;
|
||||
_ = callframe;
|
||||
return jsc.JSValue.jsNumber(bun.http.max_http_headers_count);
|
||||
}
|
||||
|
||||
pub fn setMaxHTTPHeadersCount(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
||||
const arguments = callframe.arguments_old(1).slice();
|
||||
if (arguments.len < 1) {
|
||||
return globalThis.throwNotEnoughArguments("setMaxHTTPHeadersCount", 1, arguments.len);
|
||||
}
|
||||
const value = arguments[0];
|
||||
const num = try value.coerceToInt64(globalThis);
|
||||
if (num < 0) {
|
||||
return globalThis.throwInvalidArgumentTypeValue("maxHeadersCount", "non-negative integer", value);
|
||||
}
|
||||
if (num == 0) {
|
||||
bun.http.max_http_headers_count = std.math.maxInt(u32);
|
||||
} else {
|
||||
bun.http.max_http_headers_count = @intCast(num);
|
||||
}
|
||||
return jsc.JSValue.jsNumber(bun.http.max_http_headers_count);
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const jsc = bun.jsc;
|
||||
|
||||
@@ -103,6 +103,7 @@ pub const runtime_params_ = [_]ParamType{
|
||||
clap.parseParam("--conditions <STR>... Pass custom conditions to resolve") catch unreachable,
|
||||
clap.parseParam("--fetch-preconnect <STR>... Preconnect to a URL while code is loading") catch unreachable,
|
||||
clap.parseParam("--max-http-header-size <INT> Set the maximum size of HTTP headers in bytes. Default is 16KiB") catch unreachable,
|
||||
clap.parseParam("--max-http-header-count <INT> Set the maximum number of HTTP headers. Default is 100") catch unreachable,
|
||||
clap.parseParam("--dns-result-order <STR> Set the default order of DNS lookup results. Valid orders: verbatim (default), ipv4first, ipv6first") catch unreachable,
|
||||
clap.parseParam("--expose-gc Expose gc() on the global object. Has no effect on Bun.gc().") catch unreachable,
|
||||
clap.parseParam("--no-deprecation Suppress all reporting of the custom deprecation.") catch unreachable,
|
||||
@@ -735,6 +736,18 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
}
|
||||
}
|
||||
|
||||
if (args.option("--max-http-header-count")) |count_str| {
|
||||
const count = std.fmt.parseInt(u32, count_str, 10) catch {
|
||||
Output.errGeneric("Invalid value for --max-http-header-count: \"{s}\". Must be a positive integer\n", .{count_str});
|
||||
Global.exit(1);
|
||||
};
|
||||
if (count == 0) {
|
||||
bun.http.max_http_headers_count = std.math.maxInt(u32);
|
||||
} else {
|
||||
bun.http.max_http_headers_count = count;
|
||||
}
|
||||
}
|
||||
|
||||
if (args.option("--user-agent")) |user_agent| {
|
||||
bun.http.overridden_default_user_agent = user_agent;
|
||||
}
|
||||
|
||||
@@ -531,6 +531,15 @@ extern "C"
|
||||
uwsApp->setMaxHTTPHeaderSize(max_header_size);
|
||||
}
|
||||
}
|
||||
void uws_app_set_max_http_headers_count(int ssl, uws_app_t *app, uint32_t max_headers_count) {
|
||||
if (ssl) {
|
||||
uWS::SSLApp *uwsApp = (uWS::SSLApp *)app;
|
||||
uwsApp->setMaxHTTPHeadersCount(max_headers_count);
|
||||
} else {
|
||||
uWS::App *uwsApp = (uWS::App *)app;
|
||||
uwsApp->setMaxHTTPHeadersCount(max_headers_count);
|
||||
}
|
||||
}
|
||||
void uws_app_set_flags(int ssl, uws_app_t *app, bool require_host_header, bool use_strict_method_validation) {
|
||||
if (ssl) {
|
||||
uWS::SSLApp *uwsApp = (uWS::SSLApp *)app;
|
||||
|
||||
@@ -63,6 +63,10 @@ pub fn NewApp(comptime ssl: bool) type {
|
||||
return c.uws_app_set_max_http_header_size(ssl_flag, @as(*uws_app_t, @ptrCast(this)), max_header_size);
|
||||
}
|
||||
|
||||
pub fn setMaxHTTPHeadersCount(this: *ThisApp, max_headers_count: u32) void {
|
||||
return c.uws_app_set_max_http_headers_count(ssl_flag, @as(*uws_app_t, @ptrCast(this)), max_headers_count);
|
||||
}
|
||||
|
||||
pub fn clearRoutes(app: *ThisApp) void {
|
||||
return c.uws_app_clear_routes(ssl_flag, @as(*uws_app_t, @ptrCast(app)));
|
||||
}
|
||||
@@ -404,6 +408,7 @@ pub const c = struct {
|
||||
pub extern fn uws_app_destroy(ssl: i32, app: *uws_app_t) void;
|
||||
pub extern fn uws_app_set_flags(ssl: i32, app: *uws_app_t, require_host_header: bool, use_strict_method_validation: bool) void;
|
||||
pub extern fn uws_app_set_max_http_header_size(ssl: i32, app: *uws_app_t, max_header_size: u64) void;
|
||||
pub extern fn uws_app_set_max_http_headers_count(ssl: i32, app: *uws_app_t, max_headers_count: u32) void;
|
||||
pub extern fn uws_app_get(ssl: i32, app: *uws_app_t, pattern: [*]const u8, pattern_len: usize, handler: uws_method_handler, user_data: ?*anyopaque) void;
|
||||
pub extern fn uws_app_post(ssl: i32, app: *uws_app_t, pattern: [*]const u8, pattern_len: usize, handler: uws_method_handler, user_data: ?*anyopaque) void;
|
||||
pub extern fn uws_app_options(ssl: i32, app: *uws_app_t, pattern: [*]const u8, pattern_len: usize, handler: uws_method_handler, user_data: ?*anyopaque) void;
|
||||
|
||||
@@ -15,6 +15,11 @@ comptime {
|
||||
@export(&max_http_header_size, .{ .name = "BUN_DEFAULT_MAX_HTTP_HEADER_SIZE" });
|
||||
}
|
||||
|
||||
pub var max_http_headers_count: u32 = 100;
|
||||
comptime {
|
||||
@export(&max_http_headers_count, .{ .name = "BUN_DEFAULT_MAX_HTTP_HEADERS_COUNT" });
|
||||
}
|
||||
|
||||
pub var overridden_default_user_agent: []const u8 = "";
|
||||
|
||||
const print_every = 0;
|
||||
|
||||
@@ -352,6 +352,8 @@ function emitErrorNt(msg, err, callback) {
|
||||
}
|
||||
const setMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "setMaxHTTPHeaderSize", 1);
|
||||
const getMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "getMaxHTTPHeaderSize", 0);
|
||||
const setMaxHTTPHeadersCount = $newZigFunction("node_http_binding.zig", "setMaxHTTPHeadersCount", 1);
|
||||
const getMaxHTTPHeadersCount = $newZigFunction("node_http_binding.zig", "getMaxHTTPHeadersCount", 0);
|
||||
const kOutHeaders = Symbol("kOutHeaders");
|
||||
|
||||
function ipToInt(ip) {
|
||||
@@ -502,6 +504,7 @@ export {
|
||||
getHeader,
|
||||
getIsNextIncomingMessageHTTPS,
|
||||
getMaxHTTPHeaderSize,
|
||||
getMaxHTTPHeadersCount,
|
||||
getRawKeys,
|
||||
hasServerResponseFinished,
|
||||
headerStateSymbol,
|
||||
@@ -551,6 +554,7 @@ export {
|
||||
setHeader,
|
||||
setIsNextIncomingMessageHTTPS,
|
||||
setMaxHTTPHeaderSize,
|
||||
setMaxHTTPHeadersCount,
|
||||
setRequestTimeout,
|
||||
setServerCustomOptions,
|
||||
setServerIdleTimeout,
|
||||
|
||||
@@ -6,7 +6,14 @@ const { IncomingMessage } = require("node:_http_incoming");
|
||||
const { OutgoingMessage } = require("node:_http_outgoing");
|
||||
const { Server, ServerResponse } = require("node:_http_server");
|
||||
|
||||
const { METHODS, STATUS_CODES, setMaxHTTPHeaderSize, getMaxHTTPHeaderSize } = require("internal/http");
|
||||
const {
|
||||
METHODS,
|
||||
STATUS_CODES,
|
||||
setMaxHTTPHeaderSize,
|
||||
getMaxHTTPHeaderSize,
|
||||
setMaxHTTPHeadersCount,
|
||||
getMaxHTTPHeadersCount,
|
||||
} = require("internal/http");
|
||||
|
||||
const { WebSocket, CloseEvent, MessageEvent } = globalThis;
|
||||
|
||||
@@ -54,6 +61,12 @@ const http_exports = {
|
||||
set maxHeaderSize(value) {
|
||||
setMaxHTTPHeaderSize(value);
|
||||
},
|
||||
get maxHeadersCount() {
|
||||
return getMaxHTTPHeadersCount();
|
||||
},
|
||||
set maxHeadersCount(value) {
|
||||
setMaxHTTPHeadersCount(value);
|
||||
},
|
||||
validateHeaderName,
|
||||
validateHeaderValue,
|
||||
setMaxIdleHTTPParsers(max) {
|
||||
|
||||
143
test/regression/issue/6982.test.ts
Normal file
143
test/regression/issue/6982.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
describe("--max-http-header-count", () => {
|
||||
test("http.maxHeadersCount getter returns default value", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", "console.log(require('http').maxHeadersCount)"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("100");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("http.maxHeadersCount setter works", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`
|
||||
const http = require('http');
|
||||
console.log(http.maxHeadersCount);
|
||||
http.maxHeadersCount = 500;
|
||||
console.log(http.maxHeadersCount);
|
||||
`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
const lines = stdout.trim().split("\n");
|
||||
expect(lines[0]).toBe("100");
|
||||
expect(lines[1]).toBe("500");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("--max-http-header-count CLI flag sets the value", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--max-http-header-count=200", "-e", "console.log(require('http').maxHeadersCount)"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("200");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("server accepts requests with many headers when limit is increased", async () => {
|
||||
using dir = tempDir("header-count-test", {
|
||||
"server.ts": `
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
const count = [...req.headers].length;
|
||||
return new Response(String(count));
|
||||
},
|
||||
});
|
||||
console.log(server.url.href);
|
||||
`,
|
||||
});
|
||||
|
||||
// Start server with higher header limit
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--max-http-header-count=200", "server.ts"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Read server URL from stdout
|
||||
const reader = proc.stdout.getReader();
|
||||
const { value } = await reader.read();
|
||||
reader.releaseLock();
|
||||
const url = new TextDecoder().decode(value).trim();
|
||||
|
||||
// Build request with 150 headers
|
||||
const headers = new Headers();
|
||||
for (let i = 0; i < 150; i++) {
|
||||
headers.set(`X-Custom-Header-${i}`, `value-${i}`);
|
||||
}
|
||||
|
||||
const res = await fetch(url, { headers });
|
||||
expect(res.status).toBe(200);
|
||||
const count = parseInt(await res.text());
|
||||
// Account for default headers that fetch adds (Host, Accept, etc.)
|
||||
expect(count).toBeGreaterThanOrEqual(150);
|
||||
|
||||
proc.kill();
|
||||
});
|
||||
|
||||
test("server rejects requests with too many headers", async () => {
|
||||
using dir = tempDir("header-count-reject-test", {
|
||||
"server.ts": `
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
const count = [...req.headers].length;
|
||||
return new Response(String(count));
|
||||
},
|
||||
});
|
||||
console.log(server.url.href);
|
||||
`,
|
||||
});
|
||||
|
||||
// Start server with low header limit (50)
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "--max-http-header-count=50", "server.ts"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Read server URL from stdout
|
||||
const reader = proc.stdout.getReader();
|
||||
const { value } = await reader.read();
|
||||
reader.releaseLock();
|
||||
const url = new TextDecoder().decode(value).trim();
|
||||
|
||||
// Build request with 60 headers (exceeds limit)
|
||||
const headers = new Headers();
|
||||
for (let i = 0; i < 60; i++) {
|
||||
headers.set(`X-Custom-Header-${i}`, `value-${i}`);
|
||||
}
|
||||
|
||||
const res = await fetch(url, { headers });
|
||||
// Should get 431 Request Header Fields Too Large
|
||||
expect(res.status).toBe(431);
|
||||
|
||||
proc.kill();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user