diff --git a/configs/main.js.example b/configs/main.js.example index 960f2129..a9621b30 100644 --- a/configs/main.js.example +++ b/configs/main.js.example @@ -71,11 +71,12 @@ module.exports = { cacheTime: 3600 //in seconds, idk whats a good value }, - //disable file posting over .onion globally, overrides any board setting. - disableOnionFilePosting: false, + //disable file posting over anonymizers globally, overrides any board setting. + disableAnonymizerFilePosting: false, - //count .onion posters as "users" in stats. if set to false, all .onion is counted as a single user. doesnt affect pph stat. - statsCountOnionUsers: true, + /* count "IP"s (bypass ids) for anonymizers as "users" in stats. if set to false, anonymous users are counted as a single user. doesnt affect pph stat. + you can use this setting to prevent spam over anonymizers from inflating user stats */ + statsCountAnonymizers: true, floodTimers: { //basic delays to stop flooding, in ms. 0 to disable sameContentSameIp: 120000, //same message or any file from same ip @@ -86,7 +87,7 @@ module.exports = { //block bypasses blockBypass: { enabled: false, - forceOnion: true, //option to override blockbypass setting for .onion users + forceAnonymizers: true, //option to override blockbypass setting for .onion users expireAfterUses: 40, //however many (attempted) posts per block bypass captcha expireAfterTime: 86400000, //expiry in ms regardless if the limit was reached, default 1 day bypassDnsbl: false, @@ -122,9 +123,11 @@ module.exports = { //max wait time in ms for obtaining locks for saving files lockWait: 3000, - //optionally prune oldest modlog entries (prunes when newer modlog entries are generated i.e. dead boards wont have older logs pruned) - pruneModlogs: true, - pruneAfterDays: 30, + //optionally prune modlog entries older than x days, false to disable (prunes when newer modlog entries are generated i.e. dead boards wont have older logs pruned) + pruneModlogs: 30, + + //option to prune ips on posts older than x days, false to disable + pruneIps: false, //enable the webring (also copy configs/webring.json.example -> configs/webring.json and edit) enableWebring: false, @@ -374,7 +377,7 @@ module.exports = { defaultName: 'Anon', customCSS: null, blockedCountries: [], //2 char ISO country codes to block - disableOnionFilePosting: false, + disableAnonymizerFilePosting: false, filters: [], //words/phrases to block filterMode: 0, //0=nothing, 1=prevent post, 2=auto ban filterBanDuration: 0, //duration (in ms) to ban if filter mode=2 diff --git a/configs/nginx/nginx.example b/configs/nginx/nginx.example index 21c905df..011acefb 100644 --- a/configs/nginx/nginx.example +++ b/configs/nginx/nginx.example @@ -9,7 +9,7 @@ server { server_tokens off; add_header Cache-Control "public"; - add_header Content-Security-Policy "default-src 'self'; img-src 'self' blob:; object-src 'self' blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' https://www.youtube.com/embed/ https://www.bitchute.com/embed/; connect-src 'self' wss://doimain.com"; + add_header Content-Security-Policy "default-src 'self'; img-src 'self' blob:; object-src 'self' blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' https://www.youtube.com/embed/ https://www.bitchute.com/embed/; connect-src 'self' wss://domain.com/"; add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin" always; add_header X-Frame-Options "sameorigin" always; add_header X-Content-Type-Options "nosniff" always; diff --git a/configs/nginx/nginx_no_https.example b/configs/nginx/nginx_no_https.example index 7cacc832..8956a9a3 100644 --- a/configs/nginx/nginx_no_https.example +++ b/configs/nginx/nginx_no_https.example @@ -305,7 +305,7 @@ server { # location ~* \.json$ { # expires 0; # root /path/to/jschan/static/json; -# try_files $uri =404; +# try_files $uri @backend; # #json doesnt hit backend if it doesnt exist yet. # } # diff --git a/controllers/forms/makepost.js b/controllers/forms/makepost.js index 40ebcd1a..7c8917be 100644 --- a/controllers/forms/makepost.js +++ b/controllers/forms/makepost.js @@ -4,7 +4,7 @@ const makePost = require(__dirname+'/../../models/forms/makepost.js') , deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js') , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') , pruneFiles = require(__dirname+'/../../schedules/prune.js') - , { pruneImmediately, globalLimits, disableOnionFilePosting } = require(__dirname+'/../../configs/main.js') + , { pruneImmediately, globalLimits, disableAnonymizerFilePosting } = require(__dirname+'/../../configs/main.js') , { Files } = require(__dirname+'/../../db/'); module.exports = async (req, res, next) => { @@ -15,10 +15,10 @@ module.exports = async (req, res, next) => { if ((!req.body.message || res.locals.messageLength === 0) && res.locals.numFiles === 0) { errors.push('Posts must include a message or file'); } - if (res.locals.tor - && (disableOnionFilePosting || res.locals.board.settings.disableOnionFilePosting) + if (res.locals.anonymizer + && (disableAnonymizerFilePosting || res.locals.board.settings.disableAnonymizerFilePosting) && res.locals.numFiles > 0) { - errors.push(`Posting files through the .onion address has been disabled ${disableOnionFilePosting ? 'globally' : 'on this board'}`); + errors.push(`Posting files through anonymizers has been disabled ${disableAnonymizerFilePosting ? 'globally' : 'on this board'}`); } if (res.locals.numFiles > res.locals.board.settings.maxFiles) { errors.push(`Too many files. Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is ${res.locals.board.settings.maxFiles}`); diff --git a/db/posts.js b/db/posts.js index 9bb06581..1597a40e 100644 --- a/db/posts.js +++ b/db/posts.js @@ -4,7 +4,7 @@ const Mongo = require(__dirname+'/db.js') , Boards = require(__dirname+'/boards.js') , Stats = require(__dirname+'/stats.js') , db = Mongo.db.collection('posts') - , { quoteLimit, previewReplies, stickyPreviewReplies, statsCountOnionUsers, + , { quoteLimit, previewReplies, stickyPreviewReplies, statsCountAnonymizers, ipHashPermLevel, early404Replies, early404Fraction } = require(__dirname+'/../configs/main.js'); module.exports = { @@ -474,7 +474,7 @@ module.exports = { //insert the post itself const postMongoId = await db.insertOne(data).then(result => result.insertedId); //_id of post - const statsIp = (statsCountOnionUsers === false && res.locals.tor === true) ? null : data.ip.single; + const statsIp = (statsCountAnonymizers === false && res.locals.anonymizer === true) ? null : data.ip.single; await Stats.updateOne(board._id, statsIp, data.thread == null); //add backlinks to the posts this post quotes diff --git a/gulp/res/css/style.css b/gulp/res/css/style.css index e088b163..c882a343 100644 --- a/gulp/res/css/style.css +++ b/gulp/res/css/style.css @@ -501,7 +501,7 @@ th { } .fw td, .fw th { - width: 15%; /*Fixes log tables when large actions are taken*/ + width: 8%; /*Fixes log tables when large actions are taken*/ } td, th { diff --git a/gulp/res/img/attachment.png b/gulp/res/img/attachment.png index 53a01b6a..3c0785da 100644 Binary files a/gulp/res/img/attachment.png and b/gulp/res/img/attachment.png differ diff --git a/gulp/res/img/audio.png b/gulp/res/img/audio.png index c13185e4..4774cf29 100644 Binary files a/gulp/res/img/audio.png and b/gulp/res/img/audio.png differ diff --git a/gulp/res/js/hideimages.js b/gulp/res/js/hideimages.js index 558123bb..8d06daa3 100644 --- a/gulp/res/js/hideimages.js +++ b/gulp/res/js/hideimages.js @@ -4,7 +4,7 @@ let imageSourcesList; const toggleAllHidden = (state) => imageSources.forEach(i => toggleSource(i, state)); const toggleSource = (source, state) => { - const images = document.querySelectorAll(`img.file-thumb[src="${source}"]`); + const images = document.querySelectorAll(`img.file-thumb[src="${source}"], img.catalog-thumb[src="${source}"]`); images.forEach(i => i.classList[state?'add':'remove']('vh')); } @@ -26,13 +26,22 @@ document.querySelectorAll('.hide-image').forEach(el => { const handleHiddenImages = (e) => { //hide any images from this post that should already be hidden const hasHiddenImages = e.detail.json.files.forEach(f => { - if (imageSources.has(f.filename)) { - toggleSource(f.filename, true); + let hideFilename = '/file/'; + if (f.hasThumb) { + hideFilename += `thumb-${f.hash}${f.thumbextension}` + } else { + hideFilename += f.filename; + } + if (imageSources.has(hideFilename)) { + toggleSource(hideFilename, true); } }); //add the hide toggle link and event listener if (!e.detail.hover) { - e.detail.post.querySelector('.hide-image').addEventListener('click', toggleHandler, false); + const hideButtons = e.detail.post.querySelectorAll('.hide-image'); + for (let i = 0; i < hideButtons.length; i++) { + hideButtons[i].addEventListener('click', toggleHandler, false); + } } } diff --git a/gulp/res/js/hover.js b/gulp/res/js/hover.js index 65eb042f..d636bc84 100644 --- a/gulp/res/js/hover.js +++ b/gulp/res/js/hover.js @@ -59,6 +59,7 @@ window.addEventListener('DOMContentLoaded', (event) => { clone.appendChild(post.cloneNode(true)); document.body.appendChild(clone); setFloatPos(quote, clone, xpos, ypos); + return clone; }; const toggleHighlightPost = async function (e) { @@ -85,13 +86,12 @@ window.addEventListener('DOMContentLoaded', (event) => { lastHover = loading; const hash = this.hash.substring(1); const anchor = document.getElementById(hash); - let hoveredPost; + let hoveredPost, postJson; if (anchor && jsonPath.split('/')[1] === anchor.nextSibling.dataset.board) { hoveredPost = anchor.nextSibling; } else { let hovercache = localStorage.getItem(`hovercache-${jsonPath}`); - let postJson; if (hovercache) { hovercache = JSON.parse(hovercache); if (hovercache.postId == hash) { @@ -135,6 +135,13 @@ window.addEventListener('DOMContentLoaded', (event) => { const wrap = document.createElement('div'); wrap.innerHTML = postHtml; hoveredPost = wrap.firstChild.nextSibling; + } + if (hovering && !isVisible(hoveredPost)) { + hoveredPost = floatPost(this, hoveredPost, e.clientX, e.clientY); + } else { + hovering ? hoveredPost.classList.add('hoverhighlighted') : hoveredPost.classList.remove('hoverhighlighted'); + } + if (postJson) { //need this event so handlers like post hiding still apply to hover introduced posts const newPostEvent = new CustomEvent('addPost', { detail: { @@ -147,11 +154,6 @@ window.addEventListener('DOMContentLoaded', (event) => { window.dispatchEvent(newPostEvent); } toggleDottedUnderlines(hoveredPost, thisId); - if (hovering && !isVisible(hoveredPost)) { - floatPost(this, hoveredPost, e.clientX, e.clientY); - } else { - hovering ? hoveredPost.classList.add('hoverhighlighted') : hoveredPost.classList.remove('hoverhighlighted'); - } } for (let i = 0; i < quotes.length; i++) { diff --git a/gulpfile.js b/gulpfile.js index 333a28a5..6be73c2e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,7 +16,7 @@ const gulp = require('gulp') , pug = require('pug') , gulppug = require('gulp-pug') , { migrateVersion } = require(__dirname+'/package.json') - , { createHash, randomBytes } = require('crypto') + , { randomBytes } = require('crypto') , paths = { styles: { src: 'gulp/res/css/', @@ -59,54 +59,11 @@ async function password() { } async function ips() { - /* - prune IPs from old posts (actually, rehash them with a temporary random salt to maintain - post history and prevent *-by-ip action unintentionally deleting many posts) - NOTE: ips may still remain in the following collections: - - bans, because bans need the IP to function - - modlog actioner ips, modlogs are already auto-pruned - - ratelimits, these only last 1 minute - - stats, these last max of 24 hours - */ const Mongo = require(__dirname+'/db/db.js') await Mongo.connect(); const Redis = require(__dirname+'/redis.js') - const { Posts } = require(__dirname+'/db/'); - const beforeDate = new Date(); - beforeDate.setDate(beforeDate.getDate() - 7); //7 days in the past, static number for now until i implement yargs or similar - const beforeDateMongoId = Mongo.ObjectId.createFromTime(Math.floor(beforeDate.getTime()/1000)); - const tempIpHashSecret = randomBytes(20).toString('base64'); - const bulkWrites = []; - await Posts.db.find({ - _id: { - $lte: beforeDateMongoId, - }, - 'ip.pruned': { - $ne: true - } - }).forEach(post => { - const randomIP = createHash('sha256').update(tempIpHashSecret + post.ip.single).digest('base64'); - bulkWrites.push({ - updateOne: { - filter: { - _id: post._id, - }, - update: { - $set: { - 'ip.pruned': true, - 'ip.raw': randomIP, - 'ip.single': randomIP, - 'ip.qrange': randomIP, - 'ip.hrange': randomIP, - } - } - } - }); - }); - console.log(`Randomising ip on ${bulkWrites.length} posts`); - if (bulkWrites.length.length > 0) { - await Posts.db.bulkWrite(bulkWrites); - } + const ipSchedule = require(__dirname+'/schedules/ips.js'); + await ipSchedule(); Redis.redisClient.quit(); return Mongo.client.close(); } @@ -160,7 +117,7 @@ async function wipe() { await Posts.db.dropIndexes() await Modlogs.db.dropIndexes() await CustomPages.db.dropIndexes() - await CustomPages.db.createIndex({ 'board': 1, 'url': 1 }, { unique: true }) + await CustomPages.db.createIndex({ 'board': 1, 'page': 1 }, { unique: true }) await Modlogs.db.createIndex({ 'board': 1 }) await Files.db.createIndex({ 'count': 1 }) await Bans.db.createIndex({ 'ip.single': 1 , 'board': 1 }) diff --git a/helpers/captcha/verify.js b/helpers/captcha/verify.js index 54d9b982..76168d48 100644 --- a/helpers/captcha/verify.js +++ b/helpers/captcha/verify.js @@ -12,7 +12,7 @@ const { Ratelimits } = require(__dirname+'/../../db/') module.exports = async (req, res, next) => { //already solved in pre stage for getting bypassID for "ip" - if (res.locals.tor && res.locals.solvedCaptcha) { + if (res.locals.anonymizer && res.locals.solvedCaptcha) { return next(); } @@ -49,7 +49,7 @@ module.exports = async (req, res, next) => { //for builtin captchas, clear captchaid cookie, delete file and reset quota res.clearCookie('captchaid'); await Promise.all([ - !res.locals.tor && Ratelimits.resetQuota(res.locals.ip.single, 'captcha'), + !res.locals.anonymizer && Ratelimits.resetQuota(res.locals.ip.single, 'captcha'), remove(`${uploadDirectory}/captcha/${captchaId}.jpg`) ]); } diff --git a/helpers/checks/blockbypass.js b/helpers/checks/blockbypass.js index 010920ca..b6f9be3f 100644 --- a/helpers/checks/blockbypass.js +++ b/helpers/checks/blockbypass.js @@ -11,8 +11,8 @@ module.exports = async (req, res, next) => { if (res.locals.preFetchedBypassId //if they already have a bypass || (!blockBypass.enabled //or if block bypass isnt enabled - && (!blockBypass.forceOnion //and we dont force it for .onion - || !res.locals.tor))) { //or they arent a .onion + && (!blockBypass.forceAnonymizers //and we dont force it for anonymizer + || !res.locals.anonymizer))) { //or they arent on an anonymizer return next(); } @@ -45,7 +45,7 @@ module.exports = async (req, res, next) => { if (bypass //if they have a valid bypass && (bypass.uses < blockBypass.expireAfterUses //and its not overused - || (res.locals.tor && !blockBypass.forceOnion))) { //OR its disabled for .onion, which ignores usage check + || (res.locals.anonymizer && !blockBypass.forceAnonymizers))) { //OR its forced for anonymizers return next(); } diff --git a/helpers/checks/dnsbl.js b/helpers/checks/dnsbl.js index 75ff0baf..0bcb83bb 100644 --- a/helpers/checks/dnsbl.js +++ b/helpers/checks/dnsbl.js @@ -9,7 +9,7 @@ const cache = require(__dirname+'/../../redis.js') module.exports = async (req, res, next) => { if (dnsbl.enabled && dnsbl.blacklists.length > 0 //if dnsbl enabled and has more than 0 blacklists - && !res.locals.tor //tor cant be dnsbl'd + && !res.locals.anonymizer //anonymizers cant be dnsbl'd && (!res.locals.blockBypass || !blockBypass.bypassDnsbl)) { //and there is no valid block bypass, or they do not bypass dnsbl const ip = req.headers[ipHeader] || req.connection.remoteAddress; let isBlacklisted = await cache.get(`blacklisted:${ip}`); diff --git a/helpers/checks/torprebypass.js b/helpers/checks/torprebypass.js index 9924493e..d17bbe50 100644 --- a/helpers/checks/torprebypass.js +++ b/helpers/checks/torprebypass.js @@ -12,14 +12,14 @@ const { Bypass } = require(__dirname+'/../../db/') module.exports = async (req, res, next) => { - //early bypass is only needed for tor users - if (!res.locals.tor) { + //early bypass is only needed for anonymizer users + if (!res.locals.anonymizer) { return next(); } let bypassId = req.signedCookies.bypassid; - if (blockBypass.enabled || blockBypass.forceOnion) { + if (blockBypass.enabled || blockBypass.forceAnonymizers) { const input = req.body.captcha; const captchaId = req.cookies.captchaid; if (input && !bypassId) { @@ -46,7 +46,7 @@ module.exports = async (req, res, next) => { if (res.locals.solvedCaptcha //if they just solved a captcha || (!blockBypass.enabled //OR blockbypass isnt enabled - && !blockBypass.forceOnion //AND its not forced for .onion + && !blockBypass.forceAnonymizers //AND its not forced for anonymizers && !bypassId)) { //AND they dont already have one, //then give the user a bypass id const newBypass = await Bypass.getBypass(); diff --git a/helpers/countries.js b/helpers/countries.js index 2a1a7469..645d8cd5 100644 --- a/helpers/countries.js +++ b/helpers/countries.js @@ -2,8 +2,11 @@ const countries = require('i18n-iso-countries') , countryNamesMap = countries.getNames('en') - , countryCodes = ['EU', 'XX', 'T1', 'TOR', 'LOKI'] - .concat(Object.keys(countryNamesMap)); + , extraCountryCodes = ['EU', 'XX', 'T1'] + , anonymizerCountryCodes = ['TOR', 'LOKI'] + , anonymizerCountryCodesSet = new Set(anonymizerCountryCodes) + , countryCodes = Object.keys(countryNamesMap) + .concat(extraCountryCodes, anonymizerCountryCodes); //this dumb library conveniently includes 2 names for some countries... Object.entries(countryNamesMap) @@ -19,4 +22,7 @@ countryNamesMap['LOKI'] = 'Lokinet SNApp'; module.exports = { countryNamesMap, countryCodes, + isAnonymizer: (code) => { + return anonymizerCountryCodesSet.has(code); + }, } diff --git a/helpers/filemiddlewares.js b/helpers/filemiddlewares.js index 7e976a60..e9ed23bc 100644 --- a/helpers/filemiddlewares.js +++ b/helpers/filemiddlewares.js @@ -63,14 +63,14 @@ module.exports = { }), handlePostFilesEarlyTor: (req, res, next) => { - if (res.locals.tor) { + if (res.locals.anonymizer) { return postFiles(req, res, next); } return next(); }, handlePostFiles: (req, res, next) => { - if (res.locals.tor) { + if (res.locals.anonymizer) { return next(); } return postFiles(req, res, next); diff --git a/helpers/geoip.js b/helpers/geoip.js index 3b5c3f43..53e5488b 100644 --- a/helpers/geoip.js +++ b/helpers/geoip.js @@ -1,11 +1,11 @@ 'use strict' -const { countryNamesMap } = require(__dirname+'/countries.js') - , { countryCodeHeader } = require(__dirname+'/../configs/main.js'); +const { countryNamesMap, isAnonymizer } = require(__dirname+'/countries.js') + , { countryCodeHeader } = require(__dirname+'/../configs/main.js') module.exports = (req, res, next) => { const code = req.headers[countryCodeHeader] || 'XX'; - res.locals.tor = code === 'TOR' || code === 'LOKI'; //NOTE: for ticket #312 change this to use a single x-anonymizer header + res.locals.anonymizer = isAnonymizer(code); res.locals.country = { code, name: countryNamesMap[code], diff --git a/helpers/processip.js b/helpers/processip.js index a3d9bcdd..f05d4d4b 100644 --- a/helpers/processip.js +++ b/helpers/processip.js @@ -9,7 +9,7 @@ const { ipHeader, ipHashPermLevel } = require(__dirname+'/../configs/main.js') module.exports = (req, res, next) => { //tor user ip uses bypass id, if they dont have one send to blockbypass - if (res.locals.tor) { + if (res.locals.anonymizer) { const pseudoIp = res.locals.preFetchedBypassId || req.signedCookies.bypassid; res.locals.ip = { raw: pseudoIp, diff --git a/helpers/tasks.js b/helpers/tasks.js index 02517330..aaa3b5ee 100644 --- a/helpers/tasks.js +++ b/helpers/tasks.js @@ -4,7 +4,7 @@ const Mongo = require(__dirname+'/../db/db.js') , timeUtils = require(__dirname+'/timeutils.js') , uploadDirectory = require(__dirname+'/files/uploadDirectory.js') , { remove } = require('fs-extra') - , { debugLogs, pruneModlogs, pruneAfterDays, enableWebring, maxRecentNews } = require(__dirname+'/../configs/main.js') + , { debugLogs, pruneModlogs, enableWebring, maxRecentNews } = require(__dirname+'/../configs/main.js') , { CustomPages, Stats, Posts, Files, Boards, News, Modlogs } = require(__dirname+'/../db/') , cache = require(__dirname+'/../redis.js') , render = require(__dirname+'/render.js') @@ -188,9 +188,9 @@ module.exports = { const label = `/${options.board._id}/logs.html`; const start = process.hrtime(); let dates = await Modlogs.getDates(options.board); - if (pruneModlogs === true) { + if (pruneModlogs) { const pruneLogs = []; - const pruneAfter = new Date(Date.now()-timeUtils.DAY*pruneAfterDays); + const pruneAfter = new Date(Date.now()-timeUtils.DAY*pruneModlogs); dates = dates.filter(date => { const { year, month, day } = date.date; if (new Date(year, month-1, day) > pruneAfter) { //-1 for 0-index months diff --git a/migrations/index.js b/migrations/index.js index a26d0b26..48c8b88d 100644 --- a/migrations/index.js +++ b/migrations/index.js @@ -18,4 +18,6 @@ module.exports = { '0.0.15': require(__dirname+'/migration-0.0.15.js'), //messages r9k option '0.0.16': require(__dirname+'/migration-0.0.16.js'), //separate tph/pph triggers '0.0.17': require(__dirname+'/migration-0.0.17.js'), //add custompages collection + '0.0.18': require(__dirname+'/migration-0.0.18.js'), //disable onion file posting to disable anonymizer file posting + '0.0.19': require(__dirname+'/migration-0.0.19.js'), //fix incorrect index causing duplicate key error } diff --git a/migrations/migration-0.0.17.js b/migrations/migration-0.0.17.js index 300e061c..d7051f7f 100644 --- a/migrations/migration-0.0.17.js +++ b/migrations/migration-0.0.17.js @@ -3,5 +3,5 @@ module.exports = async(db, redis) => { console.log('add collection for board custompages'); await db.createCollection('custompages'); - await db.collection('custompages').createIndex({ 'board': 1, 'url': 1 }, { unique: true }); + await db.collection('custompages').createIndex({ 'board': 1, 'page': 1 }, { unique: true }); }; diff --git a/migrations/migration-0.0.18.js b/migrations/migration-0.0.18.js new file mode 100644 index 00000000..871af08c --- /dev/null +++ b/migrations/migration-0.0.18.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = async(db, redis) => { + console.log('Renaming disable onion file posting to disable anonymizer file posting'); + await db.collection('boards').updateMany({}, { + '$rename': { + 'settings.disableOnionFilePosting' : 'settings.disableAnonymizerFilePosting', + } + }); + console.log('Cleared boards cache'); + await redis.deletePattern('board:*'); +}; diff --git a/migrations/migration-0.0.19.js b/migrations/migration-0.0.19.js new file mode 100644 index 00000000..1352768d --- /dev/null +++ b/migrations/migration-0.0.19.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = async(db, redis) => { + console.log('fixing index for custompages'); + try { + await db.collection('custompages').dropIndex('board_1_url_1'); + } catch (e) { + // didnt have the bad index + } + await db.collection('custompages').createIndex({ 'board': 1, 'page': 1 }, { unique: true }); +}; diff --git a/models/forms/changeboardsettings.js b/models/forms/changeboardsettings.js index a7e57b74..dae3f116 100644 --- a/models/forms/changeboardsettings.js +++ b/models/forms/changeboardsettings.js @@ -116,7 +116,7 @@ module.exports = async (req, res, next) => { 'tags': arraySetting(req.body.tags, oldSettings.tags, 10), 'filters': arraySetting(req.body.filters, oldSettings.filters, 50), 'blockedCountries': req.body.countries || [], - 'disableOnionFilePosting': booleanSetting(req.body.disable_onion_file_posting), + 'disableAnonymizerFilePosting': booleanSetting(req.body.disable_anonymizer_file_posting), 'strictFiltering': booleanSetting(req.body.strict_filtering), 'customCss': globalLimits.customCss.enabled ? (req.body.custom_css !== null ? req.body.custom_css : oldSettings.customCss) : null, 'announcement': { diff --git a/models/forms/makepost.js b/models/forms/makepost.js index 1204fadf..e0d66b9b 100644 --- a/models/forms/makepost.js +++ b/models/forms/makepost.js @@ -455,7 +455,7 @@ ${res.locals.numFiles > 0 ? req.files.file.map(f => f.name+'|'+(f.phash || '')). }); } - const postId = await Posts.insertOne(res.locals.board, data, thread, res.locals.tor); + const postId = await Posts.insertOne(res.locals.board, data, thread, res.locals.anonymizer); let enableCaptcha = false; //make this returned from some function, refactor and move the next section to another file const pphTriggerActive = (pphTriggerAction > 0 && pphTrigger > 0); diff --git a/models/pages/captcha.js b/models/pages/captcha.js index 8fc39863..7e35ff90 100644 --- a/models/pages/captcha.js +++ b/models/pages/captcha.js @@ -16,7 +16,7 @@ module.exports = async (req, res, next) => { let captchaId; let maxAge = 5*60*1000; try { - if (!res.locals.tor) { + if (!res.locals.anonymizer) { const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.single, 'captcha', rateLimitCost.captcha); if (ratelimit > 100) { return res.status(429).redirect('/file/ratelimit.png'); diff --git a/package-lock.json b/package-lock.json index d7ddff3a..9fb7e8e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3448,11 +3448,11 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { diff --git a/package.json b/package.json index b9739208..a1fdfee1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jschan", "version": "0.0.1", - "migrateVersion": "0.0.17", + "migrateVersion": "0.0.19", "description": "", "main": "server.js", "dependencies": { diff --git a/schedules/index.js b/schedules/index.js index 23245b8f..e6b1e434 100644 --- a/schedules/index.js +++ b/schedules/index.js @@ -6,7 +6,7 @@ process const timeUtils = require(__dirname+'/../helpers/timeutils.js') , Mongo = require(__dirname+'/../db/db.js') - , { pruneImmediately, debugLogs, enableWebring } = require(__dirname+'/../configs/main.js') + , { pruneIps, pruneImmediately, debugLogs, enableWebring } = require(__dirname+'/../configs/main.js') , doInterval = require(__dirname+'/../helpers/dointerval.js'); (async () => { @@ -37,6 +37,11 @@ const timeUtils = require(__dirname+'/../helpers/timeutils.js') doInterval(pruneFiles, timeUtils.DAY, true); } + if (pruneIps) { + const ipSchedule = require(__dirname+'/ips.js'); + doInterval(ipSchedule, timeUtils.DAY, true); + } + //update the webring if (enableWebring) { const updateWebring = require(__dirname+'/webring.js'); diff --git a/schedules/ips.js b/schedules/ips.js new file mode 100644 index 00000000..a1ad2e16 --- /dev/null +++ b/schedules/ips.js @@ -0,0 +1,53 @@ +'use strict'; + +/* +prune IPs from old posts (actually, rehash them with a temporary random salt to maintain +post history and prevent *-by-ip action unintentionally deleting many posts) +NOTE: ips may still remain in the following collections: +- bans, because bans need the IP to function +- modlog actioner ips, modlogs are already auto-pruned +- ratelimits, these only last 1 minute +- stats, these last max of 24 hours +*/ +const Mongo = require(__dirname+'/../db/db.js') + , { Posts } = require(__dirname+'/../db/') + , { createHash, randomBytes } = require('crypto') + , { pruneIps } = require(__dirname+'/../configs/main.js'); + +module.exports = async (days) => { + const beforeDate = new Date(); + beforeDate.setDate(beforeDate.getDate() - days); + const beforeDateMongoId = Mongo.ObjectId.createFromTime(Math.floor(beforeDate.getTime()/1000)); + const tempIpHashSecret = randomBytes(20).toString('base64'); + const bulkWrites = []; + await Posts.db.find({ + _id: { + $lte: beforeDateMongoId, + }, + 'ip.pruned': { + $ne: true + } + }).forEach(post => { + const randomIP = createHash('sha256').update(tempIpHashSecret + post.ip.single).digest('base64'); + bulkWrites.push({ + updateOne: { + filter: { + _id: post._id, + }, + update: { + $set: { + 'ip.pruned': true, + 'ip.raw': randomIP, + 'ip.single': randomIP, + 'ip.qrange': randomIP, + 'ip.hrange': randomIP, + } + } + } + }); + }); + console.log(`Randomising ip on ${bulkWrites.length} posts`); + if (bulkWrites.length.length > 0) { + await Posts.db.bulkWrite(bulkWrites); + } +} diff --git a/socketio.js b/socketio.js index 49f1dfd9..ecb5f3c0 100644 --- a/socketio.js +++ b/socketio.js @@ -42,9 +42,9 @@ module.exports = { || roomName === 'manage-recent-raw') { requiredAuth = 3; //board mod minimum if (user && authLevel === 4) { - if (user.ownedBoards.includes(board)) { + if (user.ownedBoards.includes(roomBoard)) { authLevel = 2; //user is BO - } else if (user.modBoards.includes(board)) { + } else if (user.modBoards.includes(roomBoard)) { authLevel = 3; //user is mod } } diff --git a/views/custompages/faq.pug.example b/views/custompages/faq.pug.example index b1c3c701..ded0b04c 100644 --- a/views/custompages/faq.pug.example +++ b/views/custompages/faq.pug.example @@ -299,7 +299,7 @@ block content p Trigger Reset Lock Mode: If a trigger threshold was reached, reset the lock mode to this at the end of the hour. p Trigger Reset Captcha Mode: If a trigger threshold was reached, reset the captcha mode to this at the end of the hour. p Early 404: When a new thread is posted, delete any existing threads with less than #{early404Replies} replies beyond the first 1/#{early404Fraction} of threads. - p Disable .onion file posting: Prevent users posting through a .onion hidden service posting images. + p Disable anonymizer file posting: Prevent users posting images through anonymizers such as Tor hidden services, lokinet SNApps or i2p eepsites. p Blocked Countries: Block country codes (based on geo Ip data) from posting. p Filters: Newline separated list of words or phrases to match in posts. Checks name, message, filenames, subject, and filenames. p Strict Filtering: More aggressively match filters, by normalising the input compared against the filters. diff --git a/views/pages/managesettings.pug b/views/pages/managesettings.pug index 45bad720..36bb5f98 100644 --- a/views/pages/managesettings.pug +++ b/views/pages/managesettings.pug @@ -257,9 +257,9 @@ block content label.postform-style.ph-5 input(type='checkbox', name='early404', value='true' checked=board.settings.early404) .row - .label Disable .onion file posting + .label Disable anonymizer file posting label.postform-style.ph-5 - input(type='checkbox', name='disable_onion_file_posting', value='true' checked=board.settings.disableOnionFilePosting) + input(type='checkbox', name='disable_anonymizer_file_posting', value='true' checked=board.settings.disableAnonymizerFilePosting) .row .label Blocked Countries include ../includes/2charisocountries.pug