diff 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
line wrap: on
line diff
--- a/rlgwebd.js	Wed Jun 06 10:53:29 2012 -0700
+++ b/rlgwebd.js	Thu Jun 07 15:43:06 2012 -0700
@@ -21,10 +21,7 @@
 var serveStaticRoot = "/var/www/"; // inside the chroot
 var playtimeout = 3600000; // Idle time before games are autosaved, in ms
 
-/* Global state */
-var sessions = {};
-var allowlogin = true;
-
+/* Data on the games available. */
 var games = {
   "rogue3": {
     "name": "Rogue V3",
@@ -48,6 +45,11 @@
   }
 };
 
+/* Global state */
+var logins = {};
+var sessions = {};
+var allowlogin = true;
+
 /* Constructor for TermSessions.  Note that it opens the terminal and 
  * adds itself to the sessions dict. It currently assumes the user has
  * been authenticated.
@@ -64,9 +66,9 @@
   this.player = user;
   /* This order seems to best avoid race conditions... */
   this.alive = false;
-  this.sessid = randkey();
+  this.sessid = randkey(2);
   while (this.sessid in sessions) {
-    this.sessid = randkey();
+    this.sessid = randkey(2);
   }
   /* Grab a spot in the sessions table. */
   sessions[this.sessid] = this;
@@ -225,12 +227,20 @@
   return sd.replace("T", ".");
 }
 
-function randkey() {
-  rnum = Math.floor(Math.random() * 65536 * 65536);
-  hexstr = rnum.toString(16);
-  while (hexstr.length < 8)
-    hexstr = "0" + hexstr;
-  return hexstr;
+function randkey(words) {
+  if (!words || !(words > 0))
+    words = 1;
+  function rand32() {
+    rnum = Math.floor(Math.random() * 65536 * 65536);
+    hexstr = rnum.toString(16);
+    while (hexstr.length < 8)
+      hexstr = "0" + hexstr;
+    return hexstr;
+  }
+  var key = "";
+  for (var i = 0; i < words; i++)
+    key += rand32();
+  return key;
 }
 
 function tslog() {
@@ -295,11 +305,7 @@
     sendError(res, 6, null);
     return;
   }
-  if (!("game" in formdata)) {
-    sendError(res, 2, "No game specified.");
-    return;
-  }
-  else if (!("name" in formdata)) {
+  if (!("name" in formdata)) {
     sendError(res, 2, "Username not given.");
     return;
   }
@@ -307,8 +313,60 @@
     sendError(res, 2, "Password not given.");
     return;
   }
-  var username = formdata["name"];
-  var password = formdata["pw"];
+  var username = String(formdata["name"]);
+  var password = String(formdata["pw"]);
+  function checkit(code, signal) {
+    /* Checks the exit status, see sqlickrypt.c for details. */
+    if (code != 0) {
+      sendError(res, 3);
+      if (code == 1)
+        tslog("Password check failed for user %s", username);
+      else if (code == 2)
+        tslog("Attempted login by nonexistent user %s", username);
+      else
+        tslog("Login failed: sqlickrypt error %d", code);
+      return;
+    }
+    var lkey = randkey(2);
+    while (lkey in logins)
+      lkey = randkey(2);
+    logins[lkey] = {"name": username, "ts": new Date()};
+    res.writeHead(200, {'Content-Type': 'application/json'});
+    var reply = {"t": "l", "k": lkey, "u": username};
+    res.write(JSON.stringify(reply));
+    res.end();
+    tslog("%s has logged in (key %s)", username, lkey);
+    return;
+  }
+  /* Launch the sqlickrypt utility to check the password. */
+  var pwchecker = child_process.spawn("/bin/sqlickrypt", ["check"]);
+  pwchecker.on("exit", checkit);
+  pwchecker.stdin.end(username + '\n' + password + '\n', "utf8");
+  return;
+}
+
+function startgame(req, res, formdata) {
+  if (!allowlogin) {
+    sendError(res, 6, null);
+    return;
+  }
+  if (!("key" in formdata)) {
+    sendError(res, 2, "No key given.");
+    return;
+  }
+  else if (!("game" in formdata)) {
+    sendError(res, 2, "No game specified.");
+    return;
+  }
+  var lkey = String(formdata["key"]);
+  if (!(lkey in logins)) {
+    sendError(res, 1, null);
+    return;
+  }
+  else {
+    logins[lkey].ts = new Date();
+  }
+  var username = logins[lkey].name;
   var gname = formdata["game"];
   var dims = [formdata["h"], formdata["w"]];
   if (!(gname in games)) {
@@ -316,10 +374,20 @@
     tslog("Request for nonexistant game \"%s\"", gname);
     return;
   }
+  // check for an existing game
   var progressdir = "/dgldir/inprogress-" + games[gname].uname;
-
-  // This sets up the game once starting is approved.
-  function startgame() {
+  fs.readdir(progressdir, function(err, files) {
+    if (!err) {
+      var fre = RegExp("^" + username + ":");
+      for (var i = 0; i < files.length; i++) {
+        if (files[i].match(fre)) {
+          sendError(res, 4, null);
+          tslog("%s is already playing %s", username, gname);
+          return;
+        }
+      }
+    }
+    // Game starting has been approved.
     var ts = timestamp();
     var lockfile = path.join(progressdir, username + ":node:" + ts + ".ttyrec");
     var ttyrec = path.join("/dgldir/ttyrec", username, gname, ts + ".ttyrec");
@@ -340,40 +408,7 @@
       sendError(res, 5, "Failed to open TTY");
       tslog("Unable to allocate TTY for %s", gname);
     }
-  }
-  function checkit(code, signal) {
-    // check the password
-    if (code != 0) {
-      sendError(res, 3);
-      if (code == 1)
-        tslog("Password check failed for user %s", username);
-      else if (code == 2)
-        tslog("Attempted login by nonexistent user %s", username);
-      else
-        tslog("Login failed: sqlickrypt error %d", code);
-      return;
-    }
-    // check for an existing game
-    fs.readdir(progressdir, function(err, files) {
-      if (!err) {
-        var fre = RegExp("^" + username + ":");
-        for (var i = 0; i < files.length; i++) {
-          if (files[i].match(fre)) {
-            sendError(res, 4, null);
-            tslog("%s is already playing %s", username, gname);
-            return;
-          }
-        }
-      }
-      // If progressdir isn't readable, start a new game anyway.
-      startgame();
-    });
-  }
-  /* Launch the sqlickrypt utility to check the password. */
-  var checker = child_process.spawn("/bin/sqlickrypt", ["check"]);
-  checker.on("exit", checkit);
-  checker.stdin.end(username + '\n' + password + '\n', "utf8");
-  return;
+  });
 }
 
 /* Sets things up for a new user, like dgamelaunch's commands[register] */
@@ -408,7 +443,19 @@
   else
     email = formdata["email"];
   function checkreg(code, signal) {
-    if (code == 4) {
+    if (code === 0) {
+      var lkey = randkey(2);
+      while (lkey in logins)
+        lkey = randkey(2);
+      logins[lkey] = {"name": uname, "ts": new Date()};
+      var reply = {"t": "r", "k": lkey, "u": uname};
+      res.writeHead(200, {'Content-Type': 'application/json'});
+      res.write(JSON.stringify(reply));
+      res.end();
+      tslog("Added new user: %s", uname);
+      regsetup(uname);
+    }
+    else if (code == 4) {
       sendError(res, 2, "Invalid characters in name or email.");
       tslog("Attempted registration: %s %s", uname, email);
     }
@@ -416,18 +463,10 @@
       sendError(res, 2, "Username " + uname + " is already being used.");
       tslog("Attempted duplicate registration: %s", uname);
     }
-    else if (code != 0) {
+    else {
       sendError(res, 0, null);
       tslog("sqlickrypt register failed with code %d", code);
     }
-    else {
-      res.writeHead(200, {'Content-Type': 'application/json'});
-      var reply = {"t": "r", "d": uname};
-      res.write(JSON.stringify(reply));
-      res.end();
-      tslog("Added new user: %s", uname);
-      regsetup(uname);
-    }
   }
   var child_adder = child_process.spawn("/bin/sqlickrypt", ["register"]);
   child_adder.on("exit", checkreg);
@@ -435,9 +474,9 @@
   return;
 }
 
-function logout(term, res) {
+function endgame(term, res) {
   if (!term.alive) {
-    sendError(res, 1, null);
+    sendError(res, 7, null);
     return;
   }
   term.close();
@@ -534,7 +573,7 @@
     res.end();
   }
   else {
-    sendError(res, 1, null);
+    sendError(res, 7, null);
   }
 }
 
@@ -556,7 +595,7 @@
 
 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data", 
         "Login failed", "Already playing", "Game launch failed",
-        "Server shutting down" ];
+        "Server shutting down", "Game not in progress" ];
 
 function sendError(res, ecode, msg) {
   res.writeHead(200, { "Content-Type": "application/json" });
@@ -593,12 +632,12 @@
     if (req.method == 'POST') {
       if (target == '/feed') {
         if (!cterm) {
-          sendError(res, 1, null);
+          sendError(res, 7, null);
           return;
         }
         if (formdata.t == "q") {
           /* The client wants to terminate the process. */
-          logout(cterm, res);
+          endgame(cterm, res);
         }
         else if (formdata.t == "d" && typeof(formdata.d) == "string") {
           /* process the keys */
@@ -619,6 +658,9 @@
       else if (target == "/addacct") {
         register(req, res, formdata);
       }
+      else if (target == "/play") {
+        startgame(req, res, formdata);
+      }
       else {
         res.writeHead(405, resheaders);
         res.end();
@@ -632,7 +674,7 @@
           return;
         }
         if (!cterm) {
-          sendError(res, 1, null);
+          sendError(res, 7, null);
           return;
         }
         readFeed(res, cterm);