comparison rlgwebd @ 195:3bdee6371c3f

Change various filenames. The shell script previously used to launch the daemon is now called "initscript". The script files have had the ".js" extension removed from their names.
author John "Elwin" Edwards
date Thu, 14 Jan 2016 20:52:29 -0500
parents rlgwebd.js@5483d413a45b
children ea28353d620a
comparison
equal deleted inserted replaced
194:5483d413a45b 195:3bdee6371c3f
1 #!/bin/sh 1 #!/usr/bin/env node
2 2
3 NODE_PATH=/usr/lib/node_modules 3 var http = require('http');
4 LOGFILE=/var/local/rlgwebd/log 4 var net = require('net');
5 CTLSOCKET=/var/run/rlgwebd.sock 5 var url = require('url');
6 RLGWEBDJS=./rlgwebd.js 6 var path = require('path');
7 7 var fs = require('fs');
8 export NODE_PATH 8 var events = require('events');
9 9 var child_process = require('child_process');
10 if [ $UID != 0 ] 10 // Dependencies
11 then 11 var posix = require("posix");
12 echo "$0 needs to run as root." >&2 12 var pty = require("pty.js");
13 exit 1 13 var WebSocketServer = require("websocket").server;
14 fi 14
15 15 /* Configuration variables */
16 if [ $# -gt 0 ] && [ $1 = stop ] 16 // The first file is NOT in the chroot.
17 then 17 var ctlsocket = "/var/run/rlgwebd.sock";
18 socat "EXEC:echo quit" "$CTLSOCKET" 18 var httpPort = 8080;
19 else 19 var chrootDir = "/var/dgl/";
20 # Start 20 var dropToUser = "rodney";
21 setsid node "$RLGWEBDJS" </dev/null &>>$LOGFILE & 21 var serveStaticRoot = "/var/www/"; // inside the chroot
22 fi 22
23 23 var clearbufs = [
24 exit 24 new Buffer([27, 91, 72, 27, 91, 50, 74]), // xterm: CSI H CSI 2J
25 25 new Buffer([27, 91, 72, 27, 91, 74]) // screen: CSI H CSI J
26 ];
27
28 /* Data on the games available. */
29 var games = {
30 "rogue3": {
31 "name": "Rogue V3",
32 "uname": "rogue3",
33 "suffix": ".r3sav",
34 "path": "/usr/bin/rogue3"
35 },
36 "rogue4": {
37 "name": "Rogue V4",
38 "uname": "rogue4",
39 "suffix": ".r4sav",
40 "path": "/usr/bin/rogue4"
41 },
42 "rogue5": {
43 "name": "Rogue V5",
44 "uname": "rogue5",
45 "suffix": ".r5sav",
46 "path": "/usr/bin/rogue5"
47 },
48 "srogue": {
49 "name": "Super-Rogue",
50 "uname": "srogue",
51 "suffix": ".srsav",
52 "path": "/usr/bin/srogue"
53 },
54 "arogue5": {
55 "name": "Advanced Rogue 5",
56 "uname": "arogue5",
57 "suffix": ".ar5sav",
58 "path": "/usr/bin/arogue5"
59 },
60 "arogue7": {
61 "name": "Advanced Rogue 7",
62 "uname": "arogue7",
63 "suffix": ".ar7sav",
64 "path": "/usr/bin/arogue7"
65 },
66 "xrogue": {
67 "name": "XRogue",
68 "uname": "xrogue",
69 "suffix": ".xrsav",
70 "path": "/usr/bin/xrogue"
71 }
72 };
73
74 /* Global state */
75 var logins = {};
76 var sessions = {};
77 var dglgames = {};
78 var allowlogin = true;
79 var gamemux = new events.EventEmitter();
80
81 /* A base class. TermSession and DglSession inherit from it. */
82 function BaseGame() {
83 /* Games subclass EventEmitter, though there are few listeners. */
84 events.EventEmitter.call(this);
85 /* Array of watching WebSockets. */
86 this.watchers = [];
87 /* replaybuf holds the output since the last screen clear, so watchers can
88 * begin with a complete screen. replaylen is the number of bytes stored. */
89 this.replaybuf = new Buffer(1024);
90 this.replaylen = 0;
91 /* Time of last activity. */
92 this.lasttime = new Date();
93 }
94 BaseGame.prototype = new events.EventEmitter();
95
96 BaseGame.prototype.tag = function () {
97 if (this.pname === undefined || this.gname === undefined)
98 return "";
99 return this.gname + "/" + this.pname;
100 };
101
102 BaseGame.prototype.framepush = function(chunk) {
103 /* If this chunk resets the screen, discard what preceded it. */
104 if (isclear(chunk)) {
105 this.replaybuf = new Buffer(1024);
106 this.replaylen = 0;
107 }
108 /* Make sure there's space. */
109 while (this.replaybuf.length < chunk.length + this.replaylen) {
110 var nbuf = new Buffer(this.replaybuf.length * 2);
111 this.replaybuf.copy(nbuf, 0, 0, this.replaylen);
112 this.replaybuf = nbuf;
113 if (this.replaybuf.length > 65536) {
114 tslog("Warning: %s frame buffer at %d bytes", this.tag(),
115 this.replaybuf.length);
116 }
117 }
118 chunk.copy(this.replaybuf, this.replaylen);
119 this.replaylen += chunk.length;
120 };
121
122 /* Adds a watcher. */
123 BaseGame.prototype.attach = function (wsReq) {
124 var conn = wsReq.accept(null, wsReq.origin);
125 conn.sendUTF(JSON.stringify({
126 "t": "w", "w": this.w, "h": this.h, "p": this.pname, "g": this.gname
127 }));
128 conn.sendUTF(JSON.stringify({"t": "d",
129 "d": this.replaybuf.toString("hex", 0, this.replaylen)}));
130 conn.on('close', this.detach.bind(this, conn));
131 this.watchers.push(conn);
132 };
133
134 BaseGame.prototype.detach = function (socket) {
135 var n = this.watchers.indexOf(socket);
136 if (n >= 0) {
137 this.watchers.splice(n, 1);
138 tslog("A WebSocket watcher has left game %s", this.tag());
139 }
140 };
141
142 /* Constructor. A TermSession handles a pty and the game running on it.
143 * gname: (String) Name of the game to launch.
144 * pname: (String) The player's name.
145 * wsReq: (WebSocketRequest) The request from the client.
146 *
147 * Events:
148 * "data": Data generated by child. Parameters: buf (Buffer)
149 * "exit": Child terminated. Parameters: none
150 */
151 function TermSession(gname, pname, wsReq) {
152 BaseGame.call(this);
153 /* Don't launch anything that's not a real game. */
154 if (gname in games) {
155 this.game = games[gname];
156 this.gname = gname;
157 }
158 else {
159 this.failed = true;
160 wsReq.reject(404, errorcodes[2], "No such game");
161 tslog("Game %s is not available", game);
162 return;
163 }
164 this.pname = pname;
165 /* Set up the sizes. */
166 this.w = Math.floor(Number(wsReq.resourceURL.query.w));
167 if (!(this.w > 0 && this.w < 256))
168 this.w = 80;
169 this.h = Math.floor(Number(wsReq.resourceURL.query.h));
170 if (!(this.h > 0 && this.h < 256))
171 this.h = 24;
172 /* Environment. */
173 var childenv = {};
174 for (var key in process.env) {
175 childenv[key] = process.env[key];
176 }
177 var args = ["-n", this.pname];
178 var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w,
179 "name": "xterm-256color"};
180 this.term = pty.spawn(this.game.path, args, spawnopts);
181 tslog("%s playing %s (pid %d)", this.pname, this.gname, this.term.pid);
182 this.failed = false;
183 sessions[this.gname + "/" + this.pname] = this;
184 gamemux.emit('begin', this.gname, this.pname, 'rlg');
185 /* Set up the lockfile and ttyrec */
186 var ts = timestamp(this.lasttime);
187 var progressdir = path.join("/dgldir/inprogress", this.gname);
188 this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec");
189 var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n';
190 fs.writeFile(this.lock, lmsg, "utf8");
191 var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname,
192 ts + ".ttyrec");
193 this.record = fs.createWriteStream(ttyrec, { mode: 0664 });
194 /* The player's WebSocket and its handlers. */
195 this.playerconn = wsReq.accept(null, wsReq.origin);
196 this.playerconn.on('message', this.input_msg.bind(this));
197 this.playerconn.on('close', this.close.bind(this));
198 /* Send initial data. */
199 this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h,
200 "p": this.pname, "g": this.gname}));
201 /* Begin the ttyrec with some metadata, like dgamelaunch does. */
202 var descstr = "\x1b[2J\x1b[1;1H\r\n";
203 descstr += "Player: " + this.pname + "\r\nGame: " + this.game.name + "\r\n";
204 descstr += "Server: Roguelike Gallery - rlgallery.org\r\n";
205 descstr += "Filename: " + ts + ".ttyrec\r\n";
206 descstr += "Time: (" + Math.floor(this.lasttime.getTime() / 1000) + ") ";
207 descstr += this.lasttime.toUTCString().slice(0, -4) + "\r\n";
208 descstr += "Size: " + this.w + "x" + this.h + "\r\n\x1b[2J";
209 this.write_ttyrec(descstr);
210 this.term.on("data", this.write_ttyrec.bind(this));
211 this.term.on("exit", this.destroy.bind(this));
212 }
213 TermSession.prototype = new BaseGame();
214
215 /* Currently this also sends to the player and any watchers. */
216 TermSession.prototype.write_ttyrec = function (datastr) {
217 this.lasttime = new Date();
218 var buf = new Buffer(datastr);
219 var chunk = new Buffer(buf.length + 12);
220 /* TTYREC headers */
221 chunk.writeUInt32LE(Math.floor(this.lasttime.getTime() / 1000), 0);
222 chunk.writeUInt32LE(1000 * (this.lasttime.getTime() % 1000), 4);
223 chunk.writeUInt32LE(buf.length, 8);
224 buf.copy(chunk, 12);
225 this.record.write(chunk);
226 this.framepush(buf);
227 /* Send to the player. */
228 var msg = JSON.stringify({"t": "d", "d": buf.toString("hex")});
229 this.playerconn.sendUTF(msg);
230 /* Send to any watchers. */
231 for (var i = 0; i < this.watchers.length; i++) {
232 if (this.watchers[i].connected)
233 this.watchers[i].sendUTF(msg);
234 }
235 this.emit('data', buf);
236 };
237
238 /* For writing to the subprocess's stdin. */
239 TermSession.prototype.write = function (data) {
240 this.term.write(data);
241 };
242
243 TermSession.prototype.input_msg = function (message) {
244 var parsedMsg = getMsgWS(message);
245 if (parsedMsg.t == 'q') {
246 this.close();
247 }
248 else if (parsedMsg.t == 'd') {
249 var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, "");
250 if (hexstr.length % 2 != 0) {
251 hexstr = hexstr.slice(0, -1);
252 }
253 var keybuf = new Buffer(hexstr, "hex");
254 this.write(keybuf);
255 }
256 };
257
258 /* Teardown. */
259 TermSession.prototype.close = function () {
260 if (this.tag() in sessions)
261 this.term.kill('SIGHUP');
262 };
263
264 TermSession.prototype.destroy = function () {
265 var tag = this.tag();
266 fs.unlink(this.lock);
267 this.record.end();
268 var watchsocks = this.watchers;
269 this.watchers = [];
270 for (var i = 0; i < watchsocks.length; i++) {
271 if (watchsocks[i].connected)
272 watchsocks[i].close();
273 }
274 if (this.playerconn.connected) {
275 this.playerconn.sendUTF(JSON.stringify({"t": "q"}));
276 this.playerconn.close();
277 }
278 this.emit('exit');
279 gamemux.emit('end', this.gname, this.pname);
280 delete sessions[tag];
281 tslog("Game %s ended.", tag);
282 };
283
284 function DglSession(filename) {
285 BaseGame.call(this);
286 var pathcoms = filename.split('/');
287 this.gname = pathcoms[pathcoms.length - 2];
288 if (!(this.gname in games)) {
289 this.emit('open', false);
290 return;
291 }
292 var basename = pathcoms[pathcoms.length - 1];
293 var firstsep = basename.indexOf(':');
294 this.pname = basename.slice(0, firstsep);
295 var fname = basename.slice(firstsep + 1);
296 this.ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, fname);
297 /* Flag to prevent multiple handlers from reading simultaneously and
298 * getting into a race. */
299 this.reading = false;
300 this.rpos = 0;
301 fs.readFile(filename, {encoding: "utf8"}, (function (err, data) {
302 if (err) {
303 this.emit('open', false);
304 return;
305 }
306 var lines = data.split('\n');
307 this.h = Number(lines[1]);
308 this.w = Number(lines[2]);
309 fs.open(this.ttyrec, "r", (function (err, fd) {
310 if (err) {
311 this.emit('open', false);
312 }
313 else {
314 this.fd = fd;
315 this.emit('open', true);
316 tslog("DGL %s: open", this.tag());
317 gamemux.emit('begin', this.gname, this.pname, 'dgl');
318 this.startchunk();
319 this.recwatcher = fs.watch(this.ttyrec, this.notifier.bind(this));
320 }
321 }).bind(this));
322 }).bind(this));
323 }
324 DglSession.prototype = new BaseGame();
325
326 /* 3 functions to get data from the ttyrec file. */
327 DglSession.prototype.startchunk = function () {
328 if (this.reading)
329 return;
330 this.reading = true;
331 var header = new Buffer(12);
332 fs.read(this.fd, header, 0, 12, this.rpos, this.datachunk.bind(this));
333 };
334
335 DglSession.prototype.datachunk = function (err, n, buf) {
336 /* Stop recursion if end of file has been reached. */
337 if (err || n < 12) {
338 if (!err && n > 0) {
339 tslog("DGL %s: expected 12-byte header, got %d", this.tag(), n);
340 }
341 this.reading = false;
342 return;
343 }
344 this.rpos += 12;
345 /* Update timestamp, to within 1 second. */
346 this.lasttime = new Date(1000 * buf.readUInt32LE(0));
347 var datalen = buf.readUInt32LE(8);
348 if (datalen > 16384) {
349 // Something is probably wrong...
350 tslog("DGL %s: looking for %d bytes", this.tag(), datalen);
351 }
352 var databuf = new Buffer(datalen);
353 fs.read(this.fd, databuf, 0, datalen, this.rpos, this.handledata.bind(this));
354 };
355
356 DglSession.prototype.handledata = function (err, n, buf) {
357 if (err || n < buf.length) {
358 /* Next time, read the header again. */
359 this.rpos -= 12;
360 this.reading = false;
361 tslog("DGL %s: expected %d bytes, got %d", this.tag(), buf.length, n);
362 return;
363 }
364 this.rpos += n;
365 this.reading = false;
366 /* Process the data */
367 this.framepush(buf);
368 var wmsg = JSON.stringify({"t": "d", "d": buf.toString("hex")});
369 for (var i = 0; i < this.watchers.length; i++) {
370 if (this.watchers[i].connected)
371 this.watchers[i].sendUTF(wmsg);
372 }
373 this.emit("data", buf);
374 /* Recurse. */
375 this.startchunk();
376 };
377
378 /* Handles events from the ttyrec file watcher. */
379 DglSession.prototype.notifier = function (ev, finame) {
380 if (ev == "change")
381 this.startchunk();
382 /* If another kind of event appears, something strange happened. */
383 };
384
385 DglSession.prototype.close = function () {
386 this.recwatcher.close();
387 /* Ensure all data is handled before quitting. */
388 this.startchunk();
389 var connlist = this.watchers;
390 this.watchers = [];
391 for (var i = 0; i < connlist.length; i++) {
392 if (connlist[i].connected)
393 connlist[i].close();
394 }
395 fs.close(this.fd);
396 this.emit("close");
397 gamemux.emit('end', this.gname, this.pname);
398 tslog("DGL %s: closed", this.tag());
399 };
400
401 function wsStartGame(wsReq) {
402 var playmatch = wsReq.resourceURL.pathname.match(/^\/play\/([^\/]*)$/);
403 if (!playmatch[1] || !(playmatch[1] in games)) {
404 wsReq.reject(404, errorcodes[2]);
405 return;
406 }
407 var gname = playmatch[1];
408 if (!allowlogin) {
409 wsReq.reject(404, errorcodes[6]);
410 return;
411 }
412 if (!("key" in wsReq.resourceURL.query)) {
413 wsReq.reject(404, "No key given.");
414 return;
415 }
416 var lkey = wsReq.resourceURL.query["key"];
417 if (!(lkey in logins)) {
418 wsReq.reject(404, errorcodes[1]);
419 return;
420 }
421 var pname = logins[lkey].name;
422 function progcallback(err, fname) {
423 if (fname) {
424 wsReq.reject(404, errorcodes[4]);
425 tslog("%s is already playing %s", pname, gname);
426 }
427 else {
428 new TermSession(gname, pname, wsReq);
429 }
430 };
431 checkprogress(pname, games[gname], progcallback, []);
432 }
433
434 /* Some functions which check whether a player is currently playing or
435 * has a saved game. Maybe someday they will provide information on
436 * the game. */
437 function checkprogress(user, game, callback, args) {
438 var progressdir = path.join("/dgldir/inprogress", game.uname);
439 fs.readdir(progressdir, function(err, files) {
440 if (err) {
441 args.unshift(err, null);
442 callback.apply(null, args);
443 return;
444 }
445 var fre = RegExp("^" + user + ":");
446 for (var i = 0; i < files.length; i++) {
447 if (files[i].match(fre)) {
448 args.unshift(null, files[i]);
449 callback.apply(null, args);
450 return;
451 }
452 }
453 args.unshift(null, false);
454 callback.apply(null, args);
455 });
456 }
457
458 function checksaved(user, game, callback, args) {
459 var savedirc = game.uname + "save";
460 var basename = String(pwent.uid) + "-" + user + game.suffix;
461 var savefile = path.join("/var/games/roguelike", savedirc, basename);
462 fs.exists(savefile, function (exist) {
463 args.unshift(exist);
464 callback.apply(null, args);
465 });
466 }
467
468 function playerstatus(user, callback) {
469 var sdata = {};
470 function finishp() {
471 for (var gname in games) {
472 if (!(gname in sdata))
473 return;
474 }
475 callback(sdata);
476 }
477 function regsaved(exists, game) {
478 if (exists)
479 sdata[game.uname] = "s";
480 else
481 sdata[game.uname] = "0";
482 finishp();
483 }
484 function regactive(err, filename, game) {
485 if (!err && filename) {
486 if (filename.match(/^[^:]*:node:/))
487 sdata[game.uname] = "p";
488 else
489 sdata[game.uname] = "d";
490 finishp();
491 }
492 else
493 checksaved(user, game, regsaved, [game]);
494 }
495 for (var gname in games) {
496 checkprogress(user, games[gname], regactive, [games[gname]]);
497 }
498 }
499
500 /* A few utility functions */
501 function timestamp(dd) {
502 if (!(dd instanceof Date)) {
503 dd = new Date();
504 }
505 sd = dd.toISOString();
506 sd = sd.slice(0, sd.indexOf("."));
507 return sd.replace("T", ".");
508 }
509
510 function randkey(words) {
511 if (!words || !(words > 0))
512 words = 1;
513 function rand32() {
514 rnum = Math.floor(Math.random() * 65536 * 65536);
515 hexstr = rnum.toString(16);
516 while (hexstr.length < 8)
517 hexstr = "0" + hexstr;
518 return hexstr;
519 }
520 var key = "";
521 for (var i = 0; i < words; i++)
522 key += rand32();
523 return key;
524 }
525
526 /* Compares two buffers, returns true for equality up to index n */
527 function bufncmp(buf1, buf2, n) {
528 if (!Buffer.isBuffer(buf1) || !Buffer.isBuffer(buf2))
529 return false;
530 for (var i = 0; i < n; i++) {
531 if (i == buf1.length && i == buf2.length)
532 return true;
533 if (i == buf1.length || i == buf2.length)
534 return false;
535 if (buf1[i] != buf2[i])
536 return false;
537 }
538 return true;
539 }
540
541 function isclear(buf) {
542 for (var i = 0; i < clearbufs.length; i++) {
543 if (bufncmp(buf, clearbufs[i], clearbufs[i].length))
544 return true;
545 }
546 return false;
547 }
548
549 function tslog() {
550 arguments[0] = new Date().toISOString() + ": " + String(arguments[0]);
551 console.log.apply(console, arguments);
552 }
553
554 /* Returns a list of the cookies in the request, obviously. */
555 function getCookies(req) {
556 cookies = [];
557 if ("cookie" in req.headers) {
558 cookstrs = req.headers["cookie"].split("; ");
559 for (var i = 0; i < cookstrs.length; i++) {
560 eqsign = cookstrs[i].indexOf("=");
561 if (eqsign > 0) {
562 name = cookstrs[i].slice(0, eqsign).toLowerCase();
563 val = cookstrs[i].slice(eqsign + 1);
564 cookies[name] = val;
565 }
566 else if (eqsign < 0)
567 cookies[cookstrs[i]] = null;
568 }
569 }
570 return cookies;
571 }
572
573 function getMsg(posttext) {
574 var jsonobj;
575 if (!posttext)
576 return {};
577 try {
578 jsonobj = JSON.parse(posttext);
579 }
580 catch (e) {
581 if (e instanceof SyntaxError)
582 return {};
583 }
584 if (typeof(jsonobj) != "object")
585 return {};
586 return jsonobj;
587 }
588
589 function getMsgWS(msgObj) {
590 if (msgObj.type != "utf8")
591 return {};
592 return getMsg(msgObj.utf8Data);
593 }
594
595 function login(req, res, formdata) {
596 if (!allowlogin) {
597 sendError(res, 6, null, false);
598 return;
599 }
600 if (!("name" in formdata)) {
601 sendError(res, 2, "Username not given.", false);
602 return;
603 }
604 else if (!("pw" in formdata)) {
605 sendError(res, 2, "Password not given.", false);
606 return;
607 }
608 var username = String(formdata["name"]);
609 var password = String(formdata["pw"]);
610 function checkit(code, signal) {
611 /* Checks the exit status, see sqlickrypt.c for details. */
612 if (code != 0) {
613 sendError(res, 3);
614 if (code == 1)
615 tslog("Password check failed for user %s", username);
616 else if (code == 2)
617 tslog("Attempted login by nonexistent user %s", username);
618 else
619 tslog("Login failed: sqlickrypt error %d", code);
620 return;
621 }
622 var lkey = randkey(2);
623 while (lkey in logins)
624 lkey = randkey(2);
625 logins[lkey] = {"name": username, "ts": new Date()};
626 res.writeHead(200, {'Content-Type': 'application/json'});
627 var reply = {"t": "l", "k": lkey, "u": username};
628 res.write(JSON.stringify(reply));
629 res.end();
630 tslog("%s has logged in", username);
631 return;
632 }
633 /* Launch the sqlickrypt utility to check the password. */
634 var pwchecker = child_process.spawn("/bin/sqlickrypt", ["check"]);
635 pwchecker.on("exit", checkit);
636 pwchecker.stdin.end(username + '\n' + password + '\n', "utf8");
637 return;
638 }
639
640 /* Sets things up for a new user, like dgamelaunch's commands[register] */
641 function regsetup(username) {
642 function regsetup_l2(err) {
643 for (var g in games) {
644 fs.mkdir(path.join("/dgldir/ttyrec", username, games[g].uname), 0755);
645 }
646 }
647 fs.mkdir(path.join("/dgldir/userdata", username), 0755);
648 fs.mkdir(path.join("/dgldir/ttyrec/", username), 0755, regsetup_l2);
649 }
650
651 function register(req, res, formdata) {
652 var uname, passwd, email;
653 if (typeof (formdata.name) != "string" || formdata.name === "") {
654 sendError(res, 2, "No name given.");
655 return;
656 }
657 else
658 uname = formdata["name"];
659 if (typeof (formdata.pw) != "string" || formdata.pw === "") {
660 sendError(res, 2, "No password given.");
661 return;
662 }
663 else
664 passwd = formdata["pw"];
665 if (typeof (formdata.email) != "string" || formdata.email === "") {
666 /* E-mail is optional */
667 email = "nobody@nowhere.not";
668 }
669 else
670 email = formdata["email"];
671 function checkreg(code, signal) {
672 if (code === 0) {
673 var lkey = randkey(2);
674 while (lkey in logins)
675 lkey = randkey(2);
676 logins[lkey] = {"name": uname, "ts": new Date()};
677 var reply = {"t": "r", "k": lkey, "u": uname};
678 res.writeHead(200, {'Content-Type': 'application/json'});
679 res.write(JSON.stringify(reply));
680 res.end();
681 tslog("Added new user: %s", uname);
682 regsetup(uname);
683 }
684 else if (code == 4) {
685 sendError(res, 2, "Invalid characters in name or email.");
686 tslog("Attempted registration: %s %s", uname, email);
687 }
688 else if (code == 1) {
689 sendError(res, 2, "Username " + uname + " is already being used.");
690 tslog("Attempted duplicate registration: %s", uname);
691 }
692 else {
693 sendError(res, 0, null);
694 tslog("sqlickrypt register failed with code %d", code);
695 }
696 }
697 var child_adder = child_process.spawn("/bin/sqlickrypt", ["register"]);
698 child_adder.on("exit", checkreg);
699 child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8");
700 return;
701 }
702
703 /* Stops a running game if the request has the proper key. */
704 function stopgame(res, formdata) {
705 if (!("key" in formdata) || !(formdata["key"] in logins)) {
706 sendError(res, 1);
707 return;
708 }
709 var pname = logins[formdata["key"]].name;
710 if (!("g" in formdata) || !(formdata["g"] in games)) {
711 sendError(res, 2, "No such game.");
712 return;
713 }
714 var gname = formdata["g"];
715 function checkback(err, fname) {
716 if (!fname) {
717 sendError(res, 7);
718 return;
719 }
720 var fullfile = path.join("/dgldir/inprogress", gname, fname);
721 fs.readFile(fullfile, "utf8", function(err, fdata) {
722 if (err) {
723 sendError(res, 7);
724 return;
725 }
726 var pid = parseInt(fdata.split('\n')[0], 10);
727 try {
728 process.kill(pid, 'SIGHUP');
729 }
730 catch (err) {
731 /* If the PID is invalid, the lockfile is stale. */
732 if (err.code == "ESRCH") {
733 var nodere = RegExp("^" + pname + ":node:");
734 if (fname.match(nodere)) {
735 fs.unlink(fullfile);
736 }
737 }
738 }
739 /* The response doesn't mean that the game is gone. The only way
740 * to make sure a dgamelaunch-supervised game is over would be to
741 * poll fname until it disappears. */
742 res.writeHead(200, {'Content-Type': 'application/json'});
743 res.write(JSON.stringify({"t": "q"}));
744 res.end();
745 });
746 }
747 checkprogress(pname, games[gname], checkback, []);
748 }
749
750 function startProgressWatcher() {
751 var watchdirs = [];
752 for (var gname in games) {
753 watchdirs.push(path.join("/dgldir/inprogress", gname));
754 }
755 var subproc = child_process.spawn("/bin/dglwatcher", watchdirs);
756 subproc.stdout.setEncoding('utf8');
757 subproc.stdout.on('data', function (chunk) {
758 var fname = chunk.slice(2, -1);
759 var filere = /.*\/([^\/]*)\/([^\/:]*):(node:)?(.*)/;
760 var matchresult = fname.match(filere);
761 if (!matchresult || matchresult[3])
762 return;
763 var gname = matchresult[1];
764 var pname = matchresult[2];
765 var tag = gname + "/" + pname;
766 if (chunk[0] == "E") {
767 tslog("DGL: %s is playing %s: %s", pname, gname, fname)
768 dglgames[tag] = new DglSession(fname);
769 }
770 else if (chunk[0] == "C") {
771 tslog("DGL: %s started playing %s: %s", pname, gname, fname)
772 dglgames[tag] = new DglSession(fname);
773 }
774 else if (chunk[0] == "D") {
775 tslog("DGL: %s finished playing %s: %s", pname, gname, fname)
776 dglgames[tag].close();
777 delete dglgames[tag];
778 }
779 else {
780 tslog("Watcher says: %s", chunk)
781 }
782 });
783 subproc.stdout.resume();
784 return subproc;
785 }
786
787 function serveStatic(req, res, fname) {
788 var nname = path.normalize(fname);
789 if (nname == "" || nname == "/")
790 nname = "index.html";
791 if (nname.match(/\/$/))
792 path.join(nname, "index.html"); /* it was a directory */
793 var realname = path.join(serveStaticRoot, nname);
794 var extension = path.extname(realname);
795 fs.exists(realname, function (exists) {
796 var resheaders = {};
797 if (!exists || !extension || extension == ".html")
798 resheaders["Content-Type"] = "text/html; charset=utf-8";
799 else if (extension == ".png")
800 resheaders["Content-Type"] = "image/png";
801 else if (extension == ".css")
802 resheaders["Content-Type"] = "text/css";
803 else if (extension == ".js")
804 resheaders["Content-Type"] = "text/javascript";
805 else if (extension == ".svg")
806 resheaders["Content-Type"] = "image/svg+xml";
807 else
808 resheaders["Content-Type"] = "application/octet-stream";
809 if (exists) {
810 fs.readFile(realname, function (error, data) {
811 if (error) {
812 res.writeHead(500, {});
813 res.end();
814 }
815 else {
816 res.writeHead(200, resheaders);
817 if (req.method != 'HEAD')
818 res.write(data);
819 res.end();
820 }
821 });
822 }
823 else {
824 send404(res, nname, req.method == 'HEAD');
825 }
826 });
827 return;
828 }
829
830 /* Currently, this doesn't do anything blocking, but keep the callback */
831 function getStatus(callback) {
832 var now = new Date();
833 var statusinfo = {"s": allowlogin, "g": []};
834 for (var tag in sessions) {
835 var gamedesc = {"c": "rlg"};
836 gamedesc["p"] = sessions[tag].pname;
837 gamedesc["g"] = sessions[tag].game.uname;
838 gamedesc["i"] = now - sessions[tag].lasttime;
839 gamedesc["w"] = sessions[tag].watchers.length;
840 statusinfo["g"].push(gamedesc);
841 }
842 for (var tag in dglgames) {
843 var dglinfo = {"c": "dgl"};
844 var slash = tag.search('/');
845 dglinfo["g"] = tag.slice(0, slash);
846 dglinfo["p"] = tag.slice(slash + 1);
847 dglinfo["i"] = now - dglgames[tag].lasttime;
848 dglinfo["w"] = dglgames[tag].watchers.length;
849 statusinfo["g"].push(dglinfo);
850 }
851 callback(statusinfo);
852 }
853
854 function statusmsg(req, res) {
855 function respond(info) {
856 res.writeHead(200, { "Content-Type": "application/json" });
857 if (req.method != 'HEAD')
858 res.write(JSON.stringify(info));
859 res.end();
860 }
861 getStatus(respond);
862 }
863
864 function pstatusmsg(req, res) {
865 if (req.method == 'HEAD') {
866 res.writeHead(200, { "Content-Type": "application/json" });
867 res.end();
868 return;
869 }
870 var target = url.parse(req.url).pathname;
871 var pmatch = target.match(/^\/pstatus\/(.*)/);
872 if (pmatch && pmatch[1])
873 var pname = pmatch[1];
874 else {
875 sendError(res, 2, "No name given.");
876 return;
877 }
878 var reply = {"name": pname};
879 playerstatus(pname, function (pdata) {
880 reply["stat"] = pdata;
881 res.writeHead(200, { "Content-Type": "application/json" });
882 res.write(JSON.stringify(reply));
883 res.end();
884 });
885 }
886
887 function getuinfo(req, res) {
888 var urlobj = url.parse(req.url, true);
889 var query = urlobj.query;
890 if (!("key" in query) || !(query["key"] in logins)) {
891 sendError(res, 1);
892 return;
893 }
894 var match = urlobj.pathname.match(/^\/[^\/]*\/(.*)/);
895 if (!match || !match[1]) {
896 send404(res, urlobj.pathname, req.method == 'HEAD');
897 return;
898 }
899 var which = match[1];
900 var name = logins[query["key"]].name;
901 var reply = { "u": name };
902 function send() {
903 res.writeHead(200, { "Content-Type": "application/json" });
904 res.write(JSON.stringify(reply));
905 res.end();
906 }
907 if (which == "pw") {
908 /* Don't actually divulge passwords. */
909 reply["pw"] = "";
910 send();
911 }
912 else if (which == "email") {
913 var address;
914 function finish(code, signal) {
915 if (code != 0) {
916 tslog("sqlickrypt: %d with name %s", code, name);
917 sendError(res, 2);
918 }
919 else {
920 reply["email"] = address;
921 send();
922 }
923 }
924 var subproc = child_process.spawn("/bin/sqlickrypt", ["getmail"]);
925 subproc.stdout.on("data", function (data) {
926 address = data.toString().replace(/\n/g, "");
927 });
928 subproc.on("exit", finish);
929 subproc.stdin.end(name + '\n', "utf8");
930 }
931 else {
932 send404(res, urlobj.pathname, req.method == 'HEAD');
933 return;
934 }
935 }
936
937 function setuinfo(req, res, postdata) {
938 var urlobj = url.parse(req.url, true);
939 var query = urlobj.query;
940 if (!("key" in query) || !(query["key"] in logins)) {
941 sendError(res, 1);
942 return;
943 }
944 var name = logins[query["key"]].name;
945 var match = urlobj.pathname.match(/^\/[^\/]*\/(.*)/);
946 if (!match || !match[1]) {
947 send404(res, urlobj.pathname, true);
948 return;
949 }
950 var which = match[1];
951 if (!("v" in postdata)) {
952 sendError(res, 2, "No value provided");
953 return;
954 }
955 if (which == "email" || which == "pw") {
956 var args;
957 if (which == "email")
958 args = ["setmail"];
959 else
960 args = ["setpw"];
961 var child = child_process.execFile("/bin/sqlickrypt", args,
962 function (err, stdout, stderr) {
963 if (err) {
964 tslog("Could not set %s: sqlickrypt error %d", which, err.code);
965 sendError(res, 2);
966 }
967 else {
968 tslog("User %s has changed %s", name, which);
969 res.writeHead(200, { "Content-Type": "application/json" });
970 res.end(JSON.stringify({"t": "t"}));
971 }
972 });
973 child.stdin.end(name + "\n" + postdata.v + "\n", "utf8");
974 }
975 else {
976 send404(res, urlobj.pathname, true);
977 }
978 }
979
980 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data",
981 "Login failed", "Already playing", "Game launch failed",
982 "Server shutting down", "Game not in progress" ];
983
984 function sendError(res, ecode, msg, box) {
985 res.writeHead(200, { "Content-Type": "application/json" });
986 var edict = {"t": "E"};
987 if (!(ecode < errorcodes.length && ecode > 0))
988 ecode = 0;
989 edict["c"] = ecode;
990 edict["s"] = errorcodes[ecode];
991 if (msg)
992 edict["s"] += ": " + msg;
993 if (box)
994 res.write(JSON.stringify([edict]));
995 else
996 res.write(JSON.stringify(edict));
997 res.end();
998 }
999
1000 function send404(res, path, nopage) {
1001 res.writeHead(404, {"Content-Type": "text/html; charset=utf-8"});
1002 if (!nopage) {
1003 res.write("<html><head><title>" + path + "</title></head>\n<body><h1>"
1004 + path + " Not Found</h1></body></html>\n");
1005 }
1006 res.end();
1007 }
1008
1009 function webHandler(req, res) {
1010 /* default headers for the response */
1011 var resheaders = {'Content-Type': 'text/html'};
1012 /* The request body will be added to this as it arrives. */
1013 var reqbody = "";
1014 var formdata;
1015
1016 /* Register a listener to get the body. */
1017 function moredata(chunk) {
1018 reqbody += chunk;
1019 }
1020 req.on('data', moredata);
1021
1022 /* This will send the response once the whole request is here. */
1023 function respond() {
1024 formdata = getMsg(reqbody);
1025 var target = url.parse(req.url).pathname;
1026 /* First figure out if the client is POSTing to a command interface. */
1027 if (req.method == 'POST') {
1028 if (target == "/login") {
1029 login(req, res, formdata);
1030 }
1031 else if (target == "/addacct") {
1032 register(req, res, formdata);
1033 }
1034 else if (target == "/quit") {
1035 stopgame(res, formdata);
1036 }
1037 else if (target.match(/^\/uinfo\//)) {
1038 setuinfo(req, res, formdata);
1039 }
1040 else {
1041 res.writeHead(405, resheaders);
1042 res.end();
1043 }
1044 }
1045 else if (req.method == 'GET' || req.method == 'HEAD') {
1046 if (target == '/status') {
1047 statusmsg(req, res);
1048 }
1049 else if (target.match(/^\/uinfo\//)) {
1050 getuinfo(req, res);
1051 }
1052 else if (target.match(/^\/pstatus\//)) {
1053 pstatusmsg(req, res);
1054 }
1055 else /* Go look for it in the filesystem */
1056 serveStatic(req, res, target);
1057 }
1058 else { /* Some other method */
1059 res.writeHead(501, resheaders);
1060 res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n");
1061 res.end();
1062 }
1063 return;
1064 }
1065 req.on('end', respond);
1066 }
1067
1068 function wsHandler(wsRequest) {
1069 var watchmatch = wsRequest.resource.match(/^\/watch\/(.*)$/);
1070 var playmatch = wsRequest.resource.match(/^\/play\//);
1071 if (watchmatch !== null) {
1072 if (watchmatch[1] in sessions) {
1073 var tsession = sessions[watchmatch[1]];
1074 tsession.attach(wsRequest);
1075 tslog("Game %s is being watched via WebSockets", tsession.tag());
1076 }
1077 else if (watchmatch[1] in dglgames) {
1078 var dsession = dglgames[watchmatch[1]];
1079 dsession.attach(wsRequest);
1080 tslog("DGL game %s is being watched via WebSockets", dsession.tag());
1081 }
1082 else {
1083 wsRequest.reject(404, errorcodes[7]);
1084 return;
1085 }
1086 }
1087 else if (playmatch !== null) {
1088 wsStartGame(wsRequest);
1089 }
1090 else if (wsRequest.resourceURL.pathname == "/status") {
1091 var conn = wsRequest.accept(null, wsRequest.origin);
1092 var tell = function () {
1093 getStatus(function (info) {
1094 info["t"] = "t";
1095 conn.sendUTF(JSON.stringify(info));
1096 });
1097 }
1098 var beginH = function (gname, pname, client) {
1099 conn.sendUTF(JSON.stringify({"t": "b", "c": client, "p": pname,
1100 "g": gname}));
1101 };
1102 var listH = function (list) {
1103 conn.sendUTF(JSON.stringify(list));
1104 };
1105 var endH = function (gname, pname) {
1106 conn.sendUTF(JSON.stringify({"t": "e", "p": pname, "g": gname}));
1107 };
1108 gamemux.on('begin', beginH);
1109 gamemux.on('list', listH);
1110 gamemux.on('end', endH);
1111 conn.on('message', tell);
1112 conn.on('close', function () {
1113 gamemux.removeListener('begin', beginH);
1114 gamemux.removeListener('list', listH);
1115 gamemux.removeListener('end', endH);
1116 });
1117 tell();
1118 }
1119 else
1120 wsRequest.reject(404, "No such resource.");
1121 }
1122
1123 /* Only games with low idle time are included. Use getStatus() for the
1124 * complete list. */
1125 function pushStatus() {
1126 var now = new Date();
1127 var statusinfo = {"t": "p", "s": allowlogin, "g": []};
1128 for (var tag in sessions) {
1129 var delta = now - sessions[tag].lasttime;
1130 if (delta < 60000) {
1131 var gamedesc = {"c": "rlg"};
1132 gamedesc["p"] = sessions[tag].pname;
1133 gamedesc["g"] = sessions[tag].game.uname;
1134 gamedesc["i"] = delta;
1135 gamedesc["w"] = sessions[tag].watchers.length;
1136 statusinfo["g"].push(gamedesc);
1137 }
1138 }
1139 for (var tag in dglgames) {
1140 var delta = now - dglgames[tag].lasttime;
1141 if (delta < 60000) {
1142 var dglinfo = {"c": "dgl"};
1143 var slash = tag.search('/');
1144 dglinfo["g"] = tag.slice(0, slash);
1145 dglinfo["p"] = tag.slice(slash + 1);
1146 dglinfo["i"] = delta;
1147 dglinfo["w"] = dglgames[tag].watchers.length;
1148 statusinfo["g"].push(dglinfo);
1149 }
1150 }
1151 gamemux.emit('list', statusinfo);
1152 }
1153
1154 function shutdown () {
1155 httpServer.close();
1156 httpServer.removeAllListeners('request');
1157 ctlServer.close();
1158 tslog("Shutting down...");
1159 process.exit();
1160 }
1161
1162 function consoleHandler(chunk) {
1163 var msg = chunk.toString().split('\n')[0];
1164 if (msg == "quit") {
1165 allowlogin = false;
1166 tslog("Disconnecting...");
1167 for (var tag in sessions) {
1168 sessions[tag].close();
1169 }
1170 progressWatcher.stdin.end("\n");
1171 setTimeout(shutdown, 2000);
1172 }
1173 }
1174
1175 process.on("exit", function () {
1176 for (var tag in sessions) {
1177 sessions[tag].term.kill('SIGHUP');
1178 }
1179 tslog("Quitting...");
1180 return;
1181 });
1182
1183 /* Initialization STARTS HERE */
1184 process.env["TERM"] = "xterm-256color";
1185
1186 if (process.getuid() != 0) {
1187 tslog("Not running as root, cannot chroot.");
1188 process.exit(1);
1189 }
1190
1191 var httpServer; // declare here so shutdown() can find it
1192 var wsServer;
1193 var progressWatcher;
1194
1195 var pwent;
1196 try {
1197 pwent = posix.getpwnam(dropToUser);
1198 }
1199 catch (err) {
1200 tslog("Could not drop to user %s: user does not exist", dropToUser);
1201 process.exit(1);
1202 }
1203
1204 /* This could be nonblocking, but nothing else can start yet anyway. */
1205 if (fs.existsSync(ctlsocket)) {
1206 fs.unlinkSync(ctlsocket);
1207 }
1208
1209 /* Open the control socket before chrooting where it can't be found */
1210 var ctlServer = net.createServer(function (sock) {
1211 sock.on('data', consoleHandler);
1212 });
1213 ctlServer.listen(ctlsocket, function () {
1214 /* rlgwebd.js now assumes that it has been started by the rlgwebd shell
1215 * script, or some other method that detaches it and sets up stdio. */
1216 /* chroot and drop permissions. posix.chroot() does chdir() itself. */
1217 try {
1218 posix.chroot(chrootDir);
1219 }
1220 catch (err) {
1221 tslog("chroot to %s failed: %s", chrootDir, err);
1222 process.exit(1);
1223 }
1224 try {
1225 // drop gid first, that requires UID=0
1226 process.setgid(pwent.gid);
1227 process.setuid(pwent.uid);
1228 }
1229 catch (err) {
1230 tslog("Could not drop permissions: %s", err);
1231 process.exit(1);
1232 }
1233 httpServer = http.createServer(webHandler);
1234 httpServer.listen(httpPort);
1235 tslog('rlgwebd running on port %d', httpPort);
1236 wsServer = new WebSocketServer({"httpServer": httpServer});
1237 wsServer.on("request", wsHandler);
1238 tslog('WebSockets are online');
1239 progressWatcher = startProgressWatcher();
1240 setInterval(pushStatus, 40000);
1241 });
1242