mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-09 17:28:51 +00:00
fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620)
Some checks failed
CI / Change Classification (push) Has been cancelled
Publish Fork Image to GHCR / Build and Push Fork Image (push) Has been cancelled
OpenSSF Scorecard / Scorecard analysis (push) Has been cancelled
semgrep / semgrep (push) Has been cancelled
CI / E2E Tests (4/9) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Quality Gates (Extended) (push) Has been cancelled
CI / Docs Sync (Strict) (push) Has been cancelled
CI / Docs Lint (prose — advisory) (push) Has been cancelled
CI / i18n UI Coverage (push) Has been cancelled
CI / i18n Validation (all languages) (push) Has been cancelled
CI / PR Test Policy (push) Has been cancelled
Publish to Docker Hub / Resolve Docker release metadata (push) Has been cancelled
CI / Quality Ratchet (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Package Artifact (push) Has been cancelled
CI / Electron Package Smoke (push) Has been cancelled
CI / Unit Tests (1/8) (push) Has been cancelled
CI / Unit Tests (2/8) (push) Has been cancelled
CI / Unit Tests (3/8) (push) Has been cancelled
CI / Unit Tests (4/8) (push) Has been cancelled
CI / Unit Tests (5/8) (push) Has been cancelled
CI / Unit Tests (6/8) (push) Has been cancelled
CI / Unit Tests (7/8) (push) Has been cancelled
CI / Unit Tests (8/8) (push) Has been cancelled
CI / Vitest (MCP / autoCombo / UI components) (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / SonarQube (push) Has been cancelled
CI / PR Coverage Comment (push) Has been cancelled
CI / E2E Tests (1/9) (push) Has been cancelled
CI / E2E Tests (2/9) (push) Has been cancelled
CI / E2E Tests (3/9) (push) Has been cancelled
CI / E2E Tests (5/9) (push) Has been cancelled
CI / E2E Tests (6/9) (push) Has been cancelled
CI / E2E Tests (7/9) (push) Has been cancelled
CI / E2E Tests (8/9) (push) Has been cancelled
CI / E2E Tests (9/9) (push) Has been cancelled
CI / Integration Tests (1/2) (push) Has been cancelled
CI / Integration Tests (2/2) (push) Has been cancelled
CI / Security Tests (push) Has been cancelled
CI / CI Dashboard (push) Has been cancelled
Publish to Docker Hub / Build Docker (linux/amd64) (push) Has been cancelled
Publish to Docker Hub / Build Docker (linux/arm64) (push) Has been cancelled
Publish to Docker Hub / Publish multi-arch manifests (push) Has been cancelled
Some checks failed
CI / Change Classification (push) Has been cancelled
Publish Fork Image to GHCR / Build and Push Fork Image (push) Has been cancelled
OpenSSF Scorecard / Scorecard analysis (push) Has been cancelled
semgrep / semgrep (push) Has been cancelled
CI / E2E Tests (4/9) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Quality Gates (Extended) (push) Has been cancelled
CI / Docs Sync (Strict) (push) Has been cancelled
CI / Docs Lint (prose — advisory) (push) Has been cancelled
CI / i18n UI Coverage (push) Has been cancelled
CI / i18n Validation (all languages) (push) Has been cancelled
CI / PR Test Policy (push) Has been cancelled
Publish to Docker Hub / Resolve Docker release metadata (push) Has been cancelled
CI / Quality Ratchet (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Package Artifact (push) Has been cancelled
CI / Electron Package Smoke (push) Has been cancelled
CI / Unit Tests (1/8) (push) Has been cancelled
CI / Unit Tests (2/8) (push) Has been cancelled
CI / Unit Tests (3/8) (push) Has been cancelled
CI / Unit Tests (4/8) (push) Has been cancelled
CI / Unit Tests (5/8) (push) Has been cancelled
CI / Unit Tests (6/8) (push) Has been cancelled
CI / Unit Tests (7/8) (push) Has been cancelled
CI / Unit Tests (8/8) (push) Has been cancelled
CI / Vitest (MCP / autoCombo / UI components) (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / SonarQube (push) Has been cancelled
CI / PR Coverage Comment (push) Has been cancelled
CI / E2E Tests (1/9) (push) Has been cancelled
CI / E2E Tests (2/9) (push) Has been cancelled
CI / E2E Tests (3/9) (push) Has been cancelled
CI / E2E Tests (5/9) (push) Has been cancelled
CI / E2E Tests (6/9) (push) Has been cancelled
CI / E2E Tests (7/9) (push) Has been cancelled
CI / E2E Tests (8/9) (push) Has been cancelled
CI / E2E Tests (9/9) (push) Has been cancelled
CI / Integration Tests (1/2) (push) Has been cancelled
CI / Integration Tests (2/2) (push) Has been cancelled
CI / Security Tests (push) Has been cancelled
CI / CI Dashboard (push) Has been cancelled
Publish to Docker Hub / Build Docker (linux/amd64) (push) Has been cancelled
Publish to Docker Hub / Build Docker (linux/arm64) (push) Has been cancelled
Publish to Docker Hub / Publish multi-arch manifests (push) Has been cancelled
fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.)
This commit is contained in:
parent
44d501ae6a
commit
9cd18bf9a1
3 changed files with 621 additions and 815 deletions
|
|
@ -472,6 +472,12 @@ export function createProxyDispatcher(proxyUrl: string): Dispatcher {
|
||||||
// valid; the cast suppresses the spurious missing-`port` error.
|
// valid; the cast suppresses the spurious missing-`port` error.
|
||||||
dispatcher = new ProxyAgent({
|
dispatcher = new ProxyAgent({
|
||||||
uri: cleanUri,
|
uri: cleanUri,
|
||||||
|
// undici 8.6+ forwards plain-HTTP requests through the proxy as an origin
|
||||||
|
// request (GET http://host/…) instead of a CONNECT tunnel; upstream proxies
|
||||||
|
// that only speak CONNECT then reject it (501). OmniRoute tunnels ALL proxied
|
||||||
|
// traffic (HTTP + HTTPS) via CONNECT, so force tunneling. Unknown option on
|
||||||
|
// undici <8.6 → silently ignored (that version already tunneled by default).
|
||||||
|
proxyTunnel: true,
|
||||||
...proxyDispatcherOptions,
|
...proxyDispatcherOptions,
|
||||||
...(family !== null
|
...(family !== null
|
||||||
? { proxyTls: { family, autoSelectFamily: false } as ProxyAgent.Options["proxyTls"] }
|
? { proxyTls: { family, autoSelectFamily: false } as ProxyAgent.Options["proxyTls"] }
|
||||||
|
|
|
||||||
1355
package-lock.json
generated
1355
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -26,7 +26,12 @@ describe("proxyDispatcher SOCKS5 host handling", () => {
|
||||||
|
|
||||||
describe("proxyDispatcher family directive", () => {
|
describe("proxyDispatcher family directive", () => {
|
||||||
it("encodes family from a config object onto the URL", () => {
|
it("encodes family from a config object onto the URL", () => {
|
||||||
const url = proxyConfigToUrl({ type: "http", host: "proxy.example.com", port: 8080, family: "ipv6" });
|
const url = proxyConfigToUrl({
|
||||||
|
type: "http",
|
||||||
|
host: "proxy.example.com",
|
||||||
|
port: 8080,
|
||||||
|
family: "ipv6",
|
||||||
|
});
|
||||||
assert.ok(url!.includes("family=ipv6"), url!);
|
assert.ok(url!.includes("family=ipv6"), url!);
|
||||||
});
|
});
|
||||||
it("derives 6 for an explicit ipv6 directive on a hostname proxy", () => {
|
it("derives 6 for an explicit ipv6 directive on a hostname proxy", () => {
|
||||||
|
|
@ -38,7 +43,10 @@ describe("proxyDispatcher family directive", () => {
|
||||||
assert.equal(__resolveDispatcherFamilyForTest("http://proxy.example.com:8080"), null);
|
assert.equal(__resolveDispatcherFamilyForTest("http://proxy.example.com:8080"), null);
|
||||||
});
|
});
|
||||||
it("throws (fail-closed) when family=ipv6 contradicts a v4 literal", () => {
|
it("throws (fail-closed) when family=ipv6 contradicts a v4 literal", () => {
|
||||||
assert.throws(() => __resolveDispatcherFamilyForTest("http://203.0.113.7:8080?family=ipv6"), /family/i);
|
assert.throws(
|
||||||
|
() => __resolveDispatcherFamilyForTest("http://203.0.113.7:8080?family=ipv6"),
|
||||||
|
/family/i
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -97,3 +105,66 @@ describe("proxyDispatcher connection pool", () => {
|
||||||
assert.equal(closeCount, 1);
|
assert.equal(closeCount, 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("proxyDispatcher CONNECT tunneling (undici 8.6+ proxyTunnel)", () => {
|
||||||
|
it("tunnels a plain-HTTP proxied request via CONNECT, not origin-forwarding", async () => {
|
||||||
|
const http = await import("node:http");
|
||||||
|
const net = await import("node:net");
|
||||||
|
const { createProxyDispatcher } = await import("../../open-sse/utils/proxyDispatcher.ts");
|
||||||
|
const { fetch: undiciFetch } = await import("undici");
|
||||||
|
|
||||||
|
// Upstream HTTP target.
|
||||||
|
const target = http.createServer((_req, res) => {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end("ok");
|
||||||
|
});
|
||||||
|
await new Promise((r) => target.listen(0, r));
|
||||||
|
const targetPort = (target.address() as { port: number }).port;
|
||||||
|
|
||||||
|
// Proxy that ONLY speaks CONNECT: 501 on a forwarded origin request, tunnels on CONNECT.
|
||||||
|
let sawConnect = false;
|
||||||
|
let sawForward = false;
|
||||||
|
const proxy = http.createServer((_req, res) => {
|
||||||
|
sawForward = true;
|
||||||
|
res.writeHead(501);
|
||||||
|
res.end("CONNECT only");
|
||||||
|
});
|
||||||
|
proxy.on("connect", (req, socket) => {
|
||||||
|
sawConnect = true;
|
||||||
|
const [host, port] = String(req.url).split(":");
|
||||||
|
const upstream = net.connect(Number(port), host, () => {
|
||||||
|
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
||||||
|
upstream.pipe(socket);
|
||||||
|
socket.pipe(upstream);
|
||||||
|
});
|
||||||
|
upstream.on("error", () => socket.destroy());
|
||||||
|
});
|
||||||
|
await new Promise((r) => proxy.listen(0, r));
|
||||||
|
const proxyPort = (proxy.address() as { port: number }).port;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dispatcher = createProxyDispatcher(`http://127.0.0.1:${proxyPort}`);
|
||||||
|
const res = await undiciFetch(`http://127.0.0.1:${targetPort}/token`, {
|
||||||
|
method: "POST",
|
||||||
|
// @ts-expect-error undici dispatcher option
|
||||||
|
dispatcher,
|
||||||
|
signal: AbortSignal.timeout(3000),
|
||||||
|
});
|
||||||
|
assert.equal(res.status, 200, "request must succeed via CONNECT tunnel");
|
||||||
|
assert.equal(
|
||||||
|
sawConnect,
|
||||||
|
true,
|
||||||
|
"proxy must receive a CONNECT (tunnel), not a forwarded request"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
sawForward,
|
||||||
|
false,
|
||||||
|
"proxy must NOT receive a forwarded origin request (undici 8.6+ regression)"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
proxy.close();
|
||||||
|
target.close();
|
||||||
|
clearDispatcherCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue