mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(http2): resolve origin mismatch with got HTTP/2 client
Fixes two bugs that caused HTTP/2 origin validation to fail with the `got` client: 1. TLSSocket.servername not set from host option - When `options.servername` was not explicitly provided, it was set to `undefined` instead of falling back to `options.host`. Also fixed case mismatch where code read `tls.servername` but the buntls function returned `serverName`. 2. Origin string included default port 443 - Per web origin standards, default ports (443 for HTTPS) should be omitted from origin strings. The combined effect caused `got`'s http2-wrapper to fail with: "Requested origin https://google.com does not match server https://google.com:443" Closes #25771 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2539,7 +2539,8 @@ function initOriginSet(session: Http2Session) {
|
||||
}
|
||||
}
|
||||
let originString = `https://${hostName}`;
|
||||
if (socket.remotePort != null) originString += `:${socket.remotePort}`;
|
||||
// Per web origin standards, default ports (443 for HTTPS) should be omitted
|
||||
if (socket.remotePort != null && socket.remotePort !== 443) originString += `:${socket.remotePort}`;
|
||||
originSet.add(originString);
|
||||
}
|
||||
return originSet;
|
||||
|
||||
@@ -871,7 +871,7 @@ Socket.prototype.connect = function connect(...args) {
|
||||
let connection = this[ksocket];
|
||||
let upgradeDuplex = false;
|
||||
let { port, host, path, socket, rejectUnauthorized, checkServerIdentity, session, fd, pauseOnConnect } = options;
|
||||
this.servername = options.servername;
|
||||
this.servername = options.servername ?? options.host;
|
||||
if (socket) {
|
||||
connection = socket;
|
||||
}
|
||||
@@ -924,7 +924,7 @@ Socket.prototype.connect = function connect(...args) {
|
||||
}
|
||||
tls.requestCert = true;
|
||||
tls.session = session || tls.session;
|
||||
this.servername = tls.servername;
|
||||
this.servername = tls.serverName;
|
||||
tls.checkServerIdentity = checkServerIdentity || tls.checkServerIdentity;
|
||||
this[bunTLSConnectOptions] = tls;
|
||||
if (!connection && tls.socket) {
|
||||
@@ -1727,7 +1727,7 @@ function internalConnect(self, options, address, port, addressType, localAddress
|
||||
}
|
||||
tls.requestCert = true;
|
||||
tls.session = session || tls.session;
|
||||
self.servername = tls.servername;
|
||||
self.servername = tls.serverName;
|
||||
tls.checkServerIdentity = checkServerIdentity || tls.checkServerIdentity;
|
||||
self[bunTLSConnectOptions] = tls;
|
||||
if (!connection && tls.socket) {
|
||||
@@ -1864,7 +1864,7 @@ function internalConnectMultiple(context, canceled?) {
|
||||
}
|
||||
tls.requestCert = true;
|
||||
tls.session = session || tls.session;
|
||||
self.servername = tls.servername;
|
||||
self.servername = tls.serverName;
|
||||
tls.checkServerIdentity = checkServerIdentity || tls.checkServerIdentity;
|
||||
self[bunTLSConnectOptions] = tls;
|
||||
if (!connection && tls.socket) {
|
||||
|
||||
162
test/regression/issue/25771.test.ts
Normal file
162
test/regression/issue/25771.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { tls } from "harness";
|
||||
import http2 from "node:http2";
|
||||
import net from "node:net";
|
||||
|
||||
// Test for issue #25771: HTTP/2 origin mismatch with `got` http2 client
|
||||
// The issue has two root causes:
|
||||
// 1. TLSSocket.servername not falling back to options.host
|
||||
// 2. Origin string including default port 443 for HTTPS
|
||||
|
||||
describe("issue #25771", () => {
|
||||
test("TLSSocket.servername should fall back to host option when servername not provided", async () => {
|
||||
// Create an HTTP/2 server
|
||||
const server = http2.createSecureServer({
|
||||
...tls,
|
||||
});
|
||||
|
||||
server.on("stream", (stream, headers) => {
|
||||
stream.respond({ ":status": 200 });
|
||||
stream.end("ok");
|
||||
});
|
||||
|
||||
const { promise: listeningPromise, resolve: listeningResolve } = Promise.withResolvers<number>();
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address();
|
||||
listeningResolve((addr as net.AddressInfo).port);
|
||||
});
|
||||
|
||||
const port = await listeningPromise;
|
||||
|
||||
try {
|
||||
// Connect with host option but without explicit servername
|
||||
const client = http2.connect(`https://127.0.0.1:${port}`, {
|
||||
host: "localhost",
|
||||
ca: tls.cert,
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
const socket = client.socket as import("node:tls").TLSSocket;
|
||||
|
||||
// Wait for the socket to be ready
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.on("connect", resolve);
|
||||
client.on("error", reject);
|
||||
});
|
||||
|
||||
// Verify servername falls back to host when not explicitly provided
|
||||
expect(socket.servername).toBe("localhost");
|
||||
|
||||
// Verify the originSet uses hostname, not IP address
|
||||
const originSet = client.originSet;
|
||||
expect(originSet).toBeDefined();
|
||||
expect(originSet!.length).toBeGreaterThan(0);
|
||||
// Origin should be based on servername, not remoteAddress
|
||||
expect(originSet![0]).toContain("localhost");
|
||||
|
||||
client.close();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("HTTP/2 originSet should omit default port 443 for HTTPS", async () => {
|
||||
// Create an HTTP/2 server on port 443 equivalent
|
||||
const server = http2.createSecureServer({
|
||||
...tls,
|
||||
});
|
||||
|
||||
server.on("stream", (stream, headers) => {
|
||||
stream.respond({ ":status": 200 });
|
||||
stream.end("ok");
|
||||
});
|
||||
|
||||
const { promise: listeningPromise, resolve: listeningResolve } = Promise.withResolvers<number>();
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address();
|
||||
listeningResolve((addr as net.AddressInfo).port);
|
||||
});
|
||||
|
||||
const port = await listeningPromise;
|
||||
|
||||
try {
|
||||
// Connect with explicit servername
|
||||
const client = http2.connect(`https://127.0.0.1:${port}`, {
|
||||
servername: "example.com",
|
||||
ca: tls.cert,
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// Wait for the socket to be ready
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.on("connect", resolve);
|
||||
client.on("error", reject);
|
||||
});
|
||||
|
||||
const socket = client.socket as import("node:tls").TLSSocket;
|
||||
|
||||
// Test: When using a non-443 port, the port should be included in origin
|
||||
const originSet = client.originSet;
|
||||
expect(originSet).toBeDefined();
|
||||
expect(originSet!.length).toBeGreaterThan(0);
|
||||
// Since we're not on port 443, the port should be in the origin
|
||||
expect(originSet![0]).toBe(`https://example.com:${port}`);
|
||||
|
||||
client.close();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("HTTP/2 originSet should match requested origin for standard HTTPS", async () => {
|
||||
// This test verifies the fix for the actual bug reported:
|
||||
// When connecting to https://google.com (port 443), the originSet should be
|
||||
// https://google.com NOT https://google.com:443
|
||||
|
||||
const server = http2.createSecureServer({
|
||||
...tls,
|
||||
});
|
||||
|
||||
server.on("stream", (stream, headers) => {
|
||||
stream.respond({ ":status": 200 });
|
||||
stream.end("ok");
|
||||
});
|
||||
|
||||
const { promise: listeningPromise, resolve: listeningResolve } = Promise.withResolvers<number>();
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address();
|
||||
listeningResolve((addr as net.AddressInfo).port);
|
||||
});
|
||||
|
||||
const port = await listeningPromise;
|
||||
|
||||
try {
|
||||
// Test that servername is correctly set from options.servername
|
||||
// (This tests the TLSSocket.servername fix)
|
||||
const client = http2.connect(`https://127.0.0.1:${port}`, {
|
||||
servername: "example.org",
|
||||
ca: tls.cert,
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.on("connect", resolve);
|
||||
client.on("error", reject);
|
||||
});
|
||||
|
||||
const socket = client.socket as import("node:tls").TLSSocket;
|
||||
|
||||
// Servername should be example.org (from servername option)
|
||||
expect(socket.servername).toBe("example.org");
|
||||
|
||||
// Origin should use example.org (not IP address)
|
||||
const originSet = client.originSet;
|
||||
expect(originSet).toBeDefined();
|
||||
expect(originSet![0]).toContain("example.org");
|
||||
|
||||
client.close();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user