add fetch() shim to proxy cross-origin requests through server
This commit is contained in:
parent
df9d53984b
commit
d5027795e9
4 changed files with 164 additions and 3 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -2,6 +2,19 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.6.1] - Slifer (2026-03-24)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `fetch()` shim that proxies cross-origin requests through `/api/proxy` to bypass CORS restrictions
|
||||||
|
- Automatic `Origin: app://obsidian.md` header injection for cross-origin requests to match Obsidian desktop app
|
||||||
|
- User-Agent forwarding from browser to proxy for cross-origin requests
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Obsidian Sync API authentication now works in browser (was blocked by CORS)
|
||||||
|
- Proxy response headers cleaned to exclude hop-by-hop headers (`content-encoding`, `transfer-encoding`, `content-length`, `connection`)
|
||||||
|
|
||||||
## [0.6.0] - Slifer (2026-03-23)
|
## [0.6.0] - Slifer (2026-03-23)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ignis",
|
"name": "ignis",
|
||||||
"version": "0.6.0",
|
"version": "0.6.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "An Electron shim and server bridge for running Obsidian in a browser.",
|
"description": "An Electron shim and server bridge for running Obsidian in a browser.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,19 @@ router.post("/", async (req, res) => {
|
||||||
const upstream = await fetch(url, fetchOpts);
|
const upstream = await fetch(url, fetchOpts);
|
||||||
const respBody = Buffer.from(await upstream.arrayBuffer());
|
const respBody = Buffer.from(await upstream.arrayBuffer());
|
||||||
|
|
||||||
// Forward response headers
|
// Forward response headers, stripping hop-by-hop / encoding headers
|
||||||
|
// since the body is already decompressed by Node's fetch
|
||||||
|
const skipHeaders = new Set([
|
||||||
|
"content-encoding",
|
||||||
|
"transfer-encoding",
|
||||||
|
"content-length",
|
||||||
|
"connection",
|
||||||
|
]);
|
||||||
const respHeaders = {};
|
const respHeaders = {};
|
||||||
upstream.headers.forEach((val, key) => {
|
upstream.headers.forEach((val, key) => {
|
||||||
|
if (!skipHeaders.has(key)) {
|
||||||
respHeaders[key] = val;
|
respHeaders[key] = val;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
function installContextMenuFix() {
|
||||||
// hacky fix to prevent browser from showing context menu while allowing obsidian context menu
|
// hacky fix to prevent browser from showing context menu while allowing obsidian context menu
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
|
|
@ -130,6 +268,7 @@ function installContextMenuFix() {
|
||||||
export function installGlobals() {
|
export function installGlobals() {
|
||||||
installProcess();
|
installProcess();
|
||||||
installBuffer();
|
installBuffer();
|
||||||
|
installFetchShim();
|
||||||
installWindowClose();
|
installWindowClose();
|
||||||
installWindowOpen();
|
installWindowOpen();
|
||||||
installContextMenuFix();
|
installContextMenuFix();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue