IP blacklist functionality -- re: #4367

Squashed commit of the following:

commit 5c42fd732d091fa66cf5b45a2af5e1697cc1efcd
Author: Julian Lam <julian@nodebb.org>
Date:   Mon Mar 14 17:29:45 2016 -0400

    allowing blacklist.test to be called synchronously

commit 979faf2dba5e6f6e2ae1bd07341e63678438daf1
Author: Julian Lam <julian@nodebb.org>
Date:   Mon Mar 14 17:01:14 2016 -0400

    added plain ipv6 support and finished middleware logic

commit d4b72fc1aadff34df3ed7dec52ca8d3c3728a078
Author: Julian Lam <julian@designcreateplay.com>
Date:   Fri Mar 11 16:05:31 2016 -0500

    WIP IP Banning logic middleware

commit f08b2553890c5522b6a1eaf521fe4e94df40574a
Author: Julian Lam <julian@designcreateplay.com>
Date:   Fri Mar 11 15:26:27 2016 -0500

    tweaks to ACP, rule validator

commit 868abacaa494e6b8a88bd4ea429b1b066a9ecb2e
Author: Julian Lam <julian@designcreateplay.com>
Date:   Fri Mar 11 13:50:05 2016 -0500

    IP Banning ACP page, styling, save&load functionality
v1.18.x
Julian Lam 9 years ago
parent 71c697037d
commit 184a2c4540

@ -33,6 +33,7 @@
"express-session": "^1.8.2",
"express-useragent": "0.2.4",
"html-to-text": "2.0.0",
"ip": "1.1.2",
"jimp": "0.2.21",
"less": "^2.0.0",
"logrotate-stream": "^0.2.3",

@ -34,6 +34,7 @@
"user-banned": "User banned",
"user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post",
"blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.",
"no-category": "Category does not exist",
"no-topic": "Topic does not exist",

@ -12,6 +12,7 @@
@import "./manage/tags";
@import "./manage/groups";
@import "./manage/users";
@import "./manage/blacklist";
@import "./appearance/customise";
@import "./appearance/themes";
@import "./extend/plugins";

@ -0,0 +1,5 @@
#blacklist-rules {
width: 100%;
height: 450px;
display: block;
}

@ -0,0 +1,41 @@
'use strict';
/* globals $, app, socket, templates */
define('admin/manage/ip-blacklist', ['settings'], function(Settings) {
var Blacklist = {};
Blacklist.init = function() {
var blacklist = ace.edit("blacklist-rules");
blacklist.on('change', function(e) {
$('#blacklist-rules-holder').val(blacklist.getValue());
});
Settings.load('blacklist', $('.blacklist-settings'), function(err, settings) {
blacklist.setValue(settings.rules);
});
$('[data-action="apply"]').on('click', function() {
Settings.save('blacklist', $('.blacklist-settings'), function() {
app.alert({
type: 'success',
alert_id: 'blacklist-saved',
title: 'Blacklist Applied',
});
});
});
$('[data-action="test"]').on('click', function() {
socket.emit('admin.blacklist.validate', {
rules: blacklist.getValue()
}, function(err, data) {
templates.parse('admin/partials/blacklist-validate', data, function(html) {
bootbox.alert(html);
});
});
});
};
return Blacklist;
});

@ -5,6 +5,7 @@ var adminController = {
categories: require('./admin/categories'),
tags: require('./admin/tags'),
flags: require('./admin/flags'),
blacklist: require('./admin/blacklist'),
groups: require('./admin/groups'),
appearance: require('./admin/appearance'),
extend: {

@ -0,0 +1,9 @@
"use strict";
var blacklistController = {};
blacklistController.get = function(req, res, next) {
res.render('admin/manage/ip-blacklist', {});
};
module.exports = blacklistController;

@ -26,6 +26,7 @@ var async = require('async'),
require('./meta/tags')(Meta);
require('./meta/dependencies')(Meta);
Meta.templates = require('./meta/templates');
Meta.blacklist = require('./meta/blacklist');
/* Assorted */
Meta.userOrGroupExists = function(slug, callback) {

@ -0,0 +1,106 @@
'use strict';
var ip = require('ip'),
winston = require('winston'),
async = require('async');
var meta = module.parent.exports;
var Blacklist = {
_rules: []
};
Blacklist.load = function(callback) {
async.waterfall([
async.apply(meta.settings.getOne, 'blacklist', 'rules'),
async.apply(Blacklist.validate)
], function(err, rules) {
if (err) {
return callback(err);
}
winston.verbose('[meta/blacklist] Loading ' + rules.valid.length + ' blacklist rules');
if (rules.invalid.length) {
winston.warn('[meta/blacklist] ' + rules.invalid.length + ' invalid blacklist rule(s) were ignored.');
}
Blacklist._rules = {
ipv4: rules.ipv4,
ipv6: rules.ipv6,
cidr: rules.cidr
};
callback();
});
};
Blacklist.test = function(clientIp, callback) {
if (
Blacklist._rules.ipv4.indexOf(clientIp) === -1 // not explicitly specified in ipv4 list
&& Blacklist._rules.ipv6.indexOf(clientIp) === -1 // not explicitly specified in ipv6 list
&& !Blacklist._rules.cidr.some(function(subnet) {
return ip.cidrSubnet(subnet).contains(clientIp);
}) // not in a blacklisted cidr range
) {
if (typeof callback === 'function') {
callback();
} else {
return false;
}
} else {
var err = new Error('[[error:blacklisted-ip]]');
err.code = 'blacklisted-ip';
if (typeof callback === 'function') {
callback(err);
} else {
return true;
}
}
};
Blacklist.validate = function(rules, callback) {
var rules = (rules || '').split('\n'),
ipv4 = [],
ipv6 = [],
cidr = [],
invalid = [];
var isCidrSubnet = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/;
// Filter out blank lines and lines starting with the hash character (comments)
rules = rules.map(function(rule) {
rule = rule.trim();
return rule.length && !rule.startsWith('#') ? rule : null;
}).filter(Boolean);
// Filter out invalid rules
rules = rules.filter(function(rule) {
if (ip.isV4Format(rule)) {
ipv4.push(rule);
return true;
} else if (ip.isV6Format(rule)) {
ipv6.push(rule);
return true;
} else if (isCidrSubnet.test(rule)) {
cidr.push(rule);
return true;
} else {
invalid.push(rule);
return false;
}
return true;
});
callback(null, {
numRules: rules.length + invalid.length,
ipv4: ipv4,
ipv6: ipv6,
cidr: cidr,
valid: rules,
invalid: invalid
});
};
module.exports = Blacklist;

@ -257,6 +257,13 @@ middleware.busyCheck = function(req, res, next) {
}
};
middleware.applyBlacklist = function(req, res, next) {
meta.blacklist.test(req.ip, function(err) {
console.log('blacklist returned:', err);
next(err);
});
};
module.exports = function(webserver) {
app = webserver;
middleware.admin = require('./admin')(webserver);

@ -54,8 +54,8 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/categories/:category_id', middlewares, controllers.admin.categories.get);
router.get('/manage/tags', middlewares, controllers.admin.tags.get);
router.get('/manage/flags', middlewares, controllers.admin.flags.get);
router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get);
router.get('/manage/users', middlewares, controllers.admin.users.sortByJoinDate);
router.get('/manage/users/search', middlewares, controllers.admin.users.search);

@ -62,8 +62,8 @@
}));
});
router.post('/register', Auth.middleware.applyCSRF, controllers.authentication.register);
router.post('/login', Auth.middleware.applyCSRF, controllers.authentication.login);
router.post('/register', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.register);
router.post('/login', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.login);
router.post('/logout', Auth.middleware.applyCSRF, controllers.authentication.logout);
hotswap.replace('auth', router);

@ -187,9 +187,14 @@ function handle404(app, middleware) {
function handleErrors(app, middleware) {
app.use(function(err, req, res, next) {
if (err.code === 'EBADCSRFTOKEN') {
winston.error(req.path + '\n', err.message);
return res.sendStatus(403);
switch (err.code) {
case 'EBADCSRFTOKEN':
winston.error(req.path + '\n', err.message);
return res.sendStatus(403);
break;
case 'blacklisted-ip':
return res.status(403).type('text/plain').send(err.message);
break;
}
if (parseInt(err.status, 10) === 302 && err.path) {

@ -33,7 +33,8 @@ var async = require('async'),
settings: {},
email: {},
analytics: {},
logs: {}
logs: {},
blacklist: {}
};
SocketAdmin.before = function(socket, method, data, next) {
@ -273,5 +274,8 @@ SocketAdmin.deleteAllEvents = function(socket, data, callback) {
events.deleteAll(callback);
};
SocketAdmin.blacklist.validate = function(socket, data, callback) {
meta.blacklist.validate(data.rules, callback);
};
module.exports = SocketAdmin;

@ -0,0 +1,31 @@
<div class="flags">
<div class="col-lg-12">
<p class="lead">
Configure your IP blacklist here.
</p>
<p>
Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs
is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and
they will be prevented from logging in to or registering a new account.
</p>
<div class="row">
<div class="col-sm-6">
<div id="blacklist-rules"></div>
<form class="blacklist-settings">
<input type="hidden" id="blacklist-rules-holder" value="" name="rules" />
</form>
</div>
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">Active Rules</div>
<div class="panel-body">
<button type="button" class="btn btn-warning" data-action="test"><i class="fa fa-bomb"></i> Validate Blacklist</button>
<button type="button" class="btn btn-primary" data-action="apply"><i class="fa fa-save"></i> Apply Blacklist</button>
</div>
</div>
</div>
</div>
</div>
</div>

@ -0,0 +1,14 @@
<p class="lead">
<strong>{valid.length}</strong> out of <strong>{numRules}</strong> rule(s) valid.
</p>
<!-- IF invalid.length -->
<p>
The following <strong>{invalid.length}</strong> rules are invalid:
</p>
<ul>
<!-- BEGIN invalid -->
<li><code>@value</code></li>
<!-- END invalid -->
</ul>
<!-- ENDIF invalid.length -->

@ -20,6 +20,7 @@
<li><a href="{relative_path}/admin/manage/registration">Registration Queue</a></li>
<li><a href="{relative_path}/admin/manage/groups">Groups</a></li>
<li><a href="{relative_path}/admin/manage/flags">Flags</a></li>
<li><a href="{relative_path}/admin/manage/ip-blacklist">IP Blacklist</a></li>
</ul>
</section>
@ -173,6 +174,7 @@
<li><a href="{relative_path}/admin/manage/registration">Registration Queue</a></li>
<li><a href="{relative_path}/admin/manage/groups">Groups</a></li>
<li><a href="{relative_path}/admin/manage/flags">Flags</a></li>
<li><a href="{relative_path}/admin/manage/ip-blacklist">IP Blacklist</a></li>
</ul>
</li>
<li class="dropdown menu-item">

@ -95,7 +95,8 @@ function initializeNodeBB(callback) {
async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, 'nodebb.min.js'),
async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, 'acp.min.js'),
async.apply(!skipLess ? meta.css.minify : meta.css.getFromFile),
async.apply(meta.sounds.init)
async.apply(meta.sounds.init),
async.apply(meta.blacklist.load)
], next);
},
function(results, next) {

Loading…
Cancel
Save