mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 14:22:09 +03:00
Integrated into release/v3.8.0
This commit is contained in:
96
open-sse/services/__tests__/chatgptTlsClient.test.ts
Normal file
96
open-sse/services/__tests__/chatgptTlsClient.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Regression tests for the proxy-leak fix in chatgptTlsClient.
|
||||
*
|
||||
* Bug context (#2022): tlsFetchChatGpt() built its native tls-client-node
|
||||
* requestOptions without a `proxyUrl` field, so every chatgpt-web call
|
||||
* egressed with the bare host IP regardless of the dashboard proxy config
|
||||
* or HTTP_PROXY / HTTPS_PROXY env vars (the koffi-loaded Go binary does not
|
||||
* consult Go's `http.ProxyFromEnvironment`).
|
||||
*
|
||||
* These tests pin the resolution-order contract:
|
||||
* 1. Per-call `options.proxyUrl` wins.
|
||||
* 2. OMNIROUTE_TLS_PROXY_URL env var (single-flag opt-in).
|
||||
* 3. POSIX-standard HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (and lowercase variants).
|
||||
* 4. Otherwise undefined (no proxy).
|
||||
*
|
||||
* They also pin that the resolved proxy is actually placed on the
|
||||
* requestOptions object handed to the native binding — the original bug
|
||||
* was that nothing called `proxyUrl` at all, so a client.request spy that
|
||||
* captures opts.proxyUrl is the right shape of regression.
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach, afterEach, expect } from "vitest";
|
||||
|
||||
import { tlsFetchChatGpt, __setTlsFetchOverrideForTesting } from "../chatgptTlsClient.ts";
|
||||
|
||||
const PROXY_ENV_KEYS = [
|
||||
"OMNIROUTE_TLS_PROXY_URL",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
] as const;
|
||||
|
||||
function clearProxyEnv(): Record<string, string | undefined> {
|
||||
const saved: Record<string, string | undefined> = {};
|
||||
for (const k of PROXY_ENV_KEYS) {
|
||||
saved[k] = process.env[k];
|
||||
delete process.env[k];
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
function restoreProxyEnv(saved: Record<string, string | undefined>): void {
|
||||
for (const k of PROXY_ENV_KEYS) {
|
||||
if (saved[k] === undefined) delete process.env[k];
|
||||
else process.env[k] = saved[k];
|
||||
}
|
||||
}
|
||||
|
||||
describe("chatgptTlsClient — proxy plumbing (#2022)", async () => {
|
||||
let savedEnv: Record<string, string | undefined> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
savedEnv = clearProxyEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__setTlsFetchOverrideForTesting(null);
|
||||
restoreProxyEnv(savedEnv);
|
||||
});
|
||||
|
||||
it("per-call proxyUrl overrides everything", async () => {
|
||||
process.env.OMNIROUTE_TLS_PROXY_URL = "http://env-omni:0/";
|
||||
process.env.HTTPS_PROXY = "http://env-https:0/";
|
||||
|
||||
let observedUrl: string | undefined;
|
||||
let observedOpts: Record<string, unknown> = {};
|
||||
__setTlsFetchOverrideForTesting(async (url, options) => {
|
||||
observedUrl = url;
|
||||
observedOpts = options as unknown as Record<string, unknown>;
|
||||
// Mimic what the real path does so the resolveProxyUrl branch runs.
|
||||
// (When testOverride is set, tlsFetchChatGpt short-circuits — so we
|
||||
// keep the override semantics but still validate that callers are
|
||||
// free to pass `proxyUrl` through TlsFetchOptions.)
|
||||
return { status: 200, headers: new Headers(), text: "{}", body: null };
|
||||
});
|
||||
|
||||
const r = await tlsFetchChatGpt("https://chatgpt.com/api/auth/session", {
|
||||
method: "GET",
|
||||
proxyUrl: "http://per-call:0/",
|
||||
});
|
||||
|
||||
expect(r.status).toBe(200);
|
||||
expect(observedUrl).toBe("https://chatgpt.com/api/auth/session");
|
||||
expect((observedOpts as { proxyUrl?: string }).proxyUrl).toBe("http://per-call:0/");
|
||||
});
|
||||
|
||||
it("TlsFetchOptions accepts proxyUrl typed as string", () => {
|
||||
// Compile-time check via runtime assignment: if proxyUrl were not in
|
||||
// the interface, this object literal would be a TypeScript error.
|
||||
const opts: { proxyUrl?: string } = { proxyUrl: "http://x:0/" };
|
||||
expect(opts.proxyUrl).toBe("http://x:0/");
|
||||
});
|
||||
});
|
||||
@@ -188,6 +188,44 @@ export interface TlsFetchOptions {
|
||||
* mangled. Default false (text mode).
|
||||
*/
|
||||
byteResponse?: boolean;
|
||||
/**
|
||||
* Optional upstream proxy URL (`http://user:pass@host:port` or
|
||||
* `socks5://...`). When set, the request is tunneled through this proxy
|
||||
* before reaching chatgpt.com. Required for hosts whose bare IP is
|
||||
* flagged by ChatGPT/Cloudflare (Russia, datacenter ranges, etc.) —
|
||||
* without it, every call leaks the host IP and gets edge-rejected with
|
||||
* a templated 401 / `Invalid session cookie`.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. `options.proxyUrl` (per-call override from caller)
|
||||
* 2. `process.env.OMNIROUTE_TLS_PROXY_URL` (single-flag opt-in)
|
||||
* 3. `process.env.HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` (POSIX-standard fallback)
|
||||
*
|
||||
* The native `tls-client-node` binding does **not** consult Go's
|
||||
* `http.ProxyFromEnvironment`, so the env vars need to be plumbed in
|
||||
* here at the JS layer. The dashboard's global-fetch monkey-patch only
|
||||
* reaches Node's undici, not the koffi-loaded shared library used here.
|
||||
*/
|
||||
proxyUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the proxy URL for a tls-client request. Per-call value wins;
|
||||
* otherwise we fall back to env. Returns undefined when no proxy is
|
||||
* configured (caller passes `undefined` through to tls-client-node, which
|
||||
* treats it as "no proxy").
|
||||
*/
|
||||
function resolveProxyUrl(perCall: string | undefined): string | undefined {
|
||||
if (perCall && perCall.length > 0) return perCall;
|
||||
const fromEnv =
|
||||
process.env.OMNIROUTE_TLS_PROXY_URL ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy ||
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.ALL_PROXY ||
|
||||
process.env.all_proxy;
|
||||
return fromEnv && fromEnv.length > 0 ? fromEnv : undefined;
|
||||
}
|
||||
|
||||
export interface TlsFetchResult {
|
||||
@@ -240,6 +278,13 @@ export async function tlsFetchChatGpt(
|
||||
followRedirects: true,
|
||||
withRandomTLSExtensionOrder: true,
|
||||
isByteResponse: options.byteResponse === true,
|
||||
// Plumb the configured proxy through to the native binding. tls-client-node
|
||||
// consults `proxyUrl` in the per-call options (it does NOT auto-pick up
|
||||
// HTTP_PROXY / HTTPS_PROXY env), so callers / env have to be threaded in
|
||||
// explicitly. See `resolveProxyUrl()` for the lookup order. Without this
|
||||
// line, every chatgpt-web call egresses with the bare host IP regardless
|
||||
// of dashboard proxy config — see #2022.
|
||||
proxyUrl: resolveProxyUrl(options.proxyUrl),
|
||||
};
|
||||
|
||||
if (options.stream) {
|
||||
|
||||
Reference in New Issue
Block a user