From 159e67b43074968eeb41f851be529e45f4866d6e Mon Sep 17 00:00:00 2001 From: Thomas Lynch Date: Mon, 28 Feb 2022 05:20:51 +1100 Subject: [PATCH 1/6] ref #426 ip "cloaking" still todo migration "ips" will make more sense for staff now qrange/hrange no longer need to be stored bypass still work like before. will have .BP suffix, normal ips are .IP filtering and stuff still works bans page will now show .*'s in the cloaked view for range bans in future version, this allows (even for those who cant see raw ips): - modlog, bans, post hisory filters including per-range - directly input ips/range cloak to ban, without selecting a post - upgrading existing bans from single to ranges --- db/bans.js | 8 +++- db/posts.js | 64 +++++++++---------------------- helpers/decodequeryip.js | 5 +-- helpers/processip.js | 19 +++++---- models/forms/banposter.js | 14 +++++-- models/forms/makepost.js | 8 ++-- models/pages/globalmanage/logs.js | 11 ++++-- models/pages/manage/recent.js | 2 +- schedules/tasks/ips.js | 2 - views/mixins/ban.pug | 7 +++- views/mixins/post.pug | 4 +- views/mixins/report.pug | 2 +- views/pages/globalmanagelogs.pug | 2 +- views/pages/managelogs.pug | 2 +- views/pages/managerecent.pug | 2 +- 15 files changed, 69 insertions(+), 83 deletions(-) diff --git a/db/bans.js b/db/bans.js index 94e30862..f57e999b 100644 --- a/db/bans.js +++ b/db/bans.js @@ -10,9 +10,13 @@ module.exports = { find: (ip, board) => { let ipQuery; - if (typeof ip === 'object') { //object with hash and ranges in bancheck + if (typeof ip === 'object') { ipQuery = { - '$in': [ip.single, ip.qrange, ip.hrange] //gets single and range ban in 1 query + '$in': [ + ip.single, //full ip + ip.single.split('.').slice(0,2).join('.'), //qrange + ip.single.split('.').slice(0,1).join('.'), //hrange + ], } } else { ipQuery = ip; diff --git a/db/posts.js b/db/posts.js index 0adc0275..908f3503 100644 --- a/db/posts.js +++ b/db/posts.js @@ -1,6 +1,7 @@ 'use strict'; const Mongo = require(__dirname+'/db.js') + , { isIP } = require('net') , Boards = require(__dirname+'/boards.js') , Stats = require(__dirname+'/stats.js') , db = Mongo.db.collection('posts') @@ -35,10 +36,12 @@ module.exports = { } else { projection['globalreports'] = 0; } - if (ip instanceof RegExp) { - query['ip.single'] = ip; - } else if (typeof ip === 'string') { - query['ip.raw'] = ip; + if (ip != null) { + if (isIP(ip)) { + query['ip.raw'] = ip; + } else { + query['ip.single'] = ip; + } } if (permLevel > config.get.ipHashPermLevel) { projection['ip.raw'] = 0; @@ -54,21 +57,6 @@ module.exports = { }).sort({ '_id': -1 }).skip(offset).limit(limit).toArray(); - posts.forEach(p => { - //kill me - p.ip.single = p.ip.single.slice(-10); - p.ip.qrange = p.ip.qrange.slice(-10); - p.ip.hrange = p.ip.hrange.slice(-10); - if (board) { - p.reports.forEach(r => { - r.ip.single = r.ip.single.slice(-10); - }); - } else { - p.globalreports.forEach(r => { - r.ip.single = r.ip.single.slice(-10); - }); - } - }); return posts; }, @@ -545,14 +533,6 @@ module.exports = { }, 'board': board }, { projection }).toArray(); - posts.forEach(p => { - p.ip.single = p.ip.single.slice(-10); - p.ip.qrange = p.ip.qrange.slice(-10); - p.ip.hrange = p.ip.hrange.slice(-10); - p.reports.forEach(r => { - r.ip.single = r.ip.single.slice(-10); - }); - }); return posts; }, @@ -571,26 +551,20 @@ module.exports = { '$exists': true } } - if (ip instanceof RegExp) { - query['$or'] = [ - { 'ip.single': ip }, - { 'globalreports.ip.single': ip } - ]; - } else if (typeof ip === 'string') { - query['$or'] = [ - { 'ip.raw': ip }, - { 'globalreports.ip.raw': ip } - ]; + if (ip != null) { + if (isIP(ip)) { + query['$or'] = [ + { 'ip.single': ip }, + { 'globalreports.ip.single': ip } + ]; + } else { + query['$or'] = [ + { 'ip.raw': ip }, + { 'globalreports.ip.raw': ip } + ]; + } } const posts = await db.find(query, { projection }).skip(offset).limit(limit).toArray(); - posts.forEach(p => { - p.ip.single = p.ip.single.slice(-10); - p.ip.qrange = p.ip.qrange.slice(-10); - p.ip.hrange = p.ip.hrange.slice(-10); - p.globalreports.forEach(r => { - r.ip.single = r.ip.single.slice(-10); - }); - }); return posts; }, diff --git a/helpers/decodequeryip.js b/helpers/decodequeryip.js index 38431767..1a067a8c 100644 --- a/helpers/decodequeryip.js +++ b/helpers/decodequeryip.js @@ -8,10 +8,9 @@ module.exports = (query, permLevel) => { const { ipHashPermLevel } = config.get; if (query.ip && typeof query.ip === 'string') { const decoded = decodeURIComponent(query.ip); - if (permLevel <= ipHashPermLevel && (isIP(decoded) || decoded.match(/[a-z0-9]{24}/i))) { //if perms to view raw ip or bypass, allow querying + if (permLevel <= ipHashPermLevel || !isIP(decoded)) { + //if they have perm to view raw IP, or its NOT a raw ip, return return decoded; - } else if (decoded.length === 10) { //otherwise, only allow last 10 char substring - return new RegExp(`${escapeRegExp(decoded)}$`); } } return null; //else, no ip filter diff --git a/helpers/processip.js b/helpers/processip.js index 994649eb..fe26a64e 100644 --- a/helpers/processip.js +++ b/helpers/processip.js @@ -12,10 +12,8 @@ module.exports = (req, res, next) => { if (res.locals.anonymizer) { const pseudoIp = res.locals.preFetchedBypassId || req.signedCookies.bypassid; res.locals.ip = { - raw: pseudoIp, - single: pseudoIp, - qrange: pseudoIp, - hrange: pseudoIp, + raw: `${pseudoIp}.BP`, + single: `${pseudoIp}.BP`, }; return next(); } @@ -31,20 +29,21 @@ module.exports = (req, res, next) => { zeroElide: false, zeroPad: false, }); - let qrange = '' - , hrange = ''; + let qrange + , hrange + , single; if (ipKind === 'ipv4') { qrange = createCIDR(ipStr, 24).toString(); hrange = createCIDR(ipStr, 16).toString(); + single = `${hashIp(hrange).substring(0,6)}.${hashIp(qrange).substring(0,3)}.${hashIp(ipStr).substring(0,3)}.IP`; } else { qrange = createCIDR(ipStr, 64).toString(); hrange = createCIDR(ipStr, 48).toString(); + single = `${hashIp(hrange).substring(0,8)}.${hashIp(qrange).substring(0,7)}.${hashIp(ipStr).substring(0,7)}.IP`; } res.locals.ip = { - raw: ipHashPermLevel === -1 ? hashIp(ipStr) : ipStr, - single: hashIp(ipStr), - qrange: hashIp(qrange), - hrange: hashIp(hrange), + raw: ipHashPermLevel === -1 ? single : ipStr, + single, } next(); } catch(e) { diff --git a/models/forms/banposter.js b/models/forms/banposter.js index a5b62508..51413b6e 100644 --- a/models/forms/banposter.js +++ b/models/forms/banposter.js @@ -26,15 +26,21 @@ module.exports = async (req, res, next) => { const thisIpPosts = ipPosts[ip]; let type = 'single'; let banIp = { - single: ip, - raw: thisIpPosts[0].ip.raw + single: thisIpPosts[0].ip.single, + raw: thisIpPosts[0].ip.raw, }; if (req.body.ban_h) { type = 'half'; - banIp.single = thisIpPosts[0].ip.hrange; + banIp.single = thisIpPosts[0].ip.single + .split('.') + .slice(0,1) + .join('.'); } else if (req.body.ban_q) { type = 'quarter'; - banIp.single = thisIpPosts[0].ip.qrange; + banIp.single = thisIpPosts[0].ip.single + .split('.') + .slice(0,2) + .join('.'); } bans.push({ type, diff --git a/models/forms/makepost.js b/models/forms/makepost.js index 7743a426..82d75b6f 100644 --- a/models/forms/makepost.js +++ b/models/forms/makepost.js @@ -614,13 +614,13 @@ ${res.locals.numFiles > 0 ? req.files.file.map(f => f.name+'|'+(f.phash || '')). } const { raw, single } = data.ip; //but emit it to manage pages because they need to get all posts through socket including thread - Socketio.emitRoom('globalmanage-recent-hashed', 'newPost', { ...projectedPost, ip: { single: single.slice(-10), raw: null } }); - Socketio.emitRoom(`${res.locals.board._id}-manage-recent-hashed`, 'newPost', { ...projectedPost, ip: { single: single.slice(-10), raw: null } }); + Socketio.emitRoom('globalmanage-recent-hashed', 'newPost', { ...projectedPost, ip: { single, raw: null } }); + Socketio.emitRoom(`${res.locals.board._id}-manage-recent-hashed`, 'newPost', { ...projectedPost, ip: { single, raw: null } }); if (ipHashPermLevel > -1) { //small optimisation for boards where this is manually set to -1 for privacy, no need to emit to rooms that cant be accessed //even if they are empty it will create extra communication noise in redis, socket adapter, etc. - Socketio.emitRoom('globalmanage-recent-raw', 'newPost', { ...projectedPost, ip: { single: single.slice(-10), raw } }); - Socketio.emitRoom(`${res.locals.board._id}-manage-recent-raw`, 'newPost', { ...projectedPost, ip: { single: single.slice(-10), raw } }); + Socketio.emitRoom('globalmanage-recent-raw', 'newPost', { ...projectedPost, ip: { single, raw } }); + Socketio.emitRoom(`${res.locals.board._id}-manage-recent-raw`, 'newPost', { ...projectedPost, ip: { single, raw } }); } //now add other pages to be built in background diff --git a/models/pages/globalmanage/logs.js b/models/pages/globalmanage/logs.js index 7852a0fe..415ffd9d 100644 --- a/models/pages/globalmanage/logs.js +++ b/models/pages/globalmanage/logs.js @@ -3,6 +3,7 @@ const { Modlogs } = require(__dirname+'/../../../db/') , pageQueryConverter = require(__dirname+'/../../../helpers/pagequeryconverter.js') , decodeQueryIP = require(__dirname+'/../../../helpers/decodequeryip.js') + , { isIP } = require('net') , limit = 50; module.exports = async (req, res, next) => { @@ -19,10 +20,12 @@ module.exports = async (req, res, next) => { filter.board = uri; } const ipMatch = decodeQueryIP(req.query, res.locals.permLevel); - if (ipMatch instanceof RegExp) { - filter['ip.single'] = ipMatch; - } else if (typeof ipMatch === 'string') { - filter['ip.raw'] = ipMatch; + if (ipMatch != null) { + if (isIP(ipMatch)) { + filter['ip.raw'] = ipMatch; + } else { + filter['ip.single'] = ipMatch; + } } let logs, maxPage; diff --git a/models/pages/manage/recent.js b/models/pages/manage/recent.js index be0dae74..14277dfa 100644 --- a/models/pages/manage/recent.js +++ b/models/pages/manage/recent.js @@ -13,7 +13,7 @@ module.exports = async (req, res, next) => { if (postId && +postId === parseInt(postId) && Number.isSafeInteger(+postId)) { const fetchedPost = await Posts.getPost(req.params.board, +postId, true); if (fetchedPost) { - ip = decodeQueryIP({ ip: fetchedPost.ip.single.slice(-10) }, res.locals.permlevel); + ip = decodeQueryIP({ ip: fetchedPost.ip.single }, res.locals.permlevel); } } diff --git a/schedules/tasks/ips.js b/schedules/tasks/ips.js index 61f06fa2..585d4cd2 100644 --- a/schedules/tasks/ips.js +++ b/schedules/tasks/ips.js @@ -33,8 +33,6 @@ module.exports = { 'ip.pruned': true, 'ip.raw': randomIP, 'ip.single': randomIP, - 'ip.qrange': randomIP, - 'ip.hrange': randomIP, } } } diff --git a/views/mixins/ban.pug b/views/mixins/ban.pug index bc862238..56ef92c1 100644 --- a/views/mixins/ban.pug +++ b/views/mixins/ban.pug @@ -10,8 +10,11 @@ mixin ban(ban, banpage) else | Global td= ban.reason - - const ip = permLevel > ipHashPermLevel ? ban.ip.single.slice(-10) : ban.ip.raw; - td #{ip} + - const ip = permLevel > ipHashPermLevel ? ban.ip.single : ban.ip.raw; + if permLevel > ipHashPermLevel + td #{ip}#{ban.type === 'half' ? '.*.*' : (ban.type === 'quarter' ? '.*' : '')} + else + td #{ip} td #{ban.type} td #{(!banpage || ban.showUser === true) ? ban.issuer : 'Hidden User'} - const banDate = new Date(ban.date); diff --git a/views/mixins/post.pug b/views/mixins/post.pug index ef12b57b..d82e954e 100644 --- a/views/mixins/post.pug +++ b/views/mixins/post.pug @@ -20,12 +20,12 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false, overboar input.post-check(type='checkbox', name='checkedposts' value=post.postId) | if manage - - const ip = permLevel > ipHashPermLevel ? post.ip.single.slice(-10) : post.ip.raw; + - const ip = permLevel > ipHashPermLevel ? post.ip.single : post.ip.raw; a.bold(href=`${upLevel ? '../' : ''}recent.html?ip=${encodeURIComponent(ip)}`) [#{ip}] else if modview a.bold(href=`${upLevel ? '../' : ''}recent.html?postid=${post.postId}`) [+] else if globalmanage - - const ip = permLevel > ipHashPermLevel ? post.ip.single.slice(-10) : post.ip.raw; + - const ip = permLevel > ipHashPermLevel ? post.ip.single : post.ip.raw; a.bold(href=`?ip=${encodeURIComponent(ip)}`) [#{ip}] | if !post.thread diff --git a/views/mixins/report.pug b/views/mixins/report.pug index 00ff4e13..33c02700 100644 --- a/views/mixins/report.pug +++ b/views/mixins/report.pug @@ -2,7 +2,7 @@ mixin report(r, manage=false) .reports.post-container input.post-check(type='checkbox', name='checkedreports' value=r.id) | - - const ip = permLevel > ipHashPermLevel ? r.ip.single.slice(-10) : r.ip.raw; + - const ip = permLevel > ipHashPermLevel ? r.ip.single : r.ip.raw; a.bold(href=`${manage ? 'recent.html' : ''}?ip=${encodeURIComponent(ip)}`) [#{ip}] | - const reportDate = new Date(r.date); diff --git a/views/pages/globalmanagelogs.pug b/views/pages/globalmanagelogs.pug index 5ba4c770..927b9054 100644 --- a/views/pages/globalmanagelogs.pug +++ b/views/pages/globalmanagelogs.pug @@ -52,7 +52,7 @@ block content | a(href=`?username=${log.user}`) [+] td - - const logIp = permLevel > ipHashPermLevel ? log.ip.single.slice(-10) : log.ip.raw; + - const logIp = permLevel > ipHashPermLevel ? log.ip.single : log.ip.raw; a(href=`recent.html?ip=${encodeURIComponent(logIp)}`) #{logIp} | a(href=`?ip=${encodeURIComponent(logIp)}`) [+] diff --git a/views/pages/managelogs.pug b/views/pages/managelogs.pug index 1398ee7b..2c94fece 100644 --- a/views/pages/managelogs.pug +++ b/views/pages/managelogs.pug @@ -40,7 +40,7 @@ block content | a(href=`?username=${log.user}`) [+] td - - const logIp = permLevel > ipHashPermLevel ? log.ip.single.slice(-10) : log.ip.raw; + - const logIp = permLevel > ipHashPermLevel ? log.ip.single : log.ip.raw; | #{logIp} td #{log.actions} td diff --git a/views/pages/managerecent.pug b/views/pages/managerecent.pug index 1ee8fb90..08a49ab3 100644 --- a/views/pages/managerecent.pug +++ b/views/pages/managerecent.pug @@ -23,7 +23,7 @@ block content if posts.length === 0 p No posts. else - - const ip = permLevel > ipHashPermLevel ? posts[0].ip.single.slice(-10) : posts[0].ip.raw; + - const ip = permLevel > ipHashPermLevel ? posts[0].ip.single : posts[0].ip.raw; if postId || (queryIp && queryIp === ip) h4.no-m-p Post history for #{ip} | From 17c9d098a57b13715e953f3ad9c3286c17977878 Mon Sep 17 00:00:00 2001 From: Thomas Lynch Date: Wed, 2 Mar 2022 15:39:44 +1100 Subject: [PATCH 2/6] not make the length different for ipv4/6 cloaks --- helpers/processip.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/helpers/processip.js b/helpers/processip.js index fe26a64e..8326d74e 100644 --- a/helpers/processip.js +++ b/helpers/processip.js @@ -30,17 +30,15 @@ module.exports = (req, res, next) => { zeroPad: false, }); let qrange - , hrange - , single; + , hrange; if (ipKind === 'ipv4') { qrange = createCIDR(ipStr, 24).toString(); hrange = createCIDR(ipStr, 16).toString(); - single = `${hashIp(hrange).substring(0,6)}.${hashIp(qrange).substring(0,3)}.${hashIp(ipStr).substring(0,3)}.IP`; } else { qrange = createCIDR(ipStr, 64).toString(); hrange = createCIDR(ipStr, 48).toString(); - single = `${hashIp(hrange).substring(0,8)}.${hashIp(qrange).substring(0,7)}.${hashIp(ipStr).substring(0,7)}.IP`; } + const single = `${hashIp(hrange).substring(0,8)}.${hashIp(qrange).substring(0,7)}.${hashIp(ipStr).substring(0,7)}.IP`; res.locals.ip = { raw: ipHashPermLevel === -1 ? single : ipStr, single, From e77aaffaa0736767a10a1e444d250d60878fad24 Mon Sep 17 00:00:00 2001 From: Thomas Lynch Date: Sat, 5 Mar 2022 14:16:08 +1100 Subject: [PATCH 3/6] wrong isIP condition, should be for raw not single --- db/posts.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/db/posts.js b/db/posts.js index 908f3503..02d9812a 100644 --- a/db/posts.js +++ b/db/posts.js @@ -554,13 +554,13 @@ module.exports = { if (ip != null) { if (isIP(ip)) { query['$or'] = [ - { 'ip.single': ip }, - { 'globalreports.ip.single': ip } + { 'ip.raw': ip }, + { 'globalreports.ip.raw': ip } ]; } else { query['$or'] = [ - { 'ip.raw': ip }, - { 'globalreports.ip.raw': ip } + { 'ip.single': ip }, + { 'globalreports.ip.single': ip } ]; } } From 363b87a498a5d7c6ccde68cd52246bc4d207e312 Mon Sep 17 00:00:00 2001 From: Thomas Lynch Date: Sat, 5 Mar 2022 14:16:42 +1100 Subject: [PATCH 4/6] make pruned ips not a long clusterfuck, and put .PRUNED on the end so staff know --- schedules/tasks/ips.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schedules/tasks/ips.js b/schedules/tasks/ips.js index 585d4cd2..28ac0065 100644 --- a/schedules/tasks/ips.js +++ b/schedules/tasks/ips.js @@ -31,8 +31,8 @@ module.exports = { update: { $set: { 'ip.pruned': true, - 'ip.raw': randomIP, - 'ip.single': randomIP, + 'ip.raw': `${randomIP.slice(-10)}.PRUNED`, + 'ip.single': `${randomIP.slice(-10)}.PRUNED`, } } } From 92c504e59c2b1c335c0cba57f0cd890c1f1ab2b2 Mon Sep 17 00:00:00 2001 From: Thomas Lynch Date: Sun, 6 Mar 2022 13:18:23 +1100 Subject: [PATCH 5/6] rename ip.single -> ip.cloak --- controllers/forms/boardsettings.js | 2 +- controllers/forms/editpost.js | 2 +- db/bans.js | 10 +++++----- db/posts.js | 8 ++++---- gulpfile.js | 2 +- helpers/captcha/verify.js | 2 +- helpers/checks/spamcheck.js | 4 ++-- helpers/processip.js | 8 ++++---- models/forms/actionhandler.js | 6 +++--- models/forms/appeal.js | 2 +- models/forms/banposter.js | 12 ++++++------ models/forms/editpost.js | 4 ++-- models/forms/makepost.js | 12 ++++++------ models/forms/reportpost.js | 2 +- models/pages/captcha.js | 2 +- models/pages/globalmanage/logs.js | 2 +- models/pages/manage/recent.js | 2 +- schedules/tasks/ips.js | 4 ++-- views/mixins/ban.pug | 2 +- views/mixins/post.pug | 4 ++-- views/mixins/report.pug | 2 +- views/pages/globalmanagelogs.pug | 2 +- views/pages/managelogs.pug | 2 +- views/pages/managerecent.pug | 2 +- 24 files changed, 50 insertions(+), 50 deletions(-) diff --git a/controllers/forms/boardsettings.js b/controllers/forms/boardsettings.js index 6cf8566b..0c311e4f 100644 --- a/controllers/forms/boardsettings.js +++ b/controllers/forms/boardsettings.js @@ -85,7 +85,7 @@ module.exports = { if (res.locals.permLevel > 1) { //if not global staff or above const ratelimitBoard = await Ratelimits.incrmentQuota(req.params.board, 'settings', rateLimitCost.boardSettings); //2 changes a minute - const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.single, 'settings', rateLimitCost.boardSettings)); + const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'settings', rateLimitCost.boardSettings)); if (ratelimitBoard > 100 || ratelimitIp > 100) { return dynamicResponse(req, res, 429, 'message', { 'title': 'Ratelimited', diff --git a/controllers/forms/editpost.js b/controllers/forms/editpost.js index e64d3205..9df1762f 100644 --- a/controllers/forms/editpost.js +++ b/controllers/forms/editpost.js @@ -43,7 +43,7 @@ module.exports = { if (res.locals.permLevel > 1) { //if not global staff or above const ratelimitUser = await Ratelimits.incrmentQuota(req.session.user, 'edit', rateLimitCost.editPost); - const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.single, 'edit', rateLimitCost.editPost)); + const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'edit', rateLimitCost.editPost)); if (ratelimitUser > 100 || ratelimitIp > 100) { return dynamicResponse(req, res, 429, 'message', { 'title': 'Ratelimited', diff --git a/db/bans.js b/db/bans.js index f57e999b..311e3977 100644 --- a/db/bans.js +++ b/db/bans.js @@ -13,16 +13,16 @@ module.exports = { if (typeof ip === 'object') { ipQuery = { '$in': [ - ip.single, //full ip - ip.single.split('.').slice(0,2).join('.'), //qrange - ip.single.split('.').slice(0,1).join('.'), //hrange + ip.cloak, //full ip + ip.cloak.split('.').slice(0,2).join('.'), //qrange + ip.cloak.split('.').slice(0,1).join('.'), //hrange ], } } else { ipQuery = ip; } return db.find({ - 'ip.single': ipQuery, + 'ip.cloak': ipQuery, 'board': { '$in': [board, null] } @@ -46,7 +46,7 @@ module.exports = { '_id': { '$in': ids }, - 'ip.single': ip, + 'ip.cloak': ip, 'allowAppeal': true, 'appeal': null }, { diff --git a/db/posts.js b/db/posts.js index 02d9812a..e994bfe8 100644 --- a/db/posts.js +++ b/db/posts.js @@ -40,7 +40,7 @@ module.exports = { if (isIP(ip)) { query['ip.raw'] = ip; } else { - query['ip.single'] = ip; + query['ip.cloak'] = ip; } } if (permLevel > config.get.ipHashPermLevel) { @@ -467,7 +467,7 @@ module.exports = { //insert the post itself const postMongoId = await db.insertOne(data).then(result => result.insertedId); //_id of post - const statsIp = (config.get.statsCountAnonymizers === false && res.locals.anonymizer === true) ? null : data.ip.single; + const statsIp = (config.get.statsCountAnonymizers === false && res.locals.anonymizer === true) ? null : data.ip.cloak; await Stats.updateOne(board._id, statsIp, data.thread == null); //add backlinks to the posts this post quotes @@ -559,8 +559,8 @@ module.exports = { ]; } else { query['$or'] = [ - { 'ip.single': ip }, - { 'globalreports.ip.single': ip } + { 'ip.cloak': ip }, + { 'globalreports.ip.cloak': ip } ]; } } diff --git a/gulpfile.js b/gulpfile.js index 6c85a8c8..4a8e1b5d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -192,7 +192,7 @@ async function wipe() { 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 }) + await Bans.db.createIndex({ 'ip.cloak': 1 , 'board': 1 }) await Bans.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }) //custom expiry, i.e. it will expire when current date > than this date await Bypass.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }) await Captchas.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 300 }) //captchas valid for 5 minutes diff --git a/helpers/captcha/verify.js b/helpers/captcha/verify.js index a59ecdd7..7418fe03 100644 --- a/helpers/captcha/verify.js +++ b/helpers/captcha/verify.js @@ -50,7 +50,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.anonymizer && Ratelimits.resetQuota(res.locals.ip.single, 'captcha'), + !res.locals.anonymizer && Ratelimits.resetQuota(res.locals.ip.cloak, 'captcha'), remove(`${uploadDirectory}/captcha/${captchaId}.jpg`) ]); } diff --git a/helpers/checks/spamcheck.js b/helpers/checks/spamcheck.js index 5086c4f8..2f0e77cc 100644 --- a/helpers/checks/spamcheck.js +++ b/helpers/checks/spamcheck.js @@ -57,7 +57,7 @@ module.exports = async (req, res) => { '_id': { '$gt': sameContentSameIpMongoId }, - 'ip.single': res.locals.ip.single, + 'ip.cloak': res.locals.ip.cloak, '$or': contentOr }); } @@ -69,7 +69,7 @@ module.exports = async (req, res) => { '_id': { '$gt': anyContentSameIpMongoId }, - 'ip.single': res.locals.ip.single + 'ip.cloak': res.locals.ip.cloak }) } diff --git a/helpers/processip.js b/helpers/processip.js index 8326d74e..d46d21df 100644 --- a/helpers/processip.js +++ b/helpers/processip.js @@ -13,7 +13,7 @@ module.exports = (req, res, next) => { const pseudoIp = res.locals.preFetchedBypassId || req.signedCookies.bypassid; res.locals.ip = { raw: `${pseudoIp}.BP`, - single: `${pseudoIp}.BP`, + cloak: `${pseudoIp}.BP`, }; return next(); } @@ -38,10 +38,10 @@ module.exports = (req, res, next) => { qrange = createCIDR(ipStr, 64).toString(); hrange = createCIDR(ipStr, 48).toString(); } - const single = `${hashIp(hrange).substring(0,8)}.${hashIp(qrange).substring(0,7)}.${hashIp(ipStr).substring(0,7)}.IP`; + const cloak = `${hashIp(hrange).substring(0,8)}.${hashIp(qrange).substring(0,7)}.${hashIp(ipStr).substring(0,7)}.IP`; res.locals.ip = { - raw: ipHashPermLevel === -1 ? single : ipStr, - single, + raw: ipHashPermLevel === -1 ? cloak : ipStr, + cloak, } next(); } catch(e) { diff --git a/models/forms/actionhandler.js b/models/forms/actionhandler.js index 84a6b7a2..57031e8e 100644 --- a/models/forms/actionhandler.js +++ b/models/forms/actionhandler.js @@ -118,13 +118,13 @@ module.exports = async (req, res, next) => { } const postsBefore = res.locals.posts.length; if (req.body.delete_ip_board || req.body.delete_ip_global || req.body.delete_ip_thread) { - const deletePostIps = res.locals.posts.map(x => x.ip.single); + const deletePostIps = res.locals.posts.map(x => x.ip.cloak); const deletePostMongoIds = res.locals.posts.map(x => x._id) let query = { '_id': { '$nin': deletePostMongoIds }, - 'ip.single': { + 'ip.cloak': { '$in': deletePostIps } }; @@ -314,7 +314,7 @@ module.exports = async (req, res, next) => { message: message, user: logUser, ip: { - single: res.locals.ip.single, + cloak: res.locals.ip.cloak, raw: res.locals.ip.raw } }; diff --git a/models/forms/appeal.js b/models/forms/appeal.js index 2944a097..ae7bca88 100644 --- a/models/forms/appeal.js +++ b/models/forms/appeal.js @@ -4,6 +4,6 @@ const { Bans } = require(__dirname+'/../../db/'); module.exports = async (req, res, next) => { - return Bans.appeal(res.locals.ip.single, req.body.checkedbans, req.body.message).then(r => r.modifiedCount); + return Bans.appeal(res.locals.ip.cloak, req.body.checkedbans, req.body.message).then(r => r.modifiedCount); } diff --git a/models/forms/banposter.js b/models/forms/banposter.js index 51413b6e..aa845bce 100644 --- a/models/forms/banposter.js +++ b/models/forms/banposter.js @@ -16,28 +16,28 @@ module.exports = async (req, res, next) => { if (req.body.ban || req.body.global_ban) { const banBoard = req.body.global_ban ? null : req.params.board; const ipPosts = res.locals.posts.reduce((acc, post) => { - if (!acc[post.ip.single]) { - acc[post.ip.single] = []; + if (!acc[post.ip.cloak]) { + acc[post.ip.cloak] = []; } - acc[post.ip.single].push(post); + acc[post.ip.cloak].push(post); return acc; }, {}); for (let ip in ipPosts) { const thisIpPosts = ipPosts[ip]; let type = 'single'; let banIp = { - single: thisIpPosts[0].ip.single, + cloak: thisIpPosts[0].ip.cloak, raw: thisIpPosts[0].ip.raw, }; if (req.body.ban_h) { type = 'half'; - banIp.single = thisIpPosts[0].ip.single + banIp.cloak = thisIpPosts[0].ip.cloak .split('.') .slice(0,1) .join('.'); } else if (req.body.ban_q) { type = 'quarter'; - banIp.single = thisIpPosts[0].ip.single + banIp.cloak = thisIpPosts[0].ip.cloak .split('.') .slice(0,2) .join('.'); diff --git a/models/forms/editpost.js b/models/forms/editpost.js index 2f2072bf..a4d86555 100644 --- a/models/forms/editpost.js +++ b/models/forms/editpost.js @@ -51,7 +51,7 @@ todo: handle some more situations const banExpiry = new Date(globalSettings.filterBanDuration + banDate.getTime()); const ban = { 'ip': { - 'single': res.locals.ip.single, + 'cloak': res.locals.ip.cloak, 'raw': res.locals.ip.raw, }, 'type': 'single', @@ -163,7 +163,7 @@ todo: handle some more situations message: req.body.log_message || null, user: req.session.user, ip: { - single: res.locals.ip.single, + cloak: res.locals.ip.cloak, raw: res.locals.ip.raw, } }); diff --git a/models/forms/makepost.js b/models/forms/makepost.js index 82d75b6f..4fe336c7 100644 --- a/models/forms/makepost.js +++ b/models/forms/makepost.js @@ -144,7 +144,7 @@ ${res.locals.numFiles > 0 ? req.files.file.map(f => f.name+'|'+(f.phash || '')). const banExpiry = new Date(useFilterBanDuration + banDate.getTime()); const ban = { 'ip': { - 'single': res.locals.ip.single, + 'cloak': res.locals.ip.cloak, 'raw': res.locals.ip.raw, }, 'type': 'single', @@ -612,15 +612,15 @@ ${res.locals.numFiles > 0 ? req.files.file.map(f => f.name+'|'+(f.phash || '')). //dont emit thread to this socket, because the room onyl exists when the thread is open Socketio.emitRoom(`${res.locals.board._id}-${data.thread}`, 'newPost', projectedPost); } - const { raw, single } = data.ip; + const { raw, cloak } = data.ip; //but emit it to manage pages because they need to get all posts through socket including thread - Socketio.emitRoom('globalmanage-recent-hashed', 'newPost', { ...projectedPost, ip: { single, raw: null } }); - Socketio.emitRoom(`${res.locals.board._id}-manage-recent-hashed`, 'newPost', { ...projectedPost, ip: { single, raw: null } }); + Socketio.emitRoom('globalmanage-recent-hashed', 'newPost', { ...projectedPost, ip: { cloak, raw: null } }); + Socketio.emitRoom(`${res.locals.board._id}-manage-recent-hashed`, 'newPost', { ...projectedPost, ip: { cloak, raw: null } }); if (ipHashPermLevel > -1) { //small optimisation for boards where this is manually set to -1 for privacy, no need to emit to rooms that cant be accessed //even if they are empty it will create extra communication noise in redis, socket adapter, etc. - Socketio.emitRoom('globalmanage-recent-raw', 'newPost', { ...projectedPost, ip: { single, raw } }); - Socketio.emitRoom(`${res.locals.board._id}-manage-recent-raw`, 'newPost', { ...projectedPost, ip: { single, raw } }); + Socketio.emitRoom('globalmanage-recent-raw', 'newPost', { ...projectedPost, ip: { cloak, raw } }); + Socketio.emitRoom(`${res.locals.board._id}-manage-recent-raw`, 'newPost', { ...projectedPost, ip: { cloak, raw } }); } //now add other pages to be built in background diff --git a/models/forms/reportpost.js b/models/forms/reportpost.js index dd6d69b2..12128ebf 100644 --- a/models/forms/reportpost.js +++ b/models/forms/reportpost.js @@ -9,7 +9,7 @@ module.exports = (req, res) => { 'reason': req.body.report_reason, 'date': new Date(), 'ip': { - 'single': res.locals.ip.single, + 'cloak': res.locals.ip.cloak, 'raw': res.locals.ip.raw } } diff --git a/models/pages/captcha.js b/models/pages/captcha.js index e17956eb..4a793b0a 100644 --- a/models/pages/captcha.js +++ b/models/pages/captcha.js @@ -21,7 +21,7 @@ module.exports = async (req, res, next) => { let maxAge = 5*60*1000; try { if (!res.locals.anonymizer) { - const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.single, 'captcha', rateLimitCost.captcha); + const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'captcha', rateLimitCost.captcha); if (ratelimit > 100) { return res.status(429).redirect('/file/ratelimit.png'); } diff --git a/models/pages/globalmanage/logs.js b/models/pages/globalmanage/logs.js index 415ffd9d..de0ae639 100644 --- a/models/pages/globalmanage/logs.js +++ b/models/pages/globalmanage/logs.js @@ -24,7 +24,7 @@ module.exports = async (req, res, next) => { if (isIP(ipMatch)) { filter['ip.raw'] = ipMatch; } else { - filter['ip.single'] = ipMatch; + filter['ip.cloak'] = ipMatch; } } diff --git a/models/pages/manage/recent.js b/models/pages/manage/recent.js index 14277dfa..af408260 100644 --- a/models/pages/manage/recent.js +++ b/models/pages/manage/recent.js @@ -13,7 +13,7 @@ module.exports = async (req, res, next) => { if (postId && +postId === parseInt(postId) && Number.isSafeInteger(+postId)) { const fetchedPost = await Posts.getPost(req.params.board, +postId, true); if (fetchedPost) { - ip = decodeQueryIP({ ip: fetchedPost.ip.single }, res.locals.permlevel); + ip = decodeQueryIP({ ip: fetchedPost.ip.cloak }, res.locals.permlevel); } } diff --git a/schedules/tasks/ips.js b/schedules/tasks/ips.js index 28ac0065..3125020f 100644 --- a/schedules/tasks/ips.js +++ b/schedules/tasks/ips.js @@ -22,7 +22,7 @@ module.exports = { $ne: true } }).forEach(post => { - const randomIP = createHash('sha256').update(tempIpHashSecret + post.ip.single).digest('base64'); + const randomIP = createHash('sha256').update(tempIpHashSecret + post.ip.cloak).digest('base64'); bulkWrites.push({ updateOne: { filter: { @@ -32,7 +32,7 @@ module.exports = { $set: { 'ip.pruned': true, 'ip.raw': `${randomIP.slice(-10)}.PRUNED`, - 'ip.single': `${randomIP.slice(-10)}.PRUNED`, + 'ip.cloak': `${randomIP.slice(-10)}.PRUNED`, } } } diff --git a/views/mixins/ban.pug b/views/mixins/ban.pug index 56ef92c1..d3ce58d6 100644 --- a/views/mixins/ban.pug +++ b/views/mixins/ban.pug @@ -10,7 +10,7 @@ mixin ban(ban, banpage) else | Global td= ban.reason - - const ip = permLevel > ipHashPermLevel ? ban.ip.single : ban.ip.raw; + - const ip = permLevel > ipHashPermLevel ? ban.ip.cloak : ban.ip.raw; if permLevel > ipHashPermLevel td #{ip}#{ban.type === 'half' ? '.*.*' : (ban.type === 'quarter' ? '.*' : '')} else diff --git a/views/mixins/post.pug b/views/mixins/post.pug index d82e954e..797b8718 100644 --- a/views/mixins/post.pug +++ b/views/mixins/post.pug @@ -20,12 +20,12 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false, overboar input.post-check(type='checkbox', name='checkedposts' value=post.postId) | if manage - - const ip = permLevel > ipHashPermLevel ? post.ip.single : post.ip.raw; + - const ip = permLevel > ipHashPermLevel ? post.ip.cloak : post.ip.raw; a.bold(href=`${upLevel ? '../' : ''}recent.html?ip=${encodeURIComponent(ip)}`) [#{ip}] else if modview a.bold(href=`${upLevel ? '../' : ''}recent.html?postid=${post.postId}`) [+] else if globalmanage - - const ip = permLevel > ipHashPermLevel ? post.ip.single : post.ip.raw; + - const ip = permLevel > ipHashPermLevel ? post.ip.cloak : post.ip.raw; a.bold(href=`?ip=${encodeURIComponent(ip)}`) [#{ip}] | if !post.thread diff --git a/views/mixins/report.pug b/views/mixins/report.pug index 33c02700..84aa2066 100644 --- a/views/mixins/report.pug +++ b/views/mixins/report.pug @@ -2,7 +2,7 @@ mixin report(r, manage=false) .reports.post-container input.post-check(type='checkbox', name='checkedreports' value=r.id) | - - const ip = permLevel > ipHashPermLevel ? r.ip.single : r.ip.raw; + - const ip = permLevel > ipHashPermLevel ? r.ip.cloak : r.ip.raw; a.bold(href=`${manage ? 'recent.html' : ''}?ip=${encodeURIComponent(ip)}`) [#{ip}] | - const reportDate = new Date(r.date); diff --git a/views/pages/globalmanagelogs.pug b/views/pages/globalmanagelogs.pug index 927b9054..9ff41364 100644 --- a/views/pages/globalmanagelogs.pug +++ b/views/pages/globalmanagelogs.pug @@ -52,7 +52,7 @@ block content | a(href=`?username=${log.user}`) [+] td - - const logIp = permLevel > ipHashPermLevel ? log.ip.single : log.ip.raw; + - const logIp = permLevel > ipHashPermLevel ? log.ip.cloak : log.ip.raw; a(href=`recent.html?ip=${encodeURIComponent(logIp)}`) #{logIp} | a(href=`?ip=${encodeURIComponent(logIp)}`) [+] diff --git a/views/pages/managelogs.pug b/views/pages/managelogs.pug index 2c94fece..d5d07371 100644 --- a/views/pages/managelogs.pug +++ b/views/pages/managelogs.pug @@ -40,7 +40,7 @@ block content | a(href=`?username=${log.user}`) [+] td - - const logIp = permLevel > ipHashPermLevel ? log.ip.single : log.ip.raw; + - const logIp = permLevel > ipHashPermLevel ? log.ip.cloak : log.ip.raw; | #{logIp} td #{log.actions} td diff --git a/views/pages/managerecent.pug b/views/pages/managerecent.pug index 08a49ab3..c1a52c70 100644 --- a/views/pages/managerecent.pug +++ b/views/pages/managerecent.pug @@ -23,7 +23,7 @@ block content if posts.length === 0 p No posts. else - - const ip = permLevel > ipHashPermLevel ? posts[0].ip.single : posts[0].ip.raw; + - const ip = permLevel > ipHashPermLevel ? posts[0].ip.cloak : posts[0].ip.raw; if postId || (queryIp && queryIp === ip) h4.no-m-p Post history for #{ip} | From 18f149180f18a1b73f7696996383bf7ba0b1fef4 Mon Sep 17 00:00:00 2001 From: Thomas Lynch Date: Sun, 6 Mar 2022 13:18:51 +1100 Subject: [PATCH 6/6] add migration, fix gulpfile index, update migrateversion and changelog --- CHANGELOG.md | 3 ++ migrations/0.4.0.js | 77 +++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 +-- package.json | 4 +-- 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 migrations/0.4.0.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1b9748..c4cf53f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### 0.4.0 + - Hashed IPs now use an irc-style "cloaking". + ### 0.3.2 - Minor bugfix to post moving. diff --git a/migrations/0.4.0.js b/migrations/0.4.0.js new file mode 100644 index 00000000..9a62a7c3 --- /dev/null +++ b/migrations/0.4.0.js @@ -0,0 +1,77 @@ +'use strict'; + +const hashIp = require(__dirname+'/../helpers/haship.js') + , { createCIDR, parse } = require('ip6addr') + , config = require(__dirname+'/../config.js'); + +module.exports = async(db, redis) => { + const postIps = await db.collection('posts').distinct('ip.raw'); + const logIps = await db.collection('modlog').distinct('ip.raw'); + const banIps = await db.collection('bans').distinct('ip.raw'); + const allDistinctIps = postIps.concat(logIps).concat(banIps); + const bulkWrites = allDistinctIps.map(ip => { + const ipSet = {}; + try { + const ipParsed = parse(ip); + const ipKind = ipParsed.kind(); + const ipStr = ipParsed.toString({ + format: ipKind === 'ipv4' ? 'v4' : 'v6', + zeroElide: false, + zeroPad: false, + }); + let qrange + , hrange; + if (ipKind === 'ipv4') { + qrange = createCIDR(ipStr, 24).toString(); + hrange = createCIDR(ipStr, 16).toString(); + } else { + qrange = createCIDR(ipStr, 64).toString(); + hrange = createCIDR(ipStr, 48).toString(); + } + ipSet['ip.cloak'] = `${hashIp(hrange).substring(0,8)}.${hashIp(qrange).substring(0,7)}.${hashIp(ipStr).substring(0,7)}.IP`; + } catch (e) { + //-1 old "iphashpermlevel" or bypass ids, just shorten them + const shortenedOldHash = `${hashIp(ip).slice(-10)}.IP`; + ipSet['ip.raw'] = shortenedOldHash; + ipSet['ip.cloak'] = shortenedOldHash; + } + return { + 'updateMany': { + 'filter': { + 'ip.raw': ip + }, + 'update': { + '$unset': { + 'ip.single': '', + 'ip.qrange': '', + 'ip.hrange': '', + }, + '$set': ipSet, + } + } + }; + }); + console.log('adjusting ip in modlogs, bans and posts'); + //the bulkwrites should work for ip, bans, and logs + await db.collection('posts').bulkWrite(bulkWrites); + await db.collection('modlog').bulkWrite(bulkWrites); + await db.collection('bans').bulkWrite(bulkWrites); + console.log('removing saved posts inside bans'); + await db.collection('bans').updateMany({}, { + '$set':{ + 'posts': null, + } + }); + console.log('clearing reports') + await db.collection('posts').updateMany({}, { + '$set':{ + 'reports': [], + 'globalreports': [], + } + }); + //drop old ban indexes that indexed ip.single, then recreate + console.log('recreating bans indexes'); + await db.collection('bans').dropIndexes(); + await db.collection('bans').createIndex({ 'ip.cloak': 1 , 'board': 1 }); + await db.collection('bans').createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }); +} diff --git a/package-lock.json b/package-lock.json index b350248c..543b2b2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jschan", - "version": "0.3.2", + "version": "0.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "jschan", - "version": "0.3.2", + "version": "0.4.0", "license": "AGPL-3.0-only", "dependencies": { "@fatchan/express-fileupload": "^1.3.1", diff --git a/package.json b/package.json index 0aca3b0f..a6d9cbc1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jschan", - "version": "0.3.2", - "migrateVersion": "0.2.0", + "version": "0.4.0", + "migrateVersion": "0.4.0", "description": "", "main": "server.js", "dependencies": {