/* * 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 * as config from 'config'; import * as cors from 'cors'; import * as express from 'express'; import * as morgan from 'morgan'; import {join} from 'path'; import {configFile, isTestEnvironment, mailer, plugins, validator} from './common'; 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'; import {Elasticsearch} from './storage/elasticsearch/elasticsearch'; /** * Created express application */ export const app = express(); /** * Configure the backend */ export async function configureApp() { // request loggers have to be the first middleware to be set in express app.use(morgan('dev')); // only accept json as content type for all requests app.use((req, res, next) => { // get the content type let contentType = ''; // the content type can be string, string[] or undefined if (typeof req.headers['Content-Type'] === 'string') { // weird type definitions require an explicit cast contentType = req.headers['Content-Type'] as string; } else if (typeof req.headers['content-type'] === 'string') { // weird type definitions require no cast here though... contentType = req.headers['content-type']; } // Only accept json as content type if (contentType === '' || contentType.match(/^application\/json$/) === null) { // return an error in the response const err = new SCUnsupportedMediaTypeErrorResponse(isTestEnvironment); res.status(err.statusCode); res.json(err); 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) { req.off('data', chunkGatherer); req.off('end', endCallback); // return an error in the response const err = new SCRequestBodyTooLargeErrorResponse(isTestEnvironment); res.status(err.statusCode); res.json(err); return; } // push the chunk in the buffer bodyBuffer.push(chunk); }; const endCallback = () => { req.body = Buffer.concat(bodyBuffer) .toString(); try { req.body = JSON.parse(req.body); next(); } catch (catchErr) { const err = new SCSyntaxErrorResponse(catchErr.message, isTestEnvironment); res.status(err.statusCode); res.json(err); return; } }; req.on('data', chunkGatherer) .on('end', endCallback); }); const databases: {[name: string]: DatabaseConstructor; } = { elasticsearch: Elasticsearch, }; // validate config file await validator.addSchemas(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 Error('No implementation for configured database found. Please check your configuration.'); } Logger.ok('Validated config file sucessfully'); // 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); const corsOptions = { allowedHeaders: [ 'DNT', 'Keep-Alive', 'User-Agent', 'X-Requested-With', 'If-Modified-Since', 'Cache-Control', 'Content-Type', 'X-StApps-Version', ], credentials: true, maxAge: 1728000, 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)); // 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 (req, res, next) => { // if the route exists then call virtual route on the plugin that registered that route if (plugins.has(req.originalUrl)) { try { res.json(await virtualPluginRoute(req, plugins.get(req.originalUrl)!)); } catch (e) { // in case of error send an error response res.status(e.statusCode); res.json(e); } } else { // pass to the next matching route (which is 404) next(); } }); // add a route for a missing resource (404) app.use((_req, res) => { const errorResponse = new SCNotFoundErrorResponse(isTestEnvironment); res.status(errorResponse.statusCode); res.json(errorResponse); }); }