sadira
Version:
Web framework
765 lines (541 loc) • 19.3 kB
JavaScript
// Sadira astro-web framework - PG Sprimont <fullmoon@swing.be> (2013) - INAF/IASF Bologna, Italy.
var fs = require("fs");
var http = require('http');
var bson = require("./www/js/community/bson");
var DLG = require("./www/js/dialog");
var DGM = require("./www/js/datagram");
var url = require("url");
var path = require("path");
var BSON=bson().BSON;
//The various content types we handle and their associated mime types.
//The second boolean array member is used to specify a cache http header is appended for this file type.
var content_types = {
'.html': [ "text/html;charset=utf-8", false],
'.css': [ "text/css", false],
'.js': ["text/javascript;charset=utf-8", false],
'.jpg': ["image/jpeg", true],
'.JPG': ["image/jpeg", true],
'.jpeg': [ "image/jpeg", true],
'.ico' : ["image/x-icon", true],
'.png': ["image/png", true],
'.svg': ["image/svg+xml", true],
'.woff' : ['application/font-woff', true],
'.eot' : ['application/vnd.ms-fontobject', true],
'.ttf' : ['application/x-font-ttf', true]
};
GLOBAL.cors_headers = {
'Access-Control-Allow-Origin' : '*',
'Access-Control-Allow-Credentials' : true,
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
};
process.on('SIGTERM', function(){
console.log('SIGTERM received, terminating !');
process.exit(1);
});
GLOBAL.dump_error =function (err) {
var rs="";
if (typeof err === 'object') {
if (err.message) {
rs='\nMessage: ' + err.message;
}
if (err.stack) {
rs+='\nStacktrace:';
rs+='====================';
rs+=err.stack;
}
} else {
console.log("message: " + err);
rs= err;
}
return rs;
}
/**
* Description
* @method toArrayBuffer
* @param {} buffer
* @return ab
*/
function toArrayBuffer(buffer) {
var ab = new ArrayBuffer(buffer.length);
var view = new Uint8Array(ab);
for (var i = 0; i < buffer.length; ++i) {
view[i] = buffer[i];
}
return ab;
}
/**
* Main framework class. A single instance of this class is created at startup.
* @class _sadira
*/
var _sadira = function(){
var sad=this;
var argv = require('minimist')(process.argv.slice(2));
//console.dir(argv);
sad.options={
http_port: 9999,
// https_port: 8888,
raw_port: 7777,
websocket : true,
webrtc : true,
dialogs : [],
get_handlers : [],
post_handlers : []
}; //Defaults options.
var option_string=null;
if(typeof argv['cf'] != 'undefined' ) {
try{
var cfpath=argv['cf'];
//console.log("Parsing json config file ["+ cfpath+ "]");
option_string=fs.readFileSync(cfpath);
}
catch(e){
console.log( "Command line parsing error : " + e);
process.exit(1);
}
}else if(typeof argv['opts'] != 'undefined' ) {
option_string=argv['opts'];
}
if(option_string){
try{
//console.log("Parsing ["+option_string+"]");
var jcmdline =JSON.parse(option_string);
for(var p in jcmdline)
sad.options[p] = jcmdline[p]; //Overwriting with user given options.
}
catch(e){
console.log( "Command line parsing error : " + e);
process.exit(1);
}
}
if(typeof process.argv[2] != 'undefined'){}
sad.cluster = require('cluster');
sad.cluster_messages=[]; //Array of active interprocess messages
// sad.ncpu = require('os').cpus().length;
sad.ncpu = 1; //Using a single thread for now.
sad.html_rootdir=process.cwd()+'/www'; //This is the root directory for all the served files.
if(sad.cluster.isMaster) console.log("Master: options are " + JSON.stringify(sad.options, null, 4));
//Creating event master
//this.event_master=new events.event_manager();
}
_sadira.prototype.start = function (){
var sad=this;
try{
//Starting cluster
if (sad.cluster.isMaster) { //This is the main thread
console.log("Master process : Starting workers");
//sad.create_events();
// We create the session master
//sad.session_master=new session.master(sad);
sad.nworkers=0;
var f=0;
if(sad.options.http_port) f+=1;
if(sad.options.https_port) f+=1;
// Fork workers.
console.log("Starting on " + sad.ncpu + " core(s) (f "+f+")...");
for (var i = 0; i < sad.ncpu; i++) {
var worker=sad.cluster.fork();
//worker.worker_id=i;
worker.on('message', function(m){ //Handling incoming messages from workers
var sob;
if(m.object!="")sob=sad[m.object];
else sob=sad;
if(typeof sob != 'undefined'){
sad[m.object].message(m, function (reply_data) {
var rm={
id : m.id,
data : reply_data
}
sad.cluster.workers[m.worker_id].send(rm);
});
}
else
console.log('MASTER: ERROR unknown message : ' + JSON.stringify(m));
//console.log('MASTER: message : ' + JSON.stringify(m));
//this.send({ roba : "Ciao bello worker " + worker.id, worker_id : "I am The Master"} );
});
}
sad.cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.id + ' pid ' + worker.process.pid + ' died.');
sad.nworkers--;
});
sad.cluster.on('listening', function(worker, address) {
//console.log("A worker is now connected to " + address.address + ":" + address.port);
worker.online=true;
sad.nworkers++;
if(sad.nworkers == f*sad.ncpu){
if(sad.use_https==true)
console.log("Sadira(SSL): "+ sad.ncpu +" thread(s) listenning @ https://"+address.address+":" + sad.https_tcp_port );
if(sad.use_http==true)
console.log("Sadira(CLR): "+ sad.ncpu +" thread(s) listenning @ http://"+address.address+":" + sad.http_tcp_port );
console.log("Let's rock and roll! (CTRL + C to shutdown)");
}
});
sad.cluster.on('online', function(worker) {
//console.log("Yay, the worker "+worker.id +" responded after it was forked");
});
} else { //This is a worker thread
//We create a slave session manager for this thread.
//sad.session_slave=new session.slave(sad);
console.log("Slave " + sad.cluster.worker.id + " starting ...");
process.on('message', function(m){ //Handling messages sent to workers
if(typeof m.id != 'undefined'){
for (var wm in sad.cluster_messages){
//console.log(sad.cluster_messages[wm].id + " ==? " + m.id );
if(sad.cluster_messages[wm].id==m.id){
var ans=sad.cluster_messages[wm].answer;
sad.cluster_messages.remove(wm);
console.log("Waiting message queue length is " + sad.cluster_messages.length);
return ans(m.data);
}
}
}
var sob;
if(m.object!="")sob=sad[m.object];
else sob=sad;
if(typeof sob != 'undefined'){
sob.message(m.data, function (reply_data) {
var rm={
id : m.id,
data : reply_data
}
process.send(rm);
});
}
else
console.log('WORKER: ERROR unknown message : ' + JSON.stringify(m));
console.log('Worker ' + sad.cluster.worker.id + ' received message : ' + JSON.stringify(m));
});
//console.log("Worker "+ sad.cluster.worker.id + " created" );
sad.create_http_server();
sad.create_websocket_server();
sad.create_webrtc_server();
/*
sad.send_process_message("session_master", "Hello Master ! All good ?", function (reply){
console.log("worker "+sad.cluster.worker.id+" : Got reply from master : " + JSON.stringify(reply));
});
*/
//process.send({ worker_id: sad.worker_id, roba : ' Dear Master ?! ' });
}
}
catch (e){
console.log("Fatal error while creating http servers : " + dump_error(e));
process.exit(1);
}
}
/**
* This function redirects url management to handlers defined in (get,post)_handlers.js
* Command-type is either "get" or "post". Request and response are the original objects coming from the http_server requests.
* @method execute_command
* @param {String} command_type Type of command, either "GET" or "POST"
* @param {} request The http request object
* @param {} response
* @return
*/
_sadira.prototype.execute_command = function (command_type, request, response ){
var url_parts = url.parse(request.url,true);
var path_build=command_type+"_handlers.";
// var objects={};
try{
var path_parts = url_parts.pathname.split("/");
for(var p=1;p<path_parts.length;p++){
//console.log(" pel "+p+" : " +path_parts[p]);
path_build+=path_parts[p];
if (typeof eval(path_build) == "undefined") {
throw "undefined object";
} else {
if (typeof eval(path_build+".process") != "undefined") {
eval(path_build+".process")( url_parts.query, request, response);
}
path_build += ".";
}
}
}
catch (e){
console.log("Cannot get handler for " + path_build + " : " + e );
sadira.error_404(response, "Invalid path", function(){
response.end();
});
//response.write("Cannot get handler for " + path_build + " : " + e +"\n");
}
return;
}
/**
* Processing of all the HTTP POST requests
* @method process_post_request
* @param {} request
* @param {} res
* @param {} headers
* @return CallExpression
*/
_sadira.prototype.process_post_request=function (request, res, headers){
return this.execute_command("post", request, res );
}
/**
* Sends a 404 error HTML page to the browser
* @method error_404
* @param {} response
* @return CallExpression
*/
_sadira.prototype.error_404=function(response, uri, cb){
console.log("sending 404 for " + uri);
fs.readFile("www/404.html", "binary", function(err, file) {
if(err) {
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404.html file not found, but this is a true 404 Error : not found : " + uri);
cb();
return;
}
//response.write("Unavailable resource ["+uri+"] !\n");
response.writeHead(404, {"Content-Type": "text/html"});
response.write("<html><h1>unknown file " + uri + "</h1>");
response.write(file, "binary");
response.write("</html>");
cb();
});
}
_sadira.prototype.send_process_message=function(object_name, message_data, answer){
var m={
object : object_name,
data : message_data,
id : Math.random().toString(36).substring(2)
};
if(!this.cluster.isMaster)
m.worker_id = this.cluster.worker.id;
console.log("sending message id " + m.id);
this.cluster_messages.push({ id: m.id, answer: answer});
if(this.cluster.isMaster){
if(typeof message_data.worker_id != 'undefined')
this.cluster.workers[worker_id].send(m);
else{ //Broadcast to all workers.
for(var w in this.cluster.workers)
this.cluster.workers[w].send(m);
}
}
else
process.send(m);
}
//Entry point for inter-process messages to be handled by the sadira object itself
_sadira.prototype.message=function(md, reply){
}
//Processing of all the HTTP OPTIONS requests
_sadira.prototype.process_options_request=function(request, response, headers){
response.writeHead(200, cors_headers);
response.end();
}
//Processing of all the HTTP GET requests
_sadira.prototype.process_get_request=function(request, response, headers){
var sad=this;
var uri = unescape(url.parse(request.url).pathname);
var url_parts = url.parse(request.url,true);
//console.log('Processing request for ' + uri);
if(url_parts.search!="") //URL received with ? arguments, transferring the request to the get_handlers
return this.execute_command("get", request, response );
//From here the server is behaving as a simple file server.
//Here we should detect if the user is not trying to get something like ../../etc/passwd
//It seems that url.parse did the check for us (?) : uri is trimmed of the ../../
var filename = path.join(this.html_rootdir, uri);
path.exists(filename, function(exists) {
if(!exists) {
console.log('404 not found for uri ' + filename);
sad.error_404(response, uri, function() {
response.end();
});
//response.writeHead(404, {"Content-Type": "text/plain"});
//response.write("Unavailable resource ["+uri+"] !\n");
return;
}
if (fs.statSync(filename).isDirectory()) filename += '/index.html';
fs.readFile(filename, "binary", function(err, file) {
if(err) {
console.log('Error ! ' + err);
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(err + "\n");
response.end();
return;
}
var content_type = content_types[path.extname(filename)];
if (content_type) {
headers["Content-Type"] = content_type[0];
if(content_type[1] == true){
headers["Cache-Control"]="public,max-age=31536000";
}
}else
console.log("Unknown extension" + path.extname(filename));
//console.log('Serving ' + filename +' : headers : ' + JSON.stringify(headers) );
response.writeHead(200, headers);
response.write(file, "binary");
response.end();
});
});
}
//Main HTTP request handling function
_sadira.prototype.handle_request= function(request, response){
var headers ={};
console.log("Incoming request ");
switch (request.method){
case 'POST':
return sadira.process_post_request(request, response, headers);
case 'GET' :
return sadira.process_get_request(request, response, headers);
case 'OPTIONS':
return sadira.process_options_request(request, response, headers);
};
console.log("Unhandled request " + request.method);
}
//Creates the http servers
_sadira.prototype.create_http_server = function(){
var sad=this;
if(sad.options.https_port){
//Certificates for the https server
sad.ssl_data = {
key: fs.readFileSync('./ssl/keys/key.pem'),
cert: fs.readFileSync('./ssl/keys/cert.pem')
};
console.log("starting https server");
sad.https_server = require("https").createServer(sad.ssl_data, sad.handle_request).listen(parseInt(sad.options.https_port, 10));
}
if(sad.options.http_port){
console.log("starting http server");
sad.http_server = require("http").createServer(sad.handle_request).listen(parseInt(sad.options.http_port, 10));
}
}
/**
* Creation of the WebRTC server.
*/
_sadira.prototype.create_webrtc_server=function() {
var sad=this;
if(!sad.options.webrtc) return;
console.log("Create webRTC server... on " + sad.options.http_port);
if(!sad.options.webrtc_port) sad.options.webrtc_port=7777 ;
var io = require('socket.io').listen(sad.options.webrtc_port);
io.sockets.on('connection', function (cnx) {
//socket.emit('news', { hello: 'world' });
cnx.dialogs=new DLG.dialog_manager(cnx); //Each connexion has its own dialog manager, handling all dialogs on this websocket connexion.
cnx.on('message', function (data) {
try{
//console.log("Incoming message, creating datagram");
var dgram=new DGM.datagram();
if (message.type === 'utf8') { //Ascii
dgram.set_header(JSON.parse(message.utf8Data));
}
else if (message.type === 'binary') { //Binary
//console.log('received bin message size=' + message.binaryData.length + ' bytes.');
dgram.deserialize(message.binaryData);
}else{
throw "Unhandled socket message type " + message.type;
}
//console.log("process datagram " + JSON.stringify(dgram.header));
cnx.dialogs.process_datagram(dgram);
//console.log("process datagram done");
}
catch (e){
console.log("Datagram read error : "+ dump_error(e));
return;
}
//var msg_content=m.header.data;
//console.log("MESG:" + JSON.stringify(msg_content));
});
// socket disconnected
cnx.on('close', function(closeReason, description) {
cnx=null;
return;
});
});
}
/**
* Creation of the WebSocket server.
*/
_sadira.prototype.handle_websocket_requests=function(ws_server){
// This callback function is called every time someone
// tries to connect to the WebSocket server
ws_server.on('request', function(request) {
console.log("Connexion request from " + request.origin);
var cnx = request.accept(null, request.origin);
cnx.dialogs=new DLG.dialog_manager(cnx); //Each connexion has its own dialog manager, handling all dialogs on this websocket connexion.
cnx.request=request;
// Incoming message from web client
cnx.on('message', function(message) {
try{
//console.log("Incoming message, creating datagram");
var dgram=new DGM.datagram();
if (message.type === 'utf8') { //Ascii
dgram.set_header(JSON.parse(message.utf8Data));
}
else if (message.type === 'binary') { //Binary
//console.log('received bin message size=' + message.binaryData.length + ' bytes.');
dgram.deserialize(message.binaryData);
}else{
throw "Unhandled socket message type " + message.type;
}
//console.log("process datagram " + JSON.stringify(dgram.header));
cnx.dialogs.process_datagram(dgram);
//console.log("process datagram done");
}
catch (e){
console.log("Datagram read error : "+ dump_error(e));
return;
}
//var msg_content=m.header.data;
//console.log("MESG:" + JSON.stringify(msg_content));
});
// socket disconnected
cnx.on('close', function(closeReason, description) {
cnx=null;
return;
});
});
}
_sadira.prototype.create_websocket_server=function() {
var sad=this;
if(!sad.options.websocket) return;
var webSocketServer = require('websocket').server;
if(sad.http_server){
console.log("Create websocket http server");
this.ws_server= new webSocketServer({
// WebSocket server is tied to a HTTP server. WebSocket request is just
// an enhanced HTTP request. For more info http://tools.ietf.org/html/rfc6455#page-6
httpServer: sad.http_server
});
sad.handle_websocket_requests(this.ws_server);
}
if(sad.https_server){
console.log("Create websocket https server");
this.wss_server= new webSocketServer({
httpServer: sad.https_server
});
sad.handle_websocket_requests(this.wss_server);
}
}
_sadira.prototype.initialize_handlers=function(packname){
var sad=this;
var cwd=process.cwd();
var pkg=sad.options[packname];
if(!pkg) return;
for(w=0;w<pkg.length;w++){
var pkg_file = pkg[w].file;
console.log("Init "+packname+" : ["+pkg_file+"]");
var wpack=require(cwd+"/"+pkg_file);
var initf=wpack.init;
if(typeof initf != 'undefined')
initf(pkg[w]);
else{
console.log("No pkg init function!");
}
}
}
//Main global sadira instance
try{
GLOBAL.sadira = new _sadira();
GLOBAL.get_handlers = {};
GLOBAL.post_handlers = {};
GLOBAL.dialog_handlers = {};
sadira.initialize_handlers("handlers");
sadira.initialize_handlers("dialogs");
sadira.start();
}
catch (e){
console.log('Very bad error at startup, cannot start sadira : ' + dump_error(e) );
process.exit(1);
}