Source: org/terraswarm/accessor/accessors/web/net/WebServer.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.

/** This accessor starts a server that listens for HTTP requests
 *  on the specified hostInterface and port and issues responses.
 *  The hostInterface specifies what network interface (e.g. Ethernet,
 *  WiFi, or localhost) to use. The default is 'localhost', which (usually)
 *  means that the server listens only for requests coming from the local machine.
 *  This is useful for testing. To listen for requests on all IPv4 addresses on
 *  the local machine, specify '0.0.0.0'. This will make the server accessible
 *  to any machine that has access to an IP address for the local machine.
 *
 *  When this server receives an http requests from the network (or from
 *  the local machine), it produces a JavaScript object on the output that
 *  includes at least the following properties:
 *  
 *  * body: The body of the request, or null if there is no body.
 *  * method: A string that describes the HTTP method of the request, which
 *    may be "GET", "PUT", etc.
 *  * path: The path in the URL of the request, such as "/" when there is no
 *    path.
 *  * requestID: An identifier for the request.
 *  
 *
 *  To produce a response, this accessor waits for an input on its *response*
 *  port that is a JavaScript object containing the following properties:
 *  
 *  * requestID: An identifier matching a request for which a response has not
 *    already been issued.
 *  * response: The body of the response, such as HTML to display to the
 *    requester or a JavaScript object with a JSON representation (the JSON
 *    representation will be sent back as the response). If this property is
 *    not included in the input, then the input will be stringified as a JSON
 *    object and sent. This will include the requestID property.
 *  
 *
 *  If there is no pending request with a matching ID, then an error will be
 *  issued.
 *  
 *  A simple use case is to connect the *request* output to some other actor
 *  that generates a response, and then to feed that response back to the
 *  *response* input. Be sure to include the requestID property in the response.
 *
 *  When `wrapup()` is invoked, this accessor closes the  server.
 *
 *  This accessor requires the module httpServer.
 *
 *  @accessor net/WebServer
 *  @parameter {string} hostInterface The IP address or domain name of the
 *    network interface to listen to.
 *  @parameter {int} port The port to listen on.
 *
 *  @input response The response to issue to a request. 
 *  @input shutdown Shutdown the web server.
 *  @output {int} listening When the server is listening for connections, this output
 *    will produce the port number that the server is listening on
 *  @output request The request that came into the server.
 *  
 *  @author Edward A. Lee amd Elizabeth Osyk
 *  @version $$Id$$
 */

// Stop extra messages from jslint and jshint.  Note that there should
// be no space between the / and the * and global. See
// https://chess.eecs.berkeley.edu/ptexternal/wiki/Main/JSHint */
/*globals console, error, exports, require */
/*jshint globalstrict: true*/
'use strict';

var httpServer = require('@accessors-modules/http-server');
var util = require('util');

/** Sets up the accessor by defining inputs and outputs. */
exports.setup = function() {
    this.parameter('hostInterface', {
        value: "localhost",
        type: "string"
    });
    this.parameter('port', {
        value: 80,
        type: "int"
    });
    this.input('response');
    this.output('listening', {'type':'int'});
    this.output('request', {'spontaneous': true});
};

/** Starts the server.
 */
exports.initialize = function() {
    var self = this;
    
    if (!self.server) {
        console.log('WebServer: Creating new server.');
        self.server = new httpServer.HttpServer({
                'port': self.getParameter('port'),
                'hostInterface': self.getParameter('hostInterface')
        });
        // Using 'self.exports' rather than just 'exports' in the following allows
        // these functions to be overridden in derived accessors.
        self.server.on('listening', self.exports.onListening.bind(self));
        self.server.on('request', self.exports.request.bind(self));
        self.server.on('error', function (message) {
            self.error(message);
        });
        self.server.start();
    }

    self.addInputHandler('response', function() {
        var response = self.get('response');
        if (!response.hasOwnProperty('requestID')) {
            try {
                response = JSON.parse(response);
            } catch (err) {}
            if (!response.hasOwnProperty('requestID')) {
                error('Response has no requestID property: ' + util.inspect(response));
                return;
            }
        }
        var id = response.requestID;
        if (!self.pendingRequests.hasOwnProperty(id)) {
            error('No pending request with ID ' + id);
            return;
        }
        delete self.pendingRequests[id];
        // Default body of the response is the whole response object.
        var body = response;
        if (response.hasOwnProperty('response')) {
            body = response.response;
        }
        if (typeof body !== 'string') {
            body = JSON.stringify(body);
        }
        if (response.hasOwnProperty('responseCode')) {
        	this.server.respond(response.requestID, body, response.responseCode);
        } else {
        	this.server.respond(response.requestID, body);
        }
    });
    
    // Initialize pendingRequests to an empty object.
    self.pendingRequests = {};
};

exports.onListening = function() {
    console.log('WebServer: Listening for requests.');
    this.send('listening', this.getParameter('port'));
};

exports.request = function(request) {
	console.log('Server received request.');
    // console.log('Server received request: ' + util.inspect(request));
    if (this.server) {
        this.send('request', request);
        this.pendingRequests[request.requestID] = request;
    } else {
        console.log('WARNING: server does not exist.');
    }
};

/** Removes all inputHandlers from sockets.<br>
 * Unregisters event listeners from sockets.<br>
 * Closes server.
 */
exports.wrapup = function(){
    if (this.server !== null && typeof this.server !== 'undefined') {
        console.log('WebServer: Stopping the server.');
        this.server.removeAllListeners();
        this.server.stop();
        this.server = null;
    }
};