153 lines
3.9 KiB
TypeScript
153 lines
3.9 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { X509Certificate } from "node:crypto";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import tls from "node:tls";
|
|
import { promisify } from "node:util";
|
|
|
|
import type { GatewayTlsConfig } from "../../config/types.gateway.js";
|
|
import { CONFIG_DIR, ensureDir, resolveUserPath, shortenHomeInString } from "../../utils.js";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
export type GatewayTlsRuntime = {
|
|
enabled: boolean;
|
|
required: boolean;
|
|
certPath?: string;
|
|
keyPath?: string;
|
|
caPath?: string;
|
|
fingerprintSha256?: string;
|
|
tlsOptions?: tls.TlsOptions;
|
|
error?: string;
|
|
};
|
|
|
|
function normalizeFingerprint(input: string): string {
|
|
return input.replace(/[^a-fA-F0-9]/g, "").toLowerCase();
|
|
}
|
|
|
|
async function fileExists(filePath: string): Promise<boolean> {
|
|
try {
|
|
await fs.access(filePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function generateSelfSignedCert(params: {
|
|
certPath: string;
|
|
keyPath: string;
|
|
log?: { info?: (msg: string) => void };
|
|
}): Promise<void> {
|
|
const certDir = path.dirname(params.certPath);
|
|
const keyDir = path.dirname(params.keyPath);
|
|
await ensureDir(certDir);
|
|
if (keyDir !== certDir) {
|
|
await ensureDir(keyDir);
|
|
}
|
|
await execFileAsync("openssl", [
|
|
"req",
|
|
"-x509",
|
|
"-newkey",
|
|
"rsa:2048",
|
|
"-sha256",
|
|
"-days",
|
|
"3650",
|
|
"-nodes",
|
|
"-keyout",
|
|
params.keyPath,
|
|
"-out",
|
|
params.certPath,
|
|
"-subj",
|
|
"/CN=clawdbot-gateway",
|
|
]);
|
|
await fs.chmod(params.keyPath, 0o600).catch(() => {});
|
|
await fs.chmod(params.certPath, 0o600).catch(() => {});
|
|
params.log?.info?.(
|
|
`gateway tls: generated self-signed cert at ${shortenHomeInString(params.certPath)}`,
|
|
);
|
|
}
|
|
|
|
export async function loadGatewayTlsRuntime(
|
|
cfg: GatewayTlsConfig | undefined,
|
|
log?: { info?: (msg: string) => void; warn?: (msg: string) => void },
|
|
): Promise<GatewayTlsRuntime> {
|
|
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 = resolveUserPath(cfg.certPath ?? path.join(baseDir, "gateway-cert.pem"));
|
|
const keyPath = resolveUserPath(cfg.keyPath ?? path.join(baseDir, "gateway-key.pem"));
|
|
const caPath = cfg.caPath ? resolveUserPath(cfg.caPath) : undefined;
|
|
|
|
const hasCert = await fileExists(certPath);
|
|
const hasKey = await fileExists(keyPath);
|
|
|
|
if (!hasCert && !hasKey && autoGenerate) {
|
|
try {
|
|
await generateSelfSignedCert({ certPath, keyPath, log });
|
|
} catch (err) {
|
|
return {
|
|
enabled: false,
|
|
required: true,
|
|
certPath,
|
|
keyPath,
|
|
error: `gateway tls: failed to generate cert (${String(err)})`,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (!(await fileExists(certPath)) || !(await fileExists(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") : undefined;
|
|
const x509 = new X509Certificate(cert);
|
|
const fingerprintSha256 = normalizeFingerprint(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.2",
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
enabled: false,
|
|
required: true,
|
|
certPath,
|
|
keyPath,
|
|
caPath,
|
|
error: `gateway tls: failed to load cert (${String(err)})`,
|
|
};
|
|
}
|
|
}
|