changeset 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 b06a14876645
children f7116eb3f791
files index-rlg.html rlgterm.js rlgwebd.js
diffstat 3 files changed, 194 insertions(+), 99 deletions(-) [+]
line wrap: on
line diff
--- a/index-rlg.html	Wed Jun 06 10:53:29 2012 -0700
+++ b/index-rlg.html	Thu Jun 07 15:43:06 2012 -0700
@@ -85,6 +85,21 @@
 <span onclick="textsize(true)">Larger</span>
 </div>
 </div>
+<div class="modal" id="startgame">
+<form id="startform" action="/play" method="post">
+<div>
+Choose game: <select name="game" id="input_game">
+  <option label="Rogue V3" value="rogue3">Rogue V3</option>
+  <option label="Rogue V4" value="rogue4">Rogue V4</option>
+  <option label="Rogue V5" value="rogue5">Rogue V5</option>
+  <option label="Super-Rogue" value="srogue">Super-Rogue</option>
+</select>
+</div>
+<div>
+<input type="submit" value="Play" onclick="startgame(event)">
+</div>
+</form>
+</div>
 <div class="modal" id="login">
 <form id="loginform" action="/login" method="post">
 <div>
@@ -94,19 +109,11 @@
 Password: <input type="password" name="pw" id="input_pw">
 </div>
 <div>
-Choose game: <select name="game" id="input_game">
-  <option label="Rogue V3" value="rogue3">Rogue V3</option>
-  <option label="Rogue V4" value="rogue4">Rogue V4</option>
-  <option label="Rogue V5" value="rogue5">Rogue V5</option>
-  <option label="Super-Rogue" value="srogue">Super-Rogue</option>
-</select>
-</div>
-<div>
-<input type="submit" value="Play" onclick="formlogin(event)">
+<input type="submit" value="Log in" onclick="formlogin(event)">
 </div>
 </form>
+<div class="rbutton" onclick="setmode('register')">Register</div>
 </div>
-<div class="rbutton" onclick="setmode('register')">Register</div>
 <div class="modal" id="register">
 <form id="regform" action="/addacct" method="post">
 <div>
--- a/rlgterm.js	Wed Jun 06 10:53:29 2012 -0700
+++ b/rlgterm.js	Thu Jun 07 15:43:06 2012 -0700
@@ -48,6 +48,10 @@
   }
 };
 
+/* Login name and key */
+var lname = null;
+var lcred = null;
+
 function writeData(hexstr) {
   var codenum;
   var codes = [];
@@ -138,8 +142,8 @@
   if (!msgDict.t)
     return null;
   else if (msgDict.t == "E") {
-    if (msgDict.c == 1 || msgDict.c == 6) {
-      logout();
+    if (msgDict.c == 1 || msgDict.c == 6 || msgDict == 7) {
+      gameover();
     }
     debug(1, "Server error: " + msgDict.s);
   }
@@ -174,7 +178,7 @@
     setTitle(msgDict.d);
   }
   else if (msgDict.t == "q") {
-    logout();
+    gameover();
   }
   else {
     debug(1, "Unrecognized server message " + msg);
@@ -357,9 +361,39 @@
   var loginmsg = {};
   loginmsg["name"] = document.getElementById("input_name").value;
   loginmsg["pw"] = document.getElementById("input_pw").value;
-  loginmsg["game"] = document.getElementById("input_game").value;
-  loginmsg["h"] = 24;
-  loginmsg["w"] = 80;
+  var req = new XMLHttpRequest();
+  req.onreadystatechange = function () {
+    if (req.readyState != 4 || req.status != 200) 
+      return;
+    var reply = JSON.parse(req.responseText);
+    if (reply.t == 'l') {
+      /* Success */
+      lcred = reply.k;
+      lname = reply.u;
+      setTitle("Logged in as " + reply.u);
+      debug(1, "Logged in as " + reply.u + " with id " + reply.k);
+      setmode("choose");
+    }
+    else if (reply.t == 'E') {
+      debug(1, "Could not log in: " + reply.s);
+      document.getElementById("input_name").value = "";
+      document.getElementById("input_pw").value = "";
+    }
+  };
+  req.open('POST', '/login', true);
+  req.send(JSON.stringify(loginmsg));
+  return;
+}
+
+function startgame(ev) {
+  ev.preventDefault();
+  if (termemu.sessid != null || !lcred)
+    return;
+  var smsg = {};
+  smsg["key"] = lcred;
+  smsg["game"] = document.getElementById("input_game").value;
+  smsg["h"] = 24;
+  smsg["w"] = 80;
   var req = new XMLHttpRequest();
   req.onreadystatechange = function () {
     if (req.readyState != 4 || req.status != 200) 
@@ -369,20 +403,17 @@
       /* Success */
       termemu.sessid = reply.id;
       termemu.resize(reply.h, reply.w);
-      setTitle("Playing as " + loginmsg["name"]);
-      debug(1, "Logged in with id " + termemu.sessid);
-      //document.getElementById("loginform").style.display = "none";
+      setTitle("Playing as " + lname);
+      debug(1, "Playing with id " + termemu.sessid);
       setmode("play");
       getData();
     }
     else if (reply.t == 'E') {
       debug(1, "Could not start game: " + reply.s);
-      document.getElementById("input_name").value = "";
-      document.getElementById("input_pw").value = "";
     }
   };
-  req.open('POST', '/login', true);
-  req.send(JSON.stringify(loginmsg));
+  req.open('POST', '/play', true);
+  req.send(JSON.stringify(smsg));
   return;
 }
 
@@ -402,7 +433,11 @@
     if (reply.t == 'r') {
       /* Success */
       debug(1, "Registered account: " + reply.d);
-      setmode("login");
+      lcred = reply.k;
+      lname = reply.u;
+      setTitle("Logged in as " + lname);
+      debug(1, "Logged in as " + lname + "with id " + lcred);
+      setmode("choose");
     }
     else if (reply.t == 'E') {
       debug(1, "Could not register: " + reply.s);
@@ -416,7 +451,7 @@
   return;
 }
 
-function logout() {
+function gameover() {
   if (termemu.sessid == null)
     return;
   /* TODO IFACE2 If the end was unexpected, tell player the game was saved. */
@@ -426,11 +461,13 @@
   nsend = 0;
   nrecv = 0;
   msgQ = [];
-  setmode("login");
+  setmode("choose");
   return;
 }
 
 function stop() {
+  if (!termemu.sessid)
+    return;
   var req = new XMLHttpRequest();
   req.onreadystatechange = function () {
     if (req.readyState == 4 && req.status == 200) {
@@ -448,16 +485,25 @@
     ev.preventDefault();
   if (mode == "play") {
     document.getElementById("keyboard").style.display = "block";
+    document.getElementById("startgame").style.display = "none";
+    document.getElementById("login").style.display = "none";
+    document.getElementById("register").style.display = "none";
+  }
+  if (mode == "choose") {
+    document.getElementById("keyboard").style.display = "none";
+    document.getElementById("startgame").style.display = "block";
     document.getElementById("login").style.display = "none";
     document.getElementById("register").style.display = "none";
   }
   else if (mode == "login") {
     document.getElementById("keyboard").style.display = "none";
+    document.getElementById("startgame").style.display = "none";
     document.getElementById("login").style.display = "block";
     document.getElementById("register").style.display = "none";
   }
   else if (mode == "register") {
     document.getElementById("keyboard").style.display = "none";
+    document.getElementById("startgame").style.display = "none";
     document.getElementById("login").style.display = "none";
     document.getElementById("register").style.display = "block";
   }
--- 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);