Source: node-server.js

/**
 * @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");
	}
}