diff --git a/package.json b/package.json index ae4abd2e41c..07d1f90da86 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "debug-loader": "^0.0.1", "deepmerge": "^4.2.2", "express": "4.16.2", + "express-rate-limit": "^5.1.3", "fast-json-patch": "^2.0.7", "file-saver": "^1.3.8", "filesize": "^6.1.0", diff --git a/server.ts b/server.ts index c640a95ef44..202d5a58bcb 100644 --- a/server.ts +++ b/server.ts @@ -28,12 +28,13 @@ import * as compression from 'compression'; import * as cookieParser from 'cookie-parser'; import { join } from 'path'; -import { enableProdMode, NgModuleFactory, Type } from '@angular/core'; +import { enableProdMode } from '@angular/core'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import { hasValue, hasNoValue } from './src/app/shared/empty.util'; +import { hasNoValue, hasValue } from './src/app/shared/empty.util'; +import { UIServerConfig } from './src/config/ui-server-config.interface'; /* * Set path for the browser application's dist folder @@ -121,6 +122,19 @@ function cacheControl(req, res, next) { next(); } +/** + * Checks if the rateLimiter property is present + * When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled. + */ +if (hasValue((environment.ui as UIServerConfig).rateLimiter)) { + const RateLimit = require('express-rate-limit'); + const limiter = new RateLimit({ + windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs, + max: (environment.ui as UIServerConfig).rateLimiter.max + }); + app.use(limiter); +} + /* * Serve static resources (images, i18n messages, …) */ @@ -209,8 +223,9 @@ if (environment.ui.ssl) { certificate: certificate }); } else { + console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.'); - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation] pem.createCertificate({ days: 1, diff --git a/src/config/global-config.interface.ts b/src/config/global-config.interface.ts index 07ee4ca4440..0bc3d5eec4b 100644 --- a/src/config/global-config.interface.ts +++ b/src/config/global-config.interface.ts @@ -11,9 +11,10 @@ import { ItemPageConfig } from './item-page-config.interface'; import { CollectionPageConfig } from './collection-page-config.interface'; import { Theme } from './theme.inferface'; import {AuthConfig} from './auth-config.interfaces'; +import { UIServerConfig } from './ui-server-config.interface'; export interface GlobalConfig extends Config { - ui: ServerConfig; + ui: UIServerConfig; rest: ServerConfig; production: boolean; cache: CacheConfig; diff --git a/src/config/ui-server-config.interface.ts b/src/config/ui-server-config.interface.ts new file mode 100644 index 00000000000..93f90c345cd --- /dev/null +++ b/src/config/ui-server-config.interface.ts @@ -0,0 +1,14 @@ +import { ServerConfig } from './server-config.interface'; + +/** + * Server configuration related to the UI. + */ +export class UIServerConfig extends ServerConfig { + + // rateLimiter is used to limit the amount of requests a user is allowed make in an amount of time, in order to prevent overloading the server + rateLimiter?: { + windowMs: number; + max: number; + }; + +} diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 4f733396902..eb961a38eb5 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -13,6 +13,11 @@ export const environment: GlobalConfig = { port: 4000, // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript nameSpace: '/', + // The rateLimiter settings limit each IP to a "max" of 500 requests per "windowMs" (1 minute). + rateLimiter: { + windowMs: 1 * 60 * 1000, // 1 minute + max: 500 // limit each IP to 500 requests per windowMs + } }, // The REST API server settings. // NOTE: these must be "synced" with the 'dspace.server.url' setting in your backend's local.cfg. diff --git a/src/environments/mock-environment.ts b/src/environments/mock-environment.ts index 6e4d60e2686..b220c460836 100644 --- a/src/environments/mock-environment.ts +++ b/src/environments/mock-environment.ts @@ -19,6 +19,7 @@ export const environment: Partial = { port: 80, // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript nameSpace: '/angular-dspace', + rateLimiter: undefined }, // Caching settings cache: { diff --git a/yarn.lock b/yarn.lock index d44182cc519..cb6a29e9f5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4124,6 +4124,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" +express-rate-limit@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.1.3.tgz#656bacce3f093034976346958a0f0199902c9174" + integrity sha512-TINcxve5510pXj4n9/1AMupkj3iWxl3JuZaWhCdYDlZeoCPqweGZrxbrlqTCFb1CT5wli7s8e2SH/Qz2c9GorA== + express@4.16.2: version "4.16.2" resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c"