102 lines
2.8 KiB
JavaScript
102 lines
2.8 KiB
JavaScript
import { getQuery, joinURL, parseURL, withQuery, withoutBase } from "ufo";
|
|
const MAX_DECODE_PASSES = 8;
|
|
const PERCENT_ESCAPE_RE = /%([\da-f]{2})/gi;
|
|
const HAS_PERCENT_ESCAPE_RE = /%[\da-f]{2}/i;
|
|
export function canonicalizePath(pathname) {
|
|
let path = pathname;
|
|
for (let pass = 0; HAS_PERCENT_ESCAPE_RE.test(path); pass++) {
|
|
if (pass >= MAX_DECODE_PASSES) {
|
|
return void 0;
|
|
}
|
|
path = path.replace(
|
|
PERCENT_ESCAPE_RE,
|
|
(_, hex) => String.fromCharCode(Number.parseInt(hex, 16))
|
|
);
|
|
}
|
|
const segments = [];
|
|
for (const segment of path.replace(/\\/g, "/").split("/")) {
|
|
if (segment === "" || segment === ".") {
|
|
continue;
|
|
}
|
|
if (segment === "..") {
|
|
segments.pop();
|
|
continue;
|
|
}
|
|
segments.push(segment);
|
|
}
|
|
return "/" + segments.join("/");
|
|
}
|
|
function whatwgPathname(pathname) {
|
|
try {
|
|
return new URL(pathname, "http://_").pathname;
|
|
} catch {
|
|
return void 0;
|
|
}
|
|
}
|
|
const READINGS = [
|
|
(p) => canonicalizePath(p),
|
|
(p) => {
|
|
const whatwg = whatwgPathname(p);
|
|
return whatwg === void 0 ? void 0 : canonicalizePath(whatwg);
|
|
}
|
|
];
|
|
export function isPathInScope(pathname, base) {
|
|
if (!base || base === "/") {
|
|
return true;
|
|
}
|
|
for (const read of READINGS) {
|
|
const canonicalBase = read(base);
|
|
const canonical = read(pathname);
|
|
if (canonicalBase === void 0 || canonical === void 0) {
|
|
return false;
|
|
}
|
|
if (canonicalBase === "/") {
|
|
continue;
|
|
}
|
|
if (canonical !== canonicalBase && !canonical.startsWith(canonicalBase + "/")) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
export function isTargetInScope(target, targetBase) {
|
|
return isPathInScope(
|
|
parseURL(target).pathname || "/",
|
|
parseURL(targetBase).pathname || ""
|
|
);
|
|
}
|
|
const LEADING_SEPARATORS_RE = (
|
|
// eslint-disable-next-line no-control-regex
|
|
/^(?:[\u0000-\u0020/\\]|%(?:2f|5c|[01][\da-f]|20))+/i
|
|
);
|
|
export function normalizeRedirectPath(path) {
|
|
return path.replace(LEADING_SEPARATORS_RE, "/");
|
|
}
|
|
export function resolveRedirectTarget(path, to, stripBase) {
|
|
if (!to.endsWith("/**")) {
|
|
return path.includes("?") ? withQuery(to, getQuery(path)) : to;
|
|
}
|
|
let targetPath = path;
|
|
if (stripBase) {
|
|
if (!isPathInScope(path.split("?")[0], stripBase)) {
|
|
return void 0;
|
|
}
|
|
targetPath = withoutBase(targetPath, stripBase);
|
|
}
|
|
targetPath = normalizeRedirectPath(targetPath);
|
|
const targetBase = to.slice(0, -3);
|
|
if (targetBase && targetBase !== "/" && !isPathInScope(joinURL("/_", targetPath.split("?")[0]), "/_")) {
|
|
return void 0;
|
|
}
|
|
let target = joinURL(targetBase, targetPath);
|
|
if (!targetBase || targetBase === "/") {
|
|
target = normalizeRedirectPath(target);
|
|
if (!target.startsWith("/")) {
|
|
target = "/" + target;
|
|
}
|
|
}
|
|
if (!isTargetInScope(target, targetBase)) {
|
|
return void 0;
|
|
}
|
|
return target;
|
|
}
|