bridge-node-server/node_server/swagger_api/api_server.js
Martin Donnelly 57bd6c8e6a init
2018-06-24 21:15:03 +01:00

300 lines
11 KiB
JavaScript

/* eslint-disable filenames/match-exported */
/* eslint import/max-dependencies: ["error", {"max": 15}] */
'use strict';
/**
* The core page for the configuration and deployment of the API server for
* the Web Console.
*
* The API server is powered by a Swagger API definition:
* @see {@link http://swagger.io}
*
* Express middleware is then used to take the Swagger API definition and
* handle most of the essential but repetitive parts of the API:
* - Connecting routes to handler functions
* - Checking security
* - Validating paramters
* - Validating reponses
* - Managing CORS responses
*
* In development mode there is also middleware to serve interactive API
* documentation and the API doc itself.
*/
const config = require(global.configFile);
const log = require(global.pathPrefix + 'log.js');
const sessionTimeout = require(global.pathPrefix + 'utils.js').sessionTimeout;
const _ = require('lodash');
const express = require('express');
const compression = require('compression');
const session = require('express-session');
const morgan = require('morgan'); // Logging middleware by expressjs
const MongoStore = require('connect-mongo')(session);
const swaggerTools = require('swagger-tools');
const RateLimit = require('express-rate-limit');
const router = express.Router();
const corsMiddleware = require('./api_cors_middleware.js');
const security = require('./api_security.js');
const securityDevice = require('./api_security_device.js');
const errorHandler = require('./api_error_handler.js');
const expiryMiddleware = require('./api_expiry_middleware');
const bodyParserMiddleware = require('./api_body_middleware.js');
const initMorgan = require(global.pathPrefix + '../utils/init_morgan.js');
const JsonRefs = require('json-refs');
//
// Export the router
//
module.exports = initWebConsoleApi;
//
// Swagger Router configuration
// @see {@link https://github.com/apigee-127/swagger-tools/blob/master/docs/Middleware.md#swagger-router}
//
const swaggerRouterOptions = {
// @member {String} - path to the controllers
controllers: global.rootPath + 'swagger_api/controllers',
// @member {Boolean} - enable autogenerated stubs for dev environment
useStubs: config.isDevEnv
};
//
// Swagger Validator configuration options
// @see {@link https://github.com/apigee-127/swagger-tools/blob/master/docs/Middleware.md#swagger-validator}
//
const swaggerValidatorOptions = {
// @member{Boolean} - validate responses as well as requests
// swagger stubs don't match the validation entirely, so responses can't
// be validated if they are enabled.
validateResponse: Boolean(swaggerRouterOptions.useStubs)
};
/**
* Function to intialise the swagger tools for serving the swagger-based
* web console API. This also uses express-session persisted in the mongo
* database, so requires the connection parameters to be passed through.
*
* @param {string} mongoConnectString - mongo db connect string
* @param {Object} mongoOpts - mongo db connect options
* @param {string} collection - the collection for persisting sessions
*
* @returns {Object} - router with middleware included
*/
async function initWebConsoleApi(mongoConnectString, mongoOpts, collection) {
try {
//
// Resolve any external references in the swagger file.
//
const resolved = await JsonRefs.resolveRefsAt(require.resolve('./api_swagger_def.json'));
const swaggerDoc = resolved.resolved;
//
// We are going to be used as an express router under /api so remove that from
// the front of the base path in the swagger API definition. If we don't
// remove it we end up with a path of /api/api/v0/...
//
swaggerDoc.basePath = swaggerDoc.basePath.replace('/api', '');
//
// Set up the retry behaviour so that it is more manageable in most cases.
// This should be updated in the long run, but for now we are just
// mitigating the issues. For more details see:
// Task: {T580} Express Session + Connect Mongo will fail forever if
// Database offline for >30s (or longer after mitigation)
// {@link http://10.0.10.242/T580}
//
let opts = _.clone(mongoOpts);
opts = _.merge({}, opts, {
autoReconnect: true, // Enable reconnecting in the driver
reconnectTries: 1000, // Retry connection 1000 times
reconnectInterval: 1000, // 1000 ms (1s) between retries
bufferMaxEntries: 0 // Don't cache queries on failure
});
//
// Create the persistent session store
// @see {@link https://github.com/kcbanner/connect-mongo}
//
const store = new MongoStore({
url: mongoConnectString,
mongoOptions: opts,
ttl: sessionTimeout * 60, // Convert to seconds
autoRemove: 'ignore', // Use a TTL index in mongo db to delete
touchAfter: 60, // Only update the session token every 1 min
collection
});
// Catch errors
store.on('error', (error) => {
log.system(
'ERROR',
('Error connecting to Session database. ' + error),
'MongoDbStore',
'',
'System',
'127.0.0.1');
});
//
// Session handling configuration
// @see {@link https://github.com/expressjs/session}
//
const cookieName = swaggerDoc.securityDefinitions.bridge_session['x-session-cookie'];
const sessionOptions = {
name: cookieName, // Cookie name
secret: config.webconsole.cookieSecret, // Cookie secret key
cookie: {
path: '/api', // Only applies to the API path
httpOnly: true, // Not accessible by javascript running on the page
secure: true, // Only available over HTTPS
maxAge: null // Session cookie
},
resave: false, // Don't resave if nothing changes
rolling: false, // We'll manage session timeout ourselves
saveUninitialized: false, // Only use sessions for logged in users
unset: 'destroy', // Delete the session storage when it is cleared
store // Persistent session storage to MongoDb
};
//
// Initialise the morgan format
//
initMorgan.init();
//
// Rate limiting options
// Warning: we must clone the value from config so that when we change the
// keyGenerator etc. it doesn't affect other places using the same
// config.
//
const rateLimitConfig = _.clone(config.rateLimits.api);
rateLimitConfig.keyGenerator = function(req) {
//
// Limit per-client if we know who the client is, or by IP if we don't
//
if (req.session && req.session.data) {
return req.session.data.clientID;
} else {
return req.ip;
}
};
rateLimitConfig.handler = function(req, res) {
// Always send a JSON response
res.status(rateLimitConfig.statusCode).json({
code: 30500,
info: 'Rate limit reached. Please wait and try again'
});
};
const limiter = new RateLimit(rateLimitConfig);
//
// Initialize the Swagger middleware from the Swagger API definition.
// This is asynchronous so we need to wait until its done before configuring
// all the express middleware we will use for managing the API
//
swaggerTools.initializeMiddleware(swaggerDoc, (middleware) => {
//
// Compression middleware
//
router.use(compression());
//
// Custom body-parser to store the raw body as well as the parsed JSON body.
// Swagger tools automatically uses the rsults of this rather than its default parser.
//
router.use(bodyParserMiddleware.bridgeBodyParser());
//
// Logging middleware
//
router.use(morgan('bridge-combined'));
//
// Middleware to interpret Swagger resources and attach metadata to request
// - must be first in swagger - tools middleware chain
//
router.use(middleware.swaggerMetadata());
//
// Enable session handling
//
router.use(session(sessionOptions));
/*
* Rate Limiting
*/
router.use(limiter);
//
// Cors middleware
//
router.use(corsMiddleware());
//
// Session expiry reporting middleware
//
router.use(expiryMiddleware);
//
// Middleware to enforce the security rules definedin the Swagger file.
// Ignore lack of camel case for the swagger defines:
router.use(middleware.swaggerSecurity({
awaiting_accept_eula_bridge_session: security.awaitingAcceptEulaSession,
awaiting_2fa_bridge_session: security.awaiting2FASession,
bridge_session: security.bridgeSession,
elevated_bridge_session: security.elevatedBridgeSession,
recovery_session: security.recoverySession,
device_session: securityDevice.deviceSession,
device_hmac_nosession: securityDevice.deviceHmacNoSession
}));
//
// Middleware to validate Swagger request and response parameters
//
router.use(middleware.swaggerValidator(swaggerValidatorOptions));
//
// Middleware to route validated requests to the appropriate controller
//
router.use(middleware.swaggerRouter(swaggerRouterOptions));
//
// Middleware to serve the Swagger documents and Swagger UI.
// This provides access to the Swagger UI at /api/docs and the full
// swagger json file at /api/api-docs
// Note: only enabled in development environments
//
if (config.isDevEnv) {
router.use(middleware.swaggerUi());
}
//
// Error handler middleware to correct server errors as JSON if needed
//
router.use(errorHandler.errorHandlerMiddleware);
//
// Stop any requests that didn't get handled above going any further.
// This only applies to requests under this router, so no other part of
// server could handle it.
//
router.use((req, res) => {
res.status(404).json({
code: 30000,
info: 'API path not found'
});
});
});
return router;
} catch (error) {
// Failed to retreive swagger references
// eslint-disable-next-line no-console
console.log('Failed to read the swagger definition files: ' + error.toString());
return null;
}
}