Files
pangolin/server/lib/traefik/rule.ts
T
2026-09-18 20:59:18 +02:00

130 lines
3.9 KiB
TypeScript

// Build the Host()/HostRegexp() Traefik rule for a resource's domain.
// Wildcard resources match any single subdomain via HostRegexp.
export function buildHostRule(
fullDomain: string,
wildcard?: boolean | null
): string {
if (wildcard && fullDomain.startsWith("*.")) {
// Convert *.foo.bar.com -> HostRegexp(`^[^.]+\.foo\.bar\.com$`)
const escaped = fullDomain.slice(2).replace(/\./g, "\\.");
return `HostRegexp(\`^[^.]+\\.${escaped}$\`)`;
}
return `Host(\`${fullDomain}\`)`;
}
// Append a path-matching clause to a Traefik rule based on the resource's
// configured path and pathMatchType.
export function appendPathMatch(
rule: string,
path: string | null | undefined,
pathMatchType: string | null | undefined
): string {
if (!path || !pathMatchType) return rule;
let p = path;
if (!p.startsWith("/")) {
p = `/${p}`;
}
if (pathMatchType === "exact") {
return `${rule} && Path(\`${p}\`)`;
} else if (pathMatchType === "prefix") {
return `${rule} && PathPrefix(\`${p}\`)`;
} else if (pathMatchType === "regex") {
return `${rule} && PathRegexp(\`${path}\`)`; // this is the raw path because it's a regex
}
return rule;
}
/**
* Server-side equivalent of the clause appendPathMatch emits, so badger can
* tell whether a request would have matched a given path config. Mirrors
* Traefik v3 semantics: Path is exact, PathPrefix is segment-aware
* (`/products` matches `/products/shoes` but not `/productsforsale`), and
* PathRegexp is an unanchored regex test.
*/
export function matchesPath(
requestPath: string,
path: string | null | undefined,
pathMatchType: string | null | undefined
): boolean {
if (!path || !pathMatchType) return true;
if (pathMatchType === "regex") {
try {
return new RegExp(path).test(requestPath);
} catch {
return false;
}
}
let p = path;
if (!p.startsWith("/")) {
p = `/${p}`;
}
if (pathMatchType === "exact") {
return requestPath === p;
} else if (pathMatchType === "prefix") {
if (!requestPath.startsWith(p)) {
return false;
}
if (p.endsWith("/")) {
return true;
}
const rest = requestPath.slice(p.length);
return rest === "" || rest.startsWith("/");
}
return true;
}
// Compute the router priority for a resource, favoring an explicit override
// and otherwise deriving it from the path match specificity.
export function computeRoutePriority(
priority: number | null | undefined,
path: string | null | undefined,
pathMatchType: string | null | undefined
): number {
if (priority && priority != 100) {
return priority;
}
let p = 100;
if (path && pathMatchType) {
p += 10;
if (pathMatchType === "exact") {
p += 5;
} else if (pathMatchType === "prefix") {
p += 3;
} else if (pathMatchType === "regex") {
p += 2;
}
if (path === "/") {
p = 1; // lowest for catch-all
}
}
return p;
}
// Redirects must always be evaluated before resource routers on the same
// host. Target and redirect priorities are both capped at 1000, so lifting
// every redirect by this offset puts them in a band (1001-2000) no resource
// router can reach, while explicit priorities still order redirects among
// themselves.
export const REDIRECT_PRIORITY_OFFSET = 1000;
/**
* Compute the router priority for a redirect: the same derivation as a
* resource router, shifted into the redirect band.
*/
export function computeRedirectPriority(
priority: number | null | undefined,
path: string | null | undefined,
pathMatchType: string | null | undefined
): number {
return (
computeRoutePriority(priority, path, pathMatchType) +
REDIRECT_PRIORITY_OFFSET
);
}