UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

197 lines (196 loc) 7.9 kB
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js"; import { I as sameFileIdentity, a as canonicalPathFromExistingAncestor, d as pathExists } from "./fs-safe-B6pvPGnf.js"; import { p as shortenHomeInString, t as CONFIG_DIR } from "./utils-P__uGsPB.js"; import { n as ok, t as err } from "./result-BQGgYouL.js"; import { t as resolveSystemBin } from "./resolve-system-bin-IIzLwiri.js"; import { n as runExec } from "./exec-BIE-3oLG.js"; import { r as normalizeTlsFingerprint } from "./client-address-utils-DUCasR_v.js"; import { o as publishFileNoClobber, t as ensureDurableDirectory } from "./directory-durability-CINgXRM4.js"; import path from "node:path"; import fs from "node:fs/promises"; import { X509Certificate } from "node:crypto"; import tls from "node:tls"; //#region src/infra/tls/gateway.ts const GATEWAY_TLS_CERT_GENERATION_TIMEOUT_MS = 3e4; function gatewayTlsDegradation(reason) { return { event: "gateway.tls.degraded", ownerKind: "gateway", ownerId: "tls", reason, state: "best-effort" }; } async function publishGeneratedTlsOutput(stagedPath, finalPath) { const degradationReasons = []; const stagedHandle = await fs.open(stagedPath, "r+"); let stagedIdentity; try { await stagedHandle.sync(); stagedIdentity = await stagedHandle.stat(); } finally { await stagedHandle.close(); } const publication = await publishFileNoClobber(stagedPath, finalPath, { strategy: "link-or-copy", durability: "degrade" }); if (publication.method === "exclusive-copy") degradationReasons.push("atomic hard-link publication unavailable"); if (publication.durability === "degraded") degradationReasons.push("directory durability unavailable"); const [currentStagedIdentity, currentPublishedIdentity] = await Promise.all([fs.lstat(stagedPath), fs.lstat(finalPath)]); const hardlinkChanged = publication.method === "hardlink" && !sameFileIdentity(stagedIdentity, publication.identity); if (!currentStagedIdentity.isFile() || !currentPublishedIdentity.isFile() || !sameFileIdentity(stagedIdentity, currentStagedIdentity) || !sameFileIdentity(publication.identity, currentPublishedIdentity) || hardlinkChanged) throw new Error(`Generated TLS output changed during publication: ${finalPath}`); return { degradationReasons, identity: publication.identity }; } async function generateSelfSignedCert(params) { const certDir = await canonicalPathFromExistingAncestor(path.dirname(params.certPath)); const keyDir = await canonicalPathFromExistingAncestor(path.dirname(params.keyPath)); const certDirectory = await ensureDurableDirectory({ directoryPath: certDir }); const keyDirectory = keyDir === certDir ? certDirectory : await ensureDurableDirectory({ directoryPath: keyDir }); const opensslBin = resolveSystemBin("openssl"); if (!opensslBin) throw new Error("openssl not found in trusted system directories. Install it in an OS-managed location."); const certStageDir = await fs.mkdtemp(path.join(certDir, ".openclaw-gateway-tls-cert-")); const stagedCertPath = path.join(certStageDir, "cert.pem"); let keyStageDir; try { keyStageDir = await fs.mkdtemp(path.join(keyDir, ".openclaw-gateway-tls-key-")); const stagedKeyPath = path.join(keyStageDir, "key.pem"); await Promise.all([fs.chmod(certStageDir, 448), fs.chmod(keyStageDir, 448)]); await runExec(opensslBin, [ "req", "-x509", "-newkey", "rsa:2048", "-sha256", "-days", "3650", "-nodes", "-keyout", stagedKeyPath, "-out", stagedCertPath, "-subj", "/CN=openclaw-gateway" ], { logOutput: false, timeoutMs: GATEWAY_TLS_CERT_GENERATION_TIMEOUT_MS }); await Promise.all([fs.chmod(stagedKeyPath, 384), fs.chmod(stagedCertPath, 384)]); const [cert, key] = await Promise.all([fs.readFile(stagedCertPath, "utf8"), fs.readFile(stagedKeyPath, "utf8")]); tls.createSecureContext({ cert, key, minVersion: "TLSv1.3" }); const degradationReasons = /* @__PURE__ */ new Set(); if (certDirectory.parentSync.status === "unsupported" || keyDirectory.parentSync.status === "unsupported") degradationReasons.add("directory durability unavailable"); (await publishGeneratedTlsOutput(stagedCertPath, path.join(certDirectory.path, path.basename(params.certPath)))).degradationReasons.forEach((reason) => degradationReasons.add(reason)); (await publishGeneratedTlsOutput(stagedKeyPath, path.join(keyDirectory.path, path.basename(params.keyPath)))).degradationReasons.forEach((reason) => degradationReasons.add(reason)); for (const reason of degradationReasons) { const degradation = gatewayTlsDegradation(reason); params.log?.warn?.(`[GATEWAY_TLS_DEGRADED] best-effort gateway:tls: ${degradation.reason}.`, degradation); } params.log?.info?.(`gateway tls: generated self-signed cert at ${shortenHomeInString(params.certPath)}`); } finally { await Promise.allSettled([certStageDir, keyStageDir].filter((dir) => Boolean(dir)).map((dir) => fs.rm(dir, { force: true, recursive: true }))); } } function resolveGatewayTlsCertPath(certPath) { return resolveUserPath(typeof certPath === "string" && certPath.trim() ? certPath : path.join(CONFIG_DIR, "gateway", "tls", "gateway-cert.pem")); } /** Read only public certificate bytes. Inspection never provisions or requires server secrets. */ async function inspectGatewayTlsCertificate(cfg) { if (cfg?.enabled !== true) return err("gateway tls is disabled"); try { const cert = await fs.readFile(resolveGatewayTlsCertPath(cfg.certPath), "utf8"); const fingerprintSha256 = normalizeTlsFingerprint(new X509Certificate(cert).fingerprint256); return fingerprintSha256 ? ok({ cert, fingerprintSha256 }) : err("gateway tls: unable to compute certificate fingerprint"); } catch (error) { return err(`gateway tls: failed to load cert (${String(error)})`); } } /** Server startup only: load or provision TLS material and return listener options. */ async function loadGatewayTlsServerRuntime(cfg, log) { if (!cfg || cfg.enabled !== true) return { enabled: false, required: false }; const autoGenerate = cfg.autoGenerate !== false; const baseDir = path.join(CONFIG_DIR, "gateway", "tls"); const certPath = resolveGatewayTlsCertPath(cfg.certPath); const keyPath = resolveUserPath(typeof cfg.keyPath === "string" && cfg.keyPath.trim() ? cfg.keyPath : path.join(baseDir, "gateway-key.pem")); const caPath = cfg.caPath ? resolveUserPath(cfg.caPath) : void 0; const hasCert = await pathExists(certPath); const hasKey = await pathExists(keyPath); if (!hasCert && !hasKey && autoGenerate) try { await generateSelfSignedCert({ certPath, keyPath, log }); } catch (error) { return { enabled: false, required: true, certPath, keyPath, error: `gateway tls: failed to generate cert (${String(error)})` }; } if (!await pathExists(certPath) || !await pathExists(keyPath)) return { enabled: false, required: true, certPath, keyPath, error: "gateway tls: cert/key missing" }; try { const cert = await fs.readFile(certPath, "utf8"); const key = await fs.readFile(keyPath, "utf8"); const ca = caPath ? await fs.readFile(caPath, "utf8") : void 0; const x509 = new X509Certificate(cert); const fingerprintSha256 = normalizeTlsFingerprint(x509.fingerprint256 ?? ""); if (!fingerprintSha256) return { enabled: false, required: true, certPath, keyPath, caPath, error: "gateway tls: unable to compute certificate fingerprint" }; return { enabled: true, required: true, certPath, keyPath, caPath, fingerprintSha256, tlsOptions: { cert, key, ca, minVersion: "TLSv1.3" } }; } catch (error) { return { enabled: false, required: true, certPath, keyPath, caPath, error: `gateway tls: failed to load cert (${String(error)})` }; } } //#endregion export { loadGatewayTlsServerRuntime as n, inspectGatewayTlsCertificate as t };