@universis/janitor
Version:
Universis api plugin for handling user authorization and rate limiting
112 lines (109 loc) • 6.13 kB
JavaScript
import { ApplicationService, TraceUtils } from '@themost/common';
import slowDown from 'express-slow-down';
import express from 'express';
import path from 'path';
export class SpeedLimitService extends ApplicationService {
constructor(app) {
super(app);
app.serviceRouter.subscribe(serviceRouter => {
if (serviceRouter == null) {
return;
}
try {
const addRouter = express.Router();
let serviceConfiguration = app.getConfiguration().getSourceAt('settings/universis/janitor/speedLimit') || {
profiles: [],
paths: []
};
if (serviceConfiguration.extends) {
// get additional configuration
const configurationPath = app.getConfiguration().getConfigurationPath();
const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
TraceUtils.log(`@universis/janitor#SpeedLimitService will try to extend service configuration from ${extendsPath}`);
serviceConfiguration = require(extendsPath);
}
const pathsArray = serviceConfiguration.paths || [];
const profilesArray = serviceConfiguration.profiles || [];
// create maps
const paths = new Map(pathsArray);
const profiles = new Map(profilesArray);
if (paths.size === 0) {
TraceUtils.warn('@universis/janitor#SpeedLimitService is being started but the collection of paths is empty.');
}
// get proxy address forwarding option
let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
if (typeof proxyAddressForwarding !== 'boolean') {
proxyAddressForwarding = false;
}
paths.forEach((value, path) => {
let profile;
// get profile
if (value.profile) {
profile = profiles.get(value.profile);
} else {
// or options defined inline
profile = value
}
if (profile != null) {
const slowDownOptions = Object.assign({
windowMs: 5 * 60 * 1000, // 5 minutes
delayAfter: 20, // 20 requests
delayMs: 500, // 500 ms
maxDelayMs: 10000 // 10 seconds
}, profile, {
keyGenerator: (req) => {
let remoteAddress;
if (proxyAddressForwarding) {
// get proxy headers or remote address
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
} else {
// get remote address
remoteAddress = (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
}
return `${path}:${remoteAddress}`;
}
});
if (Array.isArray(slowDownOptions.randomDelayMs)) {
slowDownOptions.delayMs = () => {
const delayMs = Math.floor(Math.random() * (slowDownOptions.randomDelayMs[1] - slowDownOptions.randomDelayMs[0] + 1) + slowDownOptions.randomDelayMs[0]);
return delayMs;
}
}
if (Array.isArray(slowDownOptions.randomMaxDelayMs)) {
slowDownOptions.maxDelayMs = () => {
const maxDelayMs = Math.floor(Math.random() * (slowDownOptions.randomMaxDelayMs[1] - slowDownOptions.randomMaxDelayMs[0] + 1) + slowDownOptions.randomMaxDelayMs[0]);
return maxDelayMs;
}
}
if (typeof slowDownOptions.store === 'string') {
// load store
const store = slowDownOptions.store.split('#');
let StoreClass;
if (store.length === 2) {
const storeModule = require(store[0]);
if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
StoreClass = storeModule[store[1]];
slowDownOptions.store = new StoreClass(this, slowDownOptions);
} else {
throw new Error(`${store} cannot be found or is inaccessible`);
}
} else {
StoreClass = require(store[0]);
// create store
slowDownOptions.store = new StoreClass(this, slowDownOptions);
}
}
addRouter.use(path, slowDown(slowDownOptions));
}
});
if (addRouter.stack.length) {
serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
}
} catch (err) {
TraceUtils.error('An error occurred while validating speed limit configuration.');
TraceUtils.error(err);
TraceUtils.warn('Speed limit service is inactive due to an error occured while loading configuration.')
}
});
}
}