mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(providers): honor configured proxy on Grok Build egress (#7244)
* fix(providers): honor configured proxy on Grok Build egress The grok-cli executor reaches Grok Build over raw `https.request()` (forced IPv4, to dodge Cloudflare blocking on the direct path) rather than the process-wide patched `fetch()` that every other executor uses. `https.request()` never consults the proxy AsyncLocalStorage context, so the proxy the caller already pinned upstream in chatHelpers.ts (`runWithProxyContext`) was silently ignored on BOTH grok-cli paths: chat inference (`nativePost`) and OAuth token refresh (`nativeHttpsPost`, POST https://auth.x.ai/oauth2/token). User-visible effect: an operator who assigns a proxy to a Grok Build connection (or provider/global scope) still egresses on the host's real IP — an IP leak that defeats account-isolation/anonymity setups, and breaks Grok Build entirely for operators who must egress through a proxy. Fix is delta-only: `resolveGrokRequestDispatch()` reads the already-resolved proxy via the shared `resolveProxyForRequest()` and returns either an HttpsProxyAgent bound to it, or — when no proxy is configured — the existing forced-IPv4 direct options, unchanged. Only HTTP/HTTPS CONNECT proxies are supported on this path; an explicitly configured proxy of another kind (SOCKS5) fails closed rather than silently leaking direct, matching the fail-closed convention for OAuth/account proxies (#3051). The proxy URL is never logged, so proxy credentials cannot leak into logs. Regression test: tests/unit/grok-cli-proxy-selection.test.ts (RED before the fix — `resolveGrokRequestDispatch` did not exist and both request builders hardcoded `family: 4` with no agent; GREEN after). Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com> Inspired-by: https://github.com/decolua/9router/pull/2343 * chore(changelog): fragment for #7244 --------- Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
d811a1a0e7
commit
606aa9a7b0
1
changelog.d/fixes/7244-grok-cli-honor-proxy.md
Normal file
1
changelog.d/fixes/7244-grok-cli-honor-proxy.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(providers):** honor a configured proxy on Grok Build egress — the grok-cli executor used raw `https.request()` and bypassed the proxy context, leaking the host IP on chat inference and OAuth token refresh. (thanks @ryanngit)
|
||||
@@ -2,7 +2,8 @@
|
||||
* GrokCliExecutor — Grok Build Provider
|
||||
*
|
||||
* Routes requests through Grok's chat proxy endpoint using OAuth authentication.
|
||||
* Uses Node.js https module directly with IPv4 forced to bypass Cloudflare blocking.
|
||||
* Uses Node.js https module directly with IPv4 forced to bypass Cloudflare blocking
|
||||
* (only for the no-proxy direct path — see resolveGrokRequestDispatch below).
|
||||
* Supports automatic token refresh via refresh_token.
|
||||
*/
|
||||
|
||||
@@ -14,11 +15,59 @@ import {
|
||||
} from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
import { resolvePublicCred } from "../utils/publicCreds.ts";
|
||||
import { resolveProxyForRequest } from "../utils/proxyFetch.ts";
|
||||
import https from "node:https";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
|
||||
const GROK_TOKEN_URL = "https://auth.x.ai/oauth2/token";
|
||||
const REQUEST_TIMEOUT_MS = 60_000;
|
||||
|
||||
type ProxyResolution = { source: string; proxyUrl: string | null };
|
||||
type GrokRequestDispatch = { agent?: https.Agent; family?: 4 };
|
||||
|
||||
/**
|
||||
* Resolve how a Grok Build request to `targetUrl` should egress: through the
|
||||
* operator's configured proxy (connection/provider/global — whatever the caller
|
||||
* already pinned via `runWithProxyContext` upstream in chatHelpers.ts) when one
|
||||
* is set, or direct with the existing forced-IPv4 workaround when none is.
|
||||
*
|
||||
* This executor talks to Grok via raw `https.request()` instead of the global
|
||||
* patched `fetch()` (every other executor's path), so it never consulted the
|
||||
* proxy context at all — a configured proxy was silently ignored and the
|
||||
* request always egressed on the host's real IP. Only HTTP/HTTPS (CONNECT)
|
||||
* proxies are supported here; an explicitly configured proxy of another kind
|
||||
* (e.g. SOCKS5) fails closed rather than silently falling back to direct,
|
||||
* matching the "fail closed for OAuth usage account proxies" convention (#3051).
|
||||
*
|
||||
* `resolveProxy` is injectable for tests; defaults to the shared
|
||||
* `resolveProxyForRequest` used by the patched global fetch.
|
||||
*/
|
||||
export function resolveGrokRequestDispatch(
|
||||
targetUrl: string,
|
||||
resolveProxy: (url: string) => ProxyResolution = resolveProxyForRequest
|
||||
): GrokRequestDispatch {
|
||||
const { proxyUrl } = resolveProxy(targetUrl);
|
||||
|
||||
if (!proxyUrl) {
|
||||
return { family: 4 };
|
||||
}
|
||||
|
||||
let protocol: string;
|
||||
try {
|
||||
protocol = new URL(proxyUrl).protocol;
|
||||
} catch {
|
||||
throw new Error("Grok Build: configured proxy URL could not be parsed");
|
||||
}
|
||||
|
||||
if (protocol === "http:" || protocol === "https:") {
|
||||
return { agent: new HttpsProxyAgent(proxyUrl) as unknown as https.Agent };
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Grok Build: configured proxy protocol is not supported for this provider (HTTP/HTTPS proxies only)"
|
||||
);
|
||||
}
|
||||
|
||||
export class GrokCliExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("grok-cli", PROVIDERS["grok-cli"]);
|
||||
@@ -100,6 +149,7 @@ export class GrokCliExecutor extends BaseExecutor {
|
||||
timeoutMs = 10_000
|
||||
): Promise<{ status: number; body: string }> {
|
||||
const urlObj = new URL(url);
|
||||
const dispatch = resolveGrokRequestDispatch(url);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => req.destroy(new Error("Timeout")), timeoutMs);
|
||||
@@ -110,7 +160,8 @@ export class GrokCliExecutor extends BaseExecutor {
|
||||
port: 443,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: "POST",
|
||||
family: 4,
|
||||
...(dispatch.family ? { family: dispatch.family } : {}),
|
||||
...(dispatch.agent ? { agent: dispatch.agent } : {}),
|
||||
headers: {
|
||||
...headers,
|
||||
"Content-Length": Buffer.byteLength(bodyStr),
|
||||
@@ -145,6 +196,7 @@ export class GrokCliExecutor extends BaseExecutor {
|
||||
signal?: AbortSignal | null
|
||||
): Promise<Response> {
|
||||
const urlObj = new URL(url);
|
||||
const dispatch = resolveGrokRequestDispatch(url);
|
||||
|
||||
if (signal?.aborted) {
|
||||
return Promise.reject(new Error("Aborted"));
|
||||
@@ -167,7 +219,8 @@ export class GrokCliExecutor extends BaseExecutor {
|
||||
port: 443,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: "POST",
|
||||
family: 4,
|
||||
...(dispatch.family ? { family: dispatch.family } : {}),
|
||||
...(dispatch.agent ? { agent: dispatch.agent } : {}),
|
||||
headers: {
|
||||
...headers,
|
||||
"Content-Length": Buffer.byteLength(bodyStr),
|
||||
|
||||
56
tests/unit/grok-cli-proxy-selection.test.ts
Normal file
56
tests/unit/grok-cli-proxy-selection.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// Regression test for: Grok Build (grok-cli) inference/refresh requests bypassed the
|
||||
// operator's configured proxy entirely. The executor talks to Grok's upstream via raw
|
||||
// Node `https.request()` (forced IPv4, to dodge Cloudflare blocking on the direct path)
|
||||
// instead of the process-wide patched `fetch()` that every other executor uses — so a
|
||||
// proxy pinned to the connection/provider/global scope was silently ignored, leaking the
|
||||
// real egress IP and defeating account-isolation/anonymity setups. Mirrors the class of
|
||||
// bug fixed upstream in decolua/9router#2343 ("fix(oauth): honor proxy selection during
|
||||
// OAuth login"), adapted to OmniRoute's actual grok-cli architecture (import-token flow,
|
||||
// no device-code polling) where the leak lives in `resolveGrokRequestDispatch()`.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveGrokRequestDispatch } from "../../open-sse/executors/grok-cli.ts";
|
||||
|
||||
const TARGET_URL = "https://grok.x.ai/rest/app-chat/conversations/new";
|
||||
|
||||
test("resolveGrokRequestDispatch: no proxy configured -> direct IPv4 dispatch (unchanged behavior)", () => {
|
||||
const dispatch = resolveGrokRequestDispatch(TARGET_URL, () => ({
|
||||
source: "direct",
|
||||
proxyUrl: null,
|
||||
}));
|
||||
|
||||
assert.equal(dispatch.family, 4);
|
||||
assert.equal(dispatch.agent, undefined);
|
||||
});
|
||||
|
||||
test("resolveGrokRequestDispatch: HTTP proxy configured -> request is dispatched through a proxy agent, not direct", () => {
|
||||
const dispatch = resolveGrokRequestDispatch(TARGET_URL, () => ({
|
||||
source: "context",
|
||||
proxyUrl: "http://proxy.internal:8080",
|
||||
}));
|
||||
|
||||
// The fix: an agent bound to the configured proxy must be present, and the
|
||||
// direct-IPv4 workaround must NOT be applied (it would race the proxy tunnel).
|
||||
assert.ok(dispatch.agent, "expected a proxy agent to be constructed");
|
||||
assert.notEqual(dispatch.family, 4);
|
||||
});
|
||||
|
||||
test("resolveGrokRequestDispatch: HTTPS proxy configured -> request is dispatched through a proxy agent", () => {
|
||||
const dispatch = resolveGrokRequestDispatch(TARGET_URL, () => ({
|
||||
source: "context",
|
||||
proxyUrl: "https://user:pass@proxy.internal:8443",
|
||||
}));
|
||||
|
||||
assert.ok(dispatch.agent, "expected a proxy agent to be constructed");
|
||||
});
|
||||
|
||||
test("resolveGrokRequestDispatch: unsupported proxy protocol (socks5) fails closed instead of leaking direct", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveGrokRequestDispatch(TARGET_URL, () => ({
|
||||
source: "context",
|
||||
proxyUrl: "socks5://proxy.internal:1080",
|
||||
})),
|
||||
/proxy/i
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user