fix(proxy): cap connect tunnel timeouts

This commit is contained in:
Peter Steinberger
2026-05-29 18:23:48 -04:00
parent 6037a74660
commit bafa6de76d
2 changed files with 33 additions and 4 deletions

View File

@@ -1,5 +1,6 @@
import { EventEmitter } from "node:events";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MAX_TIMER_TIMEOUT_MS } from "../../shared/number-coercion.js";
class FakeSocket extends EventEmitter {
public readonly writes: string[] = [];
@@ -349,4 +350,27 @@ describe("openHttpConnectTunnel", () => {
await rejected;
expect(proxySocket.destroyed).toBe(true);
});
it("caps oversized CONNECT timeouts before arming the watchdog", async () => {
vi.useFakeTimers();
const proxySocket = new FakeSocket();
setNextNetSocket(proxySocket);
const { openHttpConnectTunnel } = await import("./http-connect-tunnel.js");
const tunnel = openHttpConnectTunnel({
proxyUrl: new URL("http://proxy.example:8080"),
targetHost: "api.push.apple.com",
targetPort: 443,
timeoutMs: Number.MAX_SAFE_INTEGER,
});
await vi.advanceTimersByTimeAsync(1);
expect(proxySocket.destroyed).toBe(false);
await vi.advanceTimersByTimeAsync(MAX_TIMER_TIMEOUT_MS - 1);
await expect(tunnel).rejects.toThrow(
`Proxy CONNECT failed via http://proxy.example:8080: Proxy CONNECT timed out after ${MAX_TIMER_TIMEOUT_MS}ms`,
);
expect(proxySocket.destroyed).toBe(true);
});
});

View File

@@ -1,5 +1,6 @@
import * as net from "node:net";
import * as tls from "node:tls";
import { resolveTimerTimeoutMs } from "../../shared/number-coercion.js";
import type { ManagedProxyTlsOptions } from "./proxy/proxy-tls.js";
export type HttpConnectTunnelParams = {
@@ -11,6 +12,7 @@ export type HttpConnectTunnelParams = {
};
const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
const MIN_CONNECT_TIMEOUT_MS = 1;
type ProxySocket = net.Socket | tls.TLSSocket;
type ConnectResponseBuffer = Buffer;
@@ -159,11 +161,14 @@ class HttpConnectTunnelAttempt {
}
private startTimeout(): void {
const timeoutMs = this.params.timeoutMs;
if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
const timeoutMs =
this.params.timeoutMs === undefined || this.params.timeoutMs <= 0
? undefined
: resolveTimerTimeoutMs(this.params.timeoutMs, MIN_CONNECT_TIMEOUT_MS);
if (timeoutMs !== undefined) {
this.timeout = setTimeout(() => {
this.fail(new Error(`Proxy CONNECT timed out after ${Math.trunc(timeoutMs)}ms`));
}, Math.trunc(timeoutMs));
this.fail(new Error(`Proxy CONNECT timed out after ${timeoutMs}ms`));
}, timeoutMs);
}
}