Many CrowdSec improvements.
Ylian Saint-Hilaire committed
Jul 7, 2022 at 21:51 UTC
40bc91b6f3322ac2e44c08dc8393e8885baab363
3 files changed
+159
-7
crowdsec.js
new
+131
@@ -0,0 +1,131 @@
1
+module.exports.CreateCrowdSecBouncer = function (parent, config) {
2
+ const obj = {};
3
+
4
+ // Setup constants
5
+ const { getLogger } = require('@crowdsec/express-bouncer/src/nodejs-bouncer/lib/logger');
6
+ const { configure, renderBanWall, testConnectionToCrowdSec, getRemediationForIp } = require('@crowdsec/express-bouncer/src/nodejs-bouncer');
7
+ const applyCaptcha = require('@crowdsec/express-bouncer/src/express-crowdsec-middleware/lib/captcha');
8
+ const { BYPASS_REMEDIATION, CAPTCHA_REMEDIATION, BAN_REMEDIATION } = require('@crowdsec/express-bouncer/src/nodejs-bouncer/lib/constants');
9
+ const svgCaptcha = require('svg-captcha');
10
+ const { renderCaptchaWall } = require('@crowdsec/express-bouncer/src/nodejs-bouncer');
11
+
12
+ // Current captcha state
13
+ const currentCaptchaIpList = {};
14
+
15
+ // Set the default values
16
+ if (typeof config.userAgent != 'string') { config.userAgent = "CrowdSec Express-NodeJS bouncer/v0.0.1"; }
17
+ if (typeof config.timeout != 'number') { config.timeout = 2000; }
18
+ if (typeof config.fallbackRemediation != 'number') { config.fallbackRemediation = BAN_REMEDIATION; }
19
+ if (typeof config.maxRemediation != 'number') { config.maxRemediation = BAN_REMEDIATION; }
20
+ if (typeof config.captchaGenerationCacheDuration != 'number') { config.captchaGenerationCacheDuration = 60 * 1000; }
21
+ if (typeof config.captchaResolutionCacheDuration != 'number') { config.captchaResolutionCacheDuration = 30 * 60 * 1000; }
22
+ if (typeof config.captchaTexts != 'object') { config.captchaTexts = {}; }
23
+ if (typeof config.banTexts != 'object') { config.banTexts = {}; }
24
+ if (typeof config.colors != 'object') { config.colors = {}; }
25
+ if (typeof config.hideCrowdsecMentions != 'boolean') { config.hideCrowdsecMentions = false; }
26
+ if (typeof config.customCss != 'string') { config.customCss = ''; }
27
+ if (typeof config.bypass != 'boolean') { config.bypass = false; }
28
+ if (typeof config.trustedRangesForIpForwarding != 'object') { config.trustedRangesForIpForwarding = []; }
29
+ if (typeof config.customLogger != 'object') { config.customLogger = null; }
30
+ if (typeof config.bypassConnectionTest != 'boolean') { config.bypassConnectionTest = false; }
31
+
32
+ // Setup the logger
33
+ var logger = config.customLogger ? config.customLogger : getLogger();
34
+
35
+ // Configure the bouncer
36
+ configure({
37
+ url: config.url,
38
+ apiKey: config.apiKey,
39
+ userAgent: config.userAgent,
40
+ timeout: config.timeout,
41
+ fallbackRemediation: config.fallbackRemediation,
42
+ maxRemediation: config.maxRemediation,
43
+ captchaTexts: config.captchaTexts,
44
+ banTexts: config.banTexts,
45
+ colors: config.colors,
46
+ hideCrowdsecMentions: config.hideCrowdsecMentions,
47
+ customCss: config.customCss
48
+ });
49
+
50
+ // Test connectivity
51
+ obj.testConnectivity = async function() { return (await testConnectionToCrowdSec())['success']; }
52
+
53
+ // Process a web request
54
+ obj.process = async function (domain, req, res, next) {
55
+ try {
56
+ const remediation = await getRemediationForIp(req.clientIp);
57
+ //console.log('CrowdSec', req.clientIp, remediation, req.url);
58
+ switch (remediation) {
59
+ case BAN_REMEDIATION:
60
+ const banWallTemplate = await renderBanWall();
61
+ res.status(403);
62
+ res.send(banWallTemplate);
63
+ return true;
64
+ case CAPTCHA_REMEDIATION:
65
+ if ((currentCaptchaIpList[req.clientIp] == null) || (currentCaptchaIpList[req.clientIp].resolved !== true)) {
66
+ var domainCaptchaUrl = ((domain != null) && (domain.id != '') && (domain.dns == null)) ? ('/' + domain.id + '/captcha.ashx') : '/captcha.ashx';
67
+ if (req.url != domainCaptchaUrl) { res.redirect(domainCaptchaUrl); return true; }
68
+ }
69
+ break;
70
+ }
71
+ } catch (ex) { }
72
+ return false;
73
+ }
74
+
75
+ // Process a captcha request
76
+ obj.applyCaptcha = async function (req, res, next) {
77
+ await applyCaptchaEx(req.clientIp, req, res, next, config.captchaGenerationCacheDuration, config.captchaResolutionCacheDuration, logger);
78
+ }
79
+
80
+ // Process a captcha request
81
+ async function applyCaptchaEx(ip, req, res, next, captchaGenerationCacheDuration, captchaResolutionCacheDuration, loggerInstance) {
82
+ logger = loggerInstance;
83
+ let error = false;
84
+
85
+ if (currentCaptchaIpList[ip] == null) {
86
+ generateCaptcha(ip, captchaGenerationCacheDuration);
87
+ } else {
88
+ if (currentCaptchaIpList[ip] && currentCaptchaIpList[ip].resolved) {
89
+ logger.debug({ type: 'CAPTCHA_ALREADY_SOLVED', ip });
90
+ next();
91
+ return;
92
+ } else {
93
+ if (req.body && req.body.crowdsec_captcha) {
94
+ if (req.body.refresh === '1') { generateCaptcha(ip, captchaGenerationCacheDuration); }
95
+ if (req.body.phrase !== '') {
96
+ if (currentCaptchaIpList[ip].text === req.body.phrase) {
97
+ currentCaptchaIpList[ip].resolved = true;
98
+ setTimeout(function() { if (currentCaptchaIpList[ip]) { delete currentCaptchaIpList[ip]; } }, captchaResolutionCacheDuration);
99
+ res.redirect(req.originalUrl);
100
+ logger.info({ type: 'CAPTCHA_RESOLUTION', ip, result: true });
101
+ return;
102
+ } else {
103
+ logger.info({ type: 'CAPTCHA_RESOLUTION', ip, result: false });
104
+ error = true;
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ const captchaWallTemplate = await renderCaptchaWall({ captchaImageTag: currentCaptchaIpList[ip].data, captchaResolutionFormUrl: '', error });
112
+ res.status(401);
113
+ res.send(captchaWallTemplate);
114
+ };
115
+
116
+ // Generate a CAPTCHA
117
+ function generateCaptcha(ip, captchaGenerationCacheDuration) {
118
+ const captcha = svgCaptcha.create();
119
+ currentCaptchaIpList[ip] = {
120
+ data: captcha.data,
121
+ text: captcha.text,
122
+ resolved: false,
123
+ };
124
+ setTimeout(() => {
125
+ if (currentCaptchaIpList[ip]) { delete currentCaptchaIpList[ip]; }
126
+ }, captchaGenerationCacheDuration);
127
+ logger.debug({ type: "GENERATE_CAPTCHA", ip });
128
+ };
129
+
130
+ return obj;
131
+}
meshcentral.js
+1
-4
@@ -1233,10 +1233,7 @@ function CreateMeshCentralServer(config, args) {
1233
}
1234
1235
// Start CrowdSec bouncer if needed: https://www.crowdsec.net/
1236
- if (typeof obj.args.crowdsec == 'object') {
1237
- const expressCrowdsecBouncer = require("@crowdsec/express-bouncer");
1238
- try { obj.crowdsecMiddleware = await expressCrowdsecBouncer(obj.args.crowdsec); } catch (ex) { delete obj.crowdsecMiddleware; }
1239
- }
1236
+ if (typeof obj.args.crowdsec == 'object') { obj.crowdSecBounser = require('./crowdsec.js').CreateCrowdSecBouncer(obj, obj.args.crowdsec); }
1237
1238
// Check if self update is allowed. If running as a Windows service, self-update is not possible.
1239
if (obj.fs.existsSync(obj.path.join(__dirname, 'daemon'))) { obj.serverSelfWriteAllowed = false; }
webserver.js
+27
-3
@@ -3196,6 +3196,23 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3196
}
3197
}
3198
3199
+ // Handle Captcha GET
3200
+ function handleCaptchaGetRequest(req, res) {
3201
+ const domain = checkUserIpAddress(req, res);
3202
+ if (domain == null) { return; }
3203
+ if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
3204
+ parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect((((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id))); });
3205
+ }
3206
+
3207
+ // Handle Captcha POST
3208
+ function handleCaptchaPostRequest(req, res) {
3209
+ if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
3210
+ const domain = checkUserIpAddress(req, res);
3211
+ if (domain == null) { return; }
3212
+ req.originalUrl = (((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id));
3213
+ parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect(req.originalUrl); });
3214
+ }
3215
+
3216
// Render the terms of service.
3217
function handleTermsRequest(req, res) {
3218
const domain = checkUserIpAddress(req, res);
@@ -5714,11 +5731,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5731
obj.tlsAltServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
5732
obj.expressWsAlt = require('express-ws')(obj.agentapp, obj.tlsAltServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
5733
}
5717
- if (parent.crowdsecMiddleware != null) { obj.agentapp.use(parent.crowdsecMiddleware); } // Setup CrowdSec bouncer middleware if needed
5734
}
5735
5736
// Setup middleware
5721
- if (parent.crowdsecMiddleware != null) { obj.app.use(parent.crowdsecMiddleware); } // Setup CrowdSec bouncer middleware if needed
5737
obj.app.engine('handlebars', obj.exphbs({ defaultLayout: false }));
5738
obj.app.set('view engine', 'handlebars');
5739
if (obj.args.trustedproxy) {
@@ -5762,7 +5777,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5777
});
5778
5779
// Add HTTP security headers to all responses
5765
- obj.app.use(function (req, res, next) {
5780
+ obj.app.use(async function (req, res, next) {
5781
// Check if a session is destroyed
5782
if (typeof req.session.userid == 'string') {
5783
if (typeof req.session.x == 'string') {
@@ -5905,6 +5920,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5920
// Extend the session time by forcing a change to the session every minute.
5921
if (req.session.userid != null) { req.session.t = Math.floor(Date.now() / 60e3); } else { delete req.session.t; }
5922
5923
+ // Check CrowdSec Bounser if configured
5924
+ if ((parent.crowdSecBounser != null) && (req.headers['upgrade'] != 'websocket') && (req.session.userid == null)) { if ((await parent.crowdSecBounser.process(domain, req, res, next)) == true) { return; } }
5925
+
5926
// Debugging code, this will stop the agent from crashing if two responses are made to the same request.
5927
const render = res.render;
5928
const send = res.send;
@@ -6080,6 +6098,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6098
obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
6099
}
6100
6101
+ // Check CrowdSec Bounser if configured
6102
+ if (parent.crowdSecBounser != null) {
6103
+ obj.app.get(url + 'captcha.ashx', handleCaptchaGetRequest);
6104
+ obj.app.post(url + 'captcha.ashx', obj.bodyParser.urlencoded({ extended: false }), handleCaptchaPostRequest);
6105
+ }
6106
+
6107
// Setup IP-KVM relay if supported
6108
if (domain.ipkvm) {
6109
obj.app.ws(url + 'ipkvm.ashx/*', function (ws, req) {