mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-29 06:59:06 +01:00
✨ handle redirect request in badger's verifySession
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { assertEquals } from "../../../test/assert";
|
||||
import { rewriteRequestPath } from "./middleware";
|
||||
|
||||
function runTests() {
|
||||
console.log("Running rewriteRequestPath tests...");
|
||||
|
||||
// no rewrite configured
|
||||
assertEquals(
|
||||
rewriteRequestPath("/a/b", "/a", "prefix", null, null),
|
||||
"/a/b",
|
||||
"no rewrite type"
|
||||
);
|
||||
|
||||
// exact rewrite
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old", "/old", "exact", "/new", "exact"),
|
||||
"/new",
|
||||
"exact -> exact"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old/x", "/old", "exact", "/new", "exact"),
|
||||
"/old/x",
|
||||
"exact rewrite only on exact match"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old", "/old", "exact", "new", "exact"),
|
||||
"/new",
|
||||
"leading slash added to rewrite"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/anything", null, null, "/new", "exact"),
|
||||
"/new",
|
||||
"no match path + exact rewrite replaces path"
|
||||
);
|
||||
|
||||
// prefix rewrite
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old/a/b", "/old", "prefix", "/new", "prefix"),
|
||||
"/new/a/b",
|
||||
"prefix -> prefix keeps rest"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old", "/old", "prefix", "/new", "prefix"),
|
||||
"/new",
|
||||
"prefix -> prefix bare"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old", "/old", "exact", "/new", "prefix"),
|
||||
"/new",
|
||||
"exact -> prefix"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath(
|
||||
"/api/v1/users",
|
||||
"^/api/v1/(.*)",
|
||||
"regex",
|
||||
"/v2/$1",
|
||||
"prefix"
|
||||
),
|
||||
"/v2/users",
|
||||
"regex -> prefix uses capture"
|
||||
);
|
||||
|
||||
// regex rewrite
|
||||
assertEquals(
|
||||
rewriteRequestPath(
|
||||
"/blog/2020/post",
|
||||
"^/blog/(\\d+)/(.*)$",
|
||||
"regex",
|
||||
"/archive/$2-$1",
|
||||
"regex"
|
||||
),
|
||||
"/archive/post-2020",
|
||||
"regex -> regex"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old/x", "/old", "prefix", "/new$1", "regex"),
|
||||
"/new/x",
|
||||
"prefix -> regex has (.*) capture"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old", "/old", "exact", "/new", "regex"),
|
||||
"/new",
|
||||
"exact -> regex"
|
||||
);
|
||||
|
||||
// stripPrefix
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old/a", "/old", "prefix", null, "stripPrefix"),
|
||||
"/a",
|
||||
"stripPrefix"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old", "/old", "prefix", null, "stripPrefix"),
|
||||
"/",
|
||||
"stripPrefix to root"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old/a", "/old", "prefix", "/new", "stripPrefix"),
|
||||
"/new/a",
|
||||
"stripPrefix + addPrefix"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old", "/old", "exact", null, "stripPrefix"),
|
||||
"/",
|
||||
"stripPrefix exact"
|
||||
);
|
||||
// Same result the replacePathRegex middleware produces for this config
|
||||
// (regex `^/old` -> `/`), quirky double slash included.
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old/a", "^/old", "regex", null, "stripPrefix"),
|
||||
"//a",
|
||||
"stripPrefix regex mirrors Traefik"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/old/a", "^/old/", "regex", null, "stripPrefix"),
|
||||
"/a",
|
||||
"stripPrefix regex with trailing slash"
|
||||
);
|
||||
assertEquals(
|
||||
rewriteRequestPath("/a/b", null, null, null, "stripPrefix"),
|
||||
"/a/b",
|
||||
"stripPrefix without match path is a no-op"
|
||||
);
|
||||
|
||||
console.log("All rewriteRequestPath tests passed!");
|
||||
}
|
||||
|
||||
try {
|
||||
runTests();
|
||||
} catch (error) {
|
||||
console.error("Test failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -194,6 +194,78 @@ export default function createPathRewriteMiddleware(
|
||||
return { middlewares };
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a path rewrite to a request path in-process, producing the same
|
||||
* result the replacePathRegex / stripPrefix middlewares built above would.
|
||||
* Used where Pangolin issues the redirect itself (badger) instead of
|
||||
* handing it to Traefik.
|
||||
*/
|
||||
export function rewriteRequestPath(
|
||||
requestPath: string,
|
||||
path: string | null,
|
||||
pathMatchType: string | null,
|
||||
rewritePath: string | null,
|
||||
rewritePathType: string | null
|
||||
): string {
|
||||
if (!rewritePathType) {
|
||||
return requestPath;
|
||||
}
|
||||
|
||||
let target = rewritePath ?? "";
|
||||
if (
|
||||
rewritePathType !== "regex" &&
|
||||
target !== "" &&
|
||||
!target.startsWith("/")
|
||||
) {
|
||||
target = `/${target}`;
|
||||
}
|
||||
|
||||
// Nothing was matched against, so there is nothing to strip or replace;
|
||||
// an exact rewrite is the only one that still means something.
|
||||
if (!path || !pathMatchType) {
|
||||
return rewritePathType === "exact" ? target || "/" : requestPath;
|
||||
}
|
||||
|
||||
let matched = path;
|
||||
if (pathMatchType !== "regex" && !matched.startsWith("/")) {
|
||||
matched = `/${matched}`;
|
||||
}
|
||||
|
||||
const matchRegex =
|
||||
pathMatchType === "regex"
|
||||
? matched
|
||||
: pathMatchType === "prefix"
|
||||
? `^${escapeRegex(matched)}(.*)`
|
||||
: `^${escapeRegex(matched)}$`;
|
||||
|
||||
switch (rewritePathType) {
|
||||
case "exact":
|
||||
return requestPath.replace(
|
||||
new RegExp(`^${escapeRegex(matched)}$`),
|
||||
target
|
||||
);
|
||||
case "prefix":
|
||||
return requestPath.replace(
|
||||
new RegExp(matchRegex),
|
||||
pathMatchType === "prefix" ? `${target}$1` : target
|
||||
);
|
||||
case "regex":
|
||||
return requestPath.replace(new RegExp(matchRegex), target);
|
||||
case "stripPrefix": {
|
||||
if (pathMatchType === "prefix") {
|
||||
const stripped = requestPath.startsWith(matched)
|
||||
? requestPath.slice(matched.length)
|
||||
: requestPath;
|
||||
const prefix = target && target !== "/" ? target : "";
|
||||
return `${prefix}${stripped}` || "/";
|
||||
}
|
||||
return requestPath.replace(new RegExp(matchRegex), target || "/");
|
||||
}
|
||||
default:
|
||||
return requestPath;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegex(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import config from "@server/lib/config";
|
||||
import {
|
||||
buildHostRule,
|
||||
appendPathMatch,
|
||||
computeRoutePriority
|
||||
computeRedirectPriority
|
||||
} from "@server/lib/traefik/rule";
|
||||
|
||||
export type RedirectRouteRow = {
|
||||
@@ -97,17 +97,11 @@ export function buildRedirectConfig(params: {
|
||||
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);
|
||||
const priority = computeRedirectPriority(
|
||||
redirect.priority,
|
||||
redirect.matchPath,
|
||||
redirect.pathMatchType
|
||||
);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { assertEquals } from "../../../test/assert";
|
||||
import {
|
||||
computeRedirectPriority,
|
||||
computeRoutePriority,
|
||||
matchesPath
|
||||
} from "./rule";
|
||||
|
||||
function runTests() {
|
||||
console.log("Running matchesPath tests...");
|
||||
|
||||
// No path config matches everything
|
||||
assertEquals(matchesPath("/anything", null, null), true, "null path");
|
||||
assertEquals(matchesPath("/anything", "/a", null), true, "null type");
|
||||
assertEquals(
|
||||
matchesPath("/anything", null, "prefix"),
|
||||
true,
|
||||
"null path w/ type"
|
||||
);
|
||||
|
||||
// exact
|
||||
assertEquals(matchesPath("/api", "/api", "exact"), true, "exact match");
|
||||
assertEquals(
|
||||
matchesPath("/api/", "/api", "exact"),
|
||||
false,
|
||||
"exact trailing slash"
|
||||
);
|
||||
assertEquals(matchesPath("/api/x", "/api", "exact"), false, "exact child");
|
||||
assertEquals(
|
||||
matchesPath("/api", "api", "exact"),
|
||||
true,
|
||||
"exact leading slash added"
|
||||
);
|
||||
|
||||
// prefix (segment-aware, like Traefik v3 PathPrefix)
|
||||
assertEquals(
|
||||
matchesPath("/products", "/products", "prefix"),
|
||||
true,
|
||||
"prefix itself"
|
||||
);
|
||||
assertEquals(
|
||||
matchesPath("/products/", "/products", "prefix"),
|
||||
true,
|
||||
"prefix slash"
|
||||
);
|
||||
assertEquals(
|
||||
matchesPath("/products/shoes", "/products", "prefix"),
|
||||
true,
|
||||
"prefix child"
|
||||
);
|
||||
assertEquals(
|
||||
matchesPath("/productsforsale", "/products", "prefix"),
|
||||
false,
|
||||
"prefix partial segment"
|
||||
);
|
||||
assertEquals(
|
||||
matchesPath("/products/shoes", "/products/", "prefix"),
|
||||
true,
|
||||
"prefix with trailing slash"
|
||||
);
|
||||
assertEquals(
|
||||
matchesPath("/products", "/products/", "prefix"),
|
||||
false,
|
||||
"trailing-slash prefix vs bare"
|
||||
);
|
||||
assertEquals(
|
||||
matchesPath("/other", "/products", "prefix"),
|
||||
false,
|
||||
"prefix miss"
|
||||
);
|
||||
|
||||
// regex (unanchored, like PathRegexp)
|
||||
assertEquals(
|
||||
matchesPath("/api/v1/x", "^/api/.*", "regex"),
|
||||
true,
|
||||
"regex anchored"
|
||||
);
|
||||
assertEquals(
|
||||
matchesPath("/x/api/v1", "/api/", "regex"),
|
||||
true,
|
||||
"regex unanchored"
|
||||
);
|
||||
assertEquals(matchesPath("/foo", "^/api", "regex"), false, "regex miss");
|
||||
assertEquals(
|
||||
matchesPath("/foo", "(", "regex"),
|
||||
false,
|
||||
"invalid regex never matches"
|
||||
);
|
||||
|
||||
console.log("All matchesPath tests passed!");
|
||||
|
||||
console.log("Running priority tests...");
|
||||
|
||||
// Resource routers: explicit override, else derived from path specificity
|
||||
assertEquals(computeRoutePriority(null, null, null), 100, "default");
|
||||
assertEquals(computeRoutePriority(100, null, null), 100, "100 is auto");
|
||||
assertEquals(computeRoutePriority(500, "/a", "exact"), 500, "explicit");
|
||||
assertEquals(computeRoutePriority(null, "/a", "exact"), 115, "exact");
|
||||
assertEquals(computeRoutePriority(null, "/a", "prefix"), 113, "prefix");
|
||||
assertEquals(computeRoutePriority(null, "/a", "regex"), 112, "regex");
|
||||
assertEquals(computeRoutePriority(null, "/", "prefix"), 1, "catch-all");
|
||||
|
||||
// Redirect routers always land above any resource router (max 1000)
|
||||
assertEquals(
|
||||
computeRedirectPriority(null, null, null),
|
||||
1100,
|
||||
"redirect default"
|
||||
);
|
||||
assertEquals(
|
||||
computeRedirectPriority(null, "/", "prefix"),
|
||||
1001,
|
||||
"redirect catch-all"
|
||||
);
|
||||
assertEquals(
|
||||
computeRedirectPriority(1, null, null),
|
||||
1001,
|
||||
"redirect lowest explicit"
|
||||
);
|
||||
assertEquals(
|
||||
computeRedirectPriority(1000, null, null),
|
||||
2000,
|
||||
"redirect highest explicit"
|
||||
);
|
||||
assertEquals(
|
||||
computeRedirectPriority(1, "/", "prefix") >
|
||||
computeRoutePriority(1000, "/a", "exact"),
|
||||
true,
|
||||
"weakest redirect beats strongest resource"
|
||||
);
|
||||
|
||||
console.log("All priority tests passed!");
|
||||
}
|
||||
|
||||
try {
|
||||
runTests();
|
||||
} catch (error) {
|
||||
console.error("Test failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -40,6 +40,48 @@ export function appendPathMatch(
|
||||
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.
|
||||
@@ -69,3 +111,25 @@ export function computeRoutePriority(
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user