/* * Copyright (C) 2019 StApps * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import { SCNotFoundErrorResponse, SCRequestBodyTooLargeErrorResponse, SCSyntaxErrorResponse, SCUnsupportedMediaTypeErrorResponse, } from '@openstapps/core'; import {Logger} from '@openstapps/logger'; import config from 'config'; import cors from 'cors'; import {Express} from 'express'; import morgan from 'morgan'; import path from 'path'; import {configFile, DEFAULT_TIMEOUT, isTestEnvironment, mailer, plugins, validator} from './common'; import {getPrometheusMiddleware} from './middleware/prometheus'; import {MailQueue} from './notification/mail-queue'; import {bulkAddRouter} from './routes/bulk-add-route'; import {bulkDoneRouter} from './routes/bulk-done-route'; import {bulkRouter} from './routes/bulk-route'; import {indexRouter} from './routes/index-route'; import {multiSearchRouter} from './routes/multi-search-route'; import {pluginRegisterRouter} from './routes/plugin-register-route'; import {searchRouter} from './routes/search-route'; import {thingUpdateRouter} from './routes/thing-update-route'; import {virtualPluginRoute} from './routes/virtual-plugin-route'; import {BulkStorage} from './storage/bulk-storage'; import {DatabaseConstructor} from './storage/database'; /** * Configure the backend */ export async function configureApp(app: Express, databases: {[name: string]: DatabaseConstructor}) { let integrationTestTimeout: NodeJS.Timeout; // request loggers have to be the first middleware to be set in express app.use( morgan('dev', { skip: (_request, response) => { if (process.env.NODE_ENV === 'integration-test') { clearTimeout(integrationTestTimeout); integrationTestTimeout = setTimeout(() => { process.exit(1); }, DEFAULT_TIMEOUT); return false; } return response.statusCode < 400; }, stream: process.stdout, }), ); if (process.env.PROMETHEUS_MIDDLEWARE === 'true') { app.use(getPrometheusMiddleware()); } const corsOptions = { allowedHeaders: [ 'DNT', 'Keep-Alive', 'User-Agent', 'X-Requested-With', 'If-Modified-Since', 'Cache-Control', 'Content-Type', 'X-StApps-Version', ], credentials: true, maxAge: 1_728_000, methods: ['GET', 'POST', 'PUT', 'OPTIONS'], optionsSuccessStatus: 204, }; // allow all origins on all routes app.use(cors(corsOptions)); // TODO: See if it can handle options request with no content-type // allow cors preflight requests on every route app.options('*', [cors(corsOptions)]); // only accept json as content type for all requests app.use((request, response, next) => { // Only accept json as content type if (request.is('application/json') !== 'application/json') { // return an error in the response const error = new SCUnsupportedMediaTypeErrorResponse(isTestEnvironment); response.status(error.statusCode); response.json(error); return; } const bodyBuffer: Buffer[] = []; // we don't know the full size, the only way we can get is by adding up all individual chunk sizes let bodySize = 0; const chunkGatherer = (chunk: Buffer) => { bodySize += chunk.byteLength; // when adding each chunk size to the total size, check how large it now is. if (bodySize > configFile.backend.maxRequestBodySize) { request.off('data', chunkGatherer); request.off('end', endCallback); // return an error in the response const error = new SCRequestBodyTooLargeErrorResponse(isTestEnvironment); response.status(error.statusCode); response.json(error); return; } // push the chunk in the buffer bodyBuffer.push(chunk); }; const endCallback = () => { request.body = Buffer.concat(bodyBuffer).toString(); try { request.body = JSON.parse(request.body); next(); } catch (error) { const error_ = new SCSyntaxErrorResponse(error.message, isTestEnvironment); response.status(error_.statusCode); response.json(error_); return; } }; request.on('data', chunkGatherer).on('end', endCallback); }); // validate config file await validator.addSchemas(path.join('node_modules', '@openstapps', 'core', 'lib', 'schema')); // validate the config file const configValidation = validator.validate(configFile, 'SCConfigFile'); // validation failed if (configValidation.errors.length > 0) { throw new Error( `Validation of config file failed. Errors were: ${JSON.stringify(configValidation.errors)}`, ); } // check if a database name was given if (!config.has('internal.database.name')) { throw new Error('You have to configure a database'); } const database = new databases[config.get('internal.database.name')]( configFile, // mailQueue typeof mailer !== 'undefined' && config.has('internal.monitoring') ? new MailQueue(mailer) : undefined, ); await database.init(); if (typeof database === 'undefined') { throw new TypeError('No implementation for configured database found. Please check your configuration.'); } Logger.ok('Validated config file successfully'); // treats /foo and /foo/ as two different routes // see http://expressjs.com/en/api.html#app.set app.enable('strict routing'); // make the bulk storage available to all http middlewares/routes app.set('bulk', new BulkStorage(database)); app.set('env', process.env.NODE_ENV); // load routes before plugins // they now can be used or overwritten by any plugin app.use( bulkAddRouter, bulkDoneRouter, bulkRouter, indexRouter, multiSearchRouter, pluginRegisterRouter, searchRouter, thingUpdateRouter, ); // for plugins, as Express doesn't really want you to unregister routes (and doesn't offer any method to do so at all) app.all('*', async (request, response, next) => { // if the route exists then call virtual route on the plugin that registered that route if (plugins.has(request.originalUrl)) { try { response.json(await virtualPluginRoute(request, plugins.get(request.originalUrl)!)); } catch (error) { // in case of error send an error response response.status(error.statusCode); response.json(error); } } else { // pass to the next matching route (which is 404) next(); } }); // add a route for a missing resource (404) app.use((_request, response) => { const errorResponse = new SCNotFoundErrorResponse(isTestEnvironment); response.status(errorResponse.statusCode); response.json(errorResponse); }); }