/**
* @file node-server.js
* @copyright Monohm 2014
*/
monohm.provide ("sensible.node.Server");
/**
* Web and Websockets REST server for node.
* Listens for HTTP and WS requests and dispatches to REST handlers or files.
*
* @class
* @constructor
* @param {integer} inPort - port number on which to listen
* @param {object} inDelegate - delegate to resolve REST requests
*/
sensible.node.Server = function (inPort, inDelegate)
{
this.delegate = inDelegate;
this.httpServer = new sensible.node.WebServer (this.onHTTPRequest.bind (this));
// some ports can't do web sockets, eg Tessel
if (websocket)
{
this.webSocketServer = new websocket.server
({
httpServer: this.httpServer.server,
autoAcceptConnections: false
});
console.log ("WebSocket server listening on port " + inPort);
var self = this;
this.webSocketServer.on
(
"request",
function (inRequest)
{
var webSocket = inRequest.accept ("sensible-protocol", inRequest.origin);
webSocket.on
(
"message",
function (inMessage)
{
self.delegate.onWebSocketOpen (webSocket);
if (inMessage.type == "utf8")
{
self.onWSMessage (inMessage.utf8Data, webSocket);
}
else
if (inMessage.type == "binary")
{
console.log ("binary websockets message received (ignored)");
}
}
);
webSocket.on
(
"close",
function ()
{
self.delegate.onWebSocketClose (webSocket);
}
);
}
);
}
else
{
console.error ("websocket undefined, disabling web socket server");
}
this.httpServer.listen (inPort);
}
/**
* Stop serving requests.
*/
sensible.node.Server.prototype.stop = function ()
{
this.httpServer.stop ();
}
/**
* Called on receipt of an HTTP request.
* Calls the REST dispatcher to try and resolve the request against our delegate.
* If the resolution succeeds, the handler is called and the response assumed to be JSON.
* If the resolution fails, we attempt to serve a file with the appropriate path.
*
* @param {object} inRequest - HTTP request
* @param {object} outResponse - HTTP response
* @param {object} inRequestURL - parsed URL
* @param {object} inRequestParams - parsed request parameters
*/
sensible.node.Server.prototype.onHTTPRequest = function (inRequest, outResponse, inRequestURL, inRequestParams, inRequestFiles)
{
console.log (inRequest.socket.remoteAddress + ":" + inRequest.socket.remotePort + ":" + inRequest.url);
try
{
// map the node request to a regular parsed-anchor one
var request =
{
method: inRequest.method,
url:
{
pathname: inRequestURL.pathname,
search: inRequestURL.search ? inRequestURL.search.replace ("?", "") : "",
hash: inRequestURL.hash ? inRequestURL.hash.replace ("#", "") : ""
},
parameters: inRequestParams,
files: inRequestFiles
};
var self = this;
sensible.RESTDispatcher.dispatchRequest
(
request,
this.delegate,
function (inResponse)
{
// note we provide a Neutrino/Positron compatible wrapper
// all servers should do this :-)
// note we serve errors back as regular JSON
// with some useful information - woo!
if (inResponse.type == "json" || inResponse.type == "error")
{
var wrapper = new Object ();
if (inResponse.type == "json")
{
wrapper.meta = new Object ();
if (Array.isArray (inResponse.object))
{
wrapper.type = "array";
wrapper.meta.size = inResponse.object.length;
}
else
{
wrapper.type = "map";
wrapper.meta.size = 0;
// i don't think anyone uses wrapper size for maps
for (var key in inResponse.object)
{
wrapper.meta.size++;
}
}
wrapper.data = inResponse.object;
}
else
{
wrapper.type = "error";
wrapper.data = new Object ();
wrapper.data.error = inResponse.error;
}
var json = JSON.stringify (wrapper);
var jsonpCallback = inRequestParams.callback;
if (jsonpCallback == null || jsonpCallback.length == 0)
{
jsonpCallback = inRequestParams.jsonp_callback;
}
if (jsonpCallback && jsonpCallback.length)
{
json = jsonpCallback + "(" + json + ")";
}
outResponse.writeHead
(
200,
{
"Content-Type" : "application/json",
"Content-Length" : json.length
}
);
outResponse.write (json);
}
else
if (inResponse.type == "file")
{
// we satisfy all file references off the web root
var path = "www/" + inResponse.path;
self.sendFile (path, outResponse);
}
else
{
console.error ("sensible.node.Server can't deal with response type " + inResponse.type);
}
outResponse.end ();
}
);
}
catch (inError)
{
console.log ("error processing " + inRequest.url);
console.log (inError.message);
outResponse.writeHead
(
500,
{
},
inError.message
);
outResponse.end ();
}
}
/**
* Called on receipt of a WebSockets message.
* Calls the REST dispatcher to try and resolve the request against our delegate.
* If the resolution succeeds, the handler is called and the response assumed to be JSON.
* If the resolution fails, we attempt to serve a file with the appropriate path.
*
* @param {object} inMessage - WebSockets message
* @param {object} inWebSocket - the WebSocket on which the message arrived
*/
sensible.node.Server.prototype.onWSMessage = function (inMessage, inWebSocket)
{
var responseObject = null;
var error = null;
var message = JSON.parse (inMessage);
console.log ("onWSMessage()");
console.log (inMessage);
// some of the handlers need to know we're on WS instead of HTTP
message.webSocket = inWebSocket;
try
{
var response = sensible.RESTDispatcher.dispatchMessage (message, this.delegate);
if (response.object)
{
var packet =
{
controller: message.controller,
action: message.action,
data: response.object
};
inWebSocket.sendUTF (JSON.stringify (packet));
}
}
catch (inError)
{
console.log ("error processing message " + inMessage.controller + "/" + inMessage.action);
console.log (inError.message);
}
}
/**
* Send a file with the specified pathname to the specified response.
* Called when REST dispatch fails. If the file cannot be found or another
* error occurs, a 404 file not found response is sent.
*
* @param {string} inPathName - path of file
* @param {object} outResponse - HTTP response
*/
sensible.node.Server.prototype.sendFile = function (inPathName, outResponse)
{
console.log ("sensible.node.Server.sendFile(" + inPathName + ")");
var file = null;
try
{
file = fs.readFileSync (inPathName);
outResponse.writeHead
(
200,
{
"Content-Type" : mime.lookup (inPathName),
"Content-Length" : file.length
}
);
outResponse.write (file);
}
catch (inError)
{
console.log (inError.message);
outResponse.writeHead (404, "File Not Found");
}
}