| import http from "node:http"; |
| import https from "node:https"; |
| import { afterEach, beforeEach, describe, expect, it } from "vitest"; |
| import { |
| getDirectAgentForCdp, |
| hasProxyEnv, |
| withNoProxyForCdpUrl, |
| withNoProxyForLocalhost, |
| } from "./cdp-proxy-bypass.js"; |
|
|
| const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); |
|
|
| async function withIsolatedNoProxyEnv(fn: () => Promise<void>) { |
| const origNoProxy = process.env.NO_PROXY; |
| const origNoProxyLower = process.env.no_proxy; |
| const origHttpProxy = process.env.HTTP_PROXY; |
| delete process.env.NO_PROXY; |
| delete process.env.no_proxy; |
| process.env.HTTP_PROXY = "http://proxy:8080"; |
|
|
| try { |
| await fn(); |
| } finally { |
| if (origHttpProxy !== undefined) { |
| process.env.HTTP_PROXY = origHttpProxy; |
| } else { |
| delete process.env.HTTP_PROXY; |
| } |
| if (origNoProxy !== undefined) { |
| process.env.NO_PROXY = origNoProxy; |
| } else { |
| delete process.env.NO_PROXY; |
| } |
| if (origNoProxyLower !== undefined) { |
| process.env.no_proxy = origNoProxyLower; |
| } else { |
| delete process.env.no_proxy; |
| } |
| } |
| } |
|
|
| describe("cdp-proxy-bypass", () => { |
| describe("getDirectAgentForCdp", () => { |
| it("returns http.Agent for http://localhost URLs", () => { |
| const agent = getDirectAgentForCdp("http://localhost:9222"); |
| expect(agent).toBeInstanceOf(http.Agent); |
| }); |
|
|
| it("returns http.Agent for http://127.0.0.1 URLs", () => { |
| const agent = getDirectAgentForCdp("http://127.0.0.1:9222/json/version"); |
| expect(agent).toBeInstanceOf(http.Agent); |
| }); |
|
|
| it("returns https.Agent for wss://localhost URLs", () => { |
| const agent = getDirectAgentForCdp("wss://localhost:9222"); |
| expect(agent).toBeInstanceOf(https.Agent); |
| }); |
|
|
| it("returns https.Agent for https://127.0.0.1 URLs", () => { |
| const agent = getDirectAgentForCdp("https://127.0.0.1:9222/json/version"); |
| expect(agent).toBeInstanceOf(https.Agent); |
| }); |
|
|
| it("returns http.Agent for ws://[::1] URLs", () => { |
| const agent = getDirectAgentForCdp("ws://[::1]:9222"); |
| expect(agent).toBeInstanceOf(http.Agent); |
| }); |
|
|
| it("returns undefined for non-loopback URLs", () => { |
| expect(getDirectAgentForCdp("http://remote-host:9222")).toBeUndefined(); |
| expect(getDirectAgentForCdp("https://example.com:9222")).toBeUndefined(); |
| }); |
|
|
| it("returns undefined for invalid URLs", () => { |
| expect(getDirectAgentForCdp("not-a-url")).toBeUndefined(); |
| }); |
| }); |
|
|
| describe("hasProxyEnv", () => { |
| const proxyVars = [ |
| "HTTP_PROXY", |
| "http_proxy", |
| "HTTPS_PROXY", |
| "https_proxy", |
| "ALL_PROXY", |
| "all_proxy", |
| ]; |
| const saved: Record<string, string | undefined> = {}; |
|
|
| beforeEach(() => { |
| for (const v of proxyVars) { |
| saved[v] = process.env[v]; |
| } |
| for (const v of proxyVars) { |
| delete process.env[v]; |
| } |
| }); |
|
|
| afterEach(() => { |
| for (const v of proxyVars) { |
| if (saved[v] !== undefined) { |
| process.env[v] = saved[v]; |
| } else { |
| delete process.env[v]; |
| } |
| } |
| }); |
|
|
| it("returns false when no proxy vars set", () => { |
| expect(hasProxyEnv()).toBe(false); |
| }); |
|
|
| it("returns true when HTTP_PROXY is set", () => { |
| process.env.HTTP_PROXY = "http://proxy:8080"; |
| expect(hasProxyEnv()).toBe(true); |
| }); |
|
|
| it("returns true when ALL_PROXY is set", () => { |
| process.env.ALL_PROXY = "socks5://proxy:1080"; |
| expect(hasProxyEnv()).toBe(true); |
| }); |
| }); |
|
|
| describe("withNoProxyForLocalhost", () => { |
| const saved: Record<string, string | undefined> = {}; |
| const vars = ["HTTP_PROXY", "NO_PROXY", "no_proxy"]; |
|
|
| beforeEach(() => { |
| for (const v of vars) { |
| saved[v] = process.env[v]; |
| } |
| }); |
|
|
| afterEach(() => { |
| for (const v of vars) { |
| if (saved[v] !== undefined) { |
| process.env[v] = saved[v]; |
| } else { |
| delete process.env[v]; |
| } |
| } |
| }); |
|
|
| it("sets NO_PROXY when proxy is configured", async () => { |
| process.env.HTTP_PROXY = "http://proxy:8080"; |
| delete process.env.NO_PROXY; |
| delete process.env.no_proxy; |
|
|
| let capturedNoProxy: string | undefined; |
| await withNoProxyForLocalhost(async () => { |
| capturedNoProxy = process.env.NO_PROXY; |
| }); |
|
|
| expect(capturedNoProxy).toContain("localhost"); |
| expect(capturedNoProxy).toContain("127.0.0.1"); |
| expect(capturedNoProxy).toContain("[::1]"); |
| |
| expect(process.env.NO_PROXY).toBeUndefined(); |
| }); |
|
|
| it("extends existing NO_PROXY", async () => { |
| process.env.HTTP_PROXY = "http://proxy:8080"; |
| process.env.NO_PROXY = "internal.corp"; |
|
|
| let capturedNoProxy: string | undefined; |
| await withNoProxyForLocalhost(async () => { |
| capturedNoProxy = process.env.NO_PROXY; |
| }); |
|
|
| expect(capturedNoProxy).toContain("internal.corp"); |
| expect(capturedNoProxy).toContain("localhost"); |
| |
| expect(process.env.NO_PROXY).toBe("internal.corp"); |
| }); |
|
|
| it("skips when no proxy env is set", async () => { |
| delete process.env.HTTP_PROXY; |
| delete process.env.HTTPS_PROXY; |
| delete process.env.ALL_PROXY; |
| delete process.env.NO_PROXY; |
|
|
| await withNoProxyForLocalhost(async () => { |
| expect(process.env.NO_PROXY).toBeUndefined(); |
| }); |
| }); |
|
|
| it("restores env even on error", async () => { |
| process.env.HTTP_PROXY = "http://proxy:8080"; |
| delete process.env.NO_PROXY; |
|
|
| await expect( |
| withNoProxyForLocalhost(async () => { |
| throw new Error("boom"); |
| }), |
| ).rejects.toThrow("boom"); |
|
|
| expect(process.env.NO_PROXY).toBeUndefined(); |
| }); |
| }); |
| }); |
|
|
| describe("withNoProxyForLocalhost concurrency", () => { |
| it("does not leak NO_PROXY when called concurrently", async () => { |
| await withIsolatedNoProxyEnv(async () => { |
| const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js"); |
|
|
| |
| const callA = withNoProxyForLocalhost(async () => { |
| |
| expect(process.env.NO_PROXY).toContain("localhost"); |
| expect(process.env.NO_PROXY).toContain("[::1]"); |
| await delay(50); |
| return "a"; |
| }); |
| const callB = withNoProxyForLocalhost(async () => { |
| await delay(20); |
| return "b"; |
| }); |
|
|
| await Promise.all([callA, callB]); |
|
|
| |
| expect(process.env.NO_PROXY).toBeUndefined(); |
| expect(process.env.no_proxy).toBeUndefined(); |
| }); |
| }); |
| }); |
|
|
| describe("withNoProxyForLocalhost reverse exit order", () => { |
| it("restores NO_PROXY when first caller exits before second", async () => { |
| await withIsolatedNoProxyEnv(async () => { |
| const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js"); |
|
|
| |
| |
| const callA = withNoProxyForLocalhost(async () => { |
| await delay(10); |
| return "a"; |
| }); |
| const callB = withNoProxyForLocalhost(async () => { |
| await delay(60); |
| return "b"; |
| }); |
|
|
| await Promise.all([callA, callB]); |
|
|
| |
| expect(process.env.NO_PROXY).toBeUndefined(); |
| expect(process.env.no_proxy).toBeUndefined(); |
| }); |
| }); |
| }); |
|
|
| describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => { |
| it("does not delete NO_PROXY when loopback entries already present", async () => { |
| const userNoProxy = "localhost,127.0.0.1,[::1],myhost.internal"; |
| process.env.NO_PROXY = userNoProxy; |
| process.env.no_proxy = userNoProxy; |
| process.env.HTTP_PROXY = "http://proxy:8080"; |
|
|
| try { |
| const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js"); |
|
|
| await withNoProxyForLocalhost(async () => { |
| |
| expect(process.env.NO_PROXY).toBe(userNoProxy); |
| return "ok"; |
| }); |
|
|
| |
| expect(process.env.NO_PROXY).toBe(userNoProxy); |
| expect(process.env.no_proxy).toBe(userNoProxy); |
| } finally { |
| delete process.env.HTTP_PROXY; |
| delete process.env.NO_PROXY; |
| delete process.env.no_proxy; |
| } |
| }); |
| }); |
|
|
| describe("withNoProxyForCdpUrl", () => { |
| it("does not mutate NO_PROXY for non-loopback CDP URLs", async () => { |
| process.env.HTTP_PROXY = "http://proxy:8080"; |
| delete process.env.NO_PROXY; |
| delete process.env.no_proxy; |
| try { |
| await withNoProxyForCdpUrl("https://browserless.example/chrome?token=abc", async () => { |
| expect(process.env.NO_PROXY).toBeUndefined(); |
| expect(process.env.no_proxy).toBeUndefined(); |
| }); |
| } finally { |
| delete process.env.HTTP_PROXY; |
| delete process.env.NO_PROXY; |
| delete process.env.no_proxy; |
| } |
| }); |
|
|
| it("does not overwrite external NO_PROXY changes made during execution", async () => { |
| process.env.HTTP_PROXY = "http://proxy:8080"; |
| delete process.env.NO_PROXY; |
| delete process.env.no_proxy; |
| try { |
| await withNoProxyForCdpUrl("http://127.0.0.1:9222", async () => { |
| process.env.NO_PROXY = "externally-set"; |
| process.env.no_proxy = "externally-set"; |
| }); |
| expect(process.env.NO_PROXY).toBe("externally-set"); |
| expect(process.env.no_proxy).toBe("externally-set"); |
| } finally { |
| delete process.env.HTTP_PROXY; |
| delete process.env.NO_PROXY; |
| delete process.env.no_proxy; |
| } |
| }); |
| }); |
|
|