add fetch() shim to proxy cross-origin requests through server

This commit is contained in:
Nystik
2026-03-24 01:06:30 +01:00
parent df9d53984b
commit d5027795e9
4 changed files with 164 additions and 3 deletions

View File

@@ -115,6 +115,144 @@ function installWindowOpen() {
};
}
function arrayBufferToBase64(buf) {
const bytes = new Uint8Array(buf);
let binary = "";
const chunk = 8192;
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
return btoa(binary);
}
function base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
function isSameOrigin(url) {
if (
!url ||
url.startsWith("/") ||
url.startsWith("./") ||
url.startsWith("../")
) {
return true;
}
if (url.startsWith("data:") || url.startsWith("blob:")) {
return true;
}
try {
const parsed = new URL(url, window.location.origin);
return parsed.origin === window.location.origin;
} catch {
return true;
}
}
function installFetchShim() {
const originalFetch = window.fetch.bind(window);
window.__originalFetch = originalFetch;
window.fetch = async function (input, init) {
let url;
if (typeof input === "string") {
url = input;
} else if (input instanceof URL) {
url = input.href;
} else if (input instanceof Request) {
url = input.url;
} else {
url = String(input);
}
if (isSameOrigin(url)) {
return originalFetch(input, init);
}
// Cross-origin - route through server proxy
const method = (
init?.method || (input instanceof Request ? input.method : "GET")
).toUpperCase();
const headers = {};
if (init?.headers) {
const h =
init.headers instanceof Headers
? init.headers
: new Headers(init.headers);
h.forEach((val, key) => {
headers[key] = val;
});
} else if (input instanceof Request) {
input.headers.forEach((val, key) => {
headers[key] = val;
});
}
// Mimic the real Obsidian desktop app headers for cross-origin requests
if (!headers["user-agent"] && !headers["User-Agent"]) {
headers["user-agent"] = navigator.userAgent;
}
if (!headers["origin"] && !headers["Origin"]) {
headers["origin"] = "app://obsidian.md";
}
let body = null;
let binary = false;
if (init?.body && method !== "GET" && method !== "HEAD") {
if (typeof init.body === "string") {
body = init.body;
} else if (init.body instanceof ArrayBuffer) {
body = arrayBufferToBase64(init.body);
binary = true;
} else if (init.body instanceof Uint8Array) {
body = arrayBufferToBase64(init.body.buffer);
binary = true;
} else if (typeof init.body === "object") {
body = JSON.stringify(init.body);
} else {
body = String(init.body);
}
}
console.log("[shim:fetch] Proxying cross-origin:", method, url);
const proxyRes = await originalFetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, method, headers, body, binary }),
});
if (!proxyRes.ok) {
const err = await proxyRes
.json()
.catch(() => ({ error: "Proxy request failed" }));
throw new TypeError(err.error || "Failed to fetch");
}
const result = await proxyRes.json();
const respBody = base64ToArrayBuffer(result.body);
return new Response(respBody, {
status: result.status,
headers: result.headers,
});
};
}
function installContextMenuFix() {
// hacky fix to prevent browser from showing context menu while allowing obsidian context menu
window.addEventListener(
@@ -130,6 +268,7 @@ function installContextMenuFix() {
export function installGlobals() {
installProcess();
installBuffer();
installFetchShim();
installWindowClose();
installWindowOpen();
installContextMenuFix();