mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
feat: integrate Pi backend and Pi Web
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { isIP } from "node:net";
|
||||
|
||||
function normalizeHostname(value: string): string {
|
||||
const unbracketed = value.startsWith("[") && value.endsWith("]")
|
||||
? value.slice(1, -1)
|
||||
: value;
|
||||
return unbracketed.toLowerCase().replace(/\.$/, "");
|
||||
}
|
||||
|
||||
function hostnameFromAuthority(value: string): string | null {
|
||||
if (!value || /[\s/@\\]/.test(value)) return null;
|
||||
try {
|
||||
const parsed = new URL(`http://${value}`);
|
||||
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
||||
return null;
|
||||
}
|
||||
return normalizeHostname(parsed.hostname);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeConfiguredHostname(value: string | undefined): string | null {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return null;
|
||||
return isIP(trimmed) ? normalizeHostname(trimmed) : hostnameFromAuthority(trimmed);
|
||||
}
|
||||
|
||||
function isLoopbackHostname(hostname: string): boolean {
|
||||
return hostname === "localhost" || hostname.endsWith(".localhost");
|
||||
}
|
||||
|
||||
function configuredHostnamesFromEnvironment(): string[] {
|
||||
return [
|
||||
process.env.PI_WEB_HOSTNAME,
|
||||
...(process.env.PI_WEB_ALLOWED_HOSTS?.split(",") ?? []),
|
||||
].filter((value): value is string => Boolean(value?.trim()));
|
||||
}
|
||||
|
||||
function canonicalOrigin(value: string): string | null {
|
||||
try {
|
||||
return new URL(value).origin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getRequestOrigin(request: Request): string | null {
|
||||
const requestUrl = new URL(request.url);
|
||||
const host = request.headers.get("host");
|
||||
return host ? canonicalOrigin(`${requestUrl.protocol}//${host}`) : null;
|
||||
}
|
||||
|
||||
function isUserInitiatedSessionExportNavigation(request: Request): boolean {
|
||||
if (
|
||||
request.method !== "GET"
|
||||
|| request.headers.get("sec-fetch-mode") !== "navigate"
|
||||
|| request.headers.get("sec-fetch-dest") !== "document"
|
||||
|| request.headers.get("sec-fetch-user") !== "?1"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return /^\/api\/sessions\/[^/]+\/export$/.test(new URL(request.url).pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only trust local names, IP literals, or the hostname explicitly selected by
|
||||
* the operator. IP literals preserve LAN access but cannot be DNS-rebound
|
||||
* because the browser keeps the literal address in the Host header.
|
||||
*/
|
||||
export function isApiRequestHostAllowed(
|
||||
request: Request,
|
||||
configuredHostnames = configuredHostnamesFromEnvironment(),
|
||||
): boolean {
|
||||
const host = request.headers.get("host");
|
||||
const hostname = host ? hostnameFromAuthority(host) : null;
|
||||
if (!hostname) return false;
|
||||
if (isLoopbackHostname(hostname) || isIP(hostname)) return true;
|
||||
|
||||
return configuredHostnames.some(
|
||||
(configured) => normalizeConfiguredHostname(configured) === hostname,
|
||||
);
|
||||
}
|
||||
|
||||
/** Reject browser cross-site API requests while preserving non-browser clients. */
|
||||
export function isApiRequestOriginAllowed(request: Request): boolean {
|
||||
const origin = request.headers.get("origin");
|
||||
const fetchSite = request.headers.get("sec-fetch-site");
|
||||
if (fetchSite === "cross-site") return false;
|
||||
if (!origin) return true;
|
||||
|
||||
const requestOrigin = getRequestOrigin(request);
|
||||
return requestOrigin !== null && canonicalOrigin(origin) === requestOrigin;
|
||||
}
|
||||
|
||||
export function shouldCheckApiRequestOrigin(request: Request): boolean {
|
||||
return request.headers.has("origin") || request.headers.has("sec-fetch-site");
|
||||
}
|
||||
|
||||
export function isApiRequestAllowed(
|
||||
request: Request,
|
||||
configuredHostnames = configuredHostnamesFromEnvironment(),
|
||||
): boolean {
|
||||
if (!isApiRequestHostAllowed(request, configuredHostnames)) return false;
|
||||
if (isUserInitiatedSessionExportNavigation(request)) return true;
|
||||
return !shouldCheckApiRequestOrigin(request) || isApiRequestOriginAllowed(request);
|
||||
}
|
||||
|
||||
export function hasJsonContentType(request: Request): boolean {
|
||||
const mediaType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
||||
return mediaType === "application/json"
|
||||
|| Boolean(mediaType?.startsWith("application/") && mediaType.endsWith("+json"));
|
||||
}
|
||||
Reference in New Issue
Block a user