diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts index 19d70799..b95077a0 100644 --- a/src/lib/caddy.ts +++ b/src/lib/caddy.ts @@ -50,7 +50,7 @@ import { proxyHosts, l4ProxyHosts } from "./db/schema"; -import { type GeoBlockMode, type WafHostConfig, type MtlsConfig, type RedirectRule, type RewriteConfig } from "./models/proxy-hosts"; +import { type GeoBlockMode, type WafHostConfig, type MtlsConfig, type RedirectRule, type RewriteConfig, type LocationRule } from "./models/proxy-hosts"; import { buildClientAuthentication, groupMtlsDomainsByCaSet } from "./caddy-mtls"; import { buildWafHandler, resolveEffectiveWaf } from "./caddy-waf"; @@ -116,6 +116,7 @@ type ProxyHostMeta = { waf?: WafHostConfig; redirects?: RedirectRule[]; rewrite?: RewriteConfig; + location_rules?: { path: string; upstreams: string[] }[]; }; type L4Meta = { @@ -622,6 +623,38 @@ type BuildProxyRoutesOptions = { globalWaf?: WafSettings | null; }; +function buildLocationReverseProxy( + rule: { path: string; upstreams: string[] }, + skipHttpsValidation: boolean, + preserveHostHeader: boolean +): { safePath: string; reverseProxyHandler: Record } { + const parsedTargets = rule.upstreams.map(parseUpstreamTarget); + const hasHttps = parsedTargets.some((t) => t.scheme === "https"); + + // Sanitize path to prevent Caddy placeholder injection + const safePath = rule.path.replace(/\{[^}]*\}/g, ""); + + const reverseProxyHandler: Record = { + handler: "reverse_proxy", + upstreams: parsedTargets.map((t) => ({ dial: t.dial })), + }; + + if (preserveHostHeader) { + reverseProxyHandler.headers = { + request: { set: { Host: ["{http.request.host}"] } }, + }; + } + + if (hasHttps) { + reverseProxyHandler.transport = { + protocol: "http", + tls: skipHttpsValidation ? { insecure_skip_verify: true } : {}, + }; + } + + return { safePath, reverseProxyHandler }; +} + async function buildProxyRoutes( rows: ProxyHostRow[], accessAccounts: Map, @@ -1009,19 +1042,31 @@ async function buildProxyRoutes( }); } + const locationRules = meta.location_rules ?? []; + for (const rule of locationRules) { + const { safePath, reverseProxyHandler: locationProxy } = buildLocationReverseProxy( + rule, + Boolean(row.skip_https_hostname_validation), + Boolean(row.preserve_host_header) + ); + if (!safePath) continue; + hostRoutes.push({ + match: [{ host: domainGroup, path: [safePath] }], + handle: [...handlers, locationProxy], + terminal: true, + }); + } + const unprotectedHandlers: Record[] = [...handlers, reverseProxyHandler]; hostRoutes.push({ - match: [ - { - host: domainGroup - } - ], + match: [{ host: domainGroup }], handle: unprotectedHandlers, terminal: true }); } } else { + const locationRules = meta.location_rules ?? []; for (const domainGroup of domainGroups) { if (outpostRoute) { const outpostMatches = (outpostRoute.match as Array> | undefined) ?? []; @@ -1034,32 +1079,50 @@ async function buildProxyRoutes( }); } + for (const rule of locationRules) { + const { safePath, reverseProxyHandler: locationProxy } = buildLocationReverseProxy( + rule, + Boolean(row.skip_https_hostname_validation), + Boolean(row.preserve_host_header) + ); + if (!safePath) continue; + hostRoutes.push({ + match: [{ host: domainGroup, path: [safePath] }], + handle: [...handlers, forwardAuthHandler, locationProxy], + terminal: true, + }); + } + const routeHandlers: Record[] = [...handlers, forwardAuthHandler, reverseProxyHandler]; const route: CaddyHttpRoute = { - match: [ - { - host: domainGroup - } - ], + match: [{ host: domainGroup }], handle: routeHandlers, terminal: true }; - hostRoutes.push(route); } } } else { + const locationRules = meta.location_rules ?? []; for (const domainGroup of domainGroups) { + for (const rule of locationRules) { + const { safePath, reverseProxyHandler: locationProxy } = buildLocationReverseProxy( + rule, + Boolean(row.skip_https_hostname_validation), + Boolean(row.preserve_host_header) + ); + if (!safePath) continue; + hostRoutes.push({ + match: [{ host: domainGroup, path: [safePath] }], + handle: [...handlers, locationProxy], + terminal: true, + }); + } const route: CaddyHttpRoute = { - match: [ - { - host: domainGroup - } - ], + match: [{ host: domainGroup }], handle: [...handlers, reverseProxyHandler], - terminal: true + terminal: true, }; - hostRoutes.push(route); } }