Read IP blocks from database

This commit is contained in:
Phillip Thelen
2025-05-27 17:58:54 +02:00
parent efe0b3cd9e
commit a9f84d3307
2 changed files with 133 additions and 1 deletions

View File

@@ -3,6 +3,7 @@ import {
Forbidden, Forbidden,
} from '../libs/errors'; } from '../libs/errors';
import { apiError } from '../libs/apiError'; import { apiError } from '../libs/apiError';
import { model as Blocker } from '../models/blocker';
// Middleware to block unwanted IP addresses // Middleware to block unwanted IP addresses
@@ -22,10 +23,28 @@ const blockedIps = BLOCKED_IPS_RAW
.filter(blockedIp => Boolean(blockedIp)) .filter(blockedIp => Boolean(blockedIp))
: []; : [];
Blocker.watchBlockers({
type: 'ipaddress',
area: 'full',
}, {
initial: true,
}).on('change', async change => {
const { operation, blocker } = change;
if (operation === 'add') {
if (blocker.value && !blockedIps.includes(blocker.value)) {
blockedIps.push(blocker.value);
}
} else if (operation === 'delete') {
const index = blockedIps.indexOf(blocker.value);
if (index !== -1) {
blockedIps.splice(index, 1);
}
}
});
export default function ipBlocker (req, res, next) { export default function ipBlocker (req, res, next) {
// If there are no IPs to block, skip the middleware // If there are no IPs to block, skip the middleware
if (blockedIps.length === 0) return next(); if (blockedIps.length === 0) return next();
// Is the client IP, req.ip, blocked? // Is the client IP, req.ip, blocked?
const match = blockedIps.find(blockedIp => blockedIp === req.ip) !== undefined; const match = blockedIps.find(blockedIp => blockedIp === req.ip) !== undefined;

View File

@@ -0,0 +1,113 @@
/* eslint-disable camelcase */
import mongoose from 'mongoose';
import EventEmitter from 'events';
import { v4 as uuid } from 'uuid';
import validator from 'validator';
import baseModel from '../libs/baseModel';
export const blockTypes = [
'ipaddress',
'email',
];
export const blockArea = [
'full',
'payment',
];
export const schema = new mongoose.Schema({
id: {
$type: String,
default: uuid,
validate: [v => validator.isUUID(v), 'Invalid uuid for tag.'],
required: true,
},
disabled: {
$type: Boolean, default: false, // If true, the block is disabled
},
type: {
$type: String, enum: blockTypes, required: true,
},
area: {
$type: String, enum: blockArea, default: 'full', // full or payment
},
value: {
$type: String, required: true, // e.g. IP address
},
blockSource: {
$type: String, enum: ['user', 'system', 'worker'], default: 'user', // who created the block
},
reason: {
$type: String, required: false, // e.g. 'abusive behavior'
},
}, {
strict: true,
minimize: false, // So empty objects are returned
_id: false, // use id instead of _id
typeKey: '$type', // So that we can use fields named `type`
});
schema.plugin(baseModel, {
timestamps: true,
noSet: ['_id'],
_id: false, // use id instead of _id
});
schema.statics.watchBlockers = function watchBlockers (query, options) {
const emitter = new EventEmitter();
const matchQuery = {
$match: {},
};
if (query) {
if (query.type) {
matchQuery.$match['fullDocument.type'] = query.type;
}
if (query.area) {
matchQuery.$match['fullDocument.area'] = query.area;
}
}
process.nextTick(() => {
this.watch([matchQuery], {
fullDocument: 'updateLookup',
})
.on('change', change => {
if (!change.fullDocument) {
return; // Ignore changes that don't have a fullDocument
}
if (change.operationType === 'insert' || !change.fullDocument.disabled) {
emitter.emit('change', {
operation: 'add',
blocker: change.fullDocument,
});
} else if (change.operationType === 'update' && change.fullDocument.disabled) {
emitter.emit('change', {
operation: 'delete',
blocker: change.fullDocument,
});
}
})
.on('error', error => {
emitter.emit('error', error);
});
if (options.initial) {
const initialQuery = {
disabled: false,
...query,
};
this.find(initialQuery).then(docs => {
for (const doc of docs) {
emitter.emit('change', {
operation: 'add',
blocker: doc,
});
}
}).catch(error => {
emitter.emit('error', error);
});
}
});
return emitter;
};
export const model = mongoose.model('Blocker', schema);