Bans can now be "upgraded" retroactively to expand single IP bans to qrange/hrange bans.

The ban table now has a column or whether a ban is of an IP or Bypass ID. (Or pruned IP, if you were dumb enough to ban one of those)
merge-requests/341/head
Thomas Lynch 2 years ago
parent 00ec5182f0
commit 62678c2b19
  1. 2
      CHANGELOG.md
  2. 9
      controllers/forms/editbans.js
  3. 43
      db/bans.js
  4. 2
      lib/middleware/ip/processip.js
  5. 26
      migrations/0.6.0.js
  6. 14
      models/forms/banposter.js
  7. 3
      models/forms/editpost.js
  8. 3
      models/forms/makepost.js
  9. 17
      models/forms/upgradebans.js
  10. 2
      package.json
  11. 31
      test/actions.js
  12. 1
      views/includes/bantable.pug
  13. 8
      views/includes/managebanform.pug
  14. 5
      views/mixins/ban.pug
  15. 2
      views/mixins/modal.pug

@ -1,5 +1,7 @@
### 0.6.0
- Bans+Appeal form will now appear in the modal popup when js enabled, instead of a dodgy workaround which often caused posting bugs and broke over Tor.
- Bans can now be "upgraded" retroactively to expand single IP bans to qrange/hrange bans.
- The ban table now has a column or whether a ban is of an IP or Bypass ID. (Or pruned IP, if you were dumb enough to ban one of those)
- Big refactor of backend, the awfully named and disorganised "helpers" is now the more appropriately named and better organised "lib".
- Some form and input handling code made more robust based on test feedback.
- More tests.

@ -4,6 +4,7 @@ const removeBans = require(__dirname+'/../../models/forms/removebans.js')
, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
, denyAppeals = require(__dirname+'/../../models/forms/denybanappeals.js')
, editBans = require(__dirname+'/../../models/forms/editbans.js')
, upgradeBans = require(__dirname+'/../../models/forms/upgradebans.js')
, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
, { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable,
inArrayBody, arrayInBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');
@ -12,6 +13,7 @@ module.exports = {
paramConverter: paramConverter({
timeFields: ['ban_duration'],
numberFields: ['upgrade'],
trimFields: ['option'],
allowedArrays: ['checkedbans'],
objectIdArrays: ['checkedbans']
@ -21,8 +23,9 @@ module.exports = {
const errors = await checkSchema([
{ result: lengthBody(req.body.checkedbans, 1), expected: false, error: 'Must select at least one ban' },
{ result: inArrayBody(req.body.option, ['unban', 'edit', 'deny_appeal']), expected: true, error: 'Invalid ban action' },
{ result: inArrayBody(req.body.option, ['unban', 'edit', 'upgrade', 'deny_appeal']), expected: true, error: 'Invalid ban action' },
{ result: req.body.option !== 'edit' || numberBody(req.body.ban_duration, 1), expected: true, error: 'Invalid ban duration' },
{ result: req.body.option !== 'upgrade' || inArrayBody(req.body.upgrade, [1, 2]), expected: true, error: 'Invalid ban upgrade option' },
]);
const redirect = req.params.board ? `/${req.params.board}/manage/bans.html` : '/globalmanage/bans.html';
@ -47,6 +50,10 @@ module.exports = {
amount = await denyAppeals(req, res, next);
message = `Denied ${amount} appeals`;
break;
case 'upgrade':
amount = await upgradeBans(req, res, next);
message = `Upgraded ${amount} bans`;
break;
case 'edit': //could do other properties in future
amount = await editBans(req, res, next);
message = `Edited ${amount} bans`;

@ -29,6 +29,49 @@ module.exports = {
}).toArray();
},
upgrade: async (board, ids, upgradeType) => {
const substrProjection = upgradeType === 1
? ['$ip.cloak', 0, 16]
: ['$ip.cloak', 0, 8];
const aggregateCursor = await db.aggregate([
{
'$match': {
'_id': {
'$in': ids,
},
'board': board,
//bypass or pruned IP bans aren't upgraded, duh!
'category': 0,
//dont allow half -> quarter
'type': {
'$lt': upgradeType
}
}
}, {
'$project': {
'_id': 1,
'board': 1,
'type': {
//mongoloidDB
'$literal': upgradeType,
},
'ip.cloak': {
'$substr': substrProjection,
},
'ip.raw': '$ip.raw',
}
}, {
'$merge': {
'into': 'bans',
}
}
]);
//changing the order of these will result in a differene explain() output! nice.
const aggregateExplained = await aggregateCursor.explain();
await aggregateCursor.toArray();
return aggregateExplained;
},
markSeen: (ids) => {
return db.updateMany({
'_id': {

@ -43,8 +43,6 @@ module.exports = (req, res, next) => {
raw: dontStoreRawIps === true ? cloak : ipStr,
cloak,
}
//#426
//console.log(`net-${hashIp(hrange).substring(0,6)}.${hashIp(qrange).substring(0,4)}.${hashIp(ipStr).substring(0,4)}.IP`)
next();
} catch(e) {
console.error('Ip parse failed', e);

@ -0,0 +1,26 @@
'use strict';
module.exports = async(db, redis) => {
console.log('Addjusting bans to categorise between normal/bypass/pruned, required for ban upgrading capabilities');
await db.collection('bans').updateMany({
'ip.cloak': /\.IP$/
}, {
'$set':{
'category': 0,
},
});
await db.collection('bans').updateMany({
'ip.cloak': /\.BP$/
}, {
'$set':{
'category': 1,
},
});
await db.collection('bans').updateMany({
'ip.cloak': /\.PRUNED$/
}, {
'$set':{
'category': 2,
},
});
};

@ -23,20 +23,25 @@ module.exports = async (req, res, next) => {
return acc;
}, {});
for (let ip in ipPosts) {
const banCategory = ip.endsWith('.IP') ? 0 :
ip.endsWith('.BP') ? 1 :
2;
/* should we at some point filter these to not bother banning pruned ips,
and/or not range banning bypasses (since it does nothing)? */
const thisIpPosts = ipPosts[ip];
let type = 'single';
let type = 0;
let banIp = {
cloak: thisIpPosts[0].ip.cloak,
raw: thisIpPosts[0].ip.raw,
};
if (req.body.ban_h) {
type = 'half';
type = 2;
banIp.cloak = thisIpPosts[0].ip.cloak
.split('.')
.slice(0,1)
.join('.');
} else if (req.body.ban_q) {
type = 'quarter';
type = 1;
banIp.cloak = thisIpPosts[0].ip.cloak
.split('.')
.slice(0,2)
@ -44,6 +49,7 @@ module.exports = async (req, res, next) => {
}
bans.push({
type,
'category': banCategory,
'ip': banIp,
'reason': banReason,
'board': banBoard,
@ -83,7 +89,7 @@ module.exports = async (req, res, next) => {
[...new Set(ips)].forEach(ip => {
bans.push({
'ip': ip,
'type': 'single',
'type': 0,
'reason': banReason,
'board': banBoard,
'posts': null,

@ -56,7 +56,8 @@ todo: handle some more situations
'cloak': res.locals.ip.cloak,
'raw': res.locals.ip.raw,
},
'type': 'single',
'category': res.locals.anonymizer ? 1 : 0,
'type': 0,
'reason': 'global word filter auto ban',
'board': null,
'posts': null,

@ -150,7 +150,8 @@ ${res.locals.numFiles > 0 ? req.files.file.map(f => f.name+'|'+(f.phash || '')).
'cloak': res.locals.ip.cloak,
'raw': res.locals.ip.raw,
},
'type': 'single',
'category': res.locals.anonymizer ? 1 : 0, //no 2, because that only happens during pruning
'type': 0,
'reason': `${hitGlobalFilter ? 'global ' :''}word filter auto ban`,
'board': banBoard,
'posts': null,

@ -0,0 +1,17 @@
'use strict';
const { Bans } = require(__dirname+'/../../db/');
module.exports = async (req, res, next) => {
const nReturned = await Bans.upgrade(req.params.board, req.body.checkedbans, req.body.upgrade)
.then(explain => {
if (explain && explain.stages){
return explain.stages[0].nReturned;
}
return 0;
});
return nReturned;
}

@ -1,7 +1,7 @@
{
"name": "jschan",
"version": "0.6.0",
"migrateVersion": "0.5.0",
"migrateVersion": "0.6.0",
"description": "",
"main": "server.js",
"dependencies": {

@ -7,7 +7,7 @@ module.exports = () => describe('Test post modactions', () => {
let sessionCookie
, csrfToken;
test('login as admin', async () => {
test.only('login as admin', async () => {
const params = new URLSearchParams();
params.append('username', 'admin');
params.append('password', process.env.TEST_ADMIN_PASSWORD);
@ -593,6 +593,35 @@ int main() {...}
expect(response.ok).toBe(true);
});
test.only('test upgrade a ban to qrange', async () => {
const banPage = await fetch('http://localhost/globalmanage/bans.html', {
headers: {
'cookie': sessionCookie,
},
}).then(res => res.text());
const checkString = 'name="checkedbans" value="';
const checkIndex = banPage.indexOf(checkString);
banId = banPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
const params = new URLSearchParams({
_csrf: csrfToken,
checkedbans: banId,
option: 'upgrade',
upgrade: 1,
});
const response = await fetch('http://localhost/forms/global/editbans', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('edit ban duration', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,

@ -5,6 +5,7 @@
th Board
th Reason
th IP
th Category
th Type
th Issuer
th Issue Date

@ -14,6 +14,14 @@ else
.label Deny Appeal
label.postform-style.ph-5
input(type='radio' name='option' value='deny_appeal')
.row
.label Upgrade Ban
label.postform-style.ph-5
input(type='radio' name='option' value='upgrade')
select(name='upgrade')
option(value='')
option(value='1') Narrow Range
option(value='2') Wide Range
.row
.label Edit Duration
label.postform-style.ph-5.mr-1

@ -14,8 +14,9 @@ mixin ban(ban, banpage)
if viewRawIp === true
td #{ip}
else
td #{ip}#{ban.type === 'half' ? '.*.*' : (ban.type === 'quarter' ? '.*' : '')}
td #{ban.type}
td #{ip}#{ban.type === 'hrange' ? '.*.*' : (ban.type === 'qrange' ? '.*' : '')}
td #{ban.category === 0 ? 'IP' : ban.category === 1 ? 'Bypass' : 'Pruned IP'}
td #{ban.type === 0 ? 'Single' : ban.type === 1 ? 'Quarter' : 'Half'}
td #{(!banpage || ban.showUser === true) ? ban.issuer : 'Hidden User'}
- const banDate = new Date(ban.date);
td: time.right.reltime(datetime=banDate.toISOString()) #{banDate.toLocaleString(undefined, {hourCycle:'h23'})}

@ -8,7 +8,7 @@ mixin modal(data)
if data.bans
h1.board-title Banned!
.row#modalbanned
- const bans = data.bans
- const bans = data.bans;
include ../includes/banform.pug
if data.message || data.messages || data.error || data.errors
.row

Loading…
Cancel
Save