// 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) {
};