diff --git a/changelog.d/fixes/13646-socks-flag-reader.md b/changelog.d/fixes/13646-socks-flag-reader.md new file mode 100644 index 0000000000..af2032e4f8 --- /dev/null +++ b/changelog.d/fixes/13646-socks-flag-reader.md @@ -0,0 +1 @@ +- **fix(api):** share the single SOCKS5 flag reader across the settings proxy routes so the dashboard and the dispatcher stay consistent ([#13646](https://github.com/diegosouzapw/OmniRoute/pull/13646)) — thanks @maxmad64bis diff --git a/src/app/api/settings/proxies/route.ts b/src/app/api/settings/proxies/route.ts index fe3153629d..35031d4aa4 100644 --- a/src/app/api/settings/proxies/route.ts +++ b/src/app/api/settings/proxies/route.ts @@ -1,3 +1,4 @@ +import { isSocks5ProxyEnabled } from "@omniroute/open-sse/utils/proxyDispatcher"; import { listProxies } from "@/lib/db/proxies"; import { handleProxyCreate, @@ -42,10 +43,8 @@ export async function GET(request: Request) { // #5890: coarse relay health pulse for the dashboard — how many relay // probes have run, and how many came back alive. relayProbeStats: getRelayProbeStats(), - // Default ON (opt-out): only an explicit falsey value disables SOCKS5. - socks5Enabled: !["false", "0", "no", "off"].includes( - (process.env.ENABLE_SOCKS5_PROXY ?? "").trim().toLowerCase() - ), + // SOCKS5 defaults ON — see isSocks5ProxyEnabled(). + socks5Enabled: isSocks5ProxyEnabled(), }); } catch (error) { return createErrorResponseFromUnknown(error, "Failed to load proxies"); diff --git a/src/app/api/settings/proxy/route.ts b/src/app/api/settings/proxy/route.ts index d05d0eb4c2..01d07e6331 100755 --- a/src/app/api/settings/proxy/route.ts +++ b/src/app/api/settings/proxy/route.ts @@ -6,7 +6,10 @@ import { resolveProxyForConnection, } from "@/lib/db/settings"; import { getProxyAssignments, getProxyById } from "@/lib/db/proxies"; -import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher"; +import { + clearDispatcherCache, + isSocks5ProxyEnabled, +} from "@omniroute/open-sse/utils/proxyDispatcher"; import { updateProxyConfigSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { @@ -29,21 +32,15 @@ const PROXY_LEVEL_TO_REGISTRY_SCOPE = { key: "account", } as const; -function isSocks5Enabled() { - // Default ON (opt-out): only an explicit falsey value disables SOCKS5. - const raw = (process.env.ENABLE_SOCKS5_PROXY ?? "").trim().toLowerCase(); - return !["false", "0", "no", "off"].includes(raw); -} - function getSupportedProxyTypes() { - if (isSocks5Enabled()) { + if (isSocks5ProxyEnabled()) { return new Set([...BASE_SUPPORTED_PROXY_TYPES, "socks5"]); } return BASE_SUPPORTED_PROXY_TYPES; } function supportedTypesMessage() { - return isSocks5Enabled() ? "http, https, or socks5" : "http or https"; + return isSocks5ProxyEnabled() ? "http, https, or socks5" : "http or https"; } function createInvalidProxyError(message: string): ApiRouteError { @@ -104,7 +101,7 @@ function normalizeAndValidateProxy( } const type = String(proxy.type || "http").toLowerCase() as NonNullable; - if (type === "socks5" && !isSocks5Enabled()) { + if (type === "socks5" && !isSocks5ProxyEnabled()) { throw createInvalidProxyError( "SOCKS5 proxy is disabled (remove ENABLE_SOCKS5_PROXY=false to enable — it is ON by default)" ); diff --git a/tests/unit/settings-socks-flag-reader.test.ts b/tests/unit/settings-socks-flag-reader.test.ts new file mode 100644 index 0000000000..9c36410c3e --- /dev/null +++ b/tests/unit/settings-socks-flag-reader.test.ts @@ -0,0 +1,98 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-socks-flag-routes-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "test-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const { isSocks5ProxyEnabled } = await import("../../open-sse/utils/proxyDispatcher.ts"); +const proxiesRoute = await import("../../src/app/api/settings/proxies/route.ts"); +const proxyRoute = await import("../../src/app/api/settings/proxy/route.ts"); + +// ENABLE_SOCKS5_PROXY is opt-out: only an explicit falsey value disables SOCKS5. +const MATRIX: Array<[string | undefined, boolean]> = [ + [undefined, true], + ["", true], + ["true", true], + ["1", true], + ["yes", true], + ["false", false], + ["0", false], + ["no", false], + ["off", false], + [" OFF ", false], + ["False", false], +]; + +async function withSocksFlag(value: string | undefined, fn: () => Promise | T): Promise { + const previous = process.env.ENABLE_SOCKS5_PROXY; + if (value === undefined) delete process.env.ENABLE_SOCKS5_PROXY; + else process.env.ENABLE_SOCKS5_PROXY = value; + try { + return await fn(); + } finally { + if (previous === undefined) delete process.env.ENABLE_SOCKS5_PROXY; + else process.env.ENABLE_SOCKS5_PROXY = previous; + } +} + +function putProxy(body: unknown) { + return proxyRoute.PUT( + new Request("http://localhost/api/settings/proxy", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }) + ); +} + +test.before(() => { + delete process.env.INITIAL_PASSWORD; + core.resetDbInstance(); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("flag reader honors the opt-out matrix (unset defaults ON)", async () => { + for (const [value, expected] of MATRIX) { + await withSocksFlag(value, () => { + assert.equal(isSocks5ProxyEnabled(), expected, `ENABLE_SOCKS5_PROXY=${String(value)}`); + }); + } +}); + +test("GET /api/settings/proxies reports socks5Enabled exactly as the flag reader", async () => { + for (const [value, expected] of MATRIX) { + await withSocksFlag(value, async () => { + const response = await proxiesRoute.GET(new Request("http://localhost/api/settings/proxies")); + assert.equal(response.status, 200); + const body = (await response.json()) as { socks5Enabled: boolean }; + assert.equal(body.socks5Enabled, expected, `ENABLE_SOCKS5_PROXY=${String(value)}`); + }); + } +}); + +test("PUT /api/settings/proxy accepts or rejects socks5 following the flag", async () => { + for (const [value, expected] of MATRIX) { + await withSocksFlag(value, async () => { + const response = await putProxy({ + level: "global", + proxy: { type: "socks5", host: "127.0.0.1", port: 1080 }, + }); + const body = (await response.json()) as { error?: { message?: string } }; + if (expected) { + assert.equal(response.status, 200, `ENABLE_SOCKS5_PROXY=${String(value)}`); + } else { + assert.equal(response.status, 400, `ENABLE_SOCKS5_PROXY=${String(value)}`); + assert.match(body.error?.message ?? "", /SOCKS5 proxy is disabled/); + } + }); + } +});