comparison rlgwebd.js @ 39:e8ac0e3d2614

RLG-Web: separate logging in and starting a game. The user now logs in with a username and password, receiving a token which is then used for any actions requiring authentication. Starting a game is one such action. Games use a different set of id keys. This allows users to supply their passwords once and then play any number of successive games. Also, newly registered users do not need to supply their passwords again.
author John "Elwin" Edwards <elwin@sdf.org>
date Thu, 07 Jun 2012 15:43:06 -0700
parents 353be34de307
children f7116eb3f791
comparison
equal deleted inserted replaced
38:b06a14876645 39:e8ac0e3d2614
19 var dropToUID = 501; 19 var dropToUID = 501;
20 var dropToGID = 501; 20 var dropToGID = 501;
21 var serveStaticRoot = "/var/www/"; // inside the chroot 21 var serveStaticRoot = "/var/www/"; // inside the chroot
22 var playtimeout = 3600000; // Idle time before games are autosaved, in ms 22 var playtimeout = 3600000; // Idle time before games are autosaved, in ms
23 23
24 /* Global state */ 24 /* Data on the games available. */
25 var sessions = {};
26 var allowlogin = true;
27
28 var games = { 25 var games = {
29 "rogue3": { 26 "rogue3": {
30 "name": "Rogue V3", 27 "name": "Rogue V3",
31 "uname": "rogue3", 28 "uname": "rogue3",
32 "path": "/bin/rogue3" 29 "path": "/bin/rogue3"
46 "uname": "srogue", 43 "uname": "srogue",
47 "path": "/bin/srogue" 44 "path": "/bin/srogue"
48 } 45 }
49 }; 46 };
50 47
48 /* Global state */
49 var logins = {};
50 var sessions = {};
51 var allowlogin = true;
52
51 /* Constructor for TermSessions. Note that it opens the terminal and 53 /* Constructor for TermSessions. Note that it opens the terminal and
52 * adds itself to the sessions dict. It currently assumes the user has 54 * adds itself to the sessions dict. It currently assumes the user has
53 * been authenticated. 55 * been authenticated.
54 */ 56 */
55 function TermSession(game, user, files, dims) { 57 function TermSession(game, user, files, dims) {
62 return null; 64 return null;
63 } 65 }
64 this.player = user; 66 this.player = user;
65 /* This order seems to best avoid race conditions... */ 67 /* This order seems to best avoid race conditions... */
66 this.alive = false; 68 this.alive = false;
67 this.sessid = randkey(); 69 this.sessid = randkey(2);
68 while (this.sessid in sessions) { 70 while (this.sessid in sessions) {
69 this.sessid = randkey(); 71 this.sessid = randkey(2);
70 } 72 }
71 /* Grab a spot in the sessions table. */ 73 /* Grab a spot in the sessions table. */
72 sessions[this.sessid] = this; 74 sessions[this.sessid] = this;
73 /* State for messaging. */ 75 /* State for messaging. */
74 this.nsend = 0; 76 this.nsend = 0;
223 sd = dd.toISOString(); 225 sd = dd.toISOString();
224 sd = sd.slice(0, sd.indexOf(".")); 226 sd = sd.slice(0, sd.indexOf("."));
225 return sd.replace("T", "."); 227 return sd.replace("T", ".");
226 } 228 }
227 229
228 function randkey() { 230 function randkey(words) {
229 rnum = Math.floor(Math.random() * 65536 * 65536); 231 if (!words || !(words > 0))
230 hexstr = rnum.toString(16); 232 words = 1;
231 while (hexstr.length < 8) 233 function rand32() {
232 hexstr = "0" + hexstr; 234 rnum = Math.floor(Math.random() * 65536 * 65536);
233 return hexstr; 235 hexstr = rnum.toString(16);
236 while (hexstr.length < 8)
237 hexstr = "0" + hexstr;
238 return hexstr;
239 }
240 var key = "";
241 for (var i = 0; i < words; i++)
242 key += rand32();
243 return key;
234 } 244 }
235 245
236 function tslog() { 246 function tslog() {
237 arguments[0] = new Date().toISOString() + ": " + String(arguments[0]); 247 arguments[0] = new Date().toISOString() + ": " + String(arguments[0]);
238 console.log.apply(console, arguments); 248 console.log.apply(console, arguments);
293 function login(req, res, formdata) { 303 function login(req, res, formdata) {
294 if (!allowlogin) { 304 if (!allowlogin) {
295 sendError(res, 6, null); 305 sendError(res, 6, null);
296 return; 306 return;
297 } 307 }
298 if (!("game" in formdata)) { 308 if (!("name" in formdata)) {
299 sendError(res, 2, "No game specified.");
300 return;
301 }
302 else if (!("name" in formdata)) {
303 sendError(res, 2, "Username not given."); 309 sendError(res, 2, "Username not given.");
304 return; 310 return;
305 } 311 }
306 else if (!("pw" in formdata)) { 312 else if (!("pw" in formdata)) {
307 sendError(res, 2, "Password not given."); 313 sendError(res, 2, "Password not given.");
308 return; 314 return;
309 } 315 }
310 var username = formdata["name"]; 316 var username = String(formdata["name"]);
311 var password = formdata["pw"]; 317 var password = String(formdata["pw"]);
318 function checkit(code, signal) {
319 /* Checks the exit status, see sqlickrypt.c for details. */
320 if (code != 0) {
321 sendError(res, 3);
322 if (code == 1)
323 tslog("Password check failed for user %s", username);
324 else if (code == 2)
325 tslog("Attempted login by nonexistent user %s", username);
326 else
327 tslog("Login failed: sqlickrypt error %d", code);
328 return;
329 }
330 var lkey = randkey(2);
331 while (lkey in logins)
332 lkey = randkey(2);
333 logins[lkey] = {"name": username, "ts": new Date()};
334 res.writeHead(200, {'Content-Type': 'application/json'});
335 var reply = {"t": "l", "k": lkey, "u": username};
336 res.write(JSON.stringify(reply));
337 res.end();
338 tslog("%s has logged in (key %s)", username, lkey);
339 return;
340 }
341 /* Launch the sqlickrypt utility to check the password. */
342 var pwchecker = child_process.spawn("/bin/sqlickrypt", ["check"]);
343 pwchecker.on("exit", checkit);
344 pwchecker.stdin.end(username + '\n' + password + '\n', "utf8");
345 return;
346 }
347
348 function startgame(req, res, formdata) {
349 if (!allowlogin) {
350 sendError(res, 6, null);
351 return;
352 }
353 if (!("key" in formdata)) {
354 sendError(res, 2, "No key given.");
355 return;
356 }
357 else if (!("game" in formdata)) {
358 sendError(res, 2, "No game specified.");
359 return;
360 }
361 var lkey = String(formdata["key"]);
362 if (!(lkey in logins)) {
363 sendError(res, 1, null);
364 return;
365 }
366 else {
367 logins[lkey].ts = new Date();
368 }
369 var username = logins[lkey].name;
312 var gname = formdata["game"]; 370 var gname = formdata["game"];
313 var dims = [formdata["h"], formdata["w"]]; 371 var dims = [formdata["h"], formdata["w"]];
314 if (!(gname in games)) { 372 if (!(gname in games)) {
315 sendError(res, 2, "No such game: " + gname); 373 sendError(res, 2, "No such game: " + gname);
316 tslog("Request for nonexistant game \"%s\"", gname); 374 tslog("Request for nonexistant game \"%s\"", gname);
317 return; 375 return;
318 } 376 }
377 // check for an existing game
319 var progressdir = "/dgldir/inprogress-" + games[gname].uname; 378 var progressdir = "/dgldir/inprogress-" + games[gname].uname;
320 379 fs.readdir(progressdir, function(err, files) {
321 // This sets up the game once starting is approved. 380 if (!err) {
322 function startgame() { 381 var fre = RegExp("^" + username + ":");
382 for (var i = 0; i < files.length; i++) {
383 if (files[i].match(fre)) {
384 sendError(res, 4, null);
385 tslog("%s is already playing %s", username, gname);
386 return;
387 }
388 }
389 }
390 // Game starting has been approved.
323 var ts = timestamp(); 391 var ts = timestamp();
324 var lockfile = path.join(progressdir, username + ":node:" + ts + ".ttyrec"); 392 var lockfile = path.join(progressdir, username + ":node:" + ts + ".ttyrec");
325 var ttyrec = path.join("/dgldir/ttyrec", username, gname, ts + ".ttyrec"); 393 var ttyrec = path.join("/dgldir/ttyrec", username, gname, ts + ".ttyrec");
326 var nsession = new TermSession(gname, username, [lockfile, ttyrec], dims); 394 var nsession = new TermSession(gname, username, [lockfile, ttyrec], dims);
327 if (nsession) { 395 if (nsession) {
338 } 406 }
339 else { 407 else {
340 sendError(res, 5, "Failed to open TTY"); 408 sendError(res, 5, "Failed to open TTY");
341 tslog("Unable to allocate TTY for %s", gname); 409 tslog("Unable to allocate TTY for %s", gname);
342 } 410 }
343 } 411 });
344 function checkit(code, signal) {
345 // check the password
346 if (code != 0) {
347 sendError(res, 3);
348 if (code == 1)
349 tslog("Password check failed for user %s", username);
350 else if (code == 2)
351 tslog("Attempted login by nonexistent user %s", username);
352 else
353 tslog("Login failed: sqlickrypt error %d", code);
354 return;
355 }
356 // check for an existing game
357 fs.readdir(progressdir, function(err, files) {
358 if (!err) {
359 var fre = RegExp("^" + username + ":");
360 for (var i = 0; i < files.length; i++) {
361 if (files[i].match(fre)) {
362 sendError(res, 4, null);
363 tslog("%s is already playing %s", username, gname);
364 return;
365 }
366 }
367 }
368 // If progressdir isn't readable, start a new game anyway.
369 startgame();
370 });
371 }
372 /* Launch the sqlickrypt utility to check the password. */
373 var checker = child_process.spawn("/bin/sqlickrypt", ["check"]);
374 checker.on("exit", checkit);
375 checker.stdin.end(username + '\n' + password + '\n', "utf8");
376 return;
377 } 412 }
378 413
379 /* Sets things up for a new user, like dgamelaunch's commands[register] */ 414 /* Sets things up for a new user, like dgamelaunch's commands[register] */
380 function regsetup(username) { 415 function regsetup(username) {
381 function regsetup_l2(err) { 416 function regsetup_l2(err) {
406 email = "nobody@nowhere.not"; 441 email = "nobody@nowhere.not";
407 } 442 }
408 else 443 else
409 email = formdata["email"]; 444 email = formdata["email"];
410 function checkreg(code, signal) { 445 function checkreg(code, signal) {
411 if (code == 4) { 446 if (code === 0) {
412 sendError(res, 2, "Invalid characters in name or email."); 447 var lkey = randkey(2);
413 tslog("Attempted registration: %s %s", uname, email); 448 while (lkey in logins)
414 } 449 lkey = randkey(2);
415 else if (code == 1) { 450 logins[lkey] = {"name": uname, "ts": new Date()};
416 sendError(res, 2, "Username " + uname + " is already being used."); 451 var reply = {"t": "r", "k": lkey, "u": uname};
417 tslog("Attempted duplicate registration: %s", uname);
418 }
419 else if (code != 0) {
420 sendError(res, 0, null);
421 tslog("sqlickrypt register failed with code %d", code);
422 }
423 else {
424 res.writeHead(200, {'Content-Type': 'application/json'}); 452 res.writeHead(200, {'Content-Type': 'application/json'});
425 var reply = {"t": "r", "d": uname};
426 res.write(JSON.stringify(reply)); 453 res.write(JSON.stringify(reply));
427 res.end(); 454 res.end();
428 tslog("Added new user: %s", uname); 455 tslog("Added new user: %s", uname);
429 regsetup(uname); 456 regsetup(uname);
430 } 457 }
458 else if (code == 4) {
459 sendError(res, 2, "Invalid characters in name or email.");
460 tslog("Attempted registration: %s %s", uname, email);
461 }
462 else if (code == 1) {
463 sendError(res, 2, "Username " + uname + " is already being used.");
464 tslog("Attempted duplicate registration: %s", uname);
465 }
466 else {
467 sendError(res, 0, null);
468 tslog("sqlickrypt register failed with code %d", code);
469 }
431 } 470 }
432 var child_adder = child_process.spawn("/bin/sqlickrypt", ["register"]); 471 var child_adder = child_process.spawn("/bin/sqlickrypt", ["register"]);
433 child_adder.on("exit", checkreg); 472 child_adder.on("exit", checkreg);
434 child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8"); 473 child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8");
435 return; 474 return;
436 } 475 }
437 476
438 function logout(term, res) { 477 function endgame(term, res) {
439 if (!term.alive) { 478 if (!term.alive) {
440 sendError(res, 1, null); 479 sendError(res, 7, null);
441 return; 480 return;
442 } 481 }
443 term.close(); 482 term.close();
444 var resheaders = {'Content-Type': 'application/json'}; 483 var resheaders = {'Content-Type': 'application/json'};
445 res.writeHead(200, resheaders); 484 res.writeHead(200, resheaders);
532 res.writeHead(200, { "Content-Type": "application/json" }); 571 res.writeHead(200, { "Content-Type": "application/json" });
533 res.write(JSON.stringify(reply)); 572 res.write(JSON.stringify(reply));
534 res.end(); 573 res.end();
535 } 574 }
536 else { 575 else {
537 sendError(res, 1, null); 576 sendError(res, 7, null);
538 } 577 }
539 } 578 }
540 579
541 function statusmsg(req, res) { 580 function statusmsg(req, res) {
542 var reply = {"s": allowlogin, "g": []}; 581 var reply = {"s": allowlogin, "g": []};
554 res.end(); 593 res.end();
555 } 594 }
556 595
557 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data", 596 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data",
558 "Login failed", "Already playing", "Game launch failed", 597 "Login failed", "Already playing", "Game launch failed",
559 "Server shutting down" ]; 598 "Server shutting down", "Game not in progress" ];
560 599
561 function sendError(res, ecode, msg) { 600 function sendError(res, ecode, msg) {
562 res.writeHead(200, { "Content-Type": "application/json" }); 601 res.writeHead(200, { "Content-Type": "application/json" });
563 var edict = {"t": "E"}; 602 var edict = {"t": "E"};
564 if (!(ecode < errorcodes.length && ecode > 0)) 603 if (!(ecode < errorcodes.length && ecode > 0))
591 var cterm = findTermSession(formdata); 630 var cterm = findTermSession(formdata);
592 /* First figure out if the client is POSTing to a command interface. */ 631 /* First figure out if the client is POSTing to a command interface. */
593 if (req.method == 'POST') { 632 if (req.method == 'POST') {
594 if (target == '/feed') { 633 if (target == '/feed') {
595 if (!cterm) { 634 if (!cterm) {
596 sendError(res, 1, null); 635 sendError(res, 7, null);
597 return; 636 return;
598 } 637 }
599 if (formdata.t == "q") { 638 if (formdata.t == "q") {
600 /* The client wants to terminate the process. */ 639 /* The client wants to terminate the process. */
601 logout(cterm, res); 640 endgame(cterm, res);
602 } 641 }
603 else if (formdata.t == "d" && typeof(formdata.d) == "string") { 642 else if (formdata.t == "d" && typeof(formdata.d) == "string") {
604 /* process the keys */ 643 /* process the keys */
605 hexstr = formdata.d.replace(/[^0-9a-f]/gi, ""); 644 hexstr = formdata.d.replace(/[^0-9a-f]/gi, "");
606 if (hexstr.length % 2 != 0) { 645 if (hexstr.length % 2 != 0) {
617 login(req, res, formdata); 656 login(req, res, formdata);
618 } 657 }
619 else if (target == "/addacct") { 658 else if (target == "/addacct") {
620 register(req, res, formdata); 659 register(req, res, formdata);
621 } 660 }
661 else if (target == "/play") {
662 startgame(req, res, formdata);
663 }
622 else { 664 else {
623 res.writeHead(405, resheaders); 665 res.writeHead(405, resheaders);
624 res.end(); 666 res.end();
625 } 667 }
626 } 668 }
630 res.writeHead(200, {"Content-Type": "application/json"}); 672 res.writeHead(200, {"Content-Type": "application/json"});
631 res.end(); 673 res.end();
632 return; 674 return;
633 } 675 }
634 if (!cterm) { 676 if (!cterm) {
635 sendError(res, 1, null); 677 sendError(res, 7, null);
636 return; 678 return;
637 } 679 }
638 readFeed(res, cterm); 680 readFeed(res, cterm);
639 } 681 }
640 else if (target == '/status') { 682 else if (target == '/status') {