bbyars/mountebank

View on GitHub
src/mountebank.js

Summary

Maintainability
B
5 hrs
Test Coverage
'use strict';

const express = require('express'),
    cors = require('cors'),
    promClient = require('prom-client'),
    errorHandler = require('errorhandler'),
    path = require('path'),
    middleware = require('./util/middleware'),
    thisPackage = require('../package.json'),
    releases = require('../releases.json'),
    helpers = require('./util/helpers.js'),
    utilLogger = require('./util/logger.js'),
    utilIp = require('./util/ip.js'),
    protocolsModule = require('./models/protocols.js'),
    imposterRepositoryModule = require('./models/impostersRepository.js'),
    homeControllerModule = require('./controllers/homeController.js'),
    impostersControllerModule = require('./controllers/impostersController.js'),
    imposterControllerModule = require('./controllers/imposterController.js'),
    logsControllerModule = require('./controllers/logsController.js'),
    configControllerModule = require('./controllers/configController.js'),
    feedControllerModule = require('./controllers/feedController.js');

/**
 * The entry point for mountebank.  This module creates the mountebank server,
 * configures all middleware and manages all routing
 * @module
 */

/**
 * Creates the mountebank express application
 * @param {object} options - The command line options
 * @returns {Object} An object with a close method to stop the server
 */

function applyDefaults (options) {
    // Minimal defaults to start bypassing the CLI (e.g. embedding in an express app)
    const defaults = {
        port: 2525,
        ipWhitelist: ['*']
    };
    Object.keys(defaults).forEach(key => {
        options[key] = typeof options[key] === 'undefined' ? defaults[key] : options[key];
    });
}

async function createApp (options) {
    applyDefaults(options);

    const app = express(),
        hostname = options.host || 'localhost',
        baseURL = `http://${hostname}:${options.port}`,
        logger = utilLogger.createLogger(options),
        isAllowedConnection = utilIp.createIPVerification(options),
        imposters = imposterRepositoryModule.create(options, logger),
        protocols = protocolsModule.loadProtocols(options, baseURL, logger, isAllowedConnection, imposters),
        homeController = homeControllerModule.create(releases),
        impostersController = impostersControllerModule.create(
            protocols, imposters, logger, options.allowInjection),
        imposterController = imposterControllerModule.create(
            protocols, imposters, logger, options.allowInjection),
        logfile = options.log.transports.file ? options.log.transports.file.path : false,
        logsController = logsControllerModule.create(logfile),
        configController = configControllerModule.create(thisPackage.version, options),
        feedController = feedControllerModule.create(releases),
        validateImposterExists = middleware.createImposterValidator(imposters),
        prometheus = promClient;

    // Clear only matters when bound using directly in-process through JS rather than the CLI
    prometheus.register.clear();
    prometheus.collectDefaultMetrics({ prefix: 'mb_' });

    app.use(middleware.useAbsoluteUrls(options.port));
    app.use(middleware.validateApiKey(options.apikey, logger));
    app.use(middleware.logger(logger, ':method :url'));
    app.use(middleware.globals({ port: options.port, version: thisPackage.version }));
    app.use(middleware.defaultIEtoHTML);
    app.use(middleware.json(logger));
    app.use(express.static(path.join(__dirname, 'public')));
    app.use(express.static(path.join(__dirname, '../node_modules')));
    app.use(errorHandler());
    app.use(cors({ origin: options.origin }));

    app.disable('etag');
    app.disable('x-powered-by');
    app.set('views', path.join(__dirname, 'views'));
    app.set('view engine', 'ejs');
    app.set('json spaces', 2);

    app.get('/', homeController.get);
    app.get('/imposters', impostersController.get);
    app.post('/imposters', impostersController.post);
    app.delete('/imposters', impostersController.del);
    app.put('/imposters', impostersController.put);
    app.get('/imposters/:id', validateImposterExists, imposterController.get);
    app.delete('/imposters/:id', imposterController.del);
    app.delete('/imposters/:id/savedProxyResponses', validateImposterExists, imposterController.resetProxies);
    app.delete('/imposters/:id/savedRequests', validateImposterExists, imposterController.resetRequests);

    // deprecated but saved for backwards compatibility
    app.delete('/imposters/:id/requests', validateImposterExists, imposterController.resetProxies);

    // Changing stubs without restarting imposter
    app.put('/imposters/:id/stubs', validateImposterExists, imposterController.putStubs);
    app.put('/imposters/:id/stubs/:stubIndex', validateImposterExists, imposterController.putStub);
    app.post('/imposters/:id/stubs', validateImposterExists, imposterController.postStub);
    app.delete('/imposters/:id/stubs/:stubIndex', validateImposterExists, imposterController.deleteStub);

    // Protocol implementation APIs
    app.post('/imposters/:id/_requests', validateImposterExists, imposterController.postRequest);
    app.post('/imposters/:id/_requests/:proxyResolutionKey', validateImposterExists, imposterController.postProxyResponse);

    app.get('/logs', logsController.get);
    app.get('/config', configController.get);
    app.get('/feed', feedController.getFeed);
    app.get('/releases', feedController.getReleases);
    app.get('/releases/:version', feedController.getRelease);

    app.get('/metrics', async function (request, response) {
        const register = promClient.register;
        response.set('Content-Type', register.contentType);
        response.end(await register.metrics());
    });

    app.get('/sitemap', (request, response) => {
        response.type('text/plain');
        response.render('sitemap', { releases: releases });
    });

    [
        '/support',
        '/license',
        '/faqs',
        '/docs/gettingStarted',
        '/docs/mentalModel',
        '/docs/commandLine',
        '/docs/communityExtensions',
        '/docs/security',
        '/docs/api/overview',
        '/docs/api/contracts',
        '/docs/api/mocks',
        '/docs/api/stubs',
        '/docs/api/predicates',
        '/docs/api/xpath',
        '/docs/api/json',
        '/docs/api/jsonpath',
        '/docs/api/proxies',
        '/docs/api/injection',
        '/docs/api/behaviors',
        '/docs/api/errors',
        '/docs/api/faults',
        '/docs/protocols/http',
        '/docs/protocols/https',
        '/docs/protocols/tcp',
        '/docs/protocols/smtp',
        '/docs/protocols/custom'
    ].forEach(endpoint => {
        app.get(endpoint, (request, response) => {
            response.render(endpoint.substring(1));
        });
    });

    process.once('exit', () => {
        imposters.stopAllSync();
    });

    if (options.allowInjection) {
        logger.warn(`Running with --allowInjection set. See ${baseURL}/docs/security for security info`);
    }

    await imposters.loadAll(protocols);

    return app;
}

/**
 * Start the mountebank server
 * @param {function} app - mountebank express application
 * @param {object} options - The command line options
 * @returns {Object} An object with a close method to stop the server
 */
async function listen (app, options) {
    const hostname = options.host || 'localhost',
        baseURL = `http://${hostname}:${options.port}`,
        logger = utilLogger.createLogger(options),
        isAllowedConnection = utilIp.createIPVerification(options);

    return new Promise(resolve => {
        const connections = {},
            server = app.listen(options.port, options.host, () => {
                logger.info(`mountebank v${thisPackage.version} now taking orders - point your browser to ${baseURL}/ for help`);
                logger.debug(`config: ${JSON.stringify({
                    options: options,
                    process: {
                        nodeVersion: process.version,
                        architecture: process.arch,
                        platform: process.platform
                    }
                })}`);

                resolve({
                    close: callback => {
                        server.close(() => {
                            logger.info('Adios - see you soon?');
                            callback();
                        });

                        // Force kill any open connections to prevent process hanging
                        Object.keys(connections).forEach(socket => {
                            connections[socket].destroy();
                        });
                    }
                });
            });

        server.on('connection', socket => {
            const name = helpers.socketName(socket),
                ipAddress = socket.remoteAddress;
            connections[name] = socket;

            socket.on('close', () => {
                delete connections[name];
            });

            socket.on('error', error => {
                logger.error(`${name} transmission error X=> ${JSON.stringify(error)}`);
            });

            if (!isAllowedConnection(ipAddress, logger)) {
                socket.destroy();
            }
        });
    });
}

/**
 * Creates the mountebank server
 * @param {object} options - The command line options
 * @returns {Object} An object with a close method to stop the server
 */
async function create (options) {
    const app = await createApp(options);
    return listen(app, options);
}

module.exports = { create, createApp };