mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(http): support proxy passwords longer than 4096 characters (#25530)
## Summary - Fixes silent 401 Unauthorized errors when using proxies with long passwords (e.g., JWT tokens > 4096 chars) - Bun was silently dropping proxy passwords exceeding 4095 characters, falling through to code that only encoded the username ## Changes - Added `PercentEncoding.decodeWithFallback` which uses a 4KB stack buffer for the common case and falls back to heap allocation only for larger inputs - Updated proxy auth encoding in `AsyncHTTP.zig` to use the new fallback method ## Test plan - [x] Added test case that verifies passwords > 4096 chars are handled correctly - [x] Test fails with system bun (v1.3.3), passes with this fix - [x] All 29 proxy tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -248,6 +248,144 @@ test("unsupported protocol", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates an HTTP proxy server that captures Proxy-Authorization headers.
|
||||
* The server forwards requests to their destination and pipes responses back.
|
||||
*/
|
||||
async function createAuthCapturingProxy() {
|
||||
const capturedAuths: string[] = [];
|
||||
const server = net.createServer((clientSocket: net.Socket) => {
|
||||
clientSocket.once("data", data => {
|
||||
const request = data.toString();
|
||||
const lines = request.split("\r\n");
|
||||
for (const line of lines) {
|
||||
if (line.toLowerCase().startsWith("proxy-authorization:")) {
|
||||
capturedAuths.push(line.substring("proxy-authorization:".length).trim());
|
||||
}
|
||||
}
|
||||
|
||||
const [method, path] = request.split(" ");
|
||||
let host: string;
|
||||
let port: number | string = 0;
|
||||
let request_path = "";
|
||||
if (path.indexOf("http") !== -1) {
|
||||
const url = new URL(path);
|
||||
host = url.hostname;
|
||||
port = url.port;
|
||||
request_path = url.pathname + (url.search || "");
|
||||
} else {
|
||||
[host, port] = path.split(":");
|
||||
}
|
||||
const destinationPort = Number.parseInt((port || "80").toString(), 10);
|
||||
const destinationHost = host || "";
|
||||
|
||||
const serverSocket = net.connect(destinationPort, destinationHost, () => {
|
||||
serverSocket.write(`${method} ${request_path} HTTP/1.1\r\n`);
|
||||
serverSocket.write(data.slice(request.indexOf("\r\n") + 2));
|
||||
serverSocket.pipe(clientSocket);
|
||||
});
|
||||
clientSocket.on("error", () => {});
|
||||
serverSocket.on("error", () => {
|
||||
clientSocket.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(0);
|
||||
await once(server, "listening");
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
return {
|
||||
server,
|
||||
port,
|
||||
capturedAuths,
|
||||
async close() {
|
||||
server.close();
|
||||
await once(server, "close");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("proxy with long password (> 4096 chars) sends correct authorization", async () => {
|
||||
const proxy = await createAuthCapturingProxy();
|
||||
|
||||
// Create a password longer than 4096 chars (e.g., simulating a JWT token)
|
||||
// Use Buffer.alloc which is faster in debug JavaScriptCore builds
|
||||
const longPassword = Buffer.alloc(5000, "a").toString();
|
||||
const username = "testuser";
|
||||
const proxyUrl = `http://${username}:${longPassword}@localhost:${proxy.port}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(httpServer.url, {
|
||||
method: "GET",
|
||||
proxy: proxyUrl,
|
||||
keepalive: false,
|
||||
});
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
// Verify the auth header was sent and contains both username and password
|
||||
expect(proxy.capturedAuths.length).toBeGreaterThanOrEqual(1);
|
||||
const capturedAuth = proxy.capturedAuths[0];
|
||||
expect(capturedAuth.startsWith("Basic ")).toBe(true);
|
||||
|
||||
// Decode and verify
|
||||
const encoded = capturedAuth.substring("Basic ".length);
|
||||
const decoded = Buffer.from(encoded, "base64url").toString();
|
||||
expect(decoded).toBe(`${username}:${longPassword}`);
|
||||
} finally {
|
||||
await proxy.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("proxy with long password (> 4096 chars) works correctly after redirect", async () => {
|
||||
// This test verifies that the reset() code path (used during redirects)
|
||||
// also handles long passwords correctly
|
||||
const proxy = await createAuthCapturingProxy();
|
||||
|
||||
// Create a server that issues a redirect
|
||||
using redirectServer = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
if (req.url.endsWith("/redirect")) {
|
||||
return Response.redirect("/final", 302);
|
||||
}
|
||||
return new Response("OK", { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
// Use Buffer.alloc which is faster in debug JavaScriptCore builds
|
||||
const longPassword = Buffer.alloc(5000, "a").toString();
|
||||
const username = "testuser";
|
||||
const proxyUrl = `http://${username}:${longPassword}@localhost:${proxy.port}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${redirectServer.url.origin}/redirect`, {
|
||||
method: "GET",
|
||||
proxy: proxyUrl,
|
||||
keepalive: false,
|
||||
});
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.status).toBe(200);
|
||||
const text = await response.text();
|
||||
expect(text).toBe("OK");
|
||||
|
||||
// Verify auth was sent on requests. Due to connection reuse, the proxy may
|
||||
// only see one request even though a redirect occurred (the redirected
|
||||
// request reuses the same connection). We verify at least one auth was sent
|
||||
// and that all captured auths are correct.
|
||||
expect(proxy.capturedAuths.length).toBeGreaterThanOrEqual(1);
|
||||
for (const capturedAuth of proxy.capturedAuths) {
|
||||
expect(capturedAuth.startsWith("Basic ")).toBe(true);
|
||||
const encoded = capturedAuth.substring("Basic ".length);
|
||||
const decoded = Buffer.from(encoded, "base64url").toString();
|
||||
expect(decoded).toBe(`${username}:${longPassword}`);
|
||||
}
|
||||
} finally {
|
||||
await proxy.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("axios with https-proxy-agent", async () => {
|
||||
httpProxyServer.log.length = 0;
|
||||
const httpsAgent = new HttpsProxyAgent(httpProxyServer.url, {
|
||||
|
||||
Reference in New Issue
Block a user