300 lines
11 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
|