mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 09:40:43 +00:00
feat: add proxy validation command
Adds `openclaw proxy validate` for operator-managed proxy preflight checks, including allowed/denied destination validation, CLI output, tests, docs, and changelog coverage. Maintainer follow-ups before landing: - validate custom allowed URLs before probing; - use a temporary loopback canary for default denied checks and fail custom denied transport errors as unverifiable; - redact proxy URL userinfo, query strings, and fragments from text/JSON validation output. Validation: - `pnpm test src/infra/net/proxy/proxy-validation.test.ts src/cli/proxy-cli.runtime.test.ts src/cli/proxy-cli.test.ts -- --reporter=verbose` - `pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/cli/proxy-cli.ts src/cli/proxy-cli.runtime.ts src/cli/proxy-cli.test.ts src/cli/proxy-cli.runtime.test.ts src/infra/net/proxy/proxy-validation.ts src/infra/net/proxy/proxy-validation.test.ts docs/cli/proxy.md docs/security/network-proxy.md` - `pnpm exec oxlint src/cli/proxy-cli.runtime.ts src/cli/proxy-cli.runtime.test.ts` - `git diff --check` - Testbox `pnpm install && OPENCLAW_TESTBOX=1 pnpm check:changed` on `tbx_01kqgz68ff20n3dtrgq0j1mykt` - GitHub CI success on `321b3aaf2b8be27dec6ce2ac5e4007ed064218b5`
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import process from "node:process";
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import {
|
||||
runProxyValidation,
|
||||
type ProxyValidationResult,
|
||||
} from "../infra/net/proxy/proxy-validation.js";
|
||||
import { ensureDebugProxyCa } from "../proxy-capture/ca.js";
|
||||
import { buildDebugProxyCoverageReport } from "../proxy-capture/coverage.js";
|
||||
import { resolveDebugProxySettings, applyDebugProxyEnv } from "../proxy-capture/env.js";
|
||||
@@ -115,6 +120,135 @@ export async function runDebugProxyRunCommand(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
function redactProxyUrl(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (url.username || url.password) {
|
||||
url.username = "redacted";
|
||||
url.password = "redacted";
|
||||
}
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
return url.toString();
|
||||
} catch {
|
||||
return "<invalid proxy URL>";
|
||||
}
|
||||
}
|
||||
|
||||
function redactProxyValidationResult(result: ProxyValidationResult): ProxyValidationResult {
|
||||
return {
|
||||
...result,
|
||||
config: {
|
||||
...result.config,
|
||||
proxyUrl: redactProxyUrl(result.config.proxyUrl),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function formatProxyCheckLine(check: ProxyValidationResult["checks"][number]): string {
|
||||
const icon = check.ok ? "✓" : "✗";
|
||||
const paddedKind = check.kind.padEnd(7, " ");
|
||||
const status = check.status === undefined ? "" : ` HTTP ${check.status}`;
|
||||
return ` ${icon} ${paddedKind} ${check.url}${status}`;
|
||||
}
|
||||
|
||||
function formatProxyValidationNextSteps(result: ProxyValidationResult): string[] {
|
||||
if (result.ok) {
|
||||
return [];
|
||||
}
|
||||
if (result.config.errors.some((error) => error.includes("proxy.enabled"))) {
|
||||
return [
|
||||
"Enable proxy.enabled with proxy.proxyUrl or OPENCLAW_PROXY_URL, or pass --proxy-url for an explicit one-off validation.",
|
||||
];
|
||||
}
|
||||
if (result.config.errors.length > 0) {
|
||||
return [
|
||||
"Fix proxy.proxyUrl, OPENCLAW_PROXY_URL, or --proxy-url so it uses a reachable http:// proxy.",
|
||||
];
|
||||
}
|
||||
if (result.checks.some((check) => !check.ok && check.kind === "allowed")) {
|
||||
return [
|
||||
"Confirm the proxy is reachable from this deployment context and permits the allowed destinations.",
|
||||
];
|
||||
}
|
||||
if (result.checks.some((check) => !check.ok && check.kind === "denied")) {
|
||||
return [
|
||||
"Update the proxy ACL so denied destinations are blocked, or pass the expected --denied-url values.",
|
||||
];
|
||||
}
|
||||
return [
|
||||
"Review the failed checks above and update proxy configuration or validation destinations.",
|
||||
];
|
||||
}
|
||||
|
||||
function formatProxyValidationText(result: ProxyValidationResult): string {
|
||||
const redactedProxyUrl = redactProxyUrl(result.config.proxyUrl);
|
||||
const lines = [
|
||||
`Proxy validation ${result.ok ? "passed" : "failed"}`,
|
||||
"",
|
||||
"Proxy",
|
||||
` Source: ${result.config.source}`,
|
||||
` URL: ${redactedProxyUrl ?? "not configured"}`,
|
||||
];
|
||||
|
||||
if (result.config.errors.length > 0) {
|
||||
lines.push("", "Problems");
|
||||
for (const error of result.config.errors) {
|
||||
lines.push(` - ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.checks.length > 0) {
|
||||
lines.push("", "Checks");
|
||||
for (const check of result.checks) {
|
||||
lines.push(formatProxyCheckLine(check));
|
||||
if (check.error) {
|
||||
lines.push(` ${check.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nextSteps = formatProxyValidationNextSteps(result);
|
||||
if (nextSteps.length > 0) {
|
||||
lines.push("", "Next steps");
|
||||
for (const nextStep of nextSteps) {
|
||||
lines.push(` ${nextStep}`);
|
||||
}
|
||||
}
|
||||
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
export async function runProxyValidateCommand(opts: {
|
||||
json?: boolean;
|
||||
proxyUrl?: string;
|
||||
allowedUrls?: string[];
|
||||
deniedUrls?: string[];
|
||||
timeoutMs?: number;
|
||||
}) {
|
||||
const config = getRuntimeConfig();
|
||||
const result = await runProxyValidation({
|
||||
config: config?.proxy,
|
||||
env: process.env,
|
||||
proxyUrlOverride: opts.proxyUrl,
|
||||
allowedUrls: opts.allowedUrls,
|
||||
deniedUrls: opts.deniedUrls,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
});
|
||||
const outputResult = redactProxyValidationResult(result);
|
||||
process.stdout.write(
|
||||
opts.json === true
|
||||
? `${JSON.stringify(outputResult, null, 2)}\n`
|
||||
: formatProxyValidationText(outputResult),
|
||||
);
|
||||
if (!result.ok) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runDebugProxySessionsCommand(opts: { limit?: number }) {
|
||||
const settings = resolveDebugProxySettings();
|
||||
const sessions = getDebugProxyCaptureStore(settings.dbPath, settings.blobDir).listSessions(
|
||||
|
||||
Reference in New Issue
Block a user