Theseus: translator talks to silentmode.st/libre + libre.x / lingua.x
The translator client now ships with the right defaults for the actual deployment: silentmode.st/libre (ICANN, via the main cert and no new subdomain) is the primary peer; libre.x and lingua.x are registered on BNS with `p` records that reverse-proxy back to the same backend; the public LibreTranslate.com key-gated tier stays as the last-resort entry. Two wiring fixes make the BNS fallback actually usable from Theseus: 1. translatorPostOnce rewrites the request URL through targetUrlFor before fetching, so a peer whose host is a BNS name (libre.x) is dispatched via the in-process bns:// handler — Chromium's net stack has no way to resolve `.x` by itself. 2. serveBns's p-record branch now forwards the method, headers and body of the original request to the upstream, not just a GET. Without that, a POST /translate against libre.x arrived at the backend as a GET with no body and 400'd — now the proxy is actually a reverse- proxy, as the record type's name promises. Verified end-to-end against the live silentmode.st/libre instance from a fresh Theseus profile with a Spanish test page: both the direct silentmode.st/libre peer and the libre.x -> bns:// -> serveP -> upstream path translate the page and the revert path restores the originals.
This commit is contained in:
parent
c74d2183e3
commit
0ba0e735ed
1 changed files with 32 additions and 8 deletions
40
main.js
40
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),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue