diff --git a/main.js b/main.js index 4843d732..94e53c15 100644 --- a/main.js +++ b/main.js @@ -529,13 +529,15 @@ const SETTINGS_DEFAULTS = { // (the libretranslate.com free tier moved behind an API key in late // 2026, several mirrors 502 at any given time), so a list beats one // endpoint: a dead mirror doesn't kill the feature, it just loses the - // round. Silent Mode's own instances come first: libre.silentmode.st - // under ICANN and libre.x on BNS (lingua.x is registered as an alias - // of libre.x — same ip record, same backend — so it's a resolution - // convenience, not another independent peer). Users can edit the list - // in Settings › General › Translate pages. + // round. Silent Mode's own instances come first: silentmode.st/libre + // under ICANN (served as a sub-path to re-use the main cert instead + // of standing up a subdomain + separate TLS) and libre.x on BNS + // (lingua.x is registered as an alias of libre.x — same ip record, + // same backend — so it's a resolution convenience, not another + // independent peer). Users can edit the list in Settings › General + // › Translate pages. translateEndpoints: [ - "https://libre.silentmode.st/translate", + "https://silentmode.st/libre/translate", "https://libre.x/translate", "https://libretranslate.com/translate", ], @@ -1983,8 +1985,25 @@ async function serveBns(request) { const base = new URL(r.p); const prefix = base.pathname === "/" ? "" : base.pathname.replace(/\/$/, ""); const target = base.origin + prefix + rawPath + url.search; + // Forward method + headers + body: a p-record is a reverse-proxy, so an + // API behind it (POST /translate, PUT, …) needs the live request as the + // caller sent it, not a bare GET. Hop-by-hop and host-rewriting headers + // are stripped — the upstream is a different origin and sees its own + // Host. + const method = request.method || "GET"; + const headers = {}; + const SKIP = new Set(["host", "connection", "content-length", "transfer-encoding", "accept-encoding"]); + try { + for (const [k, v] of request.headers.entries()) { + if (!SKIP.has(k.toLowerCase())) headers[k] = v; + } + } catch {} + const init = { method, headers, redirect: "manual" }; + if (method !== "GET" && method !== "HEAD" && request.body) { + try { init.body = Buffer.from(await request.arrayBuffer()); } catch {} + } // contentFetch so Tor covers this too (a plain fetch leaked the real IP). - const up = await contentFetch(target, { redirect: "manual" }); + const up = await contentFetch(target, init); return upstreamResponse(up, up.contentType || guessType(reqPath)); }; try { @@ -4579,11 +4598,16 @@ async function translatorPostOnce(url, texts, from, to) { format: "text", }; if (settings.translateApiKey) body.api_key = settings.translateApiKey; + // A peer whose host is a BNS name (libre.x, lingua.x, …) can't be reached + // by Chromium's net stack directly — those names aren't in ICANN DNS. Rewrite + // the request URL to bns:// so the in-process serveBns handler resolves the + // name (p-record = reverse-proxy to the upstream + path concatenation). + const target = (await targetUrlFor(url)) || url; // session.defaultSession.fetch goes through Electron's own network stack, // so Tor (session proxy) and any add-on proxy settings apply the same way // they do for a tab's fetch; Node's global fetch would bypass both. const sfetch = (session.defaultSession.fetch || fetch).bind(session.defaultSession); - const r = await sfetch(url, { + const r = await sfetch(target, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body),