bbyars/mountebank

View on GitHub
src/models/http/baseHttpServer.js

Summary

Maintainability
F
3 days
Test Coverage
F
8%
'use strict';

/**
 * The base implementation of http/s servers
 * @module
 */

const net = require('net'),
    headersMap = require('./headersMap.js'),
    errors = require('../../util/errors.js'),
    httpProxy = require('./httpProxy.js'),
    httpRequest = require('./httpRequest.js'),
    helpers = require('../../util/helpers.js');

module.exports = function (createBaseServer) {

    function create (options, logger, responseFn) {
        const connections = {},
            defaultResponse = options.defaultResponse || {};

        function postProcess (stubResponse, request) {
            /* eslint complexity: 0 */
            const defaultHeaders = defaultResponse.headers || {},
                response = {
                    statusCode: stubResponse.statusCode || defaultResponse.statusCode || 200,
                    headers: stubResponse.headers || defaultHeaders,
                    body: stubResponse.body || defaultResponse.body || '',
                    _mode: stubResponse._mode || defaultResponse._mode || 'text'
                },
                responseHeaders = headersMap.of(response.headers),
                encoding = response._mode === 'binary' ? 'base64' : 'utf8',
                isObject = helpers.isObject;

            if (isObject(response.body)) {
                // Support JSON response bodies
                response.body = JSON.stringify(response.body, null, 4);
            }

            if (options.allowCORS) {
                const requestHeaders = headersMap.of(request.headers),
                    isCrossOriginPreflight = request.method === 'OPTIONS' &&
                        requestHeaders.get('Access-Control-Request-Headers') &&
                        requestHeaders.get('Access-Control-Request-Method') &&
                        requestHeaders.get('Origin');

                if (isCrossOriginPreflight) {
                    responseHeaders.set('Access-Control-Allow-Headers', requestHeaders.get('Access-Control-Request-Headers'));
                    responseHeaders.set('Access-Control-Allow-Methods', requestHeaders.get('Access-Control-Request-Method'));
                    responseHeaders.set('Access-Control-Allow-Origin', requestHeaders.get('Origin'));
                }
            }

            if (encoding === 'base64') {
                // ensure the base64 has no newlines or other non
                // base64 chars that will cause the body to be garbled.
                response.body = response.body.replace(/[^A-Za-z0-9=+/]+/g, '');
            }

            if (!responseHeaders.has('Connection')) {
                responseHeaders.set('Connection', 'close');
            }

            if (responseHeaders.has('Content-Length')) {
                responseHeaders.set('Content-Length', Buffer.byteLength(response.body, encoding));
            }

            return response;
        }

        const baseServer = createBaseServer(options),
            server = baseServer.createNodeServer();

        // Allow long wait behaviors
        server.timeout = 0;

        server.on('connect', (response, client, head) => {
            const host = response.socket.servername;
            const port = response.socket.localPort;
            // may we prefer this method instead?
            const proxyServer = net.createConnection({ host, port }, () => {
                client.on('error', err => {
                    logger.warn('CLIENT TO PROXY ERROR: [%s] %s', err.code, err.message);
                    logger.debug('%s', err.stack);
                });
                proxyServer.on('error', err => {
                    logger.warn('SERVER PROXY ERROR: [%s] %s', err.code, err.message);
                    logger.debug('%s', err.stack);
                });

                logger.info('PROXY TO SERVER SET UP TO %s ON %s', host, port);
                client.write('HTTP/1.1 200 Connection established\r\n\r\n');
                proxyServer.write(head);
                proxyServer.pipe(client);
                client.pipe(proxyServer);
            });
        });

        server.on('connection', socket => {
            const name = helpers.socketName(socket);

            logger.debug('%s ESTABLISHED', name);

            if (socket.on) {
                connections[name] = socket;

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

                socket.on('end', () => {
                    logger.debug('%s LAST-ACK', name);
                });

                socket.on('close', () => {
                    logger.debug('%s CLOSED', name);
                    delete connections[name];
                });
            }
        });

        server.on('request', async (request, response) => {
            const clientName = helpers.socketName(request.socket);

            logger.info(`${clientName} => ${request.method} ${request.url}`);

            try {
                const simplifiedRequest = await httpRequest.createFrom(request);
                logger.debug('%s => %s', clientName, JSON.stringify(simplifiedRequest));

                const mbResponse = await responseFn(simplifiedRequest, { rawUrl: request.url }),
                    stubResponse = postProcess(mbResponse, simplifiedRequest),
                    encoding = stubResponse._mode === 'binary' ? 'base64' : 'utf8';

                if (mbResponse.blocked) {
                    request.socket.destroy();
                    return;
                }

                if (helpers.simulateFault(request.socket, mbResponse.fault, logger)) {
                    return;
                }

                response.writeHead(stubResponse.statusCode, stubResponse.headers);
                response.end(stubResponse.body.toString(), encoding);

                if (stubResponse) {
                    logger.debug('%s <= %s', clientName, JSON.stringify(stubResponse));
                }
            }
            catch (error) {
                const exceptions = errors;
                logger.error('%s X=> %s', clientName, JSON.stringify(exceptions.details(error)));
                response.writeHead(500, { 'content-type': 'application/json' });
                response.end(JSON.stringify({ errors: [exceptions.details(error)] }), 'utf8');
            }
        });

        return new Promise((resolve, reject) => {
            server.on('error', error => {
                if (error.errno === 'EADDRINUSE') {
                    reject(errors.ResourceConflictError(`Port ${options.port} is already in use`));
                }
                else if (error.errno === 'EACCES') {
                    reject(errors.InsufficientAccessError());
                }
                else {
                    reject(error);
                }
            });

            // Bind the socket to a port (the || 0 bit auto-selects a port if one isn't provided)
            server.listen(options.port || 0, options.host, () => {
                resolve({
                    port: server.address().port,
                    metadata: baseServer.metadata,
                    close: callback => {
                        server.close(callback);
                        Object.keys(connections).forEach(socket => {
                            connections[socket].destroy();
                        });
                    },
                    proxy: httpProxy.create(logger),
                    encoding: 'utf8'
                });
            });
        });
    }

    return {
        testRequest: {
            requestFrom: '',
            method: 'GET',
            path: '/',
            query: {},
            headers: {},
            form: {},
            body: ''
        },
        testProxyResponse: {
            statusCode: 200,
            headers: {},
            body: ''
        },
        create: create,
        validate: undefined
    };
};