feat: add gateway tls support
This commit is contained in:
@@ -250,6 +250,9 @@ actor GatewayEndpointStore {
|
||||
let bind = GatewayEndpointStore.resolveGatewayBindMode(
|
||||
root: ClawdbotConfigFile.loadDict(),
|
||||
env: ProcessInfo.processInfo.environment)
|
||||
let scheme = GatewayEndpointStore.resolveGatewayScheme(
|
||||
root: ClawdbotConfigFile.loadDict(),
|
||||
env: ProcessInfo.processInfo.environment)
|
||||
let host = GatewayEndpointStore.resolveLocalGatewayHost(bindMode: bind, tailscaleIP: nil)
|
||||
let token = deps.token()
|
||||
let password = deps.password()
|
||||
@@ -257,7 +260,7 @@ actor GatewayEndpointStore {
|
||||
case .local:
|
||||
self.state = .ready(
|
||||
mode: .local,
|
||||
url: URL(string: "ws://\(host):\(port)")!,
|
||||
url: URL(string: "\(scheme)://\(host):\(port)")!,
|
||||
token: token,
|
||||
password: password)
|
||||
case .remote:
|
||||
@@ -294,9 +297,12 @@ actor GatewayEndpointStore {
|
||||
self.cancelRemoteEnsure()
|
||||
let port = self.deps.localPort()
|
||||
let host = await self.deps.localHost()
|
||||
let scheme = GatewayEndpointStore.resolveGatewayScheme(
|
||||
root: ClawdbotConfigFile.loadDict(),
|
||||
env: ProcessInfo.processInfo.environment)
|
||||
self.setState(.ready(
|
||||
mode: .local,
|
||||
url: URL(string: "ws://\(host):\(port)")!,
|
||||
url: URL(string: "\(scheme)://\(host):\(port)")!,
|
||||
token: token,
|
||||
password: password))
|
||||
case .remote:
|
||||
@@ -307,9 +313,12 @@ actor GatewayEndpointStore {
|
||||
return
|
||||
}
|
||||
self.cancelRemoteEnsure()
|
||||
let scheme = GatewayEndpointStore.resolveGatewayScheme(
|
||||
root: ClawdbotConfigFile.loadDict(),
|
||||
env: ProcessInfo.processInfo.environment)
|
||||
self.setState(.ready(
|
||||
mode: .remote,
|
||||
url: URL(string: "ws://127.0.0.1:\(Int(port))")!,
|
||||
url: URL(string: "\(scheme)://127.0.0.1:\(Int(port))")!,
|
||||
token: token,
|
||||
password: password))
|
||||
case .unconfigured:
|
||||
@@ -478,6 +487,24 @@ actor GatewayEndpointStore {
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func resolveGatewayScheme(
|
||||
root: [String: Any],
|
||||
env: [String: String]) -> String
|
||||
{
|
||||
if let envValue = env["CLAWDBOT_GATEWAY_TLS"]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!envValue.isEmpty
|
||||
{
|
||||
return (envValue == "1" || envValue.lowercased() == "true") ? "wss" : "ws"
|
||||
}
|
||||
if let gateway = root["gateway"] as? [String: Any],
|
||||
let tls = gateway["tls"] as? [String: Any],
|
||||
let enabled = tls["enabled"] as? Bool
|
||||
{
|
||||
return enabled ? "wss" : "ws"
|
||||
}
|
||||
return "ws"
|
||||
}
|
||||
|
||||
private static func resolveLocalGatewayHost(
|
||||
bindMode: String?,
|
||||
tailscaleIP: String?) -> String
|
||||
|
||||
@@ -127,6 +127,8 @@ export type GatewayHttpConfig = {
|
||||
endpoints?: GatewayHttpEndpointsConfig;
|
||||
};
|
||||
|
||||
export type GatewayTlsConfig = BridgeTlsConfig;
|
||||
|
||||
export type GatewayConfig = {
|
||||
/** Single multiplexed port for Gateway WS + HTTP (default: 18789). */
|
||||
port?: number;
|
||||
@@ -151,5 +153,6 @@ export type GatewayConfig = {
|
||||
tailscale?: GatewayTailscaleConfig;
|
||||
remote?: GatewayRemoteConfig;
|
||||
reload?: GatewayReloadConfig;
|
||||
tls?: GatewayTlsConfig;
|
||||
http?: GatewayHttpConfig;
|
||||
};
|
||||
|
||||
@@ -300,6 +300,15 @@ export const ClawdbotSchema = z
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
tls: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
autoGenerate: z.boolean().optional(),
|
||||
certPath: z.string().optional(),
|
||||
keyPath: z.string().optional(),
|
||||
caPath: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
http: z
|
||||
.object({
|
||||
endpoints: z
|
||||
|
||||
@@ -56,14 +56,16 @@ export function buildGatewayConnectionDetails(
|
||||
options.configPath ?? resolveConfigPath(process.env, resolveStateDir(process.env));
|
||||
const isRemoteMode = config.gateway?.mode === "remote";
|
||||
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
||||
const tlsEnabled = config.gateway?.tls?.enabled === true;
|
||||
const localPort = resolveGatewayPort(config);
|
||||
const tailnetIPv4 = pickPrimaryTailnetIPv4();
|
||||
const bindMode = config.gateway?.bind ?? "loopback";
|
||||
const preferTailnet = bindMode === "auto" && !!tailnetIPv4;
|
||||
const scheme = tlsEnabled ? "wss" : "ws";
|
||||
const localUrl =
|
||||
preferTailnet && tailnetIPv4
|
||||
? `ws://${tailnetIPv4}:${localPort}`
|
||||
: `ws://127.0.0.1:${localPort}`;
|
||||
? `${scheme}://${tailnetIPv4}:${localPort}`
|
||||
: `${scheme}://127.0.0.1:${localPort}`;
|
||||
const urlOverride =
|
||||
typeof options.url === "string" && options.url.trim().length > 0
|
||||
? options.url.trim()
|
||||
|
||||
@@ -37,6 +37,7 @@ export type GatewayBridgeRuntime = {
|
||||
export async function startGatewayBridgeRuntime(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
port: number;
|
||||
gatewayTls?: { enabled: boolean; fingerprintSha256?: string };
|
||||
canvasHostEnabled: boolean;
|
||||
canvasHost: CanvasHostHandler | null;
|
||||
canvasRuntime: RuntimeEnv;
|
||||
@@ -221,6 +222,7 @@ export async function startGatewayBridgeRuntime(params: {
|
||||
const discovery = await startGatewayDiscovery({
|
||||
machineDisplayName: params.machineDisplayName,
|
||||
port: params.port,
|
||||
gatewayTls: params.gatewayTls,
|
||||
bridgePort: bridge?.port,
|
||||
bridgeTls: bridgeTls.enabled
|
||||
? { enabled: true, fingerprintSha256: bridgeTls.fingerprintSha256 }
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
export async function startGatewayDiscovery(params: {
|
||||
machineDisplayName: string;
|
||||
port: number;
|
||||
gatewayTls?: { enabled: boolean; fingerprintSha256?: string };
|
||||
bridgePort?: number;
|
||||
bridgeTls?: { enabled: boolean; fingerprintSha256?: string };
|
||||
canvasPort?: number;
|
||||
@@ -31,6 +32,8 @@ export async function startGatewayDiscovery(params: {
|
||||
const bonjour = await startGatewayBonjourAdvertiser({
|
||||
instanceName: formatBonjourInstanceName(params.machineDisplayName),
|
||||
gatewayPort: params.port,
|
||||
gatewayTlsEnabled: params.gatewayTls?.enabled ?? false,
|
||||
gatewayTlsFingerprintSha256: params.gatewayTls?.fingerprintSha256,
|
||||
bridgePort: params.bridgePort,
|
||||
canvasPort: params.canvasPort,
|
||||
bridgeTlsEnabled: params.bridgeTls?.enabled ?? false,
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
type IncomingMessage,
|
||||
type ServerResponse,
|
||||
} from "node:http";
|
||||
import { createServer as createHttpsServer } from "node:https";
|
||||
import type { TlsOptions } from "node:tls";
|
||||
import type { WebSocketServer } from "ws";
|
||||
import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
|
||||
import type { CanvasHostHandler } from "../canvas-host/server.js";
|
||||
@@ -193,6 +195,7 @@ export function createGatewayHttpServer(opts: {
|
||||
handleHooksRequest: HooksRequestHandler;
|
||||
handlePluginRequest?: HooksRequestHandler;
|
||||
resolvedAuth: import("./auth.js").ResolvedGatewayAuth;
|
||||
tlsOptions?: TlsOptions;
|
||||
}): HttpServer {
|
||||
const {
|
||||
canvasHost,
|
||||
@@ -203,11 +206,19 @@ export function createGatewayHttpServer(opts: {
|
||||
handlePluginRequest,
|
||||
resolvedAuth,
|
||||
} = opts;
|
||||
const httpServer: HttpServer = createHttpServer((req, res) => {
|
||||
const httpServer: HttpServer = opts.tlsOptions
|
||||
? createHttpsServer(opts.tlsOptions, (req, res) => {
|
||||
void handleRequest(req, res);
|
||||
})
|
||||
: createHttpServer((req, res) => {
|
||||
void handleRequest(req, res);
|
||||
});
|
||||
|
||||
async function handleRequest(req: IncomingMessage, res: ServerResponse) {
|
||||
// Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.
|
||||
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") return;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
if (await handleHooksRequest(req, res)) return;
|
||||
if (await handleSlackHttpRequest(req, res)) return;
|
||||
if (handlePluginRequest && (await handlePluginRequest(req, res))) return;
|
||||
@@ -230,12 +241,12 @@ export function createGatewayHttpServer(opts: {
|
||||
res.statusCode = 404;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("Not Found");
|
||||
})().catch((err) => {
|
||||
} catch (err) {
|
||||
res.statusCode = 500;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end(String(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { MAX_PAYLOAD_BYTES } from "./server-constants.js";
|
||||
import { attachGatewayUpgradeHandler, createGatewayHttpServer } from "./server-http.js";
|
||||
import type { DedupeEntry } from "./server-shared.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { GatewayTlsRuntime } from "./server/tls.js";
|
||||
|
||||
export async function createGatewayRuntimeState(params: {
|
||||
cfg: import("../config/config.js").ClawdbotConfig;
|
||||
@@ -27,6 +28,7 @@ export async function createGatewayRuntimeState(params: {
|
||||
controlUiBasePath: string;
|
||||
openAiChatCompletionsEnabled: boolean;
|
||||
resolvedAuth: ResolvedGatewayAuth;
|
||||
gatewayTls?: GatewayTlsRuntime;
|
||||
hooksConfig: () => HooksConfigResolved | null;
|
||||
pluginRegistry: PluginRegistry;
|
||||
deps: CliDeps;
|
||||
@@ -104,6 +106,7 @@ export async function createGatewayRuntimeState(params: {
|
||||
handleHooksRequest,
|
||||
handlePluginRequest,
|
||||
resolvedAuth: params.resolvedAuth,
|
||||
tlsOptions: params.gatewayTls?.enabled ? params.gatewayTls.tlsOptions : undefined,
|
||||
});
|
||||
|
||||
await listenGatewayHttpServer({
|
||||
|
||||
@@ -8,6 +8,7 @@ export function logGatewayStartup(params: {
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
bindHost: string;
|
||||
port: number;
|
||||
tlsEnabled?: boolean;
|
||||
log: { info: (msg: string, meta?: Record<string, unknown>) => void };
|
||||
isNixMode: boolean;
|
||||
}) {
|
||||
@@ -20,7 +21,8 @@ export function logGatewayStartup(params: {
|
||||
params.log.info(`agent model: ${modelRef}`, {
|
||||
consoleMessage: `agent model: ${chalk.whiteBright(modelRef)}`,
|
||||
});
|
||||
params.log.info(`listening on ws://${params.bindHost}:${params.port} (PID ${process.pid})`);
|
||||
const scheme = params.tlsEnabled ? "wss" : "ws";
|
||||
params.log.info(`listening on ${scheme}://${params.bindHost}:${params.port} (PID ${process.pid})`);
|
||||
params.log.info(`log file: ${getResolvedLoggerSettings().file}`);
|
||||
if (params.isNixMode) {
|
||||
params.log.info("gateway: running in Nix mode (config managed externally)");
|
||||
|
||||
@@ -58,6 +58,7 @@ import { resolveSessionKeyForRun } from "./server-session-key.js";
|
||||
import { startGatewaySidecars } from "./server-startup.js";
|
||||
import { logGatewayStartup } from "./server-startup-log.js";
|
||||
import { startGatewayTailscaleExposure } from "./server-tailscale.js";
|
||||
import { loadGatewayTlsRuntime } from "./server/tls.js";
|
||||
import { createWizardSessionTracker } from "./server-wizard-sessions.js";
|
||||
import { attachGatewayWsHandlers } from "./server-ws-runtime.js";
|
||||
|
||||
@@ -222,6 +223,10 @@ export async function startGatewayServer(
|
||||
|
||||
const deps = createDefaultDeps();
|
||||
let canvasHostServer: CanvasHostServer | null = null;
|
||||
const gatewayTls = await loadGatewayTlsRuntime(cfgAtStart.gateway?.tls, log.child("tls"));
|
||||
if (cfgAtStart.gateway?.tls?.enabled && !gatewayTls.enabled) {
|
||||
throw new Error(gatewayTls.error ?? "gateway tls: failed to enable");
|
||||
}
|
||||
const {
|
||||
canvasHost,
|
||||
httpServer,
|
||||
@@ -244,6 +249,7 @@ export async function startGatewayServer(
|
||||
controlUiBasePath,
|
||||
openAiChatCompletionsEnabled,
|
||||
resolvedAuth,
|
||||
gatewayTls,
|
||||
hooksConfig: () => hooksConfig,
|
||||
pluginRegistry,
|
||||
deps,
|
||||
@@ -279,6 +285,9 @@ export async function startGatewayServer(
|
||||
const bridgeRuntime = await startGatewayBridgeRuntime({
|
||||
cfg: cfgAtStart,
|
||||
port,
|
||||
gatewayTls: gatewayTls.enabled
|
||||
? { enabled: true, fingerprintSha256: gatewayTls.fingerprintSha256 }
|
||||
: undefined,
|
||||
canvasHostEnabled,
|
||||
canvasHost,
|
||||
canvasRuntime,
|
||||
@@ -412,6 +421,7 @@ export async function startGatewayServer(
|
||||
cfg: cfgAtStart,
|
||||
bindHost,
|
||||
port,
|
||||
tlsEnabled: gatewayTls.enabled,
|
||||
log,
|
||||
isNixMode,
|
||||
});
|
||||
|
||||
14
src/gateway/server/tls.ts
Normal file
14
src/gateway/server/tls.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { BridgeTlsConfig } from "../../config/types.gateway.js";
|
||||
import {
|
||||
type BridgeTlsRuntime,
|
||||
loadBridgeTlsRuntime,
|
||||
} from "../../infra/bridge/server/tls.js";
|
||||
|
||||
export type GatewayTlsRuntime = BridgeTlsRuntime;
|
||||
|
||||
export async function loadGatewayTlsRuntime(
|
||||
cfg: BridgeTlsConfig | undefined,
|
||||
log?: { info?: (msg: string) => void; warn?: (msg: string) => void },
|
||||
): Promise<GatewayTlsRuntime> {
|
||||
return await loadBridgeTlsRuntime(cfg, log);
|
||||
}
|
||||
@@ -15,6 +15,8 @@ export type GatewayBonjourAdvertiseOpts = {
|
||||
instanceName?: string;
|
||||
gatewayPort: number;
|
||||
sshPort?: number;
|
||||
gatewayTlsEnabled?: boolean;
|
||||
gatewayTlsFingerprintSha256?: string;
|
||||
bridgePort?: number;
|
||||
canvasPort?: number;
|
||||
bridgeTlsEnabled?: boolean;
|
||||
@@ -107,6 +109,12 @@ export async function startGatewayBonjourAdvertiser(
|
||||
if (typeof opts.bridgePort === "number" && opts.bridgePort > 0) {
|
||||
txtBase.bridgePort = String(opts.bridgePort);
|
||||
}
|
||||
if (opts.gatewayTlsEnabled) {
|
||||
txtBase.gatewayTls = "1";
|
||||
if (opts.gatewayTlsFingerprintSha256) {
|
||||
txtBase.gatewayTlsSha256 = opts.gatewayTlsFingerprintSha256;
|
||||
}
|
||||
}
|
||||
if (typeof opts.canvasPort === "number" && opts.canvasPort > 0) {
|
||||
txtBase.canvasPort = String(opts.canvasPort);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user