hdf
This commit is contained in:
+113
-1
@@ -26,6 +26,7 @@ import { createI18n } from "./inc/i18n.mjs";
|
||||
import { safeDeleteMediaFile, purgeExpiredUploads } from "./inc/lib_delete.mjs";
|
||||
|
||||
import security from "./inc/security.mjs";
|
||||
import { initPrivateItems, getPrivateItemFromPath, isPrivateItemPath, render502 } from "./inc/private_items.mjs";
|
||||
|
||||
import { createRequire } from 'module';
|
||||
const _require = createRequire(import.meta.url);
|
||||
@@ -350,6 +351,9 @@ const nginx502 = (cfg.websrv.private_society && cfg.websrv.private_society_gate
|
||||
? null
|
||||
: nginx502Fallback;
|
||||
|
||||
global._buildGatePage = (req) => (nginx502 ?? buildGatePage(req));
|
||||
global._nginx502 = nginx502Fallback;
|
||||
|
||||
// Custom gate template — resolved once at boot from config
|
||||
// Set private_society_gate: "custom" and private_society_gate_template: "your-template-name" (no .html)
|
||||
const _customGateTemplate = (cfg.websrv.private_society && cfg.websrv.private_society_gate === 'custom' && cfg.websrv.private_society_gate_template)
|
||||
@@ -600,7 +604,7 @@ process.on('uncaughtException', err => {
|
||||
app.use(async (req, res) => {
|
||||
const p = req.url?.pathname;
|
||||
if (!p) return;
|
||||
if (getProtectFiles()) return; // Protect-files gates these with auth — don't cache
|
||||
if (getProtectFiles() || isPrivateItemPath(p)) return; // Protect-files or private item — don't cache
|
||||
if (p.startsWith('/t/') || p.startsWith('/ca/') || p.startsWith('/b/')) {
|
||||
// Thumbnails, covers, and source blobs: 1-year cache.
|
||||
// These never change for a given ID (content-addressed by item ID).
|
||||
@@ -720,6 +724,48 @@ process.on('uncaughtException', err => {
|
||||
if (req.url.pathname === '/manifest.json' || req.url.pathname === '/sw.js')
|
||||
return;
|
||||
if (req.url.pathname.match(/^\/(b|c|t|ca|a|memes)\//) || req.url.pathname.startsWith('/s/emojis/')) {
|
||||
const privItem = getPrivateItemFromPath(req.url.pathname);
|
||||
if (privItem) {
|
||||
// Private item (visibility === 2):
|
||||
// Direct URLs MUST serve 502 when requested without a session (or by unauthorized users),
|
||||
// regardless of the protect_files setting.
|
||||
let isAuthorized = false;
|
||||
if (req.cookies?.session) {
|
||||
const _sessionHash = lib.sha256(req.cookies.session);
|
||||
let user = _scGet(_sessionHash);
|
||||
if (!user) {
|
||||
const urows = await db`
|
||||
select "user".id, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_expires
|
||||
from "user_sessions"
|
||||
left join "user" on "user".id = "user_sessions".user_id
|
||||
where "user_sessions".session = ${_sessionHash}
|
||||
limit 1
|
||||
`;
|
||||
if (urows.length > 0) {
|
||||
user = urows[0];
|
||||
_scSet(_sessionHash, user);
|
||||
}
|
||||
}
|
||||
if (user && !user.banned) {
|
||||
const isOwner = user.user && user.user.toLowerCase() === privItem.owner.toLowerCase();
|
||||
const isAdminOrMod = !!(user.admin || user.is_moderator);
|
||||
if (isOwner || isAdminOrMod) {
|
||||
isAuthorized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
render502(req, res);
|
||||
req.url.pathname = '/private_item_bypass';
|
||||
return;
|
||||
}
|
||||
|
||||
// Authorized: set private cache control so media is never cached publicly
|
||||
res.setHeader('Cache-Control', 'private, no-cache, no-store, must-revalidate');
|
||||
return;
|
||||
}
|
||||
|
||||
// protect_files gates raw file URLs behind a session (401 if not logged in).
|
||||
// private_society also gates file URLs — but only when protect_files is ALSO enabled.
|
||||
// If private_society is on but protect_files is off, direct file URLs are intentionally
|
||||
@@ -800,7 +846,70 @@ process.on('uncaughtException', err => {
|
||||
// but we'll use CSS to hide the content in header.html.
|
||||
}
|
||||
|
||||
// ── Admin impersonation overlay ───────────────────────────────────────────
|
||||
// If the admin has an `impersonate` cookie set, overlay the target user's
|
||||
// session data. The admin's real session passes all security checks above,
|
||||
// then we swap req.session to look like the target user for the rest of the
|
||||
// request. The admin's identity is preserved in _impersonated_by.
|
||||
const _impersonateCookie = req.cookies?.impersonate;
|
||||
if (_impersonateCookie && req.session.admin &&
|
||||
!req.url.pathname.startsWith('/api/v2/admin/stop-impersonate') &&
|
||||
!req.url.pathname.startsWith('/api/v2/admin/impersonate')) {
|
||||
try {
|
||||
const [impPayload, impSig] = _impersonateCookie.split('.');
|
||||
if (impPayload && impSig) {
|
||||
const { createHmac } = await import('crypto');
|
||||
const _impSecret = cfg.main.secret || cfg.main.url.full || 'f0ckm-impersonate-secret';
|
||||
const expectedSig = createHmac('sha256', _impSecret).update(impPayload).digest('hex');
|
||||
if (impSig === expectedSig) {
|
||||
const impData = JSON.parse(Buffer.from(impPayload, 'base64url').toString('utf8'));
|
||||
// Validate that the original session matches the current admin cookie
|
||||
if (impData.orig && impData.orig === lib.sha256(req.cookies.session)) {
|
||||
const targetRow = await db`
|
||||
SELECT "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_reason, "user".ban_expires, "user".force_password_change,
|
||||
"user_options".mode, "user_options".theme, "user_options".fullscreen, "user_options".excluded_tags, "user_options".avatar, "user_options".avatar_file,
|
||||
"user_options".show_motd, "user_options".strict_mode, "user_options".show_background, "user_options".use_new_layout, "user_options".username_color,
|
||||
"user_options".font, "user_options".disable_autoplay, "user_options".disable_swiping, "user_options".favorites_private, "user_options".hide_fav_badge,
|
||||
"user_options".default_upload_visibility, "user_options".description, "user_options".display_name, COALESCE("user_options".min_xd_score, 0) as min_xd_score,
|
||||
"user_options".ruffle_volume, "user_options".ruffle_background, "user_options".quote_emojis, "user_options".embed_youtube_in_comments,
|
||||
"user_options".hide_koepfe, "user_options".language, "user_options".use_alternative_infobox, "user_options".use_alternative_steuerung,
|
||||
"user_options".receive_system_notifications, "user_options".receive_user_notifications, "user_options".do_not_disturb,
|
||||
"user_options".comment_display_mode, "user_options".force_comment_display_mode
|
||||
FROM "user"
|
||||
LEFT JOIN "user_options" ON "user_options".user_id = "user".id
|
||||
WHERE "user".id = ${+impData.uid}
|
||||
LIMIT 1
|
||||
`;
|
||||
if (targetRow.length > 0) {
|
||||
const adminUser = req.session.user;
|
||||
const adminDisplayName = req.session.display_name || req.session.user;
|
||||
req.session = {
|
||||
...targetRow[0],
|
||||
// Preserve CSRF token from the real session for form submissions to still work
|
||||
csrf_token: user[0].csrf_token,
|
||||
sess_id: user[0].sess_id,
|
||||
// Impersonation metadata — used in navbar template
|
||||
_is_impersonating: true,
|
||||
_impersonated_by: adminUser,
|
||||
_impersonated_by_display: adminDisplayName,
|
||||
// Suppress admin/mod powers in the impersonated view
|
||||
admin: false,
|
||||
is_moderator: false,
|
||||
};
|
||||
req._original_admin_session = user[0]; // stash for potential future use
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_impErr) {
|
||||
// Silently ignore malformed impersonate cookie
|
||||
console.error('[IMPERSONATE] Cookie parse error:', _impErr.message);
|
||||
}
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// log last action (Fire-and-Forget)
|
||||
|
||||
if (!req.url.pathname.startsWith('/api/notifications')) {
|
||||
const { getLogUserIps, getHashUserIps } = await import("./inc/settings.mjs");
|
||||
const currentIp = security.getRealIP(req);
|
||||
@@ -1339,6 +1448,9 @@ process.on('uncaughtException', err => {
|
||||
console.log(`[BOOT] File protection ENABLED via config.json — direct file links require login`);
|
||||
}
|
||||
|
||||
// Load active private items into memory cache
|
||||
await initPrivateItems();
|
||||
|
||||
// Load private_messages from config.json (static — not a DB setting)
|
||||
// Default is true; set to false to fully disable private messaging
|
||||
setPrivateMessages(cfg.websrv.private_messages !== false);
|
||||
|
||||
Reference in New Issue
Block a user