import type { ILetsEncryptHttp01ForwardConfig, TManagedRouteKind, } from '../../ts_interfaces/data/route-management.js'; import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js'; export const letsEncryptHttp01ManagedRouteKind: TManagedRouteKind = 'letsencrypt-http01-forward'; export const letsEncryptHttp01Path = '/.well-known/acme-challenge/*'; export const letsEncryptHttp01Priority = 1000; export interface ISpecialForwardResolvedTarget { host: string | string[]; port: number; } /** * Compile operator input into the immutable portion of a managed HTTP-01 route. */ export function buildLetsEncryptHttp01Route( configArg: ILetsEncryptHttp01ForwardConfig, targetArg: ISpecialForwardResolvedTarget, ): IDcRouterRouteConfig { const name = configArg.name.trim(); if (!name) { throw new Error('Special Forward name is required'); } const domains = normalizeLetsEncryptHttp01Domains(configArg.domains); const target = normalizeTarget(targetArg); const edgeFilter = configArg.remoteIngress?.edgeFilter ?.map((entry) => entry.trim()) .filter(Boolean); if (configArg.remoteIngress?.enabled !== true) { throw new Error('Public HTTP-01 forwards require RemoteIngress'); } return { name, description: 'Managed Let\'s Encrypt HTTP-01 challenge forward', priority: letsEncryptHttp01Priority, tags: ['managed', 'special-forward', 'letsencrypt', 'http01'], match: { ports: 80, domains, path: letsEncryptHttp01Path, protocol: 'http', transport: 'tcp', }, action: { type: 'forward', targets: [target], }, ingress: { directHub: false, smartVpn: false, }, remoteIngress: { enabled: true, ...(edgeFilter?.length ? { edgeFilter } : {}), }, }; } export function normalizeLetsEncryptHttp01Domains(domainsArg: string[]): string[] { if (!Array.isArray(domainsArg)) { throw new Error('At least one domain is required'); } const domains = [...new Set(domainsArg.map((domain) => { return domain.trim().toLowerCase().replace(/\.$/, ''); }).filter(Boolean))]; if (domains.length === 0) { throw new Error('At least one domain is required'); } for (const domain of domains) { if (domain.includes('*')) { throw new Error(`HTTP-01 does not support wildcard domain '${domain}'`); } if (!isValidPublicHostname(domain)) { throw new Error(`Invalid public hostname '${domain}'`); } } return domains; } function normalizeTarget(targetArg: ISpecialForwardResolvedTarget): ISpecialForwardResolvedTarget { const hosts = (Array.isArray(targetArg.host) ? targetArg.host : [targetArg.host]) .map((host) => host.trim()) .filter(Boolean); if (hosts.length === 0) { throw new Error('A target host or Network Target is required'); } if (!Number.isInteger(targetArg.port) || targetArg.port < 1 || targetArg.port > 65535) { throw new Error('Target port must be between 1 and 65535'); } return { host: hosts.length === 1 ? hosts[0] : hosts, port: targetArg.port, }; } function isValidPublicHostname(hostname: string): boolean { if (hostname.length > 253 || !hostname.includes('.')) return false; return hostname.split('.').every((label) => { return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label); }); }