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; }