/* This file is part of AToMPM - A Tool for Multi-Paradigm Modelling * Copyright 2011 by the AToMPM team and licensed under the LGPL * See COPYING.lesser and README.md in the root of this project for full details */ /*********************************** IMPORTS **********************************/ var _util = require("util"), _fs = require('fs'), _http = require('http'), _path = require('path'), _url = require('url'), _utils = require('./utils'), _sio = require('socket.io'), _cp = require('child_process'), _fspp = require('./___fs++'), _duri = require('./___dataurize'); /*********************************** GLOBALS **********************************/ /* an array of WebWorkers ... each has its own mmmk instance */ var workers = new Array(); /* an array of response objects ... for workers to write on when they complete requests */ var responses = new Array(); /* a map of worker ids to socket.io socket session ids ... each socket is registered to exactly one worker ... several sockets may be registered to the same worker */ var workerIds2socketIds = {}; /************************************ UTILS ***********************************/ /** Remove invalid characters from a string. **/ function __clean_string(s) { if (s == undefined) { return s; } s = JSON.stringify(s); s = s.replace(/'/g, ''); s = s.replace(/"/g, ''); s = s.replace(/‘/g, ''); s = s.replace(/’/g, ''); s = s.replace(/\\/g, '\\'); s = s.replace(/\//g, '\/'); s = s.replace(/\\n/g, ' '); return s; } /** Syntactic sugar to build and send HTTP responses **/ function __respond(response, statusCode, reason, data, headers) { response.writeHead( statusCode, __clean_string(reason), (headers || {'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*'})); var encoding = (headers && (headers['Content-Type'].match(/image/) || headers['Content-Type'].match(/pdf/) || headers['Content-Type'].match(/zip/) ) ? 'binary' : 'utf8'), content = reason || data; if( _utils.isObject(content) ) response.end(_utils.jsons(content,null,'\t'), encoding); else response.end(content, encoding); } /** Syntactic sugar to build and send a socket.io message **/ function __send(socket, statusCode, reason, data, headers) { socket.json.emit('message', {'statusCode':statusCode, 'reason':reason, 'headers':(headers || {'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*'}), 'data':data}); } /************************************ LOGIC ***********************************/ var httpserver = _http.createServer( function(req, resp) { var url = _url.parse(req.url,true); url.pathname = decodeURI(url.pathname); /* serve client */ if( req.method == 'GET' && url.pathname == '/atompm' ) _fs.readFile('./client/atompm.html', 'utf8', function(err, data) { if(err) __respond(resp,500,String(err)); else __respond(resp,200,'',data,{'Content-Type': 'text/html'}); }); else if( req.method == 'GET' && url.pathname == '/favicon.png' ) _fs.readFile('./favicon.png', 'binary', function(err, data) { if(err) __respond(resp,500,String(err)); else __respond(resp,200,'',data,{'Content-Type': 'image/png'}); }); /* provide an interface to the unfortunately unavoidable dataurize module which returns data URIs for resources at arbitrary URLs */ else if( req.method == 'GET' && url.pathname == '/datauri' ) { var target = _url.parse(decodeURI(url['query']['target'])); _duri.dataurize( target, function(err,datauri) { if(err) __respond(resp,500,_utils.jsons(err)); else __respond(resp,200,'',datauri); }); } /* serve metamodels, buttons models and their icons */ else if( req.method == 'GET' && (url.pathname.match(/\.metamodel$/) || url.pathname.match(/\.buttons.model$/) || url.pathname.match(/\.icon\.png$/i)) ) { var isIcon = url.pathname.match(/\.icon\.png$/i); _fs.readFile('./users/'+url.pathname, (isIcon ? 'binary' : 'utf8'), function(err, data) { if(err) __respond(resp,500,String(err)); else { var contentType = (isIcon ? {'Content-Type': 'image/png'} : {'Content-Type': 'application/json'}); __respond(resp,200,'',data,contentType); } }); } /* serve ordinary files (e.g., js includes, images, css) NOTE:: distinguish between atompm images (e.g., grid background, filebrowser icons) and CS/Images */ else if( req.method == 'GET' && (url.pathname.match(/\.html$/) || url.pathname.match(/\.css$/) || url.pathname.match(/\.js$/) || url.pathname.match(/\.pdf$/) || url.pathname.match(/\.png$/i) || url.pathname.match(/\.jpg$/i) || url.pathname.match(/\.jpeg$/i) || url.pathname.match(/\.gif$/i) || url.pathname.match(/\.svg$/i)) ) { var isImage = url.pathname.match(/\.png$/i) || url.pathname.match(/\.jpg$/i) || url.pathname.match(/\.jpeg$/i) || url.pathname.match(/\.gif$/i) || url.pathname.match(/\.svg$/i), isText = ! isImage && ! url.pathname.match(/\.pdf$/); if( isImage && ! url.pathname.match(/^\/client\/media\//) ) url.pathname = '/users/'+url.pathname; _fs.readFile('.'+url.pathname, (isText ? 'utf8' : 'binary'), function(err, data) { if(err) __respond(resp,500,String(err)); else { var contentType = (url.pathname.match(/\.html$/) ? {'Content-Type': 'text/html'} : url.pathname.match(/\.css$/) ? {'Content-Type': 'text/css'} : url.pathname.match(/\.js$/) ? {'Content-Type': 'application/javascript'} : url.pathname.match(/\.pdf$/) ? {'Content-Type': 'application/pdf'} : url.pathname.match(/\.png$/i) ? {'Content-Type': 'image/png'} : url.pathname.match(/\.jpg$/i) || url.pathname.match(/\.jpeg$/i) ? {'Content-Type': 'image/jpeg'} : url.pathname.match(/\.gif$/i) ? {'Content-Type': 'image/gif'} : url.pathname.match(/\.svg$/i) ? {'Content-Type': 'image/svg+xml'} : undefined); __respond(resp,200,'',data,contentType); } }); } /* serve encrypted user password */ else if( req.method == 'GET' && url.pathname == '/passwd' ) _fs.readFile('./users/'+url['query']['username']+'/passwd', 'utf8', function(err, data) { if(err) __respond(resp,500,String(err)); else __respond(resp,200,'',data,{'Content-Type': 'text/html'}); }); /* create new user 1. make sure user doesn't already exist 2. make a new copy of ./users/(default) 3. create password file */ else if( req.method == 'POST' && url.pathname == '/user' ) { var userdir = './users/'+url['query']['username']; _fs.exists(userdir, function(exists) { if( exists ) { __respond(resp,500,'username already exists'); return; } _fspp.cp('./users/(default)/',userdir, function(err, stdout, stderr) { if( err ) { __respond(resp,500,String(err)); return; } _fs.writeFile( userdir+'/passwd', url['query']['password'], function(err) { if( err ) __respond(resp,500,String(err)); else __respond(resp,200); }); }); }); } /* serve [a subset of] user preferences */ else if( req.method == 'GET' && url.pathname.match(/prefs$/) ) _fs.readFile('./users/'+url.pathname, 'utf8', function(err, data) { if(err) __respond(resp,500,String(err)); else if( url['query']['subset'] == undefined ) __respond(resp,200,'',data); else try { __respond( resp, 200, '', _utils.splitDict( _utils.jsonp(data), _utils.jsonp(url['query']['subset']))); } catch(err) {__respond(resp,500,String(err));} }); /* update user preferences 1 retrieve all post data 2 read prefs file from disk 3 apply changes 4 write updated prefs to disk */ else if( req.method == 'PUT' && url.pathname.match(/prefs$/) ) { var reqData = ''; req.addListener("data", function(chunk) {reqData += chunk;}); req.addListener("end", function() { _fs.readFile('./users/'+url.pathname, 'utf8', function(err, prefs) { if(err) __respond(resp,500,String(err)); else { try { prefs = _utils.jsonp(prefs); reqData = _utils.jsonp(reqData); } catch(err) { __respond(resp,500,String(err)); return; } for( var pref in reqData ) prefs[pref]['value'] = reqData[pref]; _fs.writeFile( './users/'+url.pathname, _utils.jsons(prefs,null,'\t'), function(err, data) { if(err) __respond(resp,500,String(err)); else __respond(resp,200); }); } }); }); } /* delete specified file/folder */ else if( req.method == 'DELETE' && url.pathname.match(/\.(file|folder)$/) ) { if (url.pathname.match('_Trash_')) { __respond(resp,500,"cannot remove trash!"); } else { var matches = url.pathname.match(/^\/(.*?)\/(.*\/)?(.*)\.(file|folder)$/), username = matches[1], folder = matches[2] || '', fname = matches[3], userdir = './users/'+username+'/', ondelete = function(err, stdout, stderr) { if( err ) __respond(resp,500,String(err)); else __respond(resp,200); }, deletef = function(response) { var newname = userdir+'_Trash_/'+folder+fname; if (_fs.existsSync(newname)) { if (url.pathname.match(/\.folder$/)) { _fspp.deleteFolderRecursive(newname); } else { _fs.unlink(newname); } } _fspp.mv(userdir+folder+fname,userdir+'_Trash_/'+folder,ondelete); }; _fs.exists(userdir+'_Trash_/'+folder, function(exists) { if( ! exists ) _fspp.mkdirs(userdir+'_Trash_/'+folder,deletef); else { deletef(); } }); } } /* create folder */ else if( req.method == 'POST' && url.pathname.match(/\.folder$/) ) { var matches = url.pathname.match(/^\/(.*?)\/(.*)\.folder$/), username = matches[1], folder = matches[2], userdir = './users/'+username+'/', oncreate = function(err, stdout, stderr) { if( err ) __respond(resp,500,String(err)); else __respond(resp,200); }; _fs.exists(userdir+folder, function(exists) { if( ! exists ) _fspp.mkdirs(userdir+folder,oncreate); else { oncreate("folder " + folder + " already exists"); } }); } /* rename file/folder (or move) */ else if( req.method == 'PUT' && url.pathname.match(/\.(folder|file)$/) ) { req.setEncoding('utf8'); var data = ''; req.addListener("data", function(chunk) {data += chunk;}); req.addListener("end", function() { data = _utils.jsonp(data); if (data.match(/^\//)) { // move var matches = url.pathname.match(/^\/(.*?)\/(.*\/)?(.*)\.(file|folder)$/), username = matches[1], folder = matches[2] || '', fname = matches[3], userdir = './users/'+username, onmove = function(err, stdout, stderr) { if( err ) __respond(resp,500,String(err)); else __respond(resp,200); }; if (_fs.existsSync(userdir+data+fname)) { if (url.pathname.match(/\.folder$/)) { _fspp.deleteFolderRecursive(userdir+data+fname); } else { _fs.unlink(newname); } } _fspp.mv(userdir+"/"+folder+fname,userdir+data,onmove); } else { // rename var matches = url.pathname.match(/^\/(.*?)\/(.*\/)?(.*)\.(file|folder)$/), username = matches[1], folder = matches[2] || '', fname = matches[3], userdir = './users/'+username+'/', onrename = function(err, stdout, stderr) { if( err ) __respond(resp,500,String(err)); else __respond(resp,200); }; _fs.rename(userdir+folder+fname,userdir+folder+data,onrename); } } ); } else if (req.method == 'POST' && url.pathname.match(/\.formalism$/)) { // create new formalism var matches = url.pathname.match(/^(.*)\/(.*)\.formalism$/), username = matches[1], formalism = matches[2], userdir = './users/'+username+"/", oncreatefolder = function(err, stdout, stderr) { if( err ) __respond(resp,500,String(err)); else { _fs.createReadStream(userdir+"Formalisms/__Templates__/MetamodelTemplate.model").pipe(_fs.createWriteStream(userdir+"Formalisms/"+formalism+"/"+formalism+".model")); _fs.createReadStream(userdir+"Formalisms/__Templates__/ConcreteSyntaxTemplate.model").pipe(_fs.createWriteStream(userdir+"Formalisms/"+formalism+"/"+formalism+".defaultIcons.model")); _fs.createReadStream(userdir+"Formalisms/__Templates__/MetamodelTemplate.metamodel").pipe(_fs.createWriteStream(userdir+"Formalisms/"+formalism+"/"+formalism+".metamodel")); _fs.createReadStream(userdir+"Formalisms/__Templates__/ConcreteSyntaxTemplate.defaultIcons.metamodel").pipe(_fs.createWriteStream(userdir+"Formalisms/"+formalism+"/"+formalism+".defaultIcons.metamodel")); _fs.createReadStream(userdir+"Formalisms/__Templates__/T_TransformationTemplate.model").pipe(_fs.createWriteStream(userdir+"Formalisms/"+formalism+"/OperationalSemantics/T_OperationalSemantics.model")); _fs.createReadStream(userdir+"Formalisms/__Templates__/T_TransformationTemplate.model").pipe(_fs.createWriteStream(userdir+"Formalisms/"+formalism+"/TranslationalSemantics/T_TranslationalSemantics.model")); __respond(resp,200); } }; _fs.mkdir(userdir+"Formalisms/"+formalism,function(err, stdout, stderr) { if( err ) __respond(resp,500,String(err)); else { _fs.mkdirSync(userdir+"Formalisms/"+formalism+"/OperationalSemantics"); _fs.mkdir(userdir+"Formalisms/"+formalism+"/TranslationalSemantics", oncreatefolder); } }); } else if (req.method == 'POST' && url.pathname.match(/\.transformation$/)) { // create new transformation var matches = url.pathname.match(/^\/(.*?)\/(.*)\.transformation$/), username = matches[1], userdir = './users/'+username+"/"; _fs.createReadStream(userdir+"Formalisms/__Templates__/T_TransformationTemplate.model").pipe(_fs.createWriteStream('./users/'+url.pathname.slice(0, -(".transformation".length)))); __respond(resp,200); } else if (req.method == 'POST' && url.pathname.match(/\.rule$/)) { // create new rule var matches = url.pathname.match(/^\/(.*?)\/(.*)\.rule$/), username = matches[1], userdir = './users/'+username+"/"; _fs.createReadStream(userdir+"Formalisms/__Templates__/R_RuleTemplate.model").pipe(_fs.createWriteStream('./users/'+url.pathname.slice(0, -(".rule".length)))); __respond(resp,200); } /* extract user-uploaded archive to specified folder 1. read in all data 2. make sure destination exists and is a directory 3. write data to temp file (upload###.zip) 4. extract temp file and remove it NOTE:: it's not clear why (despite hours of googling) but the "req.setEncoding('utf8')" statement makes the difference between retrieving correct and corrupted (when non-text files in zip) data */ else if( req.method == 'PUT' && url.pathname.match(/\.file$/) ) { req.setEncoding('utf8'); let reqData = '', tmpzip = 'upload'+Date.now()+'.zip', destdir = './users/'+url.pathname.match(/(.*)\.file$/)[1]+'/'; if( url.pathname.contains("..") || url.pathname.contains(";") ) { __respond(resp, 404, 'invalid pathname, no semicolons or .. allowed :: ' + url.pathname); return; } req.addListener("data", function(chunk) {reqData += chunk;}); req.addListener("end", function() { _fs.stat(destdir, function(err,stats) { if( err ) __respond(resp,404,String(err)); else if( ! stats.isDirectory() ) __respond(resp,404, 'destination is not a directory :: '+destdir); else _fs.writeFile( destdir+tmpzip,eval('('+reqData+')'), 'binary', function(err) { _cp.exec('cd '+destdir+'; unzip -o '+tmpzip, function(err, stdout, stderr) { if( err ) __respond(resp,500,String(err)); else __respond(resp,200); _fs.unlink(destdir+tmpzip, function(err){ console.log("Unlinked " + destdir + tmpzip); } ); }); }); }); }); } /* serve specified file/folder within a zip file */ else if( req.method == 'GET' && url.pathname.match(/\.file$/) ) { let matches = url.pathname.match(/^\/(.*?)\/(.*)\.file$/), username = matches[1], fname = './'+matches[2], userdir = './users/'+username+'/', tmpzip = 'download'+Date.now()+'.zip'; if( username.contains("..") || username.contains(";") ) { __respond(resp, 404, 'invalid username, no colons or .. allowed :: ' + username); return; } _fs.exists(userdir+fname, function(exists) { if( ! exists ) __respond(resp,404, 'requested file/folder does not exist :: '+fname); else _cp.exec('cd '+userdir+'; zip -r '+tmpzip+' "'+fname+'"', function(err, stdout, stderr) { if( err ) __respond(resp,500,String(err)); else _fs.readFile(userdir+tmpzip, function(err, data) { __respond(resp,200,'',data, {'Content-Type':'application/zip', 'Content-Disposition': 'attachment; filename="'+tmpzip+'"'}); _fs.unlink(userdir+tmpzip, function(err){ console.log("Unlinked " + userdir+tmpzip); }); }); }); }); } /* serve list of all files */ else if( req.method == 'GET' && url.pathname.match(/^\/.+\/filelist$/) ) { var matches = url.pathname.match(/^\/(.+)\/filelist$/); _fspp.findfiles('./users/'+matches[1], function(err, stdout, stderr) { if( err ) __respond(resp,404,String(err)); else __respond(resp,200,'',stdout); }); } /* spawn new worker */ else if( (url.pathname == '/csworker' || url.pathname == '/asworker') && req.method == 'POST' ) { /* setup and store new worker */ var worker = _cp.fork(_path.join(__dirname, '__worker.js')), wid = workers.push(worker)-1; workerIds2socketIds[wid] = []; worker.on('message', function(msg) { /* push changes (if any) to registered sockets... even empty changelogs are pushed to facilitate sequence number-based ordering */ if( msg['changelog'] != undefined ) { var _msg = {'changelog':msg['changelog'], 'sequence#':msg['sequence#'], 'hitchhiker':msg['hitchhiker']}; workerIds2socketIds[wid].forEach( function(sid) { __send( wsserver.sockets.sockets[sid], undefined, undefined, _msg); }); } /* respond to a request */ if( msg['respIndex'] != undefined ) __respond( responses[msg['respIndex']], msg['statusCode'], msg['reason'], JSON.stringify( {'headers': (msg['headers'] || {'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*'}), 'data':msg['data'], 'sequence#':msg['sequence#']}), {'Content-Type': 'application/json'}); }); worker.send( {'workerType':url.pathname, 'workerId':wid}); /* respond worker id (used to identify associated worker) */ __respond( resp, 201, '', ''+wid); return; } /* check for worker id and it's validity */ else if( url['query'] == undefined || url['query']['wid'] == undefined ) __respond(resp, 400, 'missing worker id'); else if( workers[url['query']['wid']] == undefined ) __respond(resp, 400, 'invalid worker id :: '+url['query']['wid']); /* save resp object and forward request to worker (if request is PUT or POST, recover request data first) TBI:: only registered sockets should be allowed to speak to worker ... one way of doing this is forcing request urls to contain cid=socket.id## */ else if( req.method == 'PUT' || req.method == 'POST' ) { var reqData = ''; req.addListener("data", function(chunk) {reqData += chunk;}); req.addListener("end", function() { workers[url['query']['wid']].send( {'method':req.method, 'uri':url.pathname, 'reqData':(reqData == '' ? undefined : eval('('+reqData+')')), 'uriData':url['query'], 'respIndex':responses.push(resp)-1}); }); } else workers[url['query']['wid']].send( {'method':req.method, 'uri':url.pathname, 'uriData':url['query'], 'respIndex':responses.push(resp)-1}); }); httpserver.listen(8124); var wsserver = _sio.listen(httpserver); wsserver.configure( function() { wsserver.set('log level',2); }); wsserver.sockets.on('connection', function(socket) { /* unregister this socket from the specified worker ... when a worker has no more registered sockets, terminate it */ function unregister(wid) { var i = workerIds2socketIds[wid].indexOf(socket.id); if( i == -1 ) __send(socket,403,'already unregistered from worker'); else { workerIds2socketIds[wid].splice(i,1); if( workerIds2socketIds[wid].length == 0 ) { workers[wid].kill(); workers[wid] = undefined; delete workerIds2socketIds[wid]; } __send(socket,200); } } /* onmessage : on reception of data from client */ socket.on('message', function(msg/*{method:_,url:_}*/) { var url = _url.parse(msg.url,true); /* check for worker id and it's validity */ if( url['query'] == undefined || url['query']['wid'] == undefined ) return __send(socket,400,'missing worker id'); var wid = url['query']['wid']; if( workers[wid] == undefined ) __send(socket,400,'unknown worker id :: '+wid); /* register socket for requested worker */ else if( msg.method == 'POST' && url.pathname.match(/changeListener$/) ) { if( workerIds2socketIds[wid].indexOf(socket.id) > -1 ) __send(socket,403,'already registered to worker'); else { workerIds2socketIds[wid].push(socket.id); __send(socket,201); } } /* unregister socket for requested worker */ else if( msg.method == 'DELETE' && url.pathname.match(/changeListener$/) ) unregister(wid); /* unsupported request */ else __send(socket,501); }); /* ondisconnect : on disconnection of socket */ socket.on('disconnect', function() { for( var wid in workerIds2socketIds ) for( var i in workerIds2socketIds[wid] ) if( workerIds2socketIds[wid][i] == socket.id ) { unregister(wid); return; } }); });