init f0ckm

This commit is contained in:
2026-04-25 19:51:52 +02:00
commit b646107eb7
241 changed files with 70364 additions and 0 deletions

340
src/inc/lib.mjs Normal file
View File

@@ -0,0 +1,340 @@
import crypto from "crypto";
import util from "util";
import db from "./sql.mjs";
import cfg from "./config.mjs";
const scrypt = util.promisify(crypto.scrypt);
const epochs = [
["year", 31536000],
["month", 2592000],
["day", 86400],
["hour", 3600],
["minute", 60],
["second", 1]
];
const getDuration = timeAgoInSeconds => {
for (let [name, seconds] of epochs) {
const interval = ~~(timeAgoInSeconds / seconds);
if (interval >= 1) return {
interval: interval,
epoch: name
};
}
};
export default new class {
escapeHTML(str) {
if (!str) return "";
return str.toString()
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
formatSize(size, i = ~~(Math.log(size) / Math.log(1024))) {
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
};
calcSpeed(b, s) {
return (Math.round((b * 8 / s / 1e6) * 1e4) / 1e4);
};
timeAgo(date) {
const duration = getDuration(~~((new Date() - new Date(date)) / 1e3));
if (!duration) return "just now";
const { interval, epoch } = duration;
return `${interval} ${epoch}${interval === 1 ? "" : "s"} ago`;
};
md5(str) {
return crypto.createHash('md5').update(str).digest("hex");
};
sha256(str) {
return crypto.createHash('sha256').update(str).digest("hex");
};
getMode(mode) {
let tmp;
mode = Number(mode);
switch (mode) {
case 1: // nsfw
tmp = "items.id in (select item_id from tags_assign where tag_id = 2)";
break;
case 2: // untagged
tmp = "not exists (select 1 from tags_assign where item_id = items.id)";
break;
case 3: // all
tmp = "1 = 1";
break;
case 4: // nsfl
tmp = cfg.enable_nsfl ? `items.id in (select item_id from tags_assign where tag_id = ${parseInt(cfg.nsfl_tag_id, 10) || 3})` : "1 = 0";
break;
default: // sfw
tmp = "items.id in (select item_id from tags_assign where tag_id = 1)";
break;
}
return tmp;
};
createID() {
return crypto.randomBytes(16).toString("hex") + Date.now().toString(24);
};
generateToken() {
return crypto.randomBytes(32).toString("hex");
};
genLink(env) {
const link = [];
if (env.tag) link.push("tag", env.tag);
if (env.hall) link.push("h", env.hall);
if (env.user) link.push("user", env.user, env.type ?? 'uploads');
let tmp = link.length === 0 ? '/' : link.join('/');
if (!tmp.endsWith('/'))
tmp = tmp + '/';
if (!tmp.startsWith('/'))
tmp = '/' + tmp;
// Build suffix with query params
let suffix = env.strict ? '?strict=1' : '';
return {
main: tmp,
path: env.path ? env.path : '',
suffix: suffix
};
};
parseTag(tag) {
if (!tag)
return null;
return decodeURIComponent(tag);
}
slugify(str) {
if (!str) return "";
return str.toLowerCase().replace(/[^a-z0-9]/g, '');
}
// Escape ILIKE wildcard characters in user-supplied strings
escapeLike(str) {
if (!str) return str;
return str.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
}
// async funcs
async countf0cks() {
const tagged = +(await db`
select count(*) as total
from "items"
where id in (select item_id from tags_assign group by item_id) and active = true
`)[0].total;
const untagged = +(await db`
select count(*) as total
from "items"
where not exists (select 1 from tags_assign where item_id = items.id) and active = true
`)[0].total;
const sfw = +(await db`
select count(*) as total
from "items"
where id in (select item_id from tags_assign where tag_id = 1 group by item_id) and active = true
`)[0].total;
const nsfw = +(await db`
select count(*) as total
from "items"
where id in (select item_id from tags_assign where tag_id = 2 group by item_id) and active = true
`)[0].total;
const nsfl = cfg.enable_nsfl ? +(await db`
select count(*) as total
from "items"
where id in (select item_id from tags_assign where tag_id = ${cfg.nsfl_tag_id || 3} group by item_id) and active = true
`)[0].total : 0;
const deleted = +(await db`
select count(*) as total
from "items"
where active = false and is_deleted = true
`)[0].total;
const pending = +(await db`
select count(*) as total
from "items"
where active = false and is_deleted = false
`)[0].total;
const lastf0ck = +(await db`
select max(id) as id
from "items"
`)[0].id;
return {
tagged,
untagged,
total: tagged + untagged,
deleted,
pending,
untracked: lastf0ck - (tagged + untagged + deleted + pending),
sfw,
nsfw,
nsfl: cfg.enable_nsfl ? nsfl : 0,
};
};
async hash(str) {
const salt = crypto.randomBytes(16).toString("hex");
const derivedKey = await scrypt(str, salt, 64);
return "$f0ck$" + salt + ":" + derivedKey.toString("hex");
};
async verify(str, hash) {
const [salt, key] = hash.substring(6).split(":");
const keyBuffer = Buffer.from(key, "hex");
const derivedKey = await scrypt(str, salt, 64);
return crypto.timingSafeEqual(keyBuffer, derivedKey);
};
async getTags(itemid) {
const tags = await db`
select "tags".id, "tags".tag, "tags".normalized, "user".user, uo.display_name
from "tags_assign"
left join "tags" on "tags".id = "tags_assign".tag_id
left join "user" on "user".id = "tags_assign".user_id
left join user_options uo on uo.user_id = "user".id
where "tags_assign".item_id = ${+itemid}
order by (case when "tags".id = 1 then 0 when "tags".id = 2 then 1 when "tags".id = ${cfg.nsfl_tag_id || 3} then 2 else 3 end) asc, "tags".id asc
`;
for (let t = 0; t < tags.length; t++) {
tags[t].badge = this.getBadge(tags[t]);
}
return tags;
};
getBadge(tagObj) {
if (tagObj.tag.startsWith(">"))
return "badge-greentext badge-light";
else if (tagObj.normalized === "ukraine")
return "badge-ukraine badge-light";
else if (/[а-яё]/.test(tagObj.normalized) || tagObj.normalized === "russia")
return "badge-russia badge-light";
else if (tagObj.normalized === "german")
return "badge-german badge-light";
else if (tagObj.normalized === "dutch")
return "badge-dutch badge-light";
else if (tagObj.normalized === "sfw")
return "badge-success";
else if (tagObj.normalized === "nsfw")
return "badge-danger";
else if (tagObj.normalized === "nsfl")
return cfg.enable_nsfl ? "badge-nsfl" : "badge-light";
else
return "badge-light";
};
async hasTag(itemid, tagid) {
const tag = (await db`
select *
from "tags_assign"
where
item_id = ${+itemid} and
tag_id = ${+tagid}
limit 1
`).length;
return !!tag;
};
// detectNSFW: removed — contained shell injection via exec() with unescaped `dest` parameter.
// If re-implemented, use execFile() with argument arrays and a dedicated Python script.
async getDefaultAvatar() {
return (await db`
select column_default as avatar
from "information_schema"."columns"
where
TABLE_SCHEMA='public' and
TABLE_NAME='user_options' and
COLUMN_NAME = 'avatar'
`)[0].avatar;
};
// meddlware admin
async auth(req, res, next) {
if (!req.session || !req.session.admin) {
return res.reply({
code: 401,
body: "401 - Unauthorized"
});
}
if (req.session.force_password_change && req.url.pathname !== '/api/v2/settings/password' && req.url.pathname !== '/logout') {
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: "Password change required", force_password_change: true }), type: 'application/json' });
}
return next();
};
// meddlware user
async userauth(req, res, next) {
if (!req.session) {
return res.reply({
code: 401,
body: "401 - Unauthorized"
});
}
if (req.session.force_password_change && req.url.pathname !== '/api/v2/settings/password' && req.url.pathname !== '/logout' && req.url.pathname !== '/settings') {
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: "Password change required", force_password_change: true }), type: 'application/json' });
}
return next();
};
async loggedin(req, res, next) {
if (!req.session) {
return res.reply({
code: 401,
body: "401 - Unauthorized"
});
}
if (req.session.force_password_change && req.url.pathname !== '/api/v2/settings/password' && req.url.pathname !== '/logout') {
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: "Password change required", force_password_change: true }), type: 'application/json' });
}
return next();
};
async modAuth(req, res, next) {
if (!req.session || (!req.session.admin && !req.session.is_moderator)) {
return res.reply({
code: 401,
body: "401 - Unauthorized"
});
}
if (req.session.force_password_change && req.url.pathname !== '/api/v2/settings/password' && req.url.pathname !== '/logout') {
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: "Password change required", force_password_change: true }), type: 'application/json' });
}
return next();
};
async adminAuth(req, res, next) {
if (!req.session || !req.session.admin) {
return res.reply({
code: 401,
body: "401 - Unauthorized"
});
}
if (req.session.force_password_change && req.url.pathname !== '/api/v2/settings/password' && req.url.pathname !== '/logout') {
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: "Password change required", force_password_change: true }), type: 'application/json' });
}
return next();
};
getCookieOptions(expires = null, httpOnly = true) {
const isSecure = cfg.main.url.full && cfg.main.url.full.startsWith('https');
let options = "Path=/; SameSite=Lax";
if (httpOnly) options += "; HttpOnly";
if (isSecure) options += "; Secure";
if (expires) {
if (typeof expires === 'number') {
options += `; Max-Age=${expires}`;
} else {
options += `; Expires=${expires}`;
}
}
this.debug(`[COOKIE DEBUG] full=${cfg.main.url.full}, isSecure=${isSecure}, options=${options}`);
return options;
}
debug(...args) {
if (process.env.NODE_ENV !== 'production') {
console.log(...args);
}
}
logError(err, context = "Internal Error") {
const errId = crypto.randomUUID();
console.error(`[ERROR REF ${errId}] ${context}:`, err);
return `Internal Error. Reference: ${errId}`;
}
};