UNPKG

@universis/janitor

Version:

Universis api plugin for handling user authorization and rate limiting

232 lines (222 loc) 9.58 kB
import { ApplicationService, TraceUtils } from '@themost/common'; import { rateLimit } from 'express-rate-limit'; import express from 'express'; import path from 'path'; import { BehaviorSubject } from 'rxjs'; export class RateLimitService extends ApplicationService { /** * @param {import('@themost/express').ExpressDataApplication} app */ 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: RateLimitService }>} */ 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'; }); } // if found, move the last middleware to be after expressInit if (index > -1) { // move the last middleware to be after expressInit stack.splice(index + 1, 0, stack.pop()); } } 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 rate limit configuration.'); TraceUtils.error(err); TraceUtils.warn('Rate limit service is inactive due to an error occured while loading configuration.') } }); } /** * Returns the service router that is used to register rate limit middleware. * @returns {import('rxjs').BehaviorSubject<import('express').Router | import('express').Application>} The service router. */ getServiceContainer() { return this.getApplication() && this.getApplication().serviceRouter; } /** * Returns the service name. * @returns {string} The service name. */ getServiceName() { return '@universis/janitor#RateLimitService'; } /** * Returns the service configuration. * @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/rateLimit'); 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/rateLimit`); serviceConfiguration = Object.assign({}, { profiles: [], paths: [] }, serviceConfigurationSource); } } const pathsArray = serviceConfiguration.paths || []; const profilesArray = serviceConfiguration.profiles || []; // create maps serviceConfiguration.paths = new Map(pathsArray); serviceConfiguration.profiles = new Map(profilesArray); // set service configuration Object.defineProperty(this, 'serviceConfiguration', { value: serviceConfiguration, writable: false, enumerable: false, configurable: true }); return this.serviceConfiguration; } /** * Sets the rate limit configuration for a specific path. * @param {string} path * @param {{ profile: string } | import('express-rate-limit').Options} options * @returns {RateLimitService} 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 } /** * @type { import('express-rate-limit').Options } */ const rateLimitOptions = Object.assign({ windowMs: 5 * 60 * 1000, // 5 minutes limit: 50, // 50 requests legacyHeaders: true // send headers }, 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 (typeof rateLimitOptions.store === 'undefined') { const StoreClass = this.getStoreType(); if (typeof StoreClass === 'function') { rateLimitOptions.store = new StoreClass(this, rateLimitOptions); } } this.router.use(path, rateLimit(rateLimitOptions)); return this; } /** * @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; } } /** * Unsets the rate limit configuration for a specific path. * @param {string} path * @returns {RateLimitService} 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; } }