httpwsd.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. /*******************************************************************************
  2. AToMPM - A Tool for Multi-Paradigm Modelling
  3. Copyright (c) 2011 Raphael Mannadiar (raphael.mannadiar@mail.mcgill.ca)
  4. This file is part of AToMPM.
  5. AToMPM is free software: you can redistribute it and/or modify it under the
  6. terms of the GNU Lesser General Public License as published by the Free Software
  7. Foundation, either version 3 of the License, or (at your option) any later
  8. version.
  9. AToMPM is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
  11. PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
  12. You should have received a copy of the GNU Lesser General Public License along
  13. with AToMPM. If not, see <http://www.gnu.org/licenses/>.
  14. *******************************************************************************/
  15. /*********************************** IMPORTS **********************************/
  16. var _util = require("util"),
  17. _fs = require('fs'),
  18. _http = require('http'),
  19. _path = require('path'),
  20. _url = require('url'),
  21. _utils = require('./utils'),
  22. _sio = require('socket.io'),
  23. _cp = require('child_process'),
  24. _fspp = require('./___fs++'),
  25. _duri = require('./___dataurize');
  26. /*********************************** GLOBALS **********************************/
  27. /* an array of WebWorkers
  28. ... each has its own mmmk instance */
  29. var workers = new Array();
  30. /* an array of response objects
  31. ... for workers to write on when they complete requests */
  32. var responses = new Array();
  33. /* a map of worker ids to socket.io socket session ids
  34. ... each socket is registered to exactly one worker
  35. ... several sockets may be registered to the same worker */
  36. var workerIds2socketIds = {};
  37. /************************************ UTILS ***********************************/
  38. /** Syntactic sugar to build and send HTTP responses **/
  39. function __respond(response, statusCode, reason, data, headers)
  40. {
  41. response.writeHead(
  42. statusCode,
  43. reason,
  44. (headers || {'Content-Type': 'text/plain'}));
  45. var encoding =
  46. (headers &&
  47. (headers['Content-Type'].match(/image/) ||
  48. headers['Content-Type'].match(/pdf/) ||
  49. headers['Content-Type'].match(/zip/) ) ?
  50. 'binary' :
  51. 'utf8'),
  52. content = reason || data;
  53. if( _utils.isObject(content) )
  54. response.end(_utils.jsons(content,null,'\t'), encoding);
  55. else
  56. response.end(content, encoding);
  57. }
  58. /** Syntactic sugar to build and send a socket.io message **/
  59. function __send(socket, statusCode, reason, data, headers)
  60. {
  61. //headers['Access-Control-Allow-Origin'] = 'http://raven10.kicks-ass.net:8080';
  62. socket.json.emit('message',
  63. {'statusCode':statusCode,
  64. 'reason':reason,
  65. 'headers':(headers || {'Content-Type': 'text/plain'}),
  66. 'data':data});
  67. }
  68. /************************************ LOGIC ***********************************/
  69. var httpserver = _http.createServer(
  70. function(req, resp)
  71. {
  72. var url = _url.parse(req.url,true);
  73. url.pathname = decodeURI(url.pathname);
  74. /* serve client */
  75. if( req.method == 'GET' && url.pathname == '/atompm' )
  76. _fs.readFile('./client/atompm.html', 'utf8',
  77. function(err, data)
  78. {
  79. if(err)
  80. __respond(resp,500,String(err));
  81. else
  82. __respond(resp,200,'',data,{'Content-Type': 'text/html'});
  83. });
  84. /* provide an interface to the unfortunately unavoidable dataurize
  85. module which returns data URIs for resources at arbitrary URLs */
  86. else if( req.method == 'GET' && url.pathname == '/datauri' )
  87. {
  88. var target = _url.parse(decodeURI(url['query']['target']));
  89. _duri.dataurize(
  90. target,
  91. function(err,datauri)
  92. {
  93. if(err)
  94. __respond(resp,500,_utils.jsons(err));
  95. else
  96. __respond(resp,200,'',datauri);
  97. });
  98. }
  99. /* serve metamodels, buttons models and their icons */
  100. else if( req.method == 'GET' &&
  101. (url.pathname.match(/\.metamodel$/) ||
  102. url.pathname.match(/\.buttons.model$/) ||
  103. url.pathname.match(/\.icon\.png$/i)) )
  104. {
  105. var isIcon = url.pathname.match(/\.icon\.png$/i);
  106. _fs.readFile('./users/'+url.pathname, (isIcon ? 'binary' : 'utf8'),
  107. function(err, data)
  108. {
  109. if(err)
  110. __respond(resp,500,String(err));
  111. else
  112. {
  113. var contentType =
  114. (isIcon ?
  115. {'Content-Type': 'image/png'} :
  116. {'Content-Type': 'application/json'});
  117. __respond(resp,200,'',data,contentType);
  118. }
  119. });
  120. }
  121. /* serve ordinary files (e.g., js includes, images, css)
  122. NOTE:: distinguish between atompm images (e.g., grid background,
  123. filebrowser icons) and CS/Images */
  124. else if( req.method == 'GET' &&
  125. (url.pathname.match(/\.html$/) ||
  126. url.pathname.match(/\.css$/) ||
  127. url.pathname.match(/\.js$/) ||
  128. url.pathname.match(/\.pdf$/) ||
  129. url.pathname.match(/\.png$/i) ||
  130. url.pathname.match(/\.jpg$/i) ||
  131. url.pathname.match(/\.jpeg$/i) ||
  132. url.pathname.match(/\.gif$/i) ||
  133. url.pathname.match(/\.svg$/i)) )
  134. {
  135. var isImage = url.pathname.match(/\.png$/i) ||
  136. url.pathname.match(/\.jpg$/i) ||
  137. url.pathname.match(/\.jpeg$/i) ||
  138. url.pathname.match(/\.gif$/i) ||
  139. url.pathname.match(/\.svg$/i),
  140. isText = ! isImage && ! url.pathname.match(/\.pdf$/);
  141. if( isImage && ! url.pathname.match(/^\/client\/media\//) )
  142. url.pathname = '/users/'+url.pathname;
  143. _fs.readFile('.'+url.pathname, (isText ? 'utf8' : 'binary'),
  144. function(err, data)
  145. {
  146. if(err)
  147. __respond(resp,500,String(err));
  148. else
  149. {
  150. var contentType =
  151. (url.pathname.match(/\.html$/) ?
  152. {'Content-Type': 'text/html'} :
  153. url.pathname.match(/\.css$/) ?
  154. {'Content-Type': 'text/css'} :
  155. url.pathname.match(/\.js$/) ?
  156. {'Content-Type': 'application/javascript'} :
  157. url.pathname.match(/\.pdf$/) ?
  158. {'Content-Type': 'application/pdf'} :
  159. url.pathname.match(/\.png$/i) ?
  160. {'Content-Type': 'image/png'} :
  161. url.pathname.match(/\.jpg$/i) ||
  162. url.pathname.match(/\.jpeg$/i) ?
  163. {'Content-Type': 'image/jpeg'} :
  164. url.pathname.match(/\.gif$/i) ?
  165. {'Content-Type': 'image/gif'} :
  166. url.pathname.match(/\.svg$/i) ?
  167. {'Content-Type': 'image/svg+xml'} :
  168. undefined);
  169. __respond(resp,200,'',data,contentType);
  170. }
  171. });
  172. }
  173. /* serve encrypted user password */
  174. else if( req.method == 'GET' && url.pathname == '/passwd' )
  175. _fs.readFile('./users/'+url['query']['username']+'/passwd', 'utf8',
  176. function(err, data)
  177. {
  178. if(err)
  179. __respond(resp,500,String(err));
  180. else
  181. __respond(resp,200,'',data,{'Content-Type': 'text/html'});
  182. });
  183. /* create new user
  184. 1. make sure user doesn't already exist
  185. 2. make a new copy of ./users/(default)
  186. 3. create password file */
  187. else if( req.method == 'POST' && url.pathname == '/user' )
  188. {
  189. var userdir = './users/'+url['query']['username'];
  190. _path.exists(userdir,
  191. function(exists)
  192. {
  193. if( exists )
  194. {
  195. __respond(resp,500,'username already exists');
  196. return;
  197. }
  198. _fspp.cp('./users/(default)/',userdir,
  199. function(err, stdout, stderr)
  200. {
  201. if( err )
  202. {
  203. __respond(resp,500,String(err));
  204. return;
  205. }
  206. _fs.writeFile(
  207. userdir+'/passwd',
  208. url['query']['password'],
  209. function(err)
  210. {
  211. if( err )
  212. __respond(resp,500,String(err));
  213. else
  214. __respond(resp,200);
  215. });
  216. });
  217. });
  218. }
  219. /* serve [a subset of] user preferences */
  220. else if( req.method == 'GET' && url.pathname.match(/prefs$/) )
  221. _fs.readFile('./users/'+url.pathname, 'utf8',
  222. function(err, data)
  223. {
  224. if(err)
  225. __respond(resp,500,String(err));
  226. else if( url['query']['subset'] == undefined )
  227. __respond(resp,200,'',data);
  228. else
  229. try
  230. {
  231. __respond(
  232. resp,
  233. 200,
  234. '',
  235. _utils.splitDict(
  236. _utils.jsonp(data),
  237. _utils.jsonp(url['query']['subset'])));
  238. }
  239. catch(err) {__respond(resp,500,String(err));}
  240. });
  241. /* update user preferences
  242. 1 retrieve all post data
  243. 2 read prefs file from disk
  244. 3 apply changes
  245. 4 write updated prefs to disk */
  246. else if( req.method == 'PUT' && url.pathname.match(/prefs$/) )
  247. {
  248. var reqData = '';
  249. req.addListener("data", function(chunk) {reqData += chunk;});
  250. req.addListener("end",
  251. function()
  252. {
  253. _fs.readFile('./users/'+url.pathname, 'utf8',
  254. function(err, prefs)
  255. {
  256. if(err)
  257. __respond(resp,500,String(err));
  258. else
  259. {
  260. try
  261. {
  262. prefs = _utils.jsonp(prefs);
  263. reqData = _utils.jsonp(reqData);
  264. }
  265. catch(err)
  266. {
  267. __respond(resp,500,String(err));
  268. return;
  269. }
  270. for( var pref in reqData )
  271. prefs[pref]['value'] = reqData[pref];
  272. _fs.writeFile(
  273. './users/'+url.pathname,
  274. _utils.jsons(prefs,null,'\t'),
  275. function(err, data)
  276. {
  277. if(err)
  278. __respond(resp,500,String(err));
  279. else
  280. __respond(resp,200);
  281. });
  282. }
  283. });
  284. });
  285. }
  286. /** manage cloud data **/
  287. /* [permanently] delete specified file/folder */
  288. else if( req.method == 'DELETE' && url.pathname.match(/\.file$/) )
  289. {
  290. var matches = url.pathname.match(/^\/(.*?)\/(.*)\.file$/),
  291. username = matches[1],
  292. fname = matches[2],
  293. userdir = './users/'+username+'/',
  294. ondelete =
  295. function(err, stdout, stderr)
  296. {
  297. if( err )
  298. __respond(resp,500,String(err));
  299. else
  300. __respond(resp,200);
  301. },
  302. deletef =
  303. function()
  304. {
  305. if( fname.match(/^_Trash_\//) )
  306. _fspp.rmdirs(userdir+fname,ondelete);
  307. else
  308. _fspp.mv(userdir+fname,userdir+'_Trash_/',ondelete);
  309. };
  310. _fs.exists(userdir+'_Trash_/',
  311. function(exists)
  312. {
  313. if( ! exists )
  314. _fs.mkdir(userdir+'_Trash_/',deletef);
  315. else
  316. deletef();
  317. });
  318. }
  319. /* extract user-uploaded archive to specified folder
  320. 1. read in all data
  321. 2. make sure destination exists and is a directory
  322. 3. write data to temp file (upload###.zip)
  323. 4. extract temp file and remove it
  324. NOTE:: it's not clear why (despite hours of googling) but the
  325. "req.setEncoding('utf8')" statement makes the difference
  326. between retrieving correct and corrupted (when non-text
  327. files in zip) data */
  328. else if( req.method == 'PUT' && url.pathname.match(/\.file$/) )
  329. {
  330. req.setEncoding('utf8');
  331. var reqData = '',
  332. tmpzip = 'upload'+Date.now()+'.zip',
  333. destdir = './users/'+url.pathname.match(/(.*)\.file$/)[1]+'/';
  334. req.addListener("data", function(chunk) {reqData += chunk;});
  335. req.addListener("end",
  336. function()
  337. {
  338. _fs.stat(destdir,
  339. function(err,stats)
  340. {
  341. if( err )
  342. __respond(resp,404,String(err));
  343. else if( ! stats.isDirectory() )
  344. __respond(resp,404,
  345. 'destination is not a directory :: '+destdir);
  346. else
  347. _fs.writeFile(
  348. destdir+tmpzip,eval('('+reqData+')'),
  349. 'binary',
  350. function(err)
  351. {
  352. _cp.exec('cd '+destdir+'; unzip -o '+tmpzip,
  353. function(err, stdout, stderr)
  354. {
  355. if( err )
  356. __respond(resp,500,String(err));
  357. else
  358. __respond(resp,200);
  359. _fs.unlink(destdir+tmpzip);
  360. });
  361. });
  362. });
  363. });
  364. }
  365. /* serve specified file/folder within a zip file */
  366. else if( req.method == 'GET' && url.pathname.match(/\.file$/) )
  367. {
  368. var matches = url.pathname.match(/^\/(.*?)\/(.*)\.file$/),
  369. username = matches[1],
  370. fname = './'+matches[2],
  371. userdir = './users/'+username+'/',
  372. tmpzip = 'download'+Date.now()+'.zip';
  373. _fs.exists(userdir+fname,
  374. function(exists)
  375. {
  376. if( ! exists )
  377. __respond(resp,404,
  378. 'requested file/folder does not exist :: '+fname);
  379. else
  380. _cp.exec('cd '+userdir+'; zip -r '+tmpzip+' "'+fname+'"',
  381. function(err, stdout, stderr)
  382. {
  383. if( err )
  384. __respond(resp,500,String(err));
  385. else
  386. _fs.readFile(userdir+tmpzip,'binary',
  387. function(err, data)
  388. {
  389. __respond(resp,200,'',data,
  390. {'Content-Type':'application/zip',
  391. 'Content-Disposition':
  392. 'attachment; filename="'+tmpzip+'"'});
  393. _fs.unlink(userdir+tmpzip);
  394. });
  395. });
  396. });
  397. }
  398. /* serve list of all files */
  399. else if( req.method == 'GET' &&
  400. url.pathname.match(/^\/.+\/filelist$/) )
  401. {
  402. var matches = url.pathname.match(/^\/(.+)\/filelist$/);
  403. _fspp.findfiles('./users/'+matches[1],
  404. function(err, stdout, stderr)
  405. {
  406. if( err )
  407. __respond(resp,404,String(err));
  408. else
  409. __respond(resp,200,'',stdout);
  410. });
  411. }
  412. /* spawn new worker */
  413. else if( (url.pathname == '/csworker' || url.pathname == '/asworker')
  414. && req.method == 'POST' )
  415. {
  416. /* setup and store new worker */
  417. var worker = _cp.fork(_path.join(__dirname, '__worker.js')),
  418. wid = workers.push(worker)-1;
  419. workerIds2socketIds[wid] = [];
  420. worker.on('message',
  421. function(msg)
  422. {
  423. /* push changes (if any) to registered sockets... even empty
  424. changelogs are pushed to facilitate sequence number-based
  425. ordering */
  426. if( msg['changelog'] != undefined )
  427. {
  428. var _msg = {'changelog':msg['changelog'],
  429. 'sequence#':msg['sequence#'],
  430. 'hitchhiker':msg['hitchhiker']};
  431. workerIds2socketIds[wid].forEach(
  432. function(sid)
  433. {
  434. __send(
  435. wsserver.sockets.sockets[sid],
  436. undefined,
  437. undefined,
  438. _msg);
  439. });
  440. }
  441. /* respond to a request */
  442. if( msg['respIndex'] != undefined )
  443. __respond(
  444. responses[msg['respIndex']],
  445. msg['statusCode'],
  446. msg['reason'],
  447. JSON.stringify(
  448. {'headers':
  449. (msg['headers'] ||
  450. {'Content-Type': 'text/plain'}),
  451. 'data':msg['data'],
  452. 'sequence#':msg['sequence#']}),
  453. {'Content-Type': 'application/json'});
  454. });
  455. worker.send(
  456. {'workerType':url.pathname,
  457. 'workerId':wid});
  458. /* respond worker id (used to identify associated worker) */
  459. __respond(
  460. resp,
  461. 201,
  462. '',
  463. ''+wid);
  464. return;
  465. }
  466. /* check for worker id and it's validity */
  467. else if( url['query'] == undefined ||
  468. url['query']['wid'] == undefined )
  469. __respond(resp, 400, 'missing worker id');
  470. else if( workers[url['query']['wid']] == undefined )
  471. __respond(resp, 400, 'invalid worker id :: '+url['query']['wid']);
  472. /* save resp object and forward request to worker (if request is PUT or
  473. POST, recover request data first)
  474. TBI:: only registered sockets should be allowed to speak to worker
  475. ... one way of doing this is forcing request urls to contain
  476. cid=socket.id## */
  477. else if( req.method == 'PUT' || req.method == 'POST' )
  478. {
  479. var reqData = '';
  480. req.addListener("data", function(chunk) {reqData += chunk;});
  481. req.addListener("end",
  482. function()
  483. {
  484. workers[url['query']['wid']].send(
  485. {'method':req.method,
  486. 'uri':url.pathname,
  487. 'reqData':(reqData == '' ?
  488. undefined :
  489. eval('('+reqData+')')),
  490. 'uriData':url['query'],
  491. 'respIndex':responses.push(resp)-1});
  492. });
  493. }
  494. else
  495. workers[url['query']['wid']].send(
  496. {'method':req.method,
  497. 'uri':url.pathname,
  498. 'uriData':url['query'],
  499. 'respIndex':responses.push(resp)-1});
  500. });
  501. httpserver.listen(8124);
  502. var wsserver = _sio.listen(httpserver);
  503. wsserver.configure(
  504. function()
  505. {
  506. wsserver.set('log level',2);
  507. });
  508. wsserver.sockets.on('connection',
  509. function(socket)
  510. {
  511. /* unregister this socket from the specified worker ... when a worker
  512. has no more registered sockets, terminate it */
  513. function unregister(wid)
  514. {
  515. var i = workerIds2socketIds[wid].indexOf(socket.id)
  516. if( i == -1 )
  517. __send(socket,403,'already unregistered from worker');
  518. else
  519. {
  520. workerIds2socketIds[wid].splice(i,1);
  521. if( workerIds2socketIds[wid].length == 0 )
  522. {
  523. workers[wid].kill();
  524. workers[wid] = undefined;
  525. delete workerIds2socketIds[wid];
  526. }
  527. __send(socket,200);
  528. }
  529. }
  530. /* onmessage : on reception of data from client */
  531. socket.on('message',
  532. function(msg/*{method:_,url:_}*/)
  533. {
  534. var url = _url.parse(msg.url,true);
  535. /* check for worker id and it's validity */
  536. if( url['query'] == undefined ||
  537. url['query']['wid'] == undefined )
  538. return __send(socket,400,'missing worker id');
  539. var wid = url['query']['wid'];
  540. if( workers[wid] == undefined )
  541. __send(socket,400,'unknown worker id :: '+wid);
  542. /* register socket for requested worker */
  543. else if( msg.method == 'POST' &&
  544. url.pathname.match(/changeListener$/) )
  545. {
  546. if( workerIds2socketIds[wid].indexOf(socket.id) > -1 )
  547. __send(socket,403,'already registered to worker');
  548. else
  549. {
  550. workerIds2socketIds[wid].push(socket.id);
  551. __send(socket,201);
  552. }
  553. }
  554. /* unregister socket for requested worker */
  555. else if( msg.method == 'DELETE' &&
  556. url.pathname.match(/changeListener$/) )
  557. unregister(wid);
  558. /* unsupported request */
  559. else
  560. __send(socket,501);
  561. });
  562. /* ondisconnect : on disconnection of socket */
  563. socket.on('disconnect',
  564. function()
  565. {
  566. for( var wid in workerIds2socketIds )
  567. for( var i in workerIds2socketIds[wid] )
  568. if( workerIds2socketIds[wid][i] == socket.id )
  569. {
  570. unregister(wid);
  571. return;
  572. }
  573. });
  574. });