mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
shim plugin related APIs, proxy web requests.
This commit is contained in:
@@ -30,9 +30,11 @@ app.use((req, res, next) => {
|
||||
|
||||
const fsRoutes = require("./routes/fs");
|
||||
const vaultRoutes = require("./routes/vault");
|
||||
const proxyRoutes = require("./routes/proxy");
|
||||
|
||||
app.use("/api/fs", fsRoutes);
|
||||
app.use("/api/vault", vaultRoutes);
|
||||
app.use("/api/proxy", proxyRoutes);
|
||||
|
||||
// Serve vault files for resource URLs (images, attachments, etc.)
|
||||
// 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: () => "",
|
||||
};
|
||||
|
||||
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 = {
|
||||
send(channel, ...args) {
|
||||
console.log("[shim:ipcRenderer] send:", channel, args);
|
||||
@@ -89,6 +151,12 @@ export const ipcRenderer = {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel === "request-url") {
|
||||
const [requestId, request] = args;
|
||||
handleRequestUrl(requestId, request);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
sendSync(channel, ...args) {
|
||||
|
||||
@@ -5,6 +5,12 @@ import { pathShim } from "./path.js";
|
||||
import { urlShim } from "./url.js";
|
||||
import { cryptoShim } from "./crypto/index.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 _accessLog = new Map(); // "module.property" -> count
|
||||
@@ -53,6 +59,12 @@ const rawRegistry = {
|
||||
path: pathShim,
|
||||
url: urlShim,
|
||||
crypto: cryptoShim,
|
||||
child_process: childProcessShim,
|
||||
events: eventsShim,
|
||||
os: osShim,
|
||||
net: netShim,
|
||||
http: httpShim,
|
||||
https: httpShim,
|
||||
};
|
||||
|
||||
const shimRegistry = {};
|
||||
@@ -182,4 +194,6 @@ window.__currentVaultId = _urlParams.get("vault") || "";
|
||||
}
|
||||
})();
|
||||
|
||||
installRequestUrlShim();
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user