File size: 2,545 Bytes
4440aec | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | // Plugin HTTP path context canonicalizes request paths for route matching and protected-route auth checks.
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import {
PROTECTED_PLUGIN_ROUTE_PREFIXES,
canonicalizePathForSecurity,
} from "../../security-path.js";
/**
* Canonical path context for plugin HTTP route auth and matching.
*/
export type PluginRoutePathContext = {
pathname: string;
canonicalPath: string;
candidates: string[];
malformedEncoding: boolean;
decodePassLimitReached: boolean;
rawNormalizedPath: string;
};
function normalizeProtectedPrefix(prefix: string): string {
const collapsed = normalizeLowercaseStringOrEmpty(prefix).replace(/\/{2,}/g, "/");
if (collapsed.length <= 1) {
return collapsed || "/";
}
return collapsed.replace(/\/+$/, "");
}
/** Matches a normalized path against an exact protected prefix boundary. */
export function prefixMatchPath(pathname: string, prefix: string): boolean {
return (
pathname === prefix || pathname.startsWith(`${prefix}/`) || pathname.startsWith(`${prefix}%`)
);
}
const NORMALIZED_PROTECTED_PLUGIN_ROUTE_PREFIXES =
PROTECTED_PLUGIN_ROUTE_PREFIXES.map(normalizeProtectedPrefix);
/** Returns true when any decoded path candidate targets a protected route. */
export function isProtectedPluginRoutePathFromContext(context: PluginRoutePathContext): boolean {
if (
context.candidates.some((candidate) =>
NORMALIZED_PROTECTED_PLUGIN_ROUTE_PREFIXES.some((prefix) =>
prefixMatchPath(candidate, prefix),
),
)
) {
return true;
}
// An unresolved decode chain could still reveal a protected prefix on a later pass.
// Require auth rather than treating an intentionally over-encoded route as public.
if (context.decodePassLimitReached) {
return true;
}
if (!context.malformedEncoding) {
return false;
}
return NORMALIZED_PROTECTED_PLUGIN_ROUTE_PREFIXES.some((prefix) =>
prefixMatchPath(context.rawNormalizedPath, prefix),
);
}
/** Builds all security-relevant decoded path candidates for a request path. */
export function resolvePluginRoutePathContext(pathname: string): PluginRoutePathContext {
const canonical = canonicalizePathForSecurity(pathname);
return {
pathname,
canonicalPath: canonical.canonicalPath,
candidates: canonical.candidates,
malformedEncoding: canonical.malformedEncoding,
decodePassLimitReached: canonical.decodePassLimitReached,
rawNormalizedPath: canonical.rawNormalizedPath,
};
}
|