Files
pangolin/server/lib/traefik/redirect.ts
T
2026-09-17 21:32:55 +02:00

134 lines
4.3 KiB
TypeScript

import logger from "@server/logger";
import config from "@server/lib/config";
import {
buildHostRule,
appendPathMatch,
computeRoutePriority
} from "@server/lib/traefik/rule";
export type RedirectRouteRow = {
redirectId: number;
/** Host the redirect listens on (resource fullDomain or subdomain.baseDomain). */
fullDomain: string;
hasSubdomain: boolean;
attachedTo: "resource" | "domain";
enabled: boolean;
name: string;
wildcard: boolean | null;
ssl: boolean;
matchPath: string | null;
pathMatchType: string;
priority: number | null;
domainCertResolver?: string | null;
preferWildcardCert?: boolean | null;
};
// Traefik requires a service on every router, but a redirect router's
// middleware chain always terminates the request with a 30x, so the service
// is never reached. noop@internal answers 418 if it ever is - treat that as
// a bug in the middleware chain, not something to route around.
const NOOP_SERVICE = "noop@internal";
export function buildRedirectConfig(params: {
config_output: any;
redirects: RedirectRouteRow[];
badgerMiddlewareName: string;
redirectHttpsMiddlewareName: string;
resolveTls: (row: RedirectRouteRow) => any | null;
}): void {
const {
config_output,
redirects,
badgerMiddlewareName,
redirectHttpsMiddlewareName,
resolveTls
} = params;
if (redirects.length === 0) {
return;
}
const httpEntrypoint = config.getRawConfig().traefik.http_entrypoint;
const httpsEntrypoint = config.getRawConfig().traefik.https_entrypoint;
const additionalMiddlewares =
config.getRawConfig().traefik.additional_middlewares || [];
const routerMiddlewares = [badgerMiddlewareName, ...additionalMiddlewares];
for (const redirect of redirects) {
const routerName = `${redirect.redirectId}-redirect-${redirect.name}-router`;
logger.debug(
`Processing redirect ${redirect.name} with domain ${redirect.fullDomain}`
);
if (!redirect.enabled) {
logger.debug(
`Redirect ${redirect.name} is disabled, skipping Traefik config`
);
continue;
}
let tls: any = {};
if (redirect.ssl) {
tls = resolveTls(redirect);
if (tls === null) {
continue;
}
}
if (!config_output.http.routers) {
config_output.http.routers = {};
}
if (redirect.matchPath && redirect.pathMatchType === "regex") {
try {
new RegExp(redirect.matchPath);
} catch {
logger.debug(
`Invalid regex pattern in redirect ${redirect.redirectId} match path: ${redirect.matchPath}`
);
continue;
}
}
const rule = appendPathMatch(
buildHostRule(redirect.fullDomain, redirect.wildcard),
redirect.matchPath,
redirect.pathMatchType
);
// A redirect attached to a resource must win over that resource's
// router at the same host/path specificity, so nudge derived
// priorities up by one. Explicit priorities are used as-is.
const hasExplicitPriority =
!!redirect.priority && redirect.priority !== 100;
const priority =
computeRoutePriority(
redirect.priority,
redirect.matchPath,
redirect.pathMatchType
) + (hasExplicitPriority ? 0 : 1);
// if resource is already attached to resource, we don't need to add the https redirect
// as it is already added in the resource traefik config
if (redirect.attachedTo !== "resource" && redirect.ssl) {
config_output.http.routers[`${routerName}-redirect`] = {
entryPoints: [httpEntrypoint],
middlewares: [redirectHttpsMiddlewareName],
service: NOOP_SERVICE,
rule,
priority
};
}
config_output.http.routers[routerName] = {
entryPoints: [redirect.ssl ? httpsEntrypoint : httpEntrypoint],
middlewares: routerMiddlewares,
service: NOOP_SERVICE,
rule,
priority,
...(redirect.ssl ? { tls } : {})
};
}
}