comparison rlgwebd.js @ 55:96815eae4ebe

RLG-Web: make multiple watchers possible. Split the TermSession class into the new TermSession, which handles the PTY, and client classes, which handle HTTP sessions. These are Player and Watcher. This allows multiple watchers per game, and other improvements.
author John "Elwin" Edwards <elwin@sdf.org>
date Mon, 18 Jun 2012 13:43:51 -0700
parents 423ef87ddc9b
children 31bb3cf4f25f
comparison
equal deleted inserted replaced
54:de01aafd4dd6 55:96815eae4ebe
5 var http = require('http'); 5 var http = require('http');
6 var net = require('net'); 6 var net = require('net');
7 var url = require('url'); 7 var url = require('url');
8 var path = require('path'); 8 var path = require('path');
9 var fs = require('fs'); 9 var fs = require('fs');
10 var events = require('events');
10 var child_process = require('child_process'); 11 var child_process = require('child_process');
11 var daemon = require(path.join(localModules, "daemon")); 12 var daemon = require(path.join(localModules, "daemon"));
12 13
13 /* Configuration variables */ 14 /* Configuration variables */
14 // These first two files are NOT in the chroot. 15 // These first two files are NOT in the chroot.
50 }; 51 };
51 52
52 /* Global state */ 53 /* Global state */
53 var logins = {}; 54 var logins = {};
54 var sessions = {}; 55 var sessions = {};
56 var clients = {};
55 var allowlogin = true; 57 var allowlogin = true;
56 58 var nextsession = 0;
57 /* Constructor for TermSessions. Note that it opens the terminal and 59
58 * adds itself to the sessions dict. It currently assumes the user has 60 /* Constructor. A TermSession handles a pty and the game running on it.
59 * been authenticated. 61 * game: (String) Name of the game to launch.
62 * lkey: (String, key) The user's id, a key into logins.
63 * dims: (Array [Number, Number]) Height and width of the pty.
64 * handlers: (Object) Key-value pairs, event names and functions to
65 * install to handle them.
66 * Events:
67 * "open": Emitted on startup. Parameters: success (Boolean)
68 * "data": Data generated by child. Parameters: buf (Buffer)
69 * "exit": Child terminated. Parameters: exitcode, signal
60 */ 70 */
61 /* TODO take a callback, or emit success/err events. */ 71 function TermSession(game, lkey, dims, handlers) {
62 function TermSession(game, user, dims, lkey) { 72 var ss = this;
63 /* First make sure starting the game will work. */ 73 /* Subclass EventEmitter to do the hard work. */
74 events.EventEmitter.call(this);
75 for (var evname in handlers)
76 this.on(evname, handlers[evname]);
77 /* Don't launch anything that's not a real game. */
64 if (game in games) { 78 if (game in games) {
65 this.game = games[game]; 79 this.game = games[game];
66 } 80 }
67 else { 81 else {
68 // TODO: throw an exception instead 82 this.emit('open', false);
69 return null; 83 return;
70 } 84 }
71 this.player = String(user); 85 if (lkey in logins) {
72 this.key = lkey; 86 this.key = lkey;
73 /* This order seems to best avoid race conditions... */ 87 this.pname = logins[lkey].name;
74 this.alive = false; 88 }
75 // A kludge until TermSession is rewritten to handle real watching. 89 else {
76 this.sendq = false; 90 this.emit('open', false);
77 this.sessid = randkey(2); 91 return;
78 while (this.sessid in sessions) {
79 this.sessid = randkey(2);
80 } 92 }
81 /* Grab a spot in the sessions table. */ 93 /* Grab a spot in the sessions table. */
94 this.sessid = nextsession++;
82 sessions[this.sessid] = this; 95 sessions[this.sessid] = this;
83 /* State for messaging. */
84 this.nsend = 0;
85 this.nrecv = 0;
86 this.msgQ = []
87 this.Qtimeout = null;
88 /* Set up the sizes. */ 96 /* Set up the sizes. */
89 this.w = Math.floor(Number(dims[1])); 97 this.w = Math.floor(Number(dims[1]));
90 if (!(this.w > 0 && this.w < 256)) 98 if (!(this.w > 0 && this.w < 256))
91 this.w = 80; 99 this.w = 80;
92 this.h = Math.floor(Number(dims[0])); 100 this.h = Math.floor(Number(dims[0]));
96 var childenv = {}; 104 var childenv = {};
97 for (var key in process.env) { 105 for (var key in process.env) {
98 childenv[key] = process.env[key]; 106 childenv[key] = process.env[key];
99 } 107 }
100 childenv["PTYHELPER"] = String(this.h) + "x" + String(this.w); 108 childenv["PTYHELPER"] = String(this.h) + "x" + String(this.w);
101 /* TODO handle tty-opening errors */ 109 args = [this.game.path, "-n", this.pname];
102 /* TODO make argument-finding into a method */
103 args = [this.game.path, "-n", user.toString()];
104 this.child = child_process.spawn("/bin/ptyhelper", args, {"env": childenv}); 110 this.child = child_process.spawn("/bin/ptyhelper", args, {"env": childenv});
105 var ss = this; 111 this.emit('open', true);
106 this.alive = true;
107 this.data = [];
108 /* Set up the lockfile and ttyrec */ 112 /* Set up the lockfile and ttyrec */
109 var ts = timestamp(); 113 var ts = timestamp();
110 var progressdir = "/dgldir/inprogress-" + this.game.uname; 114 var progressdir = "/dgldir/inprogress-" + this.game.uname;
111 this.lock = path.join(progressdir, this.player + ":node:" + ts + ".ttyrec"); 115 this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec");
112 var lmsg = this.child.pid.toString() + '\n' + this.w + '\n' + this.h + '\n'; 116 var lmsg = this.child.pid.toString() + '\n' + this.w + '\n' + this.h + '\n';
113 fs.writeFile(this.lock, lmsg, "utf8"); 117 fs.writeFile(this.lock, lmsg, "utf8");
114 var ttyrec = path.join("/dgldir/ttyrec", this.player, this.game.uname, 118 var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.game.uname,
115 ts + ".ttyrec"); 119 ts + ".ttyrec");
116 this.record = fs.createWriteStream(ttyrec, { mode: 0664 }); 120 this.record = fs.createWriteStream(ttyrec, { mode: 0664 });
121 logins[lkey].sessions.push(this.sessid);
122 tslog("%s playing %s (index %d, pid %d)", this.pname, this.game.uname,
123 this.sessid, this.child.pid);
117 /* END setup */ 124 /* END setup */
118 function ttyrec_chunk(buf) { 125 function ttyrec_chunk(buf) {
119 var ts = new Date(); 126 var ts = new Date();
120 var chunk = new Buffer(buf.length + 12); 127 var chunk = new Buffer(buf.length + 12);
121 /* TTYREC headers */ 128 /* TTYREC headers */
122 chunk.writeUInt32LE(Math.floor(ts.getTime() / 1000), 0); 129 chunk.writeUInt32LE(Math.floor(ts.getTime() / 1000), 0);
123 chunk.writeUInt32LE(1000 * (ts.getTime() % 1000), 4); 130 chunk.writeUInt32LE(1000 * (ts.getTime() % 1000), 4);
124 chunk.writeUInt32LE(buf.length, 8); 131 chunk.writeUInt32LE(buf.length, 8);
125 buf.copy(chunk, 12); 132 buf.copy(chunk, 12);
126 ss.data.push(chunk);
127 ss.record.write(chunk); 133 ss.record.write(chunk);
134 ss.emit('data', buf);
128 } 135 }
129 this.child.stdout.on("data", ttyrec_chunk); 136 this.child.stdout.on("data", ttyrec_chunk);
130 this.child.stderr.on("data", ttyrec_chunk); 137 this.child.stderr.on("data", ttyrec_chunk);
138 this.write = function(data) {
139 this.child.stdin.write(data);
140 };
131 this.child.on("exit", function (code, signal) { 141 this.child.on("exit", function (code, signal) {
132 ss.exitcode = (code != null ? code : 255); 142 fs.unlink(ss.lock);
143 ss.record.end();
144 ss.emit('exit', code, signal);
145 var id = ss.sessid;
146 delete sessions[id];
147 tslog("Session %s ended.", id);
148 });
149 this.close = function () {
150 this.child.kill('SIGHUP');
151 };
152 }
153 TermSession.prototype = new events.EventEmitter();
154
155 function Watcher(session) {
156 var ss = this; // that
157 this.session = session;
158 this.alive = true;
159 /* State for messaging. */
160 this.nsend = 0;
161 this.sendQ = [];
162 /* Get a place in the table. */
163 this.id = randkey(2);
164 while (this.id in clients) {
165 this.id = randkey(2);
166 }
167 clients[this.id] = this;
168 function dataH(buf) {
169 var reply = {};
170 reply.t = "d";
171 reply.n = ss.nsend++;
172 reply.d = buf.toString("hex");
173 ss.sendQ.push(reply);
174 }
175 function exitH(code, signal) {
133 ss.alive = false; 176 ss.alive = false;
134 fs.unlink(ss.lock); 177 ss.sendQ.push({"t": "q"});
135 /* Wait for all the data to get collected */ 178 }
136 setTimeout(ss.cleanup, 1000); 179 session.on('data', dataH);
137 }); 180 session.on('exit', exitH);
181 this.read = function() {
182 /* Returns an array of all outstanding messages, empty if none. */
183 var temp = this.sendQ;
184 this.sendQ = [];
185 /* Clean up if finished. */
186 if (!this.alive) {
187 delete clients[this.id];
188 }
189 return temp;
190 };
191 this.quit = function() {
192 this.session.removeListener('data', dataH);
193 this.session.removeListener('exit', exitH);
194 delete clients[this.id];
195 };
196 }
197
198 function Player(gamename, lkey, dims, callback) {
199 var ss = this;
200 this.alive = false;
201 /* State for messaging. */
202 this.nsend = 0;
203 this.nrecv = 0;
204 this.sendQ = [];
205 this.recvQ = []
206 this.Qtimeout = null;
207 /* Get a place in the table. */
208 this.id = randkey(2);
209 while (this.id in clients) {
210 this.id = randkey(2);
211 }
212 clients[this.id] = this;
213
214 this.read = function() {
215 var temp = this.sendQ;
216 this.sendQ = [];
217 /* Clean up if finished. */
218 if (!this.alive) {
219 clearTimeout(this.Qtimeout);
220 delete clients[this.id];
221 }
222 return temp;
223 };
138 this.write = function (data, n) { 224 this.write = function (data, n) {
139 if (!this.alive || typeof (n) != "number") { 225 if (!this.alive || typeof (n) != "number") {
140 return; 226 return;
141 } 227 }
142 //console.log("Got message " + n);
143 var oindex = n - this.nrecv; 228 var oindex = n - this.nrecv;
144 if (oindex === 0) { 229 if (oindex === 0) {
145 //console.log("Writing message " + n); 230 this.session.write(data);
146 this.child.stdin.write(data);
147 this.nrecv++; 231 this.nrecv++;
148 var next; 232 var next;
149 while ((next = this.msgQ.shift()) !== undefined) { 233 while ((next = this.recvQ.shift()) !== undefined) {
150 //console.log("Writing message " + this.nrecv); 234 this.session.write(next);
151 this.child.stdin.write(next);
152 this.nrecv++; 235 this.nrecv++;
153 } 236 }
154 if (this.msgQ.length == 0 && this.Qtimeout) { 237 if (this.recvQ.length == 0 && this.Qtimeout) {
155 clearTimeout(this.Qtimeout); 238 clearTimeout(this.Qtimeout);
156 this.Qtimeout = null; 239 this.Qtimeout = null;
157 } 240 }
158 } 241 }
159 else if (oindex > 0 && oindex <= 1024) { 242 else if (oindex > 0 && oindex <= 1024) {
160 tslog("Stashing message %d at %d", n, oindex - 1); 243 tslog("Client %s: Stashing message %d at %d", this.id, n, oindex - 1);
161 this.msgQ[oindex - 1] = data; 244 this.recvQ[oindex - 1] = data;
162 if (!this.Qtimeout) { 245 if (!this.Qtimeout) {
163 var nextn = this.nrecv + this.msgQ.length + 1; 246 var nextn = this.nrecv + this.recvQ.length + 1;
164 this.Qtimeout = setTimeout(this.flushQ, 30000, this, nextn); 247 this.Qtimeout = setTimeout(this.flushQ, 30000, this, nextn);
165 } 248 }
166 } 249 }
167 /* Otherwise, discard it */ 250 /* Otherwise, discard it */
168 return; 251 return;
169 }; 252 };
170 this.flushQ = function (session, n) { 253 this.flushQ = function (client, n) {
171 /* Callback for when an unreceived message times out. 254 /* Callback for when an unreceived message times out.
172 * n is the first empty space that will not be given up on. */ 255 * n is the first empty space that will not be given up on. */
173 if (!session.alive) 256 if (!client.alive || client.nrecv >= n)
174 return; 257 return;
175 session.nrecv++; 258 client.nrecv++;
176 var next; 259 var next;
177 /* Clear the queue up to n */ 260 /* Clear the queue up to n */
178 while (session.nrecv < n) { 261 while (client.nrecv < n) {
179 next = session.msgQ.shift(); 262 next = client.recvQ.shift();
180 if (next !== undefined) 263 if (next !== undefined)
181 session.child.stdin.write(next); 264 client.session.write(next);
182 session.nrecv++; 265 client.nrecv++;
183 } 266 }
184 /* Clear out anything that's ready. */ 267 /* Clear out anything that's ready. */
185 while ((next = session.msgQ.shift()) !== undefined) { 268 while ((next = client.recvQ.shift()) !== undefined) {
186 session.child.stdin.write(next); 269 client.session.write(next);
187 session.nrecv++; 270 client.nrecv++;
188 } 271 }
189 /* Now set another timeout if necessary. */ 272 /* Now set another timeout if necessary. */
190 if (session.msgQ.length != 0) { 273 if (client.recvQ.length != 0) {
191 var nextn = session.nrecv + session.msgQ.length + 1; 274 var nextn = client.nrecv + client.recvQ.length + 1;
192 session.Qtimeout = setTimeout(session.flushQ, 30000, session, nextn); 275 client.Qtimeout = setTimeout(client.flushQ, 30000, client, nextn);
193 } 276 }
194 tslog("Flushing queue for session %s", session.sessid); 277 tslog("Flushing queue for player %s", player.id);
195 }; 278 };
196 this.read = function () { 279 this.quit = function() {
197 if (this.data.length == 0) 280 if (this.alive)
198 return null; 281 this.session.close();
199 var pos = 0;
200 var i = 0;
201 for (i = 0; i < this.data.length; i++)
202 pos += this.data[i].length - 12;
203 var nbuf = new Buffer(pos);
204 var tptr;
205 pos = 0;
206 while (this.data.length > 0) {
207 tptr = this.data.shift();
208 tptr.copy(nbuf, pos, 12);
209 pos += tptr.length - 12;
210 }
211 return nbuf;
212 }; 282 };
213 this.close = function () { 283 function openH(success) {
214 if (this.alive) 284 if (success) {
215 this.child.kill('SIGHUP'); 285 ss.alive = true;
216 }; 286 }
217 this.cleanup = function () { 287 callback(ss, success);
218 /* Call this when the child is dead. */ 288 }
219 if (ss.alive) 289 function dataH(chunk) {
220 return; 290 var reply = {};
221 ss.record.end(); 291 reply.t = "d";
222 /* Give the client a chance to read any leftover data. */ 292 reply.n = ss.nsend++;
223 if (ss.data.length > 0 || !ss.sendq) 293 reply.d = chunk.toString("hex");
224 setTimeout(ss.remove, 8000); 294 ss.sendQ.push(reply);
225 else 295 }
226 ss.remove(); 296 function exitH(code, signal) {
227 }; 297 ss.alive = false;
228 this.remove = function () { 298 ss.sendQ.push({"t": "q"});
229 var id = ss.sessid; 299 }
230 delete sessions[id]; 300 var handlers = {'open': openH, 'data': dataH, 'exit': exitH};
231 tslog("Session %s removed.", id); 301 this.session = new TermSession(gamename, lkey, dims, handlers);
232 }; 302 }
233 } 303
234 304 /* Some functions which check whether a player is currently playing or
305 * has a saved game. Maybe someday they will provide information on
306 * the game. */
235 function checkprogress(user, game, callback, args) { 307 function checkprogress(user, game, callback, args) {
236 var progressdir = "/dgldir/inprogress-" + game.uname; 308 var progressdir = "/dgldir/inprogress-" + game.uname;
237 fs.readdir(progressdir, function(err, files) { 309 fs.readdir(progressdir, function(err, files) {
238 if (err) { 310 if (err) {
239 args.unshift(err, null); 311 args.unshift(err, null);
357 } 429 }
358 430
359 function reaper() { 431 function reaper() {
360 var now = new Date(); 432 var now = new Date();
361 function reapcheck(session) { 433 function reapcheck(session) {
362 if (!session.alive)
363 return;
364 fs.fstat(session.record.fd, function (err, stats) { 434 fs.fstat(session.record.fd, function (err, stats) {
365 if (!err && now - stats.mtime > playtimeout) { 435 if (!err && now - stats.mtime > playtimeout) {
366 tslog("Reaping %s", session.sessid); 436 tslog("Reaping session %s", session.sessid);
367 /* Dissociate it with its login name. */ 437 /* Dissociate it with its login name. */
368 var sn = logins[session.key].sessions.indexOf(session.sessid); 438 var sn = logins[session.key].sessions.indexOf(session.sessid);
369 if (sn >= 0) { 439 if (sn >= 0) {
370 logins[session.key].sessions.splice(sn, 1); 440 logins[session.key].sessions.splice(sn, 1);
371 if (now - logins[session.key].ts > playtimeout) 441 if (now - logins[session.key].ts > playtimeout)
401 expired.push(targarray[i]); 471 expired.push(targarray[i]);
402 } 472 }
403 if (expired.length > 0) { 473 if (expired.length > 0) {
404 logins[lkey].ts = new Date(now); 474 logins[lkey].ts = new Date(now);
405 for (var j = 0; j < expired.length; j++) { 475 for (var j = 0; j < expired.length; j++) {
406 targarray.splice(targarray.indexOf(expired[j], 1)); 476 targarray.splice(targarray.indexOf(expired[j]), 1);
407 } 477 }
408 } 478 }
409 } 479 }
410 } 480 }
411 } 481 }
412 482
413 function login(req, res, formdata) { 483 function login(req, res, formdata) {
414 if (!allowlogin) { 484 if (!allowlogin) {
415 sendError(res, 6, null); 485 sendError(res, 6, null, false);
416 return; 486 return;
417 } 487 }
418 if (!("name" in formdata)) { 488 if (!("name" in formdata)) {
419 sendError(res, 2, "Username not given."); 489 sendError(res, 2, "Username not given.", false);
420 return; 490 return;
421 } 491 }
422 else if (!("pw" in formdata)) { 492 else if (!("pw" in formdata)) {
423 sendError(res, 2, "Password not given."); 493 sendError(res, 2, "Password not given.", false);
424 return; 494 return;
425 } 495 }
426 var username = String(formdata["name"]); 496 var username = String(formdata["name"]);
427 var password = String(formdata["pw"]); 497 var password = String(formdata["pw"]);
428 function checkit(code, signal) { 498 function checkit(code, signal) {
476 else { 546 else {
477 logins[lkey].ts = new Date(); 547 logins[lkey].ts = new Date();
478 } 548 }
479 var username = logins[lkey].name; 549 var username = logins[lkey].name;
480 var gname = formdata["game"]; 550 var gname = formdata["game"];
551 // If dims are not given or invalid, the constructor will handle it.
481 var dims = [formdata["h"], formdata["w"]]; 552 var dims = [formdata["h"], formdata["w"]];
482 if (!(gname in games)) { 553 if (!(gname in games)) {
483 sendError(res, 2, "No such game: " + gname); 554 sendError(res, 2, "No such game: " + gname);
484 tslog("Request for nonexistant game \"%s\"", gname); 555 tslog("Request for nonexistant game \"%s\"", gname);
485 return; 556 return;
490 sendError(res, 4, null); 561 sendError(res, 4, null);
491 tslog("%s is already playing %s", username, gname); 562 tslog("%s is already playing %s", username, gname);
492 return; 563 return;
493 } 564 }
494 // Game starting has been approved. 565 // Game starting has been approved.
495 var nsession = new TermSession(gname, username, dims, lkey); 566 var respondlaunch = function(nclient, success) {
496 if (nsession) { 567 if (success) {
497 res.writeHead(200, {'Content-Type': 'application/json'}); 568 res.writeHead(200, {'Content-Type': 'application/json'});
498 var reply = {"t": "l", "id": nsession.sessid, "w": nsession.w, "h": 569 var reply = {"t": "s", "id": nclient.id, "w": nclient.w, "h":
499 nsession.h}; 570 nclient.h};
500 res.write(JSON.stringify(reply)); 571 res.write(JSON.stringify(reply));
501 res.end(); 572 res.end();
502 tslog("%s playing %s (key %s, pid %d)", username, gname, 573 }
503 nsession.sessid, nsession.child.pid); 574 else {
504 logins[lkey].sessions.push(nsession.sessid); 575 sendError(res, 5, "Failed to open TTY");
505 } 576 tslog("Unable to allocate TTY for %s", gname);
506 else { 577 }
507 sendError(res, 5, "Failed to open TTY"); 578 };
508 tslog("Unable to allocate TTY for %s", gname); 579 new Player(gname, lkey, dims, respondlaunch);
509 } 580 };
510 }
511 checkprogress(username, games[gname], launch, []); 581 checkprogress(username, games[gname], launch, []);
582 }
583
584 function watch(req, res, formdata) {
585 if (!("n" in formdata)) {
586 sendError(res, 2, "Game number not given");
587 return;
588 }
589 var gamenumber = Number(formdata["n"]);
590 if (!(gamenumber in sessions)) {
591 sendError(res, 7);
592 return;
593 }
594 var session = sessions[gamenumber];
595 var watch = new Watcher(session);
596 var reply = {"t": "w", "id": watch.id, "w": session.w, "h": session.h};
597 res.writeHead(200, {'Content-Type': 'application/json'});
598 res.write(JSON.stringify(reply));
599 res.end();
600 tslog("Game %d is being watched (key %s)", gamenumber, watch.id);
512 } 601 }
513 602
514 /* Sets things up for a new user, like dgamelaunch's commands[register] */ 603 /* Sets things up for a new user, like dgamelaunch's commands[register] */
515 function regsetup(username) { 604 function regsetup(username) {
516 function regsetup_l2(err) { 605 function regsetup_l2(err) {
572 child_adder.on("exit", checkreg); 661 child_adder.on("exit", checkreg);
573 child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8"); 662 child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8");
574 return; 663 return;
575 } 664 }
576 665
577 function endgame(term, res) { 666 /* Ends the game, obviously. Less obviously, stops watching the game if
578 if (!term.alive) { 667 * the client is a Watcher instead of a Player. */
579 sendError(res, 7, null); 668 function endgame(client, res) {
580 return; 669 if (!client.alive) {
581 } 670 sendError(res, 7, null, true);
582 term.close(); 671 return;
583 var resheaders = {'Content-Type': 'application/json'}; 672 }
584 res.writeHead(200, resheaders); 673 client.quit();
585 res.write(JSON.stringify({"t": "q"})); 674 // Give things some time to happen.
586 res.end(); 675 if (client instanceof Player)
587 term.sendq = true; 676 setTimeout(readFeed, 200, client, res);
588 return; 677 return;
589 } 678 }
590 679
591 function findTermSession(formdata) { 680 function findClient(formdata, playersOnly) {
592 if (typeof(formdata) != "object") 681 if (typeof(formdata) != "object")
593 return null; 682 return null;
594 if ("id" in formdata) { 683 if ("id" in formdata) {
595 var sessid = formdata["id"]; 684 var id = formdata["id"];
596 if (sessid in sessions) { 685 if (id in clients && (!playersOnly || clients[id] instanceof Player)) {
597 return sessions[sessid]; 686 return clients[id];
598 } 687 }
599 } 688 }
600 return null; 689 return null;
601 } 690 }
602 691
646 } 735 }
647 }); 736 });
648 return; 737 return;
649 } 738 }
650 739
651 function readFeed(res, term) { 740 function readFeed(client, res) {
652 if (term) { 741 if (!client) {
653 var reply = {}; 742 sendError(res, 7, null, true);
654 var result = term.read(); 743 return;
655 if (result == null) { 744 }
656 if (term.alive) 745 var msgs = client.read();
657 reply.t = "n"; 746 if (!allowlogin && !msgs.length) {
658 else { 747 sendError(res, 6, null, true);
659 if (allowlogin) { 748 return;
660 reply.t = "q"; 749 }
661 term.sendq = true; 750 res.writeHead(200, { "Content-Type": "application/json" });
662 } 751 res.write(JSON.stringify(msgs));
663 else { 752 res.end();
664 sendError(res, 6, null);
665 return;
666 }
667 }
668 }
669 else {
670 reply.t = "d";
671 reply.n = term.nsend++;
672 reply.d = result.toString("hex");
673 }
674 res.writeHead(200, { "Content-Type": "application/json" });
675 res.write(JSON.stringify(reply));
676 res.end();
677 }
678 else {
679 sendError(res, 7, null);
680 }
681 } 753 }
682 754
683 function statusmsg(req, res) { 755 function statusmsg(req, res) {
684 var reply = {"s": allowlogin, "g": []}; 756 var reply = {"s": allowlogin, "g": []};
685 for (var sessid in sessions) { 757 for (var sessid in sessions) {
686 if (sessions[sessid].alive) { 758 var gamedesc = {};
687 var gamedesc = {}; 759 gamedesc["n"] = sessid;
688 gamedesc["p"] = sessions[sessid].player; 760 gamedesc["p"] = sessions[sessid].pname;
689 gamedesc["g"] = sessions[sessid].game.name; 761 gamedesc["g"] = sessions[sessid].game.name;
690 reply["g"].push(gamedesc); 762 reply["g"].push(gamedesc);
691 }
692 } 763 }
693 res.writeHead(200, { "Content-Type": "application/json" }); 764 res.writeHead(200, { "Content-Type": "application/json" });
694 if (req.method != 'HEAD') 765 if (req.method != 'HEAD')
695 res.write(JSON.stringify(reply)); 766 res.write(JSON.stringify(reply));
696 res.end(); 767 res.end();
721 792
722 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data", 793 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data",
723 "Login failed", "Already playing", "Game launch failed", 794 "Login failed", "Already playing", "Game launch failed",
724 "Server shutting down", "Game not in progress" ]; 795 "Server shutting down", "Game not in progress" ];
725 796
726 function sendError(res, ecode, msg) { 797 function sendError(res, ecode, msg, box) {
727 res.writeHead(200, { "Content-Type": "application/json" }); 798 res.writeHead(200, { "Content-Type": "application/json" });
728 var edict = {"t": "E"}; 799 var edict = {"t": "E"};
729 if (!(ecode < errorcodes.length && ecode > 0)) 800 if (!(ecode < errorcodes.length && ecode > 0))
730 ecode = 0; 801 ecode = 0;
731 edict["c"] = ecode; 802 edict["c"] = ecode;
732 edict["s"] = errorcodes[ecode]; 803 edict["s"] = errorcodes[ecode];
733 if (msg) 804 if (msg)
734 edict["s"] += ": " + msg; 805 edict["s"] += ": " + msg;
735 res.write(JSON.stringify(edict)); 806 if (box)
807 res.write(JSON.stringify([edict]));
808 else
809 res.write(JSON.stringify(edict));
736 res.end(); 810 res.end();
737 } 811 }
738 812
813 // TODO new-objects done to here
739 function webHandler(req, res) { 814 function webHandler(req, res) {
740 /* default headers for the response */ 815 /* default headers for the response */
741 var resheaders = {'Content-Type': 'text/html'}; 816 var resheaders = {'Content-Type': 'text/html'};
742 /* The request body will be added to this as it arrives. */ 817 /* The request body will be added to this as it arrives. */
743 var reqbody = ""; 818 var reqbody = "";
751 826
752 /* This will send the response once the whole request is here. */ 827 /* This will send the response once the whole request is here. */
753 function respond() { 828 function respond() {
754 formdata = getMsg(reqbody); 829 formdata = getMsg(reqbody);
755 var target = url.parse(req.url).pathname; 830 var target = url.parse(req.url).pathname;
756 var cterm = findTermSession(formdata);
757 /* First figure out if the client is POSTing to a command interface. */ 831 /* First figure out if the client is POSTing to a command interface. */
758 if (req.method == 'POST') { 832 if (req.method == 'POST') {
759 if (target == '/feed') { 833 if (target == '/feed') {
760 if (!cterm) { 834 var client = findClient(formdata, false);
761 sendError(res, 7, null); 835 if (!client) {
836 sendError(res, 7, null, true);
762 return; 837 return;
763 } 838 }
764 if (formdata.t == "q") { 839 if (formdata.t == "q") {
765 /* The client wants to terminate the process. */ 840 /* The client wants to terminate the process. */
766 endgame(cterm, res); 841 endgame(client, res);
767 } 842 }
768 else if (formdata.t == "d" && typeof(formdata.d) == "string") { 843 else if (formdata.t == "d" && typeof(formdata.d) == "string") {
844 if (!(client instanceof Player)) {
845 sendError(res, 7, "Watching", true);
846 return;
847 }
769 /* process the keys */ 848 /* process the keys */
770 hexstr = formdata.d.replace(/[^0-9a-f]/gi, ""); 849 hexstr = formdata.d.replace(/[^0-9a-f]/gi, "");
771 if (hexstr.length % 2 != 0) { 850 if (hexstr.length % 2 != 0) {
772 sendError(res, 2, "incomplete byte"); 851 sendError(res, 2, "incomplete byte", true);
773 return; 852 return;
774 } 853 }
775 keybuf = new Buffer(hexstr, "hex"); 854 keybuf = new Buffer(hexstr, "hex");
776 /* TODO OoO correction */ 855 client.write(keybuf, formdata.n);
777 cterm.write(keybuf, formdata.n);
778 } 856 }
779 readFeed(res, cterm); 857 readFeed(client, res);
780 } 858 }
781 else if (target == "/login") { 859 else if (target == "/login") {
782 login(req, res, formdata); 860 login(req, res, formdata);
783 } 861 }
784 else if (target == "/addacct") { 862 else if (target == "/addacct") {
785 register(req, res, formdata); 863 register(req, res, formdata);
786 } 864 }
787 else if (target == "/play") { 865 else if (target == "/play") {
788 startgame(req, res, formdata); 866 startgame(req, res, formdata);
867 }
868 else if (target == "/watch") {
869 watch(req, res, formdata);
789 } 870 }
790 else { 871 else {
791 res.writeHead(405, resheaders); 872 res.writeHead(405, resheaders);
792 res.end(); 873 res.end();
793 } 874 }
795 else if (req.method == 'GET' || req.method == 'HEAD') { 876 else if (req.method == 'GET' || req.method == 'HEAD') {
796 if (target == '/feed') { 877 if (target == '/feed') {
797 if (req.method == 'HEAD') { 878 if (req.method == 'HEAD') {
798 res.writeHead(200, {"Content-Type": "application/json"}); 879 res.writeHead(200, {"Content-Type": "application/json"});
799 res.end(); 880 res.end();
800 return;
801 } 881 }
802 if (!cterm) { 882 else
803 sendError(res, 7, null); 883 sendError(res, 7, null, true);
804 return; 884 return;
805 }
806 readFeed(res, cterm);
807 } 885 }
808 else if (target == '/status') { 886 else if (target == '/status') {
809 statusmsg(req, res); 887 statusmsg(req, res);
810 } 888 }
811 else if (target.match(/^\/pstatus\//)) { 889 else if (target.match(/^\/pstatus\//)) {
839 allowlogin = false; 917 allowlogin = false;
840 tslog("Disconnecting..."); 918 tslog("Disconnecting...");
841 for (var sessid in sessions) { 919 for (var sessid in sessions) {
842 sessions[sessid].close(); 920 sessions[sessid].close();
843 } 921 }
844 setTimeout(shutdown, 10000); 922 setTimeout(shutdown, 2000);
845 } 923 }
846 } 924 }
847 925
848 process.on("exit", function () { 926 process.on("exit", function () {
849 for (var sessid in sessions) { 927 for (var sessid in sessions) {
850 if (sessions[sessid].alive) 928 sessions[sessid].child.kill('SIGHUP');
851 sessions[sessid].child.kill('SIGHUP');
852 } 929 }
853 tslog("Quitting..."); 930 tslog("Quitting...");
854 return; 931 return;
855 }); 932 });
856 933