UNPKG

@universis/janitor

Version:

Universis api plugin for handling user authorization and rate limiting

236 lines (222 loc) 9.71 kB
import { ApplicationService, TraceUtils } from '@themost/common'; import slowDown from 'express-slow-down'; import express from 'express'; import path from 'path'; import { BehaviorSubject } from 'rxjs'; export class SpeedLimitService extends ApplicationService { constructor(app) { super(app); // get proxy address forwarding option const proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding'); this.proxyAddressForwarding = typeof proxyAddressForwarding !== 'boolean'? false : proxyAddressForwarding; /** * @type {BehaviorSubject<{ target: SpeedLimitService }>} */ this.loaded = new BehaviorSubject(null); const serviceContainer = this.getServiceContainer(); if (serviceContainer == null) { TraceUtils.warn(`${this.getServiceName()} is being started but the parent router seems to be unavailable.`); return; } serviceContainer.subscribe(router => { if (router == null) { return; } try { // set router for further processing Object.defineProperty(this, 'router', { value: express.Router(), writable: false, enumerable: false, configurable: true }); const serviceConfiguration = this.getServiceConfiguration(); // create maps const paths = serviceConfiguration.paths; if (paths.size === 0) { TraceUtils.warn(`${this.getServiceName()} is being started but the collection of paths is empty.`); } paths.forEach((value, path) => { this.set(path, value); }); if (router.stack) { router.stack.unshift.apply(router.stack, this.router.stack); } else { // use router router.use(this.router); // get router stack (use a workaround for express 4.x) const stack = router._router && router._router.stack; if (Array.isArray(stack)) { // stage #1 find logger middleware (for supporting request logging) let index = stack.findIndex((item) => { return item.name === 'logger'; }); if (index === -1) { // stage #2 find expressInit middleware index = stack.findIndex((item) => { return item.name === 'expressInit'; }); } } else { TraceUtils.warn(`${this.getServiceName()} is being started but the container stack is not available.`); } } // notify that the service is loaded this.loaded.next({ target: this }); } 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.') } }); } /** * @returns {function} The type of store used for rate limiting. */ getStoreType() { const serviceConfiguration = this.getServiceConfiguration(); if (typeof serviceConfiguration.storeType !== 'string') { return; } let StoreClass; const store = serviceConfiguration.storeType.split('#'); if (store.length === 2) { const storeModule = require(store[0]); if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) { StoreClass = storeModule[store[1]]; return StoreClass } else { throw new Error(`${store} cannot be found or is inaccessible`); } } else { StoreClass = require(store[0]); return StoreClass; } } /** * Returns the service name. * @returns {string} The service name. */ getServiceName() { return '@universis/janitor#SpeedLimitService'; } /** * Returns the service router that is used to register speed limit middleware. * @returns {import('express').Router | import('express').Application} The service router. */ getServiceContainer() { return this.getApplication() && this.getApplication().serviceRouter; } /** * Sets the speed limit configuration for a specific path. * @param {string} path * @param {{ profile: string } | import('express-slow-down').Options} options * @returns {SpeedLimitService} The service instance for chaining. */ set(path, options) { let opts; // get profile if (options.profile) { opts = this.serviceConfiguration.profiles.get(options.profile); } else { // or options defined inline opts = options } const slowDownOptions = Object.assign({ windowMs: 5 * 60 * 1000, // 5 minutes delayAfter: 20, // 20 requests delayMs: 500, // 500 ms maxDelayMs: 10000 // 10 seconds }, opts, { keyGenerator: (req) => { let remoteAddress; if (this.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 === 'undefined') { const StoreClass = this.getStoreType(); if (typeof StoreClass === 'function') { slowDownOptions.store = new StoreClass(this, slowDownOptions); } } this.router.use(path, slowDown(slowDownOptions)); return this; } /** * Unsets the speed limit configuration for a specific path. * @param {string} path * @return {SpeedLimitService} The service instance for chaining. */ unset(path) { const index =this.router.stack.findIndex((layer) => { return layer.route && layer.route.path === path; }); if (index !== -1) { this.router.stack.splice(index, 1); } return this; } /** * * @returns {{extends?: string, profiles?: Array, paths?: Array}} The service configuration. */ getServiceConfiguration() { if (this.serviceConfiguration) { return this.serviceConfiguration; } let serviceConfiguration = { profiles: [], paths: [] }; // get service configuration const serviceConfigurationSource = this.getApplication().getConfiguration().getSourceAt('settings/universis/janitor/speedLimit'); if (serviceConfigurationSource) { if (typeof serviceConfigurationSource.extends === 'string') { // get additional configuration const configurationPath = this.getApplication().getConfiguration().getConfigurationPath(); const extendsPath = path.resolve(configurationPath, serviceConfigurationSource.extends); TraceUtils.log(`${this.getServiceName()} will try to extend service configuration using ${extendsPath}`); serviceConfiguration = Object.assign({}, { profiles: [], paths: [] }, require(extendsPath)); } else { TraceUtils.log(`${this.getServiceName()} will use service configuration from settings/universis/janitor/speedLimit`); serviceConfiguration = Object.assign({}, { profiles: [], paths: [] }, serviceConfigurationSource); } } const profilesArray = serviceConfiguration.profiles || []; serviceConfiguration.profiles = new Map(profilesArray); const pathsArray = serviceConfiguration.paths || []; serviceConfiguration.paths = new Map(pathsArray); Object.defineProperty(this, 'serviceConfiguration', { value: serviceConfiguration, writable: false, enumerable: false, configurable: true }); return this.serviceConfiguration; } }