RackHD/on-http

View on GitHub
lib/services/http-service.js

Summary

Maintainability
F
4 days
Test Coverage
// Copyright 2015, EMC, Inc.

'use strict';

var di = require('di');
var express = require('express');
var cors = require('cors');
var onFinished = require('on-finished');
var http = require('http');
var https = require('https');
var fs = require('fs');
var proxyMiddleware = require('http-proxy-middleware');
var path = require('path');
var bodyParser = require('body-parser');
var rewriter = require('express-urlrewrite');
var yaml = require('js-yaml');

/**
 * Get remote address of the client.
 * @private
 * @param {express.Request} req from express
 * @returns {String|Undefined} either the ip of requester or undefined
 *                             if unavailable
 */
function remoteAddress(req) {

    if(req.get("X-Real-IP")) {
        return req.get("X-Real-IP");
    }

    if (req.ip) {
        return req.ip;
    }

    if (req._remoteAddress) {
        return req._remoteAddress;
    }

    if (req.connection) {
        return req.connection.remoteAddress;
    }

    return undefined;
}

module.exports = httpServiceFactory;

di.annotate(httpServiceFactory, new di.Provide('Http.Server'));
di.annotate(httpServiceFactory,
    new di.Inject(
        'Protocol.Events',
        'Services.Configuration',
        'Services.Lookup',
        'swagger',
        'Logger',
        'Promise',
        'Events',
        'Errors',
        'Http.Services.SkuPack',
        '_',
        'Auth.Services',
        di.Injector,
        'uuid',
        'Constants'
    )
);

/**
 * Factory that creates the express http service
 * @private
 * @param {Protocol.Events} eventsProtocol
 * @param {Services.Configuration} configuration
 * @param {Services.Lookup} lokupService
 * @param swagger
 * @param Logger
 * @param Q
 * @param {Events} events
 * @param Errors
 * @param injector
 * @returns {function} HttpService constructor
 */
function httpServiceFactory(
    eventsProtocol,
    configuration,
    lookupService,
    swagger,
    Logger,
    Promise,
    events,
    Errors,
    skuService,
    _,
    authService,
    injector,
    uuid,
    constants
) {
    var logger = Logger.initialize(httpServiceFactory);

    function HttpService(endpoint) {
        this.app = express();
        this.endpoint = this._parseEndpoint(endpoint);
        this.server = null;
        authService.init();
        this._setup();
    }

    HttpService.prototype.start = function() {
        var self = this;
        // Create Http Proxy
        if (this.endpoint.proxiesEnabled) {
            var httpProxies = configuration.get('httpProxies');
            if (httpProxies && _.isArray(httpProxies)) {
                _.forEach(httpProxies, createHttpProxy.bind(this, self.app));
            }
        }

        if (this.endpoint.httpsEnabled) {
            this.server = https.createServer(this.httpsOptions, this.app);
        } else {
            this.server = http.createServer(this.app);
        }

        this.server.on('close', function() {
            logger.info('Server Closing.');
        });

        return new Promise(function(resolve, reject) {
            return self.server.listen(
                self.endpoint.port,
                self.endpoint.address,
                function(error) {
                    return error ? reject(error) : resolve();
            });
        });
    };

    HttpService.prototype.stop = function() {
        var self = this;
        return new Promise(function(resolve, reject) {
            return self.server.close(
                function(error) {
                    return error ? reject(error) : resolve();
            });
        });
    };

    HttpService.prototype.patchYamlConfig =
        function(endpoint, swaggerConfig, di) {
            di = di && typeof di === 'object' ? di : {};
            var _fs = di.fs || fs,
                _logger = di.logger || logger,
                _Promise = di.Promise || Promise,
                _yaml = di.yaml || yaml;
            try {
                var configJson =
                    _yaml.safeLoad(
                        _fs.readFileSync(swaggerConfig.swagger, 'utf8'));
                swaggerConfig.swagger =
                    swaggerConfig.swagger.replace(/\.ya?ml/, '-patch.yaml');
                configJson.schemes =
                    [endpoint.httpsEnabled ? 'https' : 'http'];
                _fs.writeFileSync(swaggerConfig.swagger,
                    _yaml.safeDump(configJson));
                return _Promise.resolve(swaggerConfig);
            } catch (err) {
              _logger.error(err);
              return _Promise.reject(err);
            }
        };

    HttpService.prototype.createSwagger = function() {
        var self = this;
        var app = this.app;
        var endpoint = this.endpoint;

        //Swagger support
        var swaggerConfig = {
            appRoot: path.resolve(__dirname, '../../'),
            configDir: path.resolve(__dirname, '../../swagger_config'),
            authEnabled: this.endpoint.authEnabled
        };


        var swaggerCreateAsync = Promise.promisify(swagger.create);
        return Promise.each(endpoint.yamlName, function(swaggerFileName) {
            swaggerConfig.swagger = path.resolve(__dirname, '../../static/' + swaggerFileName);
            self.patchYamlConfig(endpoint, swaggerConfig);
            return swaggerCreateAsync(swaggerConfig)
                .then(function(swaggerExpress) {
                    app.use(require('restify-links')());
                    var swaggerUi = require('swagger-tools/middleware/swagger-ui');
                    app.use(
                        swaggerUi(swaggerExpress.runner.swagger, {
                            swaggerUi: '/swagger-ui',
                            swaggerUiDir: path.resolve(__dirname, '../../static/swagger-ui')
                        })
                    );
                    return swaggerExpress.register(app);
                });
        });
    };

    HttpService.prototype._setup =  function() {
        var app = this.app;
        var endpoint = this.endpoint;

        // CORS Support
        app.use(cors());
        app.options('*', cors());

        // Imaging Event Middleware
        app.use(httpEventMiddleware);

        // Override default static directory with sku specific handlers
        app.use(function(req, res, next) {
            skuService.static(req, res, next);
        });

        // Parse request body. Limit set to 50MB
        app.use(bodyParser.json({limit: '50mb'}));

        if (configuration.get('fileServerAddress') !== undefined) {
            logger.info('Use static file Server',
                {
                    fileServerAddress: configuration.get('fileServerAddress'),
                    fileServerPort: configuration.get('fileServerPort') || '80',
                    fileServerPath: configuration.get('fileServerPath') || '/'
                });
        }
        // Default Static Directory
        var defaultStaticDirectory = './static/http';
        app.use(express.static(defaultStaticDirectory));
        // Additional Static Directory
        var additionalStaticDirectory =
            configuration.get('httpStaticRoot', '/opt/monorail/static/http');

        logger.info('Use static file directory',
            {
                defaultStaticDirectory: defaultStaticDirectory,
                additionalStaticDirectory: additionalStaticDirectory
            });
        app.use(express.static(additionalStaticDirectory));
        app.use('/upnp', express.static('./static/upnp'));

        //re-route common and current
        //var versionPath = configuration.get('versionBase', '2.0');
        app.use(rewriter('/api/current/*', '/api/2.0/$1'));
        app.use(rewriter('/api/common/*', '/api/2.0/$1'));

        //enble/disable trusted proxy
        if ((configuration.get('trustedProxy') || false) === true) {
            app.enable('trust proxy');
        }

        // API Docs Directory
        app.use('/docs',
            express.static(
                configuration.get('httpDocsRoot', './build/apidoc')
            )
        );

        // UI Directory
        app.use('/ui', express.static('./static/web-ui'));

        // Task-doc Directory
        app.use('/taskdoc', express.static('./static/taskdoc/'));

        // Initialize authentication always
        app.use(authService.getPassportMidware());

        // Only apply authentication routes if auth is enabled
        if (endpoint.authEnabled) {
            // mount ./login only when authentication is enabled
            app.use(injector.get('Http.Api.Login'));
        }

        if (endpoint.httpsEnabled) {
            if (endpoint.httpsPfx) {
                this.httpsOptions = {
                    pfx: fs.readFileSync(endpoint.httpsPfx)
                };
            } else {
                this.httpsOptions = {
                    cert: fs.readFileSync(endpoint.httpsCert),
                    key: fs.readFileSync(endpoint.httpsKey)
                };
            }
        }
    };

    HttpService.prototype._parseEndpoint = function(endpoint) {
        return {
            address: endpoint.address || '0.0.0.0',
            port: endpoint.port || (endpoint.httpsEnabled ? 443 : 80),
            httpsEnabled : endpoint.httpsEnabled === true,
            httpsCert: endpoint.httpsCert || 'data/dev-cert.pem',
            httpsKey: endpoint.httpsKey || 'data/dev-key.pem',
            httpsPfx: endpoint.httpsPfx,
            proxiesEnabled: endpoint.proxiesEnabled === true,
            authEnabled: endpoint.authEnabled === true,
            yamlName: endpoint.yamlName || ["monorail-2.0.yaml", "redfish-root.yaml", "redfish.yaml", "monorail-2.0-sb.yaml"]
        };
    };

    function httpEventMiddleware(req, res, next) {
        req._startAt = process.hrtime();
        res.locals.ipAddress = remoteAddress(req);
        res.locals.scope = ['global'];
        res.locals.uuid = uuid.v4();

        onFinished(res, function () {
            if (!req._startAt) {
                return '';
            }

            var diff = process.hrtime(req._startAt),
                ms = diff[0] * 1e3 + diff[1] * 1e-6;

            var data = {
                ipAddress: res.locals.ipAddress
            };

            if (res.locals.identifier) {
                data.id = res.locals.identifier;
            }

            logger.debug(
                'http: ' + req.method +
                ' ' + res.statusCode +
                ' ' + ms.toFixed(3) +
                ' - ' + res.locals.uuid +
                ' - ' + req.originalUrl,
                data
            );
            if(res.statusCode > 299 ){
                if(configuration.get("minLogLevel") > constants.Logging.Levels.debug){

                    logger.error(
                        'http: ' + req.method +
                        ' ' + res.statusCode +
                        ' ' + ms.toFixed(3) +
                        ' - ' + res.locals.uuid +
                        ' - ' + req.originalUrl,
                        data
                    );
                }
                var errorMessage;
                if (typeof(res.body) !== 'undefined'){
                    errorMessage = {
                        "message" : res.body.message,
                        "status" : res.body.status,
                        "uuid" : res.body.uuid
                    };
                }
                logger.error('http: ' + JSON.stringify(errorMessage));
            }

            eventsProtocol.publishHttpResponse(
                res.locals.identifier || 'external',
                {
                    address: res.locals.ipAddress,
                    method: req.method,
                    url: req.originalUrl,
                    statusCode: res.statusCode,
                    time: ms.toFixed(3)
                }
            );
        });

        lookupService.ipAddressToNodeId(res.locals.ipAddress).then(function (nodeId) {
            res.locals.identifier = nodeId;
            return skuService.setupScope(nodeId);
        }).then(function(scope) {
            res.locals.scope = scope;
        }).catch(Errors.NotFoundError, function () {
            // No longer log NotFoundErrors
        }).catch(function (error) {
            events.ignoreError(error);
        }).finally(function () {
            next();
        });
    }

    /**
     * Creates http proxy
     * @private
     * @param {Object} httpProxy - proxy parameters
     * @returns
     */
    function createHttpProxy(app, httpProxy){
        var sep = path.sep; //The platform-specific file separator. '\\' or '/'
        try {
            var localPath = httpProxy.localPath || sep,
                remotePath = httpProxy.remotePath || sep,
                serverAddr = httpProxy.server;

            //localPath & remotePath should always starts with /
            if (!localPath.startsWith(sep)) {
                localPath = sep + localPath;
            }
            if (!remotePath.startsWith(sep)) {
                remotePath = sep + remotePath;
            }

            //localPath & remotePath should both ends with / or not, otherwise the pathRewrite will
            //not work well
            if (remotePath.endsWith(sep) && !localPath.endsWith(sep)) {
                localPath = localPath + sep;
            }
            else if (!remotePath.endsWith(sep) && localPath.endsWith(sep)) {
                remotePath = remotePath + sep;
            }

            var pathRewrite = {};
            pathRewrite['^' + localPath] = remotePath; //^ is a symbol in regex, means beginning

            var proxy = proxyMiddleware(localPath, {
                target: serverAddr,
                changeOrigin: true, //needed for virtual hosted sites. Without this, proxy to
                                    //external sites will fail and are blocked by corp's internal
                                    //firewall
                pathRewrite: pathRewrite,
                secure: false //ignore verify SSL Certs, so proxy works for https
            });
            app.use(proxy);
            logger.info('Create proxy succeeded',
                {
                    localPath: localPath,
                    remotePath: remotePath,
                    serverAddress: serverAddr
                });
        }
        catch(e){
            logger.error('Create proxy failed',
                {
                    message: e.message,
                    parameter: httpProxy
                }
            );
        }
    }

    return HttpService;
}