UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

223 lines (222 loc) 7.16 kB
import { t as resolveSystemBin } from "./resolve-system-bin-IIzLwiri.js"; import { _ as parseCanonicalIpAddress } from "./ip-BkT2Is0E.js"; import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.js"; import { s as withFileLock } from "./file-lock-B0wiaenm.js"; import "./file-lock-DPooFrLa.js"; import { n as runExec } from "./exec-BIE-3oLG.js"; import fs from "node:fs"; import path from "node:path"; import { X509Certificate, createHash, createPrivateKey, randomBytes } from "node:crypto"; //#region src/proxy-capture/ca.ts const DEBUG_PROXY_CA_GENERATION_TIMEOUT_MS = 3e4; const LOCAL_PROXY_CERT_GENERATION_TIMEOUT_MS = 3e4; const LOCAL_PROXY_DIR_MODE = 448; const LOCAL_PROXY_PRIVATE_KEY_MODE = 384; function buildLocalProxyCaOpenSslConfig(commonName) { return [ "[req]", "distinguished_name = subject", "prompt = no", "", "[subject]", `CN = ${commonName}`, "", "[v3_ca]", "basicConstraints = critical, CA:TRUE", "keyUsage = critical, keyCertSign, cRLSign", "" ].join("\n"); } const DEBUG_PROXY_CA_LOCK_OPTIONS = { retries: { retries: 80, factor: 1.3, minTimeout: 25, maxTimeout: 500, randomize: true }, stale: 6e4, staleRecovery: "remove-if-unchanged" }; const debugProxyCaGenerationQueue = new KeyedAsyncQueue(); function isValidDebugProxyCaPair(certPath, keyPath) { try { const certStat = fs.lstatSync(certPath); const keyStat = fs.lstatSync(keyPath); if (!certStat.isFile() || !keyStat.isFile() || certStat.size === 0 || keyStat.size === 0) return false; const cert = new X509Certificate(fs.readFileSync(certPath)); const key = createPrivateKey(fs.readFileSync(keyPath)); return cert.ca && cert.checkPrivateKey(key); } catch { return false; } } function removeStagingDirBestEffort(stagingDir) { try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch {} } async function ensureLocalProxyCa(certDir, options) { fs.mkdirSync(certDir, { recursive: true, mode: LOCAL_PROXY_DIR_MODE }); fs.chmodSync(certDir, LOCAL_PROXY_DIR_MODE); const certPath = path.join(certDir, "root-ca.pem"); const keyPath = path.join(certDir, "root-ca-key.pem"); const canonicalKeyPath = path.join(fs.realpathSync(certDir), "root-ca-key.pem"); return await debugProxyCaGenerationQueue.enqueue(canonicalKeyPath, async () => withFileLock(canonicalKeyPath, DEBUG_PROXY_CA_LOCK_OPTIONS, async () => { if (isValidDebugProxyCaPair(certPath, keyPath)) return { certPath, keyPath }; const openssl = resolveSystemBin("openssl"); if (!openssl) throw new Error(`openssl is required to generate ${options.purpose} certificates`); const stagingDir = fs.mkdtempSync(path.join(certDir, ".root-ca-")); const stagedConfigPath = path.join(stagingDir, "openssl.cnf"); const stagedCertPath = path.join(stagingDir, "root-ca.pem"); const stagedKeyPath = path.join(stagingDir, "root-ca-key.pem"); try { fs.writeFileSync(stagedConfigPath, buildLocalProxyCaOpenSslConfig(options.commonName), { mode: LOCAL_PROXY_PRIVATE_KEY_MODE }); await runExec(openssl, [ "req", "-config", stagedConfigPath, "-extensions", "v3_ca", "-x509", "-newkey", "rsa:2048", "-sha256", "-days", String(options.validityDays), "-nodes", "-keyout", stagedKeyPath, "-out", stagedCertPath ], { logOutput: false, timeoutMs: DEBUG_PROXY_CA_GENERATION_TIMEOUT_MS }); if (!isValidDebugProxyCaPair(stagedCertPath, stagedKeyPath)) throw new Error(`openssl generated invalid ${options.purpose} certificate material`); fs.chmodSync(stagedKeyPath, LOCAL_PROXY_PRIVATE_KEY_MODE); fs.chmodSync(stagedCertPath, 420); fs.renameSync(stagedKeyPath, keyPath); fs.renameSync(stagedCertPath, certPath); return { certPath, keyPath }; } finally { removeStagingDirBestEffort(stagingDir); } })); } async function ensureDebugProxyCa(certDir) { return await ensureLocalProxyCa(certDir, { commonName: "OpenClaw Debug Proxy", purpose: "debug proxy", validityDays: 7 }); } /** Generates the root CA for one Gateway-lifetime secret egress proxy. */ async function ensureSecretEgressProxyCa(certDir) { return await ensureLocalProxyCa(certDir, { commonName: "OpenClaw Secret Egress Proxy", purpose: "secret egress proxy", validityDays: 1 }); } function isValidLeafPair(params) { try { const cert = new X509Certificate(fs.readFileSync(params.certPath)); const key = createPrivateKey(fs.readFileSync(params.keyPath)); const hostMatches = parseCanonicalIpAddress(params.hostname) ? cert.checkIP(params.hostname) === params.hostname : cert.checkHost(params.hostname) === params.hostname; return !cert.ca && cert.checkPrivateKey(key) && hostMatches; } catch { return false; } } async function generateLocalProxyLeafQueued(params) { const openssl = resolveSystemBin("openssl"); if (!openssl) throw new Error("openssl is required to generate local proxy certificates"); const leafKeyPath = path.join(params.certDir, "leaf-key.pem"); if (!fs.existsSync(leafKeyPath)) { await runExec(openssl, [ "genrsa", "-out", leafKeyPath, "2048" ], { logOutput: false, timeoutMs: LOCAL_PROXY_CERT_GENERATION_TIMEOUT_MS }); fs.chmodSync(leafKeyPath, LOCAL_PROXY_PRIVATE_KEY_MODE); } const leafId = createHash("sha256").update(params.hostname).digest("hex"); const stagingDir = fs.mkdtempSync(path.join(params.certDir, `.leaf-${leafId.slice(0, 12)}-`)); const csrPath = path.join(stagingDir, "leaf.csr"); const certPath = path.join(stagingDir, "leaf.pem"); const extPath = path.join(stagingDir, "leaf.ext"); try { const sanKind = parseCanonicalIpAddress(params.hostname) ? "IP" : "DNS"; fs.writeFileSync(extPath, `subjectAltName=${sanKind}:${params.hostname}\nextendedKeyUsage=serverAuth\n`, { mode: LOCAL_PROXY_PRIVATE_KEY_MODE }); await runExec(openssl, [ "req", "-new", "-key", leafKeyPath, "-subj", `/CN=${params.hostname}`, "-out", csrPath ], { logOutput: false, timeoutMs: LOCAL_PROXY_CERT_GENERATION_TIMEOUT_MS }); await runExec(openssl, [ "x509", "-req", "-in", csrPath, "-CA", params.ca.certPath, "-CAkey", params.ca.keyPath, "-set_serial", `0x${randomBytes(16).toString("hex")}`, "-out", certPath, "-days", "1", "-sha256", "-extfile", extPath ], { logOutput: false, timeoutMs: LOCAL_PROXY_CERT_GENERATION_TIMEOUT_MS }); if (!isValidLeafPair({ certPath, keyPath: leafKeyPath, hostname: params.hostname })) throw new Error("openssl generated invalid local proxy leaf certificate material"); return { cert: fs.readFileSync(certPath), key: fs.readFileSync(leafKeyPath) }; } finally { removeStagingDirBestEffort(stagingDir); } } /** Mints one on-demand TLS leaf signed by a local proxy CA. */ async function generateLocalProxyLeaf(params) { const queueKey = path.join(fs.realpathSync(params.certDir), "leaf-key.pem"); return await debugProxyCaGenerationQueue.enqueue(queueKey, () => generateLocalProxyLeafQueued(params)); } //#endregion export { ensureSecretEgressProxyCa as n, generateLocalProxyLeaf as r, ensureDebugProxyCa as t };