mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 13:42:09 +03:00
fix: unify dashboard csrf origin fallback (#5856)
Integrated into release/v3.8.43
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { DASHBOARD_CSRF_HEADER } from "@/shared/constants/dashboardCsrf";
|
||||
import { PUBLIC_API_ROUTE_PREFIXES } from "@/shared/constants/publicApiRoutes";
|
||||
|
||||
interface CachedDashboardCsrfToken {
|
||||
token: string;
|
||||
@@ -6,46 +7,76 @@ interface CachedDashboardCsrfToken {
|
||||
}
|
||||
|
||||
let cachedToken: CachedDashboardCsrfToken | null = null;
|
||||
let pendingToken: Promise<string | null> | null = null;
|
||||
let originalFetch: typeof fetch | null = null;
|
||||
let installCount = 0;
|
||||
|
||||
const CLIENT_API_ALIAS_PREFIXES = ["/chat/completions", "/responses", "/models", "/codex"];
|
||||
const TOP_LEVEL_MANAGEMENT_PATH_PREFIXES = ["/a2a"];
|
||||
|
||||
export function __resetDashboardCsrfTokenForTests(): void {
|
||||
cachedToken = null;
|
||||
pendingToken = null;
|
||||
if (originalFetch) {
|
||||
globalThis.fetch = originalFetch;
|
||||
originalFetch = null;
|
||||
}
|
||||
installCount = 0;
|
||||
}
|
||||
|
||||
async function getDashboardCsrfToken(): Promise<string | null> {
|
||||
const now = Date.now();
|
||||
function currentDashboardCsrfToken(now: number = Date.now()): string | null {
|
||||
if (cachedToken && cachedToken.expiresAtMs - now > 30_000) {
|
||||
return cachedToken.token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
async function fetchDashboardCsrfToken(now: number): Promise<string | null> {
|
||||
try {
|
||||
response = await fetch("/api/auth/csrf", {
|
||||
const response = await fetch("/api/auth/csrf", {
|
||||
cache: "no-store",
|
||||
credentials: "same-origin",
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
|
||||
const body = (await response.json().catch(() => null)) as {
|
||||
token?: unknown;
|
||||
expiresAt?: unknown;
|
||||
} | null;
|
||||
|
||||
if (typeof body?.token !== "string" || typeof body.expiresAt !== "string") {
|
||||
cachedToken = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
const expiresAtMs = Date.parse(body.expiresAt);
|
||||
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) {
|
||||
cachedToken = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
cachedToken = { token: body.token, expiresAtMs };
|
||||
return cachedToken.token;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) return null;
|
||||
}
|
||||
|
||||
const body = (await response.json().catch(() => null)) as {
|
||||
token?: unknown;
|
||||
expiresAt?: unknown;
|
||||
} | null;
|
||||
async function getDashboardCsrfToken(): Promise<string | null> {
|
||||
const cached = currentDashboardCsrfToken();
|
||||
if (cached) return cached;
|
||||
|
||||
if (typeof body?.token !== "string" || typeof body.expiresAt !== "string") {
|
||||
cachedToken = null;
|
||||
return null;
|
||||
if (!pendingToken) {
|
||||
pendingToken = fetchDashboardCsrfToken(Date.now()).finally(() => {
|
||||
pendingToken = null;
|
||||
});
|
||||
}
|
||||
|
||||
const expiresAtMs = Date.parse(body.expiresAt);
|
||||
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) {
|
||||
cachedToken = null;
|
||||
return null;
|
||||
}
|
||||
return pendingToken;
|
||||
}
|
||||
|
||||
cachedToken = { token: body.token, expiresAtMs };
|
||||
return cachedToken.token;
|
||||
export function prefetchDashboardCsrfToken(): Promise<string | null> {
|
||||
return getDashboardCsrfToken();
|
||||
}
|
||||
|
||||
export async function withDashboardCsrfHeader(headers?: HeadersInit): Promise<Headers> {
|
||||
@@ -54,3 +85,112 @@ export async function withDashboardCsrfHeader(headers?: HeadersInit): Promise<He
|
||||
if (token) result.set(DASHBOARD_CSRF_HEADER, token);
|
||||
return result;
|
||||
}
|
||||
|
||||
function requestFromInput(input: RequestInfo | URL): Request | null {
|
||||
return typeof Request !== "undefined" && input instanceof Request ? input : null;
|
||||
}
|
||||
|
||||
function inputUrl(input: RequestInfo | URL): string | null {
|
||||
const request = requestFromInput(input);
|
||||
if (request) return request.url;
|
||||
if (input instanceof URL) return input.href;
|
||||
return typeof input === "string" ? input : null;
|
||||
}
|
||||
|
||||
function requestMethod(input: RequestInfo | URL, init?: RequestInit): string {
|
||||
if (init?.method) return init.method;
|
||||
return requestFromInput(input)?.method ?? "GET";
|
||||
}
|
||||
|
||||
function isClientApiPath(pathname: string): boolean {
|
||||
if (pathname === "/api/v1" || pathname.startsWith("/api/v1/")) return true;
|
||||
if (pathname === "/api/v1beta" || pathname.startsWith("/api/v1beta/")) return true;
|
||||
if (pathname === "/v1" || pathname.startsWith("/v1/")) return true;
|
||||
if (pathname === "/v1beta" || pathname.startsWith("/v1beta/")) return true;
|
||||
if (pathname === "/v1/v1" || pathname.startsWith("/v1/v1/")) return true;
|
||||
return CLIENT_API_ALIAS_PREFIXES.some(
|
||||
(prefix) => pathname === prefix || pathname.startsWith(prefix + "/")
|
||||
);
|
||||
}
|
||||
|
||||
function isPublicApiPath(pathname: string): boolean {
|
||||
return PUBLIC_API_ROUTE_PREFIXES.some((prefix) => pathname.startsWith(prefix));
|
||||
}
|
||||
|
||||
function shouldAttachDashboardCsrf(url: URL): boolean {
|
||||
if (
|
||||
TOP_LEVEL_MANAGEMENT_PATH_PREFIXES.some(
|
||||
(prefix) => url.pathname === prefix || url.pathname.startsWith(prefix + "/")
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
url.pathname.startsWith("/api/") &&
|
||||
url.pathname !== "/api/auth/csrf" &&
|
||||
!isPublicApiPath(url.pathname) &&
|
||||
!isClientApiPath(url.pathname)
|
||||
);
|
||||
}
|
||||
|
||||
function sameOriginDashboardMutation(input: RequestInfo | URL, init?: RequestInit): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
|
||||
const method = requestMethod(input, init).toUpperCase();
|
||||
if (!["POST", "PUT", "PATCH", "DELETE"].includes(method)) return false;
|
||||
|
||||
const rawUrl = inputUrl(input);
|
||||
if (!rawUrl) return false;
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(rawUrl, window.location.href);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return url.origin === window.location.origin && shouldAttachDashboardCsrf(url);
|
||||
}
|
||||
|
||||
function mergedHeaders(input: RequestInfo | URL, init?: RequestInit): Headers {
|
||||
return new Headers(init?.headers ?? requestFromInput(input)?.headers);
|
||||
}
|
||||
|
||||
export function installDashboardCsrfFetch(): () => void {
|
||||
if (typeof globalThis.fetch !== "function") return () => {};
|
||||
|
||||
if (installCount === 0) {
|
||||
originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
if (!originalFetch || !sameOriginDashboardMutation(input, init)) {
|
||||
return originalFetch ? originalFetch(input, init) : fetch(input, init);
|
||||
}
|
||||
|
||||
const headers = mergedHeaders(input, init);
|
||||
if (headers.has(DASHBOARD_CSRF_HEADER)) {
|
||||
return originalFetch(input, init);
|
||||
}
|
||||
|
||||
const token = currentDashboardCsrfToken() ?? (await getDashboardCsrfToken());
|
||||
if (!token) return originalFetch(input, init);
|
||||
|
||||
headers.set(DASHBOARD_CSRF_HEADER, token);
|
||||
return originalFetch(input, { ...init, headers });
|
||||
}) as typeof fetch;
|
||||
}
|
||||
|
||||
installCount++;
|
||||
let active = true;
|
||||
|
||||
return () => {
|
||||
if (!active) return;
|
||||
active = false;
|
||||
installCount = Math.max(0, installCount - 1);
|
||||
if (installCount === 0 && originalFetch) {
|
||||
globalThis.fetch = originalFetch;
|
||||
originalFetch = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user