shim plugin related APIs, proxy web requests.
This commit is contained in:
parent
9789be6d70
commit
ac41ac3c4e
10 changed files with 452 additions and 0 deletions
|
|
@ -30,9 +30,11 @@ app.use((req, res, next) => {
|
||||||
|
|
||||||
const fsRoutes = require("./routes/fs");
|
const fsRoutes = require("./routes/fs");
|
||||||
const vaultRoutes = require("./routes/vault");
|
const vaultRoutes = require("./routes/vault");
|
||||||
|
const proxyRoutes = require("./routes/proxy");
|
||||||
|
|
||||||
app.use("/api/fs", fsRoutes);
|
app.use("/api/fs", fsRoutes);
|
||||||
app.use("/api/vault", vaultRoutes);
|
app.use("/api/vault", vaultRoutes);
|
||||||
|
app.use("/api/proxy", proxyRoutes);
|
||||||
|
|
||||||
// Serve vault files for resource URLs (images, attachments, etc.)
|
// Serve vault files for resource URLs (images, attachments, etc.)
|
||||||
// Vault ID is the first path segment: /vault-files/<vault-id>/path/to/file
|
// Vault ID is the first path segment: /vault-files/<vault-id>/path/to/file
|
||||||
|
|
|
||||||
45
server/routes/proxy.js
Normal file
45
server/routes/proxy.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
const express = require("express");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// POST /api/proxy - forward a request to an external URL (bypasses browser CORS)
|
||||||
|
// Used by the requestUrl shim for plugin installation, update checks, etc.
|
||||||
|
router.post("/", async (req, res) => {
|
||||||
|
const { url, method, headers, body, binary } = req.body;
|
||||||
|
if (!url) {
|
||||||
|
return res.status(400).json({ error: "Missing url" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetchOpts = {
|
||||||
|
method: method || "GET",
|
||||||
|
headers: headers || {},
|
||||||
|
};
|
||||||
|
if (body && method !== "GET" && method !== "HEAD") {
|
||||||
|
if (binary && typeof body === "string") {
|
||||||
|
fetchOpts.body = Buffer.from(body, "base64");
|
||||||
|
} else {
|
||||||
|
fetchOpts.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstream = await fetch(url, fetchOpts);
|
||||||
|
const respBody = Buffer.from(await upstream.arrayBuffer());
|
||||||
|
|
||||||
|
// Forward response headers
|
||||||
|
const respHeaders = {};
|
||||||
|
upstream.headers.forEach((val, key) => {
|
||||||
|
respHeaders[key] = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
status: upstream.status,
|
||||||
|
headers: respHeaders,
|
||||||
|
body: respBody.toString("base64"),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
res.status(502).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -76,6 +76,68 @@ const syncHandlers = {
|
||||||
resources: () => "",
|
resources: () => "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRequestUrl(requestId, request) {
|
||||||
|
try {
|
||||||
|
let body = request.body;
|
||||||
|
let binary = false;
|
||||||
|
if (body instanceof ArrayBuffer) {
|
||||||
|
body = arrayBufferToBase64(body);
|
||||||
|
binary = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("/api/proxy", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: request.url,
|
||||||
|
method: request.method || "GET",
|
||||||
|
headers: request.headers || {},
|
||||||
|
contentType: request.contentType,
|
||||||
|
body,
|
||||||
|
binary,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const proxyResult = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
ipcRenderer._emit(requestId, {
|
||||||
|
error: proxyResult.error || "Proxy request failed",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Electron's e.reply(requestId, data) sends on the requestId channel
|
||||||
|
ipcRenderer._emit(requestId, {
|
||||||
|
status: proxyResult.status,
|
||||||
|
headers: proxyResult.headers,
|
||||||
|
body: base64ToArrayBuffer(proxyResult.body),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
ipcRenderer._emit(requestId, {
|
||||||
|
error: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const ipcRenderer = {
|
export const ipcRenderer = {
|
||||||
send(channel, ...args) {
|
send(channel, ...args) {
|
||||||
console.log("[shim:ipcRenderer] send:", channel, args);
|
console.log("[shim:ipcRenderer] send:", channel, args);
|
||||||
|
|
@ -89,6 +151,12 @@ export const ipcRenderer = {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (channel === "request-url") {
|
||||||
|
const [requestId, request] = args;
|
||||||
|
handleRequestUrl(requestId, request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
sendSync(channel, ...args) {
|
sendSync(channel, ...args) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@ import { pathShim } from "./path.js";
|
||||||
import { urlShim } from "./url.js";
|
import { urlShim } from "./url.js";
|
||||||
import { cryptoShim } from "./crypto/index.js";
|
import { cryptoShim } from "./crypto/index.js";
|
||||||
import { processShim } from "./process.js";
|
import { processShim } from "./process.js";
|
||||||
|
import { installRequestUrlShim } from "./request-url.js";
|
||||||
|
import * as childProcessShim from "./node/child_process.js";
|
||||||
|
import * as eventsShim from "./node/events.js";
|
||||||
|
import * as osShim from "./node/os.js";
|
||||||
|
import * as netShim from "./node/net.js";
|
||||||
|
import * as httpShim from "./node/http.js";
|
||||||
|
|
||||||
const DEBUG = true;
|
const DEBUG = true;
|
||||||
const _accessLog = new Map(); // "module.property" -> count
|
const _accessLog = new Map(); // "module.property" -> count
|
||||||
|
|
@ -53,6 +59,12 @@ const rawRegistry = {
|
||||||
path: pathShim,
|
path: pathShim,
|
||||||
url: urlShim,
|
url: urlShim,
|
||||||
crypto: cryptoShim,
|
crypto: cryptoShim,
|
||||||
|
child_process: childProcessShim,
|
||||||
|
events: eventsShim,
|
||||||
|
os: osShim,
|
||||||
|
net: netShim,
|
||||||
|
http: httpShim,
|
||||||
|
https: httpShim,
|
||||||
};
|
};
|
||||||
|
|
||||||
const shimRegistry = {};
|
const shimRegistry = {};
|
||||||
|
|
@ -182,4 +194,6 @@ window.__currentVaultId = _urlParams.get("vault") || "";
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
installRequestUrlShim();
|
||||||
|
|
||||||
console.log("[obsidian-bridge] Shim loader initialized");
|
console.log("[obsidian-bridge] Shim loader initialized");
|
||||||
|
|
|
||||||
15
shims/node/child_process.js
Normal file
15
shims/node/child_process.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
function notAvailable(name) {
|
||||||
|
return function () {
|
||||||
|
throw new Error(
|
||||||
|
`child_process.${name}() is not available in the web version.`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const exec = notAvailable("exec");
|
||||||
|
export const execSync = notAvailable("execSync");
|
||||||
|
export const spawn = notAvailable("spawn");
|
||||||
|
export const fork = notAvailable("fork");
|
||||||
|
export const execFile = notAvailable("execFile");
|
||||||
|
export const execFileSync = notAvailable("execFileSync");
|
||||||
|
export const spawnSync = notAvailable("spawnSync");
|
||||||
82
shims/node/events.js
Normal file
82
shims/node/events.js
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
export class EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
this._events = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, listener) {
|
||||||
|
if (!this._events[event]) this._events[event] = [];
|
||||||
|
this._events[event].push(listener);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
once(event, listener) {
|
||||||
|
const wrapped = (...args) => {
|
||||||
|
this.removeListener(event, wrapped);
|
||||||
|
listener.apply(this, args);
|
||||||
|
};
|
||||||
|
wrapped._original = listener;
|
||||||
|
return this.on(event, wrapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event, ...args) {
|
||||||
|
const listeners = this._events[event];
|
||||||
|
if (!listeners || listeners.length === 0) return false;
|
||||||
|
for (const fn of [...listeners]) {
|
||||||
|
fn.apply(this, args);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener(event, listener) {
|
||||||
|
const arr = this._events[event];
|
||||||
|
if (!arr) return this;
|
||||||
|
const idx = arr.findIndex((fn) => fn === listener || fn._original === listener);
|
||||||
|
if (idx >= 0) arr.splice(idx, 1);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event, listener) {
|
||||||
|
return this.removeListener(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllListeners(event) {
|
||||||
|
if (event) {
|
||||||
|
delete this._events[event];
|
||||||
|
} else {
|
||||||
|
this._events = {};
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners(event) {
|
||||||
|
return (this._events[event] || []).slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerCount(event) {
|
||||||
|
return (this._events[event] || []).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(event, listener) {
|
||||||
|
return this.on(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
prependListener(event, listener) {
|
||||||
|
if (!this._events[event]) this._events[event] = [];
|
||||||
|
this._events[event].unshift(listener);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventNames() {
|
||||||
|
return Object.keys(this._events);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMaxListeners() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxListeners() {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventEmitter;
|
||||||
47
shims/node/http.js
Normal file
47
shims/node/http.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Minimal http/https stub. Plugins needing full http.request won't work,
|
||||||
|
// but this prevents crashes for plugins that just import the module.
|
||||||
|
|
||||||
|
import { EventEmitter } from "./events.js";
|
||||||
|
|
||||||
|
export class IncomingMessage extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.headers = {};
|
||||||
|
this.statusCode = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClientRequest extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
end() {}
|
||||||
|
write() {}
|
||||||
|
abort() {}
|
||||||
|
destroy() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function request(options, callback) {
|
||||||
|
const req = new ClientRequest();
|
||||||
|
if (callback) {
|
||||||
|
req.once("response", callback);
|
||||||
|
}
|
||||||
|
// Immediately error - real HTTP requests need fetch or the proxy
|
||||||
|
setTimeout(() => {
|
||||||
|
req.emit("error", new Error("http.request is not available in the web version. Use requestUrl() instead."));
|
||||||
|
}, 0);
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get(options, callback) {
|
||||||
|
const req = request(options, callback);
|
||||||
|
req.end();
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createServer() {
|
||||||
|
throw new Error("http.createServer is not available in the web version.");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Agent = class {};
|
||||||
|
export const globalAgent = new Agent();
|
||||||
19
shims/node/net.js
Normal file
19
shims/node/net.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
function notAvailable(name) {
|
||||||
|
return function () {
|
||||||
|
throw new Error(`net.${name}() is not available in the web version.`);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createServer = notAvailable("createServer");
|
||||||
|
export const createConnection = notAvailable("createConnection");
|
||||||
|
export const connect = notAvailable("connect");
|
||||||
|
export class Socket {
|
||||||
|
constructor() {
|
||||||
|
throw new Error("net.Socket is not available in the web version.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class Server {
|
||||||
|
constructor() {
|
||||||
|
throw new Error("net.Server is not available in the web version.");
|
||||||
|
}
|
||||||
|
}
|
||||||
49
shims/node/os.js
Normal file
49
shims/node/os.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
export function platform() {
|
||||||
|
return "linux";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arch() {
|
||||||
|
return "x64";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function homedir() {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tmpdir() {
|
||||||
|
return "/tmp";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hostname() {
|
||||||
|
return "localhost";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function type() {
|
||||||
|
return "Linux";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function release() {
|
||||||
|
return "0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cpus() {
|
||||||
|
return [{ model: "browser", speed: 0 }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function totalmem() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function freemem() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function networkInterfaces() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endianness() {
|
||||||
|
return "LE";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EOL = "\n";
|
||||||
111
shims/request-url.js
Normal file
111
shims/request-url.js
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
// Override window.requestUrl to proxy external requests through our server,
|
||||||
|
// bypassing browser CORS restrictions. Obsidian sets window.requestUrl = UA
|
||||||
|
// in app.js, so we override it after app.js loads.
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxyRequestUrl(request) {
|
||||||
|
if (typeof request === "string") {
|
||||||
|
request = { url: request };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSameOrigin =
|
||||||
|
request.url.startsWith(window.location.origin) ||
|
||||||
|
request.url.startsWith("/");
|
||||||
|
|
||||||
|
// Same-origin requests don't need the proxy
|
||||||
|
if (isSameOrigin) {
|
||||||
|
const res = await fetch(request.url, {
|
||||||
|
method: request.method || "GET",
|
||||||
|
headers: request.headers || {},
|
||||||
|
body: request.body,
|
||||||
|
});
|
||||||
|
const arrayBuf = await res.arrayBuffer();
|
||||||
|
return makeResponse(
|
||||||
|
request,
|
||||||
|
res.status,
|
||||||
|
Object.fromEntries(res.headers),
|
||||||
|
arrayBuf,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-origin: route through server proxy
|
||||||
|
let body = request.body;
|
||||||
|
let binary = false;
|
||||||
|
if (body instanceof ArrayBuffer) {
|
||||||
|
body = arrayBufferToBase64(body);
|
||||||
|
binary = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("/api/proxy", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: request.url,
|
||||||
|
method: request.method || "GET",
|
||||||
|
headers: request.headers || {},
|
||||||
|
body,
|
||||||
|
binary,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ error: "Proxy request failed" }));
|
||||||
|
throw new Error(err.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyResult = await res.json();
|
||||||
|
const arrayBuf = base64ToArrayBuffer(proxyResult.body);
|
||||||
|
return makeResponse(
|
||||||
|
request,
|
||||||
|
proxyResult.status,
|
||||||
|
proxyResult.headers,
|
||||||
|
arrayBuf,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeResponse(request, status, headers, arrayBuf) {
|
||||||
|
const text = new TextDecoder().decode(arrayBuf);
|
||||||
|
let json;
|
||||||
|
try {
|
||||||
|
json = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
json = null;
|
||||||
|
}
|
||||||
|
return { status, headers, arrayBuffer: arrayBuf, text, json };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installRequestUrlShim() {
|
||||||
|
// Obsidian sets window.requestUrl in app.js. We override it once the page loads.
|
||||||
|
// Use a getter so it intercepts even if app.js sets it later.
|
||||||
|
let _original = null;
|
||||||
|
|
||||||
|
Object.defineProperty(window, "requestUrl", {
|
||||||
|
get() {
|
||||||
|
return proxyRequestUrl;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
_original = val;
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue