Merge pull request 'master' (#34) from master into 98

Reviewed-on: #34
This commit is contained in:
Kibi Kelburton
2026-07-28 14:26:51 +00:00
8 changed files with 87 additions and 46 deletions

View File

@@ -460,4 +460,15 @@ export default new class {
console.error(`[ERROR REF ${errId}] ${context}:`, err);
return `Internal Error. Reference: ${errId}`;
}
isOnionRequest(req) {
if (!req || !req.headers) return false;
const rawHost = req.headers['x-forwarded-host'] || req.headers['host'] || req.headers['x-forwarded-server'] || '';
if (!rawHost) return false;
const hostStr = Array.isArray(rawHost) ? rawHost[0] : String(rawHost);
const firstHost = hostStr.split(',')[0].trim();
const hostNoPort = firstHost.split(':')[0].trim().toLowerCase();
return hostNoPort.endsWith('.onion');
}
};

View File

@@ -48,7 +48,7 @@ export default (router, tpl) => {
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: errorMsg }));
}
return res.reply({
body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), error: errorMsg, registration_open: getRegistrationOpen() })
body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), error: errorMsg, registration_open: getRegistrationOpen() }, req)
});
}
@@ -58,7 +58,7 @@ export default (router, tpl) => {
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg }));
}
return res.reply({
body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), error: msg, registration_open: getRegistrationOpen() })
body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), error: msg, registration_open: getRegistrationOpen() }, req)
});
};
@@ -67,7 +67,7 @@ export default (router, tpl) => {
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true, msg }));
}
return res.reply({
body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), success: msg, registration_open: getRegistrationOpen() })
body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), success: msg, registration_open: getRegistrationOpen() }, req)
});
};
@@ -89,7 +89,7 @@ export default (router, tpl) => {
}
// reCAPTCHA verification (bypassed for .onion requests as Google reCAPTCHA cannot validate .onion domains)
const isOnion = req.headers['host']?.endsWith('.onion');
const isOnion = lib.isOnionRequest(req);
if (!isOnion && cfg.recaptcha?.enabled && cfg.recaptcha?.secret_key) {
const rcToken = req.post['g-recaptcha-response'];
if (!rcToken) return renderError("Please complete the reCAPTCHA.");

View File

@@ -79,11 +79,13 @@ const nginx502Fallback = `<html>
</html>`;
// Login + Register modal injected before </body>
// This string is built at startup so it can reference cfg.recaptcha values.
// Dynamic function so reCAPTCHA can be bypassed for .onion requests
const _rcEnabled = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key);
const _rcSiteKey = (cfg.recaptcha && cfg.recaptcha.site_key) || '';
const gateLoginInjection = `
function getGateLoginInjection(req) {
const rcEnabled = _rcEnabled && !lib.isOnionRequest(req);
return `
<div id="hot-corner" style="position:fixed;bottom:0;left:0;width:20px;height:20px;z-index:9999;"></div>
<div id="gate-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:10000;align-items:center;justify-content:center;">
@@ -123,7 +125,7 @@ const gateLoginInjection = `
<input type="text" name="token" placeholder="Invite token" autocomplete="off"
style="background:white;color:black;border:1px solid #bbb;padding:7px 10px;width:100%;box-sizing:border-box;font-size:14px;font-family:inherit;" />
<input type="text" name="email_confirm_field" style="display:none !important;" tabindex="-1" autocomplete="off" />
${_rcEnabled ? '<div id="gate-recaptcha" style="margin:4px 0;transform-origin:left top;"></div>' : ''}
${rcEnabled ? '<div id="gate-recaptcha" style="margin:4px 0;transform-origin:left top;"></div>' : ''}
<button type="submit" id="gate-register-btn" style="background:#0051c3;color:white;border:none;padding:9px;font-weight:600;font-size:14px;cursor:pointer;font-family:inherit;"
onmouseover="this.style.background='#003681'" onmouseout="if(!this.disabled)this.style.background='#0051c3'">Create account</button>
<p style="text-align:center;font-size:0.85em;margin:6px 0 0;color:#555;">
@@ -264,8 +266,9 @@ const gateLoginInjection = `
if (_sb.length > 12) _sb = _sb.slice(-12);
});
</script>
${_rcEnabled ? '<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaGateReady&render=explicit" async defer><\/script>' : ''}
${rcEnabled ? '<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaGateReady&render=explicit" async defer><\/script>' : ''}
`;
}
// Text injected into the "What can I do?" section
@@ -336,7 +339,7 @@ function buildGatePage(req) {
'<title>502: Bad Gateway</title>',
`<title>${reqHost} | 502: Bad gateway</title>`
);
html = html.replace('</body>', gateLoginInjection + '\n</body>');
html = html.replace('</body>', getGateLoginInjection(req) + '\n</body>');
return html;
}
@@ -493,8 +496,15 @@ process.on('uncaughtException', err => {
// Encourage connection reuse — helps external tools like ShareX avoid repeated TCP/TLS handshakes
res.setHeader('Connection', 'keep-alive');
// Block incoming requests to .onion if Tor Hidden Service is explicitly disabled in config
if (cfg.websrv?.enable_tor_hs === false && lib.isOnionRequest(req)) {
res.writeHead(503).end();
req.url.pathname = '/tor_hs_disabled_bypass';
return;
}
// Tor Onion-Location header: advertises .onion service counterpart to Tor Browser users when enabled
if (cfg.websrv?.enable_tor_hs !== false && cfg.main?.onion && !req.headers['host']?.endsWith('.onion')) {
if (cfg.websrv?.enable_tor_hs !== false && cfg.main?.onion && !lib.isOnionRequest(req)) {
const p = req.url?.pathname || '/';
const s = req.url?.search || '';
const onionHost = cfg.main.onion.replace(/\/+$/, '');
@@ -1452,26 +1462,39 @@ process.on('uncaughtException', err => {
}
}
// Resolve per-request recaptcha preference (disabled for .onion requests or when passed false)
const effectiveReq = req || (data && data.req);
const defaultRecaptcha = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key);
let perRequestRecaptcha = defaultRecaptcha;
if (effectiveReq && lib.isOnionRequest(effectiveReq)) {
perRequestRecaptcha = false;
} else if (data && typeof data.recaptcha_enabled === 'boolean') {
perRequestRecaptcha = data.recaptcha_enabled;
}
// Build data: globals first, then caller-supplied data, then per-request i18n last
// so t/lang always reflect the user's language, not the site default.
// ALSO mutate globals.t and globals.lang: flummpress spreads this.#globals LAST
// ALSO mutate globals.t, globals.lang, globals.recaptcha_enabled: flummpress spreads this.#globals LAST
// inside render(), so globals must carry the per-request values too.
globals.t = perRequestT;
globals.lang = perRequestLang;
globals.recaptcha_enabled = perRequestRecaptcha;
// Resolve per-request infobox preference
// Guests always get false — the alternative infobox is a logged-in user preference only
const useAltInfobox = (req && req.session && typeof req.session.use_alternative_infobox === 'boolean')
? req.session.use_alternative_infobox
: (req && !req.session
const activeReq = req || effectiveReq;
const useAltInfobox = (activeReq && activeReq.session && typeof activeReq.session.use_alternative_infobox === 'boolean')
? activeReq.session.use_alternative_infobox
: (activeReq && !activeReq.session
? false
: (data && typeof data.user_alternative_infobox === 'boolean'
? data.user_alternative_infobox
: (cfg.websrv.user_alternative_infobox !== false)));
const useAltSteuerung = (req && req.session && typeof req.session.use_alternative_steuerung === 'boolean')
? req.session.use_alternative_steuerung
: (req && !req.session
const useAltSteuerung = (activeReq && activeReq.session && typeof activeReq.session.use_alternative_steuerung === 'boolean')
? activeReq.session.use_alternative_steuerung
: (activeReq && !activeReq.session
? false
: (data && typeof data.user_alternative_steuerung === 'boolean'
? data.user_alternative_steuerung
@@ -1480,11 +1503,12 @@ process.on('uncaughtException', err => {
data = Object.assign({}, globals, data || {}, {
t: perRequestT,
lang: perRequestLang,
recaptcha_enabled: perRequestRecaptcha,
user_alternative_infobox: useAltInfobox,
user_alternative_steuerung: useAltSteuerung,
user_banner_enabled: cfg.websrv.user_banner_enabled !== false,
comment_display_mode: (req && req.session && typeof req.session.comment_display_mode === 'number')
? req.session.comment_display_mode
comment_display_mode: (activeReq && activeReq.session && typeof activeReq.session.comment_display_mode === 'number')
? activeReq.session.comment_display_mode
: (data && typeof data.comment_display_mode === 'number'
? data.comment_display_mode
: (cfg.websrv.default_comment_display_mode || 0))
@@ -1496,20 +1520,19 @@ process.on('uncaughtException', err => {
data.custom_brand_image = brand[Math.floor(Math.random() * brand.length)];
}
if (req) {
if (req.headers['host']?.endsWith('.onion')) {
data.recaptcha_enabled = false;
}
if (req.mode !== undefined) data.mode = req.mode;
data.theme = req.theme || req.cookies?.theme || cfg.websrv.theme || 'f0ck';
if (!data.url) data.url = req.url;
data.user_strict_bool = (req.session && req.session.strict_mode) ? true : false;
data.user_logged_in_bool = !!req.session;
data.csrf_token = req.session?.csrf_token || '';
data.max_file_size = lib.formatSize(cfg.main.maxfilesize * (req.session?.admin ? cfg.main.adminmultiplier : 1));
data.max_file_size_bytes = Math.floor(cfg.main.maxfilesize * (req.session?.admin ? cfg.main.adminmultiplier : 1));
if (activeReq) {
data.recaptcha_enabled = perRequestRecaptcha;
if (activeReq.mode !== undefined) data.mode = activeReq.mode;
data.theme = activeReq.theme || activeReq.cookies?.theme || cfg.websrv.theme || 'f0ck';
if (!data.url) data.url = activeReq.url;
data.user_strict_bool = (activeReq.session && activeReq.session.strict_mode) ? true : false;
data.user_logged_in_bool = !!activeReq.session;
data.csrf_token = activeReq.session?.csrf_token || '';
data.max_file_size = lib.formatSize(cfg.main.maxfilesize * (activeReq.session?.admin ? cfg.main.adminmultiplier : 1));
data.max_file_size_bytes = Math.floor(cfg.main.maxfilesize * (activeReq.session?.admin ? cfg.main.adminmultiplier : 1));
data.web_url_upload = data.web_url_upload !== undefined ? data.web_url_upload : !!cfg.websrv.web_url_upload;
} else {
data.recaptcha_enabled = perRequestRecaptcha;
data.theme = data.theme || cfg.websrv.theme || 'f0ck';
data.user_strict_bool = false;
data.user_logged_in_bool = false;