Source: org/terraswarm/accessor/accessors/web/hosts/node/node_modules/@accessors-modules/http-server/http-server.js

// Copyright (c) 2017 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
//

/**
 * Module supporting web servers.
 * This module defines one class, HttpServer, which runs a web server on the 
 * given port and host interface (e.g. localhost).  
 * 
 * HttpServer generates two events:
 * A listening event after the server has been started and is ready, and
 * Request events for each incoming request.  The request event includes
 * the request data plus a requestID number.
 * 
 * Accessors should provide a complete response back to HttpServer and include
 * the matching requestID.  
 *
 * @module @accessors-modules/http-server
 * @author Edward A. Lee ad Elizabeth Osyk
 * @version $$Id$$
 */

// Stop extra messages from jslint.  Note that there should be no
// space between the / and the * and global.
/*globals exports, Java, require, util */
/*jslint nomen: true */
/*jshint globalstrict: true */
"use strict";

//Stop extra messages from jslint.  Note that there should be no
//space between the / and the * and global.
/*globals actor */

var nodeHost = require('@accessors-hosts/node');
var express = nodeHost.installIfMissingThenRequire('express');
var helmet = nodeHost.installIfMissingThenRequire('helmet');
var portscanner = nodeHost.installIfMissingThenRequire('portscanner');

// Declare app here, since this implementation only supports a single server.
// helmet is a security library.
var app = express();
app.use(helmet());

var util = require('util');
var EventEmitter = require('events').EventEmitter;

///////////////////////////////////////////////////////////////////////////////
//// HttpServer

/** Construct an instance of HttpServer.
 *  After invoking this constructor (using new), the user script should set up listeners
 *  and then invoke the start() function on this HttpServer.
 *  This will create an HTTP server on the local host.
 *
 *  The options argument is a JSON object containing the following optional fields:
 *  * **hostInterface**: The IP address or name of the local interface for the server
 *    to listen on.  This defaults to "localhost", but if the host machine has more
 *    than one network interface, e.g. an Ethernet and WiFi interface, then you may
 *    need to specifically specify the IP address of that interface here.
 *  * **port**: The port on which to listen for requests (the default is 80,
 *    which is the default HTTP port).
 *  * **timeout**: The time in milliseconds to wait after emitting a request
 *    event for a response to be provided by invoking the respond() function.
 *   This is a long that defaults to 10,000.
 *   If this time expires before respond() is invoked, then this module
 *   will issue a generic timeout response to the HTTP request.
 *
 *
 *  This subclasses EventEmitter, emitting events:
 *  * **listening**: Emitted when the server is listening.
 *  * **request**: Emitted when an HTTP request has been received.
 *
 *	A request event contains an object with fields for the requestID, method, 
 *  path, and body (if any).  If there is no body, the body field is absent.
 *
 *  A typical usage pattern looks like this:
 *
 *     var httpServer = require('httpServer');
 *     var server = new httpServer.HttpServer({'port':8082});
 *     server.on('listening', function () {
 *         console.log('Server is listening.');
 *     });
 *     server.on('request', function (request) {
 *         console.log('Server received request: ' + util.inspect(request));
 *         server.respond(request.requestID, 'Hello World');
 *     });
 *     server.start();
 *
 *  where onListening is a handler for an event that this HttpServer emits
 *  when it is listening for requests.
 *
 *  @param options The options.
 */
exports.HttpServer = function (options) {
    var self = this;
    
    if (options.port === null || typeof options.port === 'undefined') {
        this.port = 80;
    } else {
        this.port = options.port;
    }
    
    this.hostInterface = options.hostInterface || 'localhost';
    this.pendingRequests = {};        
    this.requestIDCount = 1;
    this.timeout = 10000;
    
    /** If the given request is pending, remove it and send an error response.  
     *  A named function is defined so we can pass it (and the requestID) to 
     *  setTimeout.
     */
    this.handleTimeout = function(requestID) {
        if (self.pendingRequests.hasOwnProperty(requestID)) {
            var res = self.pendingRequests[requestID].response;
            delete(self.pendingRequests[requestID]);
            
            res.status('500');
            res.send('Error: Server timed out waiting for response to be generated.');
        }
    }
};
util.inherits(exports.HttpServer, EventEmitter);

/** Respond to a request. The provided response will be
 *  sent to the oldest request that has not already been sent a response.
 *  @param requestID An object that uniquely identifies the request.
 *   This should be the value of the requestID property of the object
 *   that was emitted as a 'request' event.
 *  @param response FIXME
 */
exports.HttpServer.prototype.respond = function (requestID, response) {
    if (this.pendingRequests.hasOwnProperty(requestID)) {
        // TODO:  Optimization: Keep track of timers and cancel them.
        var res = this.pendingRequests[requestID].response;
        delete(this.pendingRequests[requestID]);
        
        // Set status code, length header and content type header.
        res.status('200');
        res.set('Content-Type', 'text/html');
        res.set('Content-Length', response.length);
        res.send(response);
    }
};

/** Start the server. */
exports.HttpServer.prototype.start = function () {
    var self = this;
    
    // Check port status and start the server.
    portscanner.checkPortStatus(self.port, self.hostInterface, function(err, status) {
        if (err === null && status === 'closed') {
            self.server = app.listen(self.port, self.hostInterface, function(err2) {
                if(err2 === null || typeof err2 === 'undefined') { 
                    self.emit('listening', true); 
                    
                    // Listen to all requests.
                    app.get('/', function (req, res) {
                        var object = {request: req, response: res};

                        // Think this is atomic?
                        self.pendingRequests[self.requestIDCount = self.requestIDCount++] = object;
                        
                        // Start timer.
                        setTimeout(self.handleTimeout, self.timeout, 
                                   self.requestIDCount);
                        
                        // Output same request format as CapeCode, in same order
                        // to avoid errors from TrainableTest.
                        var body = null;
                        if (req.hasOwnProperty('body')) {
                            body = req.body;
                        }

                        // If you change requestEvent, then change
                        // ptolemy/actor/lib/jjs/modules/httpServer/httpServer.js
                        // and change all the HttpServer tests
                        var requestEvent = {
                            'headers' : req.headers,
                            'method' : req.method, 
                            // 'body' : body,
                            'params' : req.params,
                            'path' : req.path,
                            'requestID' : self.requestIDCount
                        };
                        self.emit('request', requestEvent);
                    })
                } else {
                    console.log('Error listening on port / interface ' +  
                                'combination ' + self.port + ',' + 
                                self.hostInterface + ': ' + err2);
                }
            });
        } else {
            console.log('Error: Port / interface combination already in use, ' + 
                        self.port + ": " + self.hostInterface);
        }
    });
};

/** Stop the server. Note that this closing happens
 *  asynchronously. The server may not be closed when this returns.
 */
exports.HttpServer.prototype.stop = function () {
    this.server.close();
};

/** This function is not needed in node since there is no separate helper class.
 */
exports.HttpServer.prototype._request = function (requestID, method, path, body) {
};