changeset 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 5483d413a45b
children 298a531776d6
files Makefile initscript rlgwebd rlgwebd-stop rlgwebd-stop.js rlgwebd.js rlgwebd.service webtty webtty.js
diffstat 9 files changed, 1554 insertions(+), 1554 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Thu Jan 14 19:10:46 2016 -0500
+++ b/Makefile	Thu Jan 14 20:52:29 2016 -0500
@@ -19,6 +19,6 @@
 	mkdir -p ${CHROOT}/bin
 	cp sqlickrypt dglwatcher ${CHROOT}/bin
 	for LIB in `ldd ./sqlickrypt | awk '$$1 ~ "^/" {print $$1}; $$3 ~ "^/" {print $$3}'`; do mkdir -p ${CHROOT}`dirname $$LIB`; cp $$LIB ${CHROOT}$$LIB; done
-	cp rlgwebd.js rlgwebd-stop.js ${BINDIR}
+	cp rlgwebd rlgwebd-stop ${BINDIR}
 	cp ${WEBASSETS} ${CHROOT}/var/www
 	cp rlgwebd.service /usr/lib/systemd/system
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/initscript	Thu Jan 14 20:52:29 2016 -0500
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+NODE_PATH=/usr/lib/node_modules
+LOGFILE=/var/log/rlgwebd.log
+CTLSOCKET=/var/run/rlgwebd.sock
+RLGWEBDJS=./rlgwebd
+
+export NODE_PATH
+
+if [ $UID != 0 ]
+then
+  echo "$0 needs to run as root." >&2
+  exit 1
+fi
+
+if [ $# -gt 0 ] && [ $1 = stop ]
+then
+  socat "EXEC:echo quit" "$CTLSOCKET"
+else
+  # Start
+  setsid node "$RLGWEBDJS" </dev/null &>>$LOGFILE &
+fi
+
+exit
+
--- a/rlgwebd	Thu Jan 14 19:10:46 2016 -0500
+++ b/rlgwebd	Thu Jan 14 20:52:29 2016 -0500
@@ -1,25 +1,1242 @@
-#!/bin/sh
+#!/usr/bin/env node
+
+var http = require('http');
+var net = require('net');
+var url = require('url');
+var path = require('path');
+var fs = require('fs');
+var events = require('events');
+var child_process = require('child_process');
+// Dependencies
+var posix = require("posix");
+var pty = require("pty.js");
+var WebSocketServer = require("websocket").server;
+
+/* Configuration variables */
+// The first file is NOT in the chroot.
+var ctlsocket = "/var/run/rlgwebd.sock";
+var httpPort = 8080;
+var chrootDir = "/var/dgl/";
+var dropToUser = "rodney";
+var serveStaticRoot = "/var/www/"; // inside the chroot
+
+var clearbufs = [
+  new Buffer([27, 91, 72, 27, 91, 50, 74]), // xterm: CSI H CSI 2J
+  new Buffer([27, 91, 72, 27, 91, 74]) // screen: CSI H CSI J
+];
+
+/* Data on the games available. */
+var games = {
+  "rogue3": {
+    "name": "Rogue V3",
+    "uname": "rogue3",
+    "suffix": ".r3sav",
+    "path": "/usr/bin/rogue3"
+  },
+  "rogue4": {
+    "name": "Rogue V4",
+    "uname": "rogue4",
+    "suffix": ".r4sav",
+    "path": "/usr/bin/rogue4"
+  },
+  "rogue5": {
+    "name": "Rogue V5",
+    "uname": "rogue5",
+    "suffix": ".r5sav",
+    "path": "/usr/bin/rogue5"
+  },
+  "srogue": {
+    "name": "Super-Rogue",
+    "uname": "srogue",
+    "suffix": ".srsav",
+    "path": "/usr/bin/srogue"
+  },
+  "arogue5": {
+    "name": "Advanced Rogue 5",
+    "uname": "arogue5",
+    "suffix": ".ar5sav",
+    "path": "/usr/bin/arogue5"
+  },
+  "arogue7": {
+    "name": "Advanced Rogue 7",
+    "uname": "arogue7",
+    "suffix": ".ar7sav",
+    "path": "/usr/bin/arogue7"
+  },
+  "xrogue": {
+    "name": "XRogue",
+    "uname": "xrogue",
+    "suffix": ".xrsav",
+    "path": "/usr/bin/xrogue"
+  }
+};
+
+/* Global state */
+var logins = {};
+var sessions = {};
+var dglgames = {};
+var allowlogin = true;
+var gamemux = new events.EventEmitter();
+
+/* A base class.  TermSession and DglSession inherit from it. */
+function BaseGame() {
+  /* Games subclass EventEmitter, though there are few listeners. */
+  events.EventEmitter.call(this);
+  /* Array of watching WebSockets. */
+  this.watchers = [];
+  /* replaybuf holds the output since the last screen clear, so watchers can
+   * begin with a complete screen. replaylen is the number of bytes stored. */
+  this.replaybuf = new Buffer(1024);
+  this.replaylen = 0;
+  /* Time of last activity. */
+  this.lasttime = new Date();
+}
+BaseGame.prototype = new events.EventEmitter();
+
+BaseGame.prototype.tag = function () {
+  if (this.pname === undefined || this.gname === undefined)
+    return "";
+  return this.gname + "/" + this.pname;
+};
+
+BaseGame.prototype.framepush = function(chunk) {
+  /* If this chunk resets the screen, discard what preceded it. */
+  if (isclear(chunk)) {
+    this.replaybuf = new Buffer(1024);
+    this.replaylen = 0;
+  }
+  /* Make sure there's space. */
+  while (this.replaybuf.length < chunk.length + this.replaylen) {
+    var nbuf = new Buffer(this.replaybuf.length * 2);
+    this.replaybuf.copy(nbuf, 0, 0, this.replaylen);
+    this.replaybuf = nbuf;
+    if (this.replaybuf.length > 65536) {
+      tslog("Warning: %s frame buffer at %d bytes", this.tag(), 
+              this.replaybuf.length);
+    }
+  }
+  chunk.copy(this.replaybuf, this.replaylen);
+  this.replaylen += chunk.length;
+};
+
+/* Adds a watcher. */
+BaseGame.prototype.attach = function (wsReq) {
+  var conn = wsReq.accept(null, wsReq.origin);
+  conn.sendUTF(JSON.stringify({
+          "t": "w", "w": this.w, "h": this.h, "p": this.pname, "g": this.gname
+  }));
+  conn.sendUTF(JSON.stringify({"t": "d",
+      "d": this.replaybuf.toString("hex", 0, this.replaylen)}));
+  conn.on('close', this.detach.bind(this, conn));
+  this.watchers.push(conn);
+};
+
+BaseGame.prototype.detach = function (socket) {
+  var n = this.watchers.indexOf(socket);
+  if (n >= 0) {
+    this.watchers.splice(n, 1);
+    tslog("A WebSocket watcher has left game %s", this.tag());
+  }
+};
+
+/* Constructor.  A TermSession handles a pty and the game running on it.
+ *   gname: (String) Name of the game to launch.
+ *   pname: (String) The player's name.
+ *   wsReq: (WebSocketRequest) The request from the client.
+ *
+ *  Events:
+ *   "data": Data generated by child. Parameters: buf (Buffer)
+ *   "exit": Child terminated. Parameters: none
+ */
+function TermSession(gname, pname, wsReq) {
+  BaseGame.call(this);
+  /* Don't launch anything that's not a real game. */
+  if (gname in games) {
+    this.game = games[gname];
+    this.gname = gname;
+  }
+  else {
+    this.failed = true;
+    wsReq.reject(404, errorcodes[2], "No such game");
+    tslog("Game %s is not available", game);
+    return;
+  }
+  this.pname = pname;
+  /* Set up the sizes. */
+  this.w = Math.floor(Number(wsReq.resourceURL.query.w));
+  if (!(this.w > 0 && this.w < 256))
+    this.w = 80;
+  this.h = Math.floor(Number(wsReq.resourceURL.query.h));
+  if (!(this.h > 0 && this.h < 256))
+    this.h = 24;
+  /* Environment. */
+  var childenv = {};
+  for (var key in process.env) {
+    childenv[key] = process.env[key];
+  }
+  var args = ["-n", this.pname];
+  var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w,
+                   "name": "xterm-256color"};
+  this.term = pty.spawn(this.game.path, args, spawnopts);
+  tslog("%s playing %s (pid %d)", this.pname, this.gname, this.term.pid);
+  this.failed = false;
+  sessions[this.gname + "/" + this.pname] = this;
+  gamemux.emit('begin', this.gname, this.pname, 'rlg');
+  /* Set up the lockfile and ttyrec */
+  var ts = timestamp(this.lasttime);
+  var progressdir = path.join("/dgldir/inprogress", this.gname);
+  this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec");
+  var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n'; 
+  fs.writeFile(this.lock, lmsg, "utf8"); 
+  var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, 
+               ts + ".ttyrec");
+  this.record = fs.createWriteStream(ttyrec, { mode: 0664 });
+  /* The player's WebSocket and its handlers. */
+  this.playerconn = wsReq.accept(null, wsReq.origin);
+  this.playerconn.on('message', this.input_msg.bind(this));
+  this.playerconn.on('close', this.close.bind(this));
+  /* Send initial data. */
+  this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h, 
+               "p": this.pname, "g": this.gname}));
+  /* Begin the ttyrec with some metadata, like dgamelaunch does. */
+  var descstr = "\x1b[2J\x1b[1;1H\r\n";
+  descstr += "Player: " + this.pname + "\r\nGame: " + this.game.name + "\r\n";
+  descstr += "Server: Roguelike Gallery - rlgallery.org\r\n";
+  descstr += "Filename: " + ts + ".ttyrec\r\n";
+  descstr += "Time: (" + Math.floor(this.lasttime.getTime() / 1000) + ") ";
+  descstr += this.lasttime.toUTCString().slice(0, -4) + "\r\n";
+  descstr += "Size: " + this.w + "x" + this.h + "\r\n\x1b[2J";
+  this.write_ttyrec(descstr);
+  this.term.on("data", this.write_ttyrec.bind(this));
+  this.term.on("exit", this.destroy.bind(this));
+}
+TermSession.prototype = new BaseGame();
+
+/* Currently this also sends to the player and any watchers. */
+TermSession.prototype.write_ttyrec = function (datastr) {
+  this.lasttime = new Date();
+  var buf = new Buffer(datastr);
+  var chunk = new Buffer(buf.length + 12);
+  /* TTYREC headers */
+  chunk.writeUInt32LE(Math.floor(this.lasttime.getTime() / 1000), 0);
+  chunk.writeUInt32LE(1000 * (this.lasttime.getTime() % 1000), 4);
+  chunk.writeUInt32LE(buf.length, 8);
+  buf.copy(chunk, 12);
+  this.record.write(chunk);
+  this.framepush(buf);
+  /* Send to the player. */
+  var msg = JSON.stringify({"t": "d", "d": buf.toString("hex")});
+  this.playerconn.sendUTF(msg);
+  /* Send to any watchers. */
+  for (var i = 0; i < this.watchers.length; i++) {
+    if (this.watchers[i].connected)
+      this.watchers[i].sendUTF(msg);
+  }
+  this.emit('data', buf);
+};
+
+/* For writing to the subprocess's stdin. */
+TermSession.prototype.write = function (data) {
+  this.term.write(data);
+};
+
+TermSession.prototype.input_msg = function (message) {
+  var parsedMsg = getMsgWS(message);
+  if (parsedMsg.t == 'q') {
+    this.close();
+  }
+  else if (parsedMsg.t == 'd') {
+    var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, "");
+    if (hexstr.length % 2 != 0) {
+      hexstr = hexstr.slice(0, -1);
+    }
+    var keybuf = new Buffer(hexstr, "hex");
+    this.write(keybuf);
+  }
+};
+
+/* Teardown. */
+TermSession.prototype.close = function () {
+  if (this.tag() in sessions)
+    this.term.kill('SIGHUP');
+};
+
+TermSession.prototype.destroy = function () {
+  var tag = this.tag();
+  fs.unlink(this.lock);
+  this.record.end();
+  var watchsocks = this.watchers;
+  this.watchers = [];
+  for (var i = 0; i < watchsocks.length; i++) {
+    if (watchsocks[i].connected)
+      watchsocks[i].close();
+  }
+  if (this.playerconn.connected) {
+    this.playerconn.sendUTF(JSON.stringify({"t": "q"}));
+    this.playerconn.close();
+  }
+  this.emit('exit');
+  gamemux.emit('end', this.gname, this.pname);
+  delete sessions[tag];
+  tslog("Game %s ended.", tag);
+};
 
-NODE_PATH=/usr/lib/node_modules
-LOGFILE=/var/local/rlgwebd/log
-CTLSOCKET=/var/run/rlgwebd.sock
-RLGWEBDJS=./rlgwebd.js
+function DglSession(filename) {
+  BaseGame.call(this);
+  var pathcoms = filename.split('/');
+  this.gname = pathcoms[pathcoms.length - 2];
+  if (!(this.gname in games)) {
+    this.emit('open', false);
+    return;
+  }
+  var basename = pathcoms[pathcoms.length - 1];
+  var firstsep = basename.indexOf(':');
+  this.pname = basename.slice(0, firstsep);
+  var fname = basename.slice(firstsep + 1);
+  this.ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, fname);
+  /* Flag to prevent multiple handlers from reading simultaneously and
+   * getting into a race. */
+  this.reading = false;
+  this.rpos = 0;
+  fs.readFile(filename, {encoding: "utf8"}, (function (err, data) {
+    if (err) {
+      this.emit('open', false);
+      return;
+    }
+    var lines = data.split('\n');
+    this.h = Number(lines[1]);
+    this.w = Number(lines[2]);
+    fs.open(this.ttyrec, "r", (function (err, fd) {
+      if (err) {
+        this.emit('open', false);
+      }
+      else {
+        this.fd = fd;
+        this.emit('open', true);
+        tslog("DGL %s: open", this.tag());
+        gamemux.emit('begin', this.gname, this.pname, 'dgl');
+        this.startchunk();
+        this.recwatcher = fs.watch(this.ttyrec, this.notifier.bind(this));
+      }
+    }).bind(this));
+  }).bind(this));
+}
+DglSession.prototype = new BaseGame();
+
+/* 3 functions to get data from the ttyrec file. */
+DglSession.prototype.startchunk = function () {
+  if (this.reading)
+    return;
+  this.reading = true;
+  var header = new Buffer(12);
+  fs.read(this.fd, header, 0, 12, this.rpos, this.datachunk.bind(this));
+};
+
+DglSession.prototype.datachunk = function (err, n, buf) {
+  /* Stop recursion if end of file has been reached. */
+  if (err || n < 12) {
+    if (!err && n > 0) {
+      tslog("DGL %s: expected 12-byte header, got %d", this.tag(), n);
+    }
+    this.reading = false;
+    return;
+  }
+  this.rpos += 12;
+  /* Update timestamp, to within 1 second. */
+  this.lasttime = new Date(1000 * buf.readUInt32LE(0));
+  var datalen = buf.readUInt32LE(8);
+  if (datalen > 16384) {
+    // Something is probably wrong...
+    tslog("DGL %s: looking for %d bytes", this.tag(), datalen);
+  }
+  var databuf = new Buffer(datalen);
+  fs.read(this.fd, databuf, 0, datalen, this.rpos, this.handledata.bind(this));
+};
+
+DglSession.prototype.handledata = function (err, n, buf) {
+  if (err || n < buf.length) {
+    /* Next time, read the header again. */
+    this.rpos -= 12;
+    this.reading = false;
+    tslog("DGL %s: expected %d bytes, got %d", this.tag(), buf.length, n);
+    return;
+  }
+  this.rpos += n;
+  this.reading = false;
+  /* Process the data */
+  this.framepush(buf);
+  var wmsg = JSON.stringify({"t": "d", "d": buf.toString("hex")});
+  for (var i = 0; i < this.watchers.length; i++) {
+    if (this.watchers[i].connected)
+      this.watchers[i].sendUTF(wmsg);
+  }
+  this.emit("data", buf);
+  /* Recurse. */
+  this.startchunk();
+};
+
+/* Handles events from the ttyrec file watcher. */
+DglSession.prototype.notifier = function (ev, finame) {
+  if (ev == "change")
+    this.startchunk();
+  /* If another kind of event appears, something strange happened. */
+};
+
+DglSession.prototype.close = function () {
+  this.recwatcher.close();
+  /* Ensure all data is handled before quitting. */
+  this.startchunk();
+  var connlist = this.watchers;
+  this.watchers = [];
+  for (var i = 0; i < connlist.length; i++) {
+    if (connlist[i].connected)
+      connlist[i].close();
+  }
+  fs.close(this.fd);
+  this.emit("close");
+  gamemux.emit('end', this.gname, this.pname);
+  tslog("DGL %s: closed", this.tag());
+};
+
+function wsStartGame(wsReq) {
+  var playmatch = wsReq.resourceURL.pathname.match(/^\/play\/([^\/]*)$/);
+  if (!playmatch[1] || !(playmatch[1] in games)) {
+    wsReq.reject(404, errorcodes[2]);
+    return;
+  }
+  var gname = playmatch[1];
+  if (!allowlogin) {
+    wsReq.reject(404, errorcodes[6]);
+    return;
+  }
+  if (!("key" in wsReq.resourceURL.query)) {
+    wsReq.reject(404, "No key given.");
+    return;
+  }
+  var lkey = wsReq.resourceURL.query["key"];
+  if (!(lkey in logins)) {
+    wsReq.reject(404, errorcodes[1]);
+    return;
+  }
+  var pname = logins[lkey].name;
+  function progcallback(err, fname) {
+    if (fname) {
+      wsReq.reject(404, errorcodes[4]);
+      tslog("%s is already playing %s", pname, gname);
+    }
+    else {
+      new TermSession(gname, pname, wsReq);
+    }
+  };
+  checkprogress(pname, games[gname], progcallback, []);
+}
 
-export NODE_PATH
+/* Some functions which check whether a player is currently playing or 
+ * has a saved game.  Maybe someday they will provide information on 
+ * the game. */
+function checkprogress(user, game, callback, args) {
+  var progressdir = path.join("/dgldir/inprogress", game.uname);
+  fs.readdir(progressdir, function(err, files) {
+    if (err) {
+      args.unshift(err, null);
+      callback.apply(null, args);
+      return;
+    }
+    var fre = RegExp("^" + user + ":");
+    for (var i = 0; i < files.length; i++) {
+      if (files[i].match(fre)) {
+        args.unshift(null, files[i]);
+        callback.apply(null, args);
+        return;
+      }
+    }
+    args.unshift(null, false);
+    callback.apply(null, args);
+  });
+}
+
+function checksaved(user, game, callback, args) {
+  var savedirc = game.uname + "save";
+  var basename = String(pwent.uid) + "-" + user + game.suffix;
+  var savefile = path.join("/var/games/roguelike", savedirc, basename);
+  fs.exists(savefile, function (exist) {
+    args.unshift(exist);
+    callback.apply(null, args);
+  });
+}
+
+function playerstatus(user, callback) {
+  var sdata = {};
+  function finishp() {
+    for (var gname in games) {
+      if (!(gname in sdata))
+        return;
+    }
+    callback(sdata);
+  }
+  function regsaved(exists, game) {
+    if (exists)
+      sdata[game.uname] = "s";
+    else
+      sdata[game.uname] = "0";
+    finishp();
+  }
+  function regactive(err, filename, game) {
+    if (!err && filename) {
+      if (filename.match(/^[^:]*:node:/))
+        sdata[game.uname] = "p";
+      else
+        sdata[game.uname] = "d";
+      finishp();
+    }
+    else
+      checksaved(user, game, regsaved, [game]);
+  }
+  for (var gname in games) {
+    checkprogress(user, games[gname], regactive, [games[gname]]);
+  }
+}
+
+/* A few utility functions */
+function timestamp(dd) {
+  if (!(dd instanceof Date)) {
+    dd = new Date();
+  }
+  sd = dd.toISOString();
+  sd = sd.slice(0, sd.indexOf("."));
+  return sd.replace("T", ".");
+}
+
+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;
+}
+
+/* Compares two buffers, returns true for equality up to index n */
+function bufncmp(buf1, buf2, n) {
+  if (!Buffer.isBuffer(buf1) || !Buffer.isBuffer(buf2))
+    return false;
+  for (var i = 0; i < n; i++) {
+    if (i == buf1.length && i == buf2.length)
+      return true;
+    if (i == buf1.length || i == buf2.length)
+      return false;
+    if (buf1[i] != buf2[i])
+      return false;
+  }
+  return true;
+}
+
+function isclear(buf) {
+  for (var i = 0; i < clearbufs.length; i++) {
+    if (bufncmp(buf, clearbufs[i], clearbufs[i].length))
+      return true;
+  }
+  return false;
+}
+
+function tslog() {
+  arguments[0] = new Date().toISOString() + ": " + String(arguments[0]);
+  console.log.apply(console, arguments);
+}
+
+/* Returns a list of the cookies in the request, obviously. */
+function getCookies(req) {
+  cookies = [];
+  if ("cookie" in req.headers) {
+    cookstrs = req.headers["cookie"].split("; ");
+    for (var i = 0; i < cookstrs.length; i++) {
+      eqsign = cookstrs[i].indexOf("=");
+      if (eqsign > 0) {
+        name = cookstrs[i].slice(0, eqsign).toLowerCase();
+        val = cookstrs[i].slice(eqsign + 1);
+        cookies[name] = val;
+      }
+      else if (eqsign < 0)
+        cookies[cookstrs[i]] = null;
+    }
+  }
+  return cookies;
+}
+
+function getMsg(posttext) {
+  var jsonobj;
+  if (!posttext)
+    return {};
+  try {
+    jsonobj = JSON.parse(posttext);
+  }
+  catch (e) {
+    if (e instanceof SyntaxError)
+      return {};
+  }
+  if (typeof(jsonobj) != "object")
+    return {};
+  return jsonobj;
+}
+
+function getMsgWS(msgObj) {
+  if (msgObj.type != "utf8")
+    return {};
+  return getMsg(msgObj.utf8Data);
+}
 
-if [ $UID != 0 ]
-then
-  echo "$0 needs to run as root." >&2
-  exit 1
-fi
+function login(req, res, formdata) {
+  if (!allowlogin) {
+    sendError(res, 6, null, false);
+    return;
+  }
+  if (!("name" in formdata)) {
+    sendError(res, 2, "Username not given.", false);
+    return;
+  }
+  else if (!("pw" in formdata)) {
+    sendError(res, 2, "Password not given.", false);
+    return;
+  }
+  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", username);
+    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;
+}
+
+/* Sets things up for a new user, like dgamelaunch's commands[register] */
+function regsetup(username) {
+  function regsetup_l2(err) {
+    for (var g in games) {
+      fs.mkdir(path.join("/dgldir/ttyrec", username, games[g].uname), 0755);
+    }
+  }
+  fs.mkdir(path.join("/dgldir/userdata", username), 0755);
+  fs.mkdir(path.join("/dgldir/ttyrec/", username), 0755, regsetup_l2);
+}
+
+function register(req, res, formdata) {
+  var uname, passwd, email;
+  if (typeof (formdata.name) != "string" || formdata.name === "") {
+    sendError(res, 2, "No name given.");
+    return;
+  }
+  else
+    uname = formdata["name"];
+  if (typeof (formdata.pw) != "string" || formdata.pw === "") {
+    sendError(res, 2, "No password given.");
+    return;
+  }
+  else
+    passwd = formdata["pw"];
+  if (typeof (formdata.email) != "string" || formdata.email === "") {
+    /* E-mail is optional */
+    email = "nobody@nowhere.not";
+  }
+  else
+    email = formdata["email"];
+  function checkreg(code, signal) {
+    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);
+    }
+    else if (code == 1) {
+      sendError(res, 2, "Username " + uname + " is already being used.");
+      tslog("Attempted duplicate registration: %s", uname);
+    }
+    else {
+      sendError(res, 0, null);
+      tslog("sqlickrypt register failed with code %d", code);
+    }
+  }
+  var child_adder = child_process.spawn("/bin/sqlickrypt", ["register"]);
+  child_adder.on("exit", checkreg);
+  child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8");
+  return;
+}
+
+/* Stops a running game if the request has the proper key. */
+function stopgame(res, formdata) {
+  if (!("key" in formdata) || !(formdata["key"] in logins)) {
+    sendError(res, 1);
+    return;
+  }
+  var pname = logins[formdata["key"]].name;
+  if (!("g" in formdata) || !(formdata["g"] in games)) {
+    sendError(res, 2, "No such game.");
+    return;
+  }
+  var gname = formdata["g"];
+  function checkback(err, fname) {
+    if (!fname) {
+      sendError(res, 7);
+      return;
+    }
+    var fullfile = path.join("/dgldir/inprogress", gname, fname);
+    fs.readFile(fullfile, "utf8", function(err, fdata) {
+      if (err) {
+        sendError(res, 7);
+        return;
+      }
+      var pid = parseInt(fdata.split('\n')[0], 10);
+      try {
+        process.kill(pid, 'SIGHUP');
+      }
+      catch (err) {
+        /* If the PID is invalid, the lockfile is stale. */
+        if (err.code == "ESRCH") {
+          var nodere = RegExp("^" + pname + ":node:");
+          if (fname.match(nodere)) {
+            fs.unlink(fullfile);
+          }
+        }
+      }
+      /* The response doesn't mean that the game is gone.  The only way
+       * to make sure a dgamelaunch-supervised game is over would be to
+       * poll fname until it disappears. */
+      res.writeHead(200, {'Content-Type': 'application/json'});
+      res.write(JSON.stringify({"t": "q"}));
+      res.end();
+    });
+  }
+  checkprogress(pname, games[gname], checkback, []);
+}
+
+function startProgressWatcher() {
+  var watchdirs = [];
+  for (var gname in games) {
+    watchdirs.push(path.join("/dgldir/inprogress", gname));
+  }
+  var subproc = child_process.spawn("/bin/dglwatcher", watchdirs);
+  subproc.stdout.setEncoding('utf8');
+  subproc.stdout.on('data', function (chunk) {
+    var fname = chunk.slice(2, -1);
+    var filere = /.*\/([^\/]*)\/([^\/:]*):(node:)?(.*)/;
+    var matchresult = fname.match(filere);
+    if (!matchresult || matchresult[3])
+      return;
+    var gname = matchresult[1];
+    var pname = matchresult[2];
+    var tag = gname + "/" + pname;
+    if (chunk[0] == "E") {
+      tslog("DGL: %s is playing %s: %s", pname, gname, fname)
+      dglgames[tag] = new DglSession(fname);
+    }
+    else if (chunk[0] == "C") {
+      tslog("DGL: %s started playing %s: %s", pname, gname, fname)
+      dglgames[tag] = new DglSession(fname);
+    }
+    else if (chunk[0] == "D") {
+      tslog("DGL: %s finished playing %s: %s", pname, gname, fname)
+      dglgames[tag].close();
+      delete dglgames[tag];
+    }
+    else {
+      tslog("Watcher says: %s", chunk)
+    }
+  });
+  subproc.stdout.resume();
+  return subproc;
+}
+
+function serveStatic(req, res, fname) {
+  var nname = path.normalize(fname);
+  if (nname == "" || nname == "/")
+    nname = "index.html";
+  if (nname.match(/\/$/))
+    path.join(nname, "index.html"); /* it was a directory */
+  var realname = path.join(serveStaticRoot, nname);
+  var extension = path.extname(realname);
+  fs.exists(realname, function (exists) {
+    var resheaders = {};
+    if (!exists || !extension || extension == ".html")
+      resheaders["Content-Type"] = "text/html; charset=utf-8";
+    else if (extension == ".png")
+      resheaders["Content-Type"] = "image/png";
+    else if (extension == ".css")
+      resheaders["Content-Type"] = "text/css";
+    else if (extension == ".js")
+      resheaders["Content-Type"] = "text/javascript";
+    else if (extension == ".svg")
+      resheaders["Content-Type"] = "image/svg+xml";
+    else
+      resheaders["Content-Type"] = "application/octet-stream";
+    if (exists) {
+      fs.readFile(realname, function (error, data) {
+        if (error) {
+          res.writeHead(500, {});
+          res.end();
+        }
+        else {
+          res.writeHead(200, resheaders);
+          if (req.method != 'HEAD')
+            res.write(data);
+          res.end();
+        }
+      });
+    }
+    else {
+      send404(res, nname, req.method == 'HEAD');
+    }
+  });
+  return;
+}
+
+/* Currently, this doesn't do anything blocking, but keep the callback */
+function getStatus(callback) {
+  var now = new Date();
+  var statusinfo = {"s": allowlogin, "g": []};
+  for (var tag in sessions) {
+    var gamedesc = {"c": "rlg"};
+    gamedesc["p"] = sessions[tag].pname;
+    gamedesc["g"] = sessions[tag].game.uname;
+    gamedesc["i"] = now - sessions[tag].lasttime;
+    gamedesc["w"] = sessions[tag].watchers.length;
+    statusinfo["g"].push(gamedesc);
+  }
+  for (var tag in dglgames) {
+    var dglinfo = {"c": "dgl"};
+    var slash = tag.search('/');
+    dglinfo["g"] = tag.slice(0, slash);
+    dglinfo["p"] = tag.slice(slash + 1);
+    dglinfo["i"] = now - dglgames[tag].lasttime;
+    dglinfo["w"] = dglgames[tag].watchers.length;
+    statusinfo["g"].push(dglinfo);
+  }
+  callback(statusinfo);
+}
+
+function statusmsg(req, res) {
+  function respond(info) {
+    res.writeHead(200, { "Content-Type": "application/json" });
+    if (req.method != 'HEAD')
+      res.write(JSON.stringify(info));
+    res.end();
+  }
+  getStatus(respond);
+}
+
+function pstatusmsg(req, res) {
+  if (req.method == 'HEAD') {
+    res.writeHead(200, { "Content-Type": "application/json" });
+    res.end();
+    return;
+  }
+  var target = url.parse(req.url).pathname;
+  var pmatch = target.match(/^\/pstatus\/(.*)/);
+  if (pmatch && pmatch[1])
+    var pname = pmatch[1];
+  else {
+    sendError(res, 2, "No name given.");
+    return;
+  }
+  var reply = {"name": pname};
+  playerstatus(pname, function (pdata) {
+    reply["stat"] = pdata;
+    res.writeHead(200, { "Content-Type": "application/json" });
+    res.write(JSON.stringify(reply));
+    res.end();
+  });
+}
 
-if [ $# -gt 0 ] && [ $1 = stop ]
-then
-  socat "EXEC:echo quit" "$CTLSOCKET"
-else
-  # Start
-  setsid node "$RLGWEBDJS" </dev/null &>>$LOGFILE &
-fi
+function getuinfo(req, res) {
+  var urlobj = url.parse(req.url, true);
+  var query = urlobj.query;
+  if (!("key" in query) || !(query["key"] in logins)) {
+    sendError(res, 1);
+    return;
+  }
+  var match = urlobj.pathname.match(/^\/[^\/]*\/(.*)/);
+  if (!match || !match[1]) {
+    send404(res, urlobj.pathname, req.method == 'HEAD');
+    return;
+  }
+  var which = match[1];
+  var name = logins[query["key"]].name;
+  var reply = { "u": name };
+  function send() {
+    res.writeHead(200, { "Content-Type": "application/json" });
+    res.write(JSON.stringify(reply));
+    res.end();
+  }
+  if (which == "pw") {
+    /* Don't actually divulge passwords. */
+    reply["pw"] = "";
+    send();
+  }
+  else if (which == "email") {
+    var address;
+    function finish(code, signal) {
+      if (code != 0) {
+        tslog("sqlickrypt: %d with name %s", code, name);
+        sendError(res, 2);
+      }
+      else {
+        reply["email"] = address;
+        send();
+      }
+    }
+    var subproc = child_process.spawn("/bin/sqlickrypt", ["getmail"]);
+    subproc.stdout.on("data", function (data) {
+      address = data.toString().replace(/\n/g, "");
+    });
+    subproc.on("exit", finish);
+    subproc.stdin.end(name + '\n', "utf8");
+  }
+  else {
+    send404(res, urlobj.pathname, req.method == 'HEAD');
+    return;
+  }
+}
+
+function setuinfo(req, res, postdata) {
+  var urlobj = url.parse(req.url, true);
+  var query = urlobj.query;
+  if (!("key" in query) || !(query["key"] in logins)) {
+    sendError(res, 1);
+    return;
+  }
+  var name = logins[query["key"]].name;
+  var match = urlobj.pathname.match(/^\/[^\/]*\/(.*)/);
+  if (!match || !match[1]) {
+    send404(res, urlobj.pathname, true);
+    return;
+  }
+  var which = match[1];
+  if (!("v" in postdata)) {
+    sendError(res, 2, "No value provided");
+    return;
+  }
+  if (which == "email" || which == "pw") {
+    var args;
+    if (which == "email")
+      args = ["setmail"];
+    else
+      args = ["setpw"];
+    var child = child_process.execFile("/bin/sqlickrypt", args, 
+                  function (err, stdout, stderr) {
+      if (err) {
+        tslog("Could not set %s: sqlickrypt error %d", which, err.code);
+        sendError(res, 2);
+      }
+      else {
+        tslog("User %s has changed %s", name, which);
+        res.writeHead(200, { "Content-Type": "application/json" });
+        res.end(JSON.stringify({"t": "t"}));
+      }
+    });
+    child.stdin.end(name + "\n" + postdata.v + "\n", "utf8");
+  }
+  else {
+    send404(res, urlobj.pathname, true);
+  }
+}
+
+var errorcodes = [ "Generic Error", "Not logged in", "Invalid data", 
+        "Login failed", "Already playing", "Game launch failed",
+        "Server shutting down", "Game not in progress" ];
+
+function sendError(res, ecode, msg, box) {
+  res.writeHead(200, { "Content-Type": "application/json" });
+  var edict = {"t": "E"};
+  if (!(ecode < errorcodes.length && ecode > 0))
+    ecode = 0;
+  edict["c"] = ecode;
+  edict["s"] = errorcodes[ecode];
+  if (msg)
+    edict["s"] += ": " + msg;
+  if (box)
+    res.write(JSON.stringify([edict]));
+  else
+    res.write(JSON.stringify(edict));
+  res.end();
+}
+
+function send404(res, path, nopage) {
+  res.writeHead(404, {"Content-Type": "text/html; charset=utf-8"});
+  if (!nopage) {
+    res.write("<html><head><title>" + path + "</title></head>\n<body><h1>"
+              + path + " Not Found</h1></body></html>\n");
+  }
+  res.end();
+}
+
+function webHandler(req, res) {
+  /* default headers for the response */
+  var resheaders = {'Content-Type': 'text/html'};
+  /* The request body will be added to this as it arrives. */
+  var reqbody = "";
+  var formdata;
+
+  /* Register a listener to get the body. */
+  function moredata(chunk) {
+    reqbody += chunk;
+  }
+  req.on('data', moredata);
 
-exit
+  /* This will send the response once the whole request is here. */
+  function respond() {
+    formdata = getMsg(reqbody);
+    var target = url.parse(req.url).pathname;
+    /* First figure out if the client is POSTing to a command interface. */
+    if (req.method == 'POST') {
+      if (target == "/login") {
+        login(req, res, formdata);
+      }
+      else if (target == "/addacct") {
+        register(req, res, formdata);
+      }
+      else if (target == "/quit") {
+        stopgame(res, formdata);
+      }
+      else if (target.match(/^\/uinfo\//)) {
+        setuinfo(req, res, formdata);
+      }
+      else {
+        res.writeHead(405, resheaders);
+        res.end();
+      }
+    }
+    else if (req.method == 'GET' || req.method == 'HEAD') {
+      if (target == '/status') {
+        statusmsg(req, res);
+      }
+      else if (target.match(/^\/uinfo\//)) {
+        getuinfo(req, res);
+      }
+      else if (target.match(/^\/pstatus\//)) {
+        pstatusmsg(req, res);
+      }
+      else /* Go look for it in the filesystem */
+        serveStatic(req, res, target);
+    }
+    else { /* Some other method */
+      res.writeHead(501, resheaders);
+      res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n");
+      res.end();
+    }
+    return;
+  }
+  req.on('end', respond);
+}
+
+function wsHandler(wsRequest) {
+  var watchmatch = wsRequest.resource.match(/^\/watch\/(.*)$/);
+  var playmatch = wsRequest.resource.match(/^\/play\//);
+  if (watchmatch !== null) {
+    if (watchmatch[1] in sessions) {
+      var tsession = sessions[watchmatch[1]];
+      tsession.attach(wsRequest);
+      tslog("Game %s is being watched via WebSockets", tsession.tag());
+    }
+    else if (watchmatch[1] in dglgames) {
+      var dsession = dglgames[watchmatch[1]];
+      dsession.attach(wsRequest);
+      tslog("DGL game %s is being watched via WebSockets", dsession.tag());
+    }
+    else {
+      wsRequest.reject(404, errorcodes[7]);
+      return;
+    }
+  }
+  else if (playmatch !== null) {
+    wsStartGame(wsRequest);
+  }
+  else if (wsRequest.resourceURL.pathname == "/status") {
+    var conn = wsRequest.accept(null, wsRequest.origin);
+    var tell = function () {
+      getStatus(function (info) {
+        info["t"] = "t";
+        conn.sendUTF(JSON.stringify(info));
+      });
+    }
+    var beginH = function (gname, pname, client) {
+      conn.sendUTF(JSON.stringify({"t": "b", "c": client, "p": pname, 
+                   "g": gname}));
+    };
+    var listH = function (list) {
+      conn.sendUTF(JSON.stringify(list));
+    };
+    var endH = function (gname, pname) {
+      conn.sendUTF(JSON.stringify({"t": "e", "p": pname, "g": gname}));
+    };
+    gamemux.on('begin', beginH);
+    gamemux.on('list', listH);
+    gamemux.on('end', endH);
+    conn.on('message', tell);
+    conn.on('close', function () {
+      gamemux.removeListener('begin', beginH);
+      gamemux.removeListener('list', listH);
+      gamemux.removeListener('end', endH);
+    });
+    tell();
+  }
+  else
+    wsRequest.reject(404, "No such resource.");
+}
 
+/* Only games with low idle time are included.  Use getStatus() for the
+ * complete list. */
+function pushStatus() {
+  var now = new Date();
+  var statusinfo = {"t": "p", "s": allowlogin, "g": []};
+  for (var tag in sessions) {
+    var delta = now - sessions[tag].lasttime;
+    if (delta < 60000) {
+      var gamedesc = {"c": "rlg"};
+      gamedesc["p"] = sessions[tag].pname;
+      gamedesc["g"] = sessions[tag].game.uname;
+      gamedesc["i"] = delta;
+      gamedesc["w"] = sessions[tag].watchers.length;
+      statusinfo["g"].push(gamedesc);
+    }
+  }
+  for (var tag in dglgames) {
+    var delta = now - dglgames[tag].lasttime;
+    if (delta < 60000) {
+      var dglinfo = {"c": "dgl"};
+      var slash = tag.search('/');
+      dglinfo["g"] = tag.slice(0, slash);
+      dglinfo["p"] = tag.slice(slash + 1);
+      dglinfo["i"] = delta;
+      dglinfo["w"] = dglgames[tag].watchers.length;
+      statusinfo["g"].push(dglinfo);
+    }
+  }
+  gamemux.emit('list', statusinfo);
+}
+
+function shutdown () {
+  httpServer.close();
+  httpServer.removeAllListeners('request');
+  ctlServer.close();
+  tslog("Shutting down...");
+  process.exit();
+}
+
+function consoleHandler(chunk) {
+  var msg = chunk.toString().split('\n')[0];
+  if (msg == "quit") {
+    allowlogin = false;
+    tslog("Disconnecting...");
+    for (var tag in sessions) {
+      sessions[tag].close();
+    }
+    progressWatcher.stdin.end("\n");
+    setTimeout(shutdown, 2000);
+  }
+}
+
+process.on("exit", function () {
+  for (var tag in sessions) {
+    sessions[tag].term.kill('SIGHUP');
+  }
+  tslog("Quitting...");
+  return;
+});
+
+/* Initialization STARTS HERE */
+process.env["TERM"] = "xterm-256color";
+
+if (process.getuid() != 0) {
+  tslog("Not running as root, cannot chroot.");
+  process.exit(1);
+}
+
+var httpServer; // declare here so shutdown() can find it
+var wsServer;
+var progressWatcher;
+
+var pwent; 
+try {
+  pwent = posix.getpwnam(dropToUser);
+}
+catch (err) {
+  tslog("Could not drop to user %s: user does not exist", dropToUser);
+  process.exit(1);
+}
+
+/* This could be nonblocking, but nothing else can start yet anyway. */
+if (fs.existsSync(ctlsocket)) {
+  fs.unlinkSync(ctlsocket);
+}
+
+/* Open the control socket before chrooting where it can't be found */
+var ctlServer = net.createServer(function (sock) {
+  sock.on('data', consoleHandler);
+});
+ctlServer.listen(ctlsocket, function () {
+  /* rlgwebd.js now assumes that it has been started by the rlgwebd shell
+   * script, or some other method that detaches it and sets up stdio. */
+  /* chroot and drop permissions.  posix.chroot() does chdir() itself. */
+  try {
+    posix.chroot(chrootDir);
+  }
+  catch (err) {
+    tslog("chroot to %s failed: %s", chrootDir, err);
+    process.exit(1);
+  }
+  try {
+    // drop gid first, that requires UID=0
+    process.setgid(pwent.gid);
+    process.setuid(pwent.uid);
+  }
+  catch (err) {
+    tslog("Could not drop permissions: %s", err);
+    process.exit(1);
+  }
+  httpServer = http.createServer(webHandler);
+  httpServer.listen(httpPort);
+  tslog('rlgwebd running on port %d', httpPort); 
+  wsServer = new WebSocketServer({"httpServer": httpServer});
+  wsServer.on("request", wsHandler);
+  tslog('WebSockets are online'); 
+  progressWatcher = startProgressWatcher();
+  setInterval(pushStatus, 40000);
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rlgwebd-stop	Thu Jan 14 20:52:29 2016 -0500
@@ -0,0 +1,12 @@
+#!/usr/bin/env node
+
+var net = require('net');
+var sockpath = "/var/run/rlgwebd.sock";
+
+var sock = net.connect(sockpath, function () {
+  sock.on('close', function () {
+    if (process.argv[2] == "debug")
+      console.log("Control socket closed");
+  });
+  sock.write("quit\n");
+});
--- a/rlgwebd-stop.js	Thu Jan 14 19:10:46 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-#!/usr/bin/env node
-
-var net = require('net');
-var sockpath = "/var/run/rlgwebd.sock";
-
-var sock = net.connect(sockpath, function () {
-  sock.on('close', function () {
-    if (process.argv[2] == "debug")
-      console.log("Control socket closed");
-  });
-  sock.write("quit\n");
-});
--- a/rlgwebd.js	Thu Jan 14 19:10:46 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1242 +0,0 @@
-#!/usr/bin/env node
-
-var http = require('http');
-var net = require('net');
-var url = require('url');
-var path = require('path');
-var fs = require('fs');
-var events = require('events');
-var child_process = require('child_process');
-// Dependencies
-var posix = require("posix");
-var pty = require("pty.js");
-var WebSocketServer = require("websocket").server;
-
-/* Configuration variables */
-// The first file is NOT in the chroot.
-var ctlsocket = "/var/run/rlgwebd.sock";
-var httpPort = 8080;
-var chrootDir = "/var/dgl/";
-var dropToUser = "rodney";
-var serveStaticRoot = "/var/www/"; // inside the chroot
-
-var clearbufs = [
-  new Buffer([27, 91, 72, 27, 91, 50, 74]), // xterm: CSI H CSI 2J
-  new Buffer([27, 91, 72, 27, 91, 74]) // screen: CSI H CSI J
-];
-
-/* Data on the games available. */
-var games = {
-  "rogue3": {
-    "name": "Rogue V3",
-    "uname": "rogue3",
-    "suffix": ".r3sav",
-    "path": "/usr/bin/rogue3"
-  },
-  "rogue4": {
-    "name": "Rogue V4",
-    "uname": "rogue4",
-    "suffix": ".r4sav",
-    "path": "/usr/bin/rogue4"
-  },
-  "rogue5": {
-    "name": "Rogue V5",
-    "uname": "rogue5",
-    "suffix": ".r5sav",
-    "path": "/usr/bin/rogue5"
-  },
-  "srogue": {
-    "name": "Super-Rogue",
-    "uname": "srogue",
-    "suffix": ".srsav",
-    "path": "/usr/bin/srogue"
-  },
-  "arogue5": {
-    "name": "Advanced Rogue 5",
-    "uname": "arogue5",
-    "suffix": ".ar5sav",
-    "path": "/usr/bin/arogue5"
-  },
-  "arogue7": {
-    "name": "Advanced Rogue 7",
-    "uname": "arogue7",
-    "suffix": ".ar7sav",
-    "path": "/usr/bin/arogue7"
-  },
-  "xrogue": {
-    "name": "XRogue",
-    "uname": "xrogue",
-    "suffix": ".xrsav",
-    "path": "/usr/bin/xrogue"
-  }
-};
-
-/* Global state */
-var logins = {};
-var sessions = {};
-var dglgames = {};
-var allowlogin = true;
-var gamemux = new events.EventEmitter();
-
-/* A base class.  TermSession and DglSession inherit from it. */
-function BaseGame() {
-  /* Games subclass EventEmitter, though there are few listeners. */
-  events.EventEmitter.call(this);
-  /* Array of watching WebSockets. */
-  this.watchers = [];
-  /* replaybuf holds the output since the last screen clear, so watchers can
-   * begin with a complete screen. replaylen is the number of bytes stored. */
-  this.replaybuf = new Buffer(1024);
-  this.replaylen = 0;
-  /* Time of last activity. */
-  this.lasttime = new Date();
-}
-BaseGame.prototype = new events.EventEmitter();
-
-BaseGame.prototype.tag = function () {
-  if (this.pname === undefined || this.gname === undefined)
-    return "";
-  return this.gname + "/" + this.pname;
-};
-
-BaseGame.prototype.framepush = function(chunk) {
-  /* If this chunk resets the screen, discard what preceded it. */
-  if (isclear(chunk)) {
-    this.replaybuf = new Buffer(1024);
-    this.replaylen = 0;
-  }
-  /* Make sure there's space. */
-  while (this.replaybuf.length < chunk.length + this.replaylen) {
-    var nbuf = new Buffer(this.replaybuf.length * 2);
-    this.replaybuf.copy(nbuf, 0, 0, this.replaylen);
-    this.replaybuf = nbuf;
-    if (this.replaybuf.length > 65536) {
-      tslog("Warning: %s frame buffer at %d bytes", this.tag(), 
-              this.replaybuf.length);
-    }
-  }
-  chunk.copy(this.replaybuf, this.replaylen);
-  this.replaylen += chunk.length;
-};
-
-/* Adds a watcher. */
-BaseGame.prototype.attach = function (wsReq) {
-  var conn = wsReq.accept(null, wsReq.origin);
-  conn.sendUTF(JSON.stringify({
-          "t": "w", "w": this.w, "h": this.h, "p": this.pname, "g": this.gname
-  }));
-  conn.sendUTF(JSON.stringify({"t": "d",
-      "d": this.replaybuf.toString("hex", 0, this.replaylen)}));
-  conn.on('close', this.detach.bind(this, conn));
-  this.watchers.push(conn);
-};
-
-BaseGame.prototype.detach = function (socket) {
-  var n = this.watchers.indexOf(socket);
-  if (n >= 0) {
-    this.watchers.splice(n, 1);
-    tslog("A WebSocket watcher has left game %s", this.tag());
-  }
-};
-
-/* Constructor.  A TermSession handles a pty and the game running on it.
- *   gname: (String) Name of the game to launch.
- *   pname: (String) The player's name.
- *   wsReq: (WebSocketRequest) The request from the client.
- *
- *  Events:
- *   "data": Data generated by child. Parameters: buf (Buffer)
- *   "exit": Child terminated. Parameters: none
- */
-function TermSession(gname, pname, wsReq) {
-  BaseGame.call(this);
-  /* Don't launch anything that's not a real game. */
-  if (gname in games) {
-    this.game = games[gname];
-    this.gname = gname;
-  }
-  else {
-    this.failed = true;
-    wsReq.reject(404, errorcodes[2], "No such game");
-    tslog("Game %s is not available", game);
-    return;
-  }
-  this.pname = pname;
-  /* Set up the sizes. */
-  this.w = Math.floor(Number(wsReq.resourceURL.query.w));
-  if (!(this.w > 0 && this.w < 256))
-    this.w = 80;
-  this.h = Math.floor(Number(wsReq.resourceURL.query.h));
-  if (!(this.h > 0 && this.h < 256))
-    this.h = 24;
-  /* Environment. */
-  var childenv = {};
-  for (var key in process.env) {
-    childenv[key] = process.env[key];
-  }
-  var args = ["-n", this.pname];
-  var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w,
-                   "name": "xterm-256color"};
-  this.term = pty.spawn(this.game.path, args, spawnopts);
-  tslog("%s playing %s (pid %d)", this.pname, this.gname, this.term.pid);
-  this.failed = false;
-  sessions[this.gname + "/" + this.pname] = this;
-  gamemux.emit('begin', this.gname, this.pname, 'rlg');
-  /* Set up the lockfile and ttyrec */
-  var ts = timestamp(this.lasttime);
-  var progressdir = path.join("/dgldir/inprogress", this.gname);
-  this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec");
-  var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n'; 
-  fs.writeFile(this.lock, lmsg, "utf8"); 
-  var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, 
-               ts + ".ttyrec");
-  this.record = fs.createWriteStream(ttyrec, { mode: 0664 });
-  /* The player's WebSocket and its handlers. */
-  this.playerconn = wsReq.accept(null, wsReq.origin);
-  this.playerconn.on('message', this.input_msg.bind(this));
-  this.playerconn.on('close', this.close.bind(this));
-  /* Send initial data. */
-  this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h, 
-               "p": this.pname, "g": this.gname}));
-  /* Begin the ttyrec with some metadata, like dgamelaunch does. */
-  var descstr = "\x1b[2J\x1b[1;1H\r\n";
-  descstr += "Player: " + this.pname + "\r\nGame: " + this.game.name + "\r\n";
-  descstr += "Server: Roguelike Gallery - rlgallery.org\r\n";
-  descstr += "Filename: " + ts + ".ttyrec\r\n";
-  descstr += "Time: (" + Math.floor(this.lasttime.getTime() / 1000) + ") ";
-  descstr += this.lasttime.toUTCString().slice(0, -4) + "\r\n";
-  descstr += "Size: " + this.w + "x" + this.h + "\r\n\x1b[2J";
-  this.write_ttyrec(descstr);
-  this.term.on("data", this.write_ttyrec.bind(this));
-  this.term.on("exit", this.destroy.bind(this));
-}
-TermSession.prototype = new BaseGame();
-
-/* Currently this also sends to the player and any watchers. */
-TermSession.prototype.write_ttyrec = function (datastr) {
-  this.lasttime = new Date();
-  var buf = new Buffer(datastr);
-  var chunk = new Buffer(buf.length + 12);
-  /* TTYREC headers */
-  chunk.writeUInt32LE(Math.floor(this.lasttime.getTime() / 1000), 0);
-  chunk.writeUInt32LE(1000 * (this.lasttime.getTime() % 1000), 4);
-  chunk.writeUInt32LE(buf.length, 8);
-  buf.copy(chunk, 12);
-  this.record.write(chunk);
-  this.framepush(buf);
-  /* Send to the player. */
-  var msg = JSON.stringify({"t": "d", "d": buf.toString("hex")});
-  this.playerconn.sendUTF(msg);
-  /* Send to any watchers. */
-  for (var i = 0; i < this.watchers.length; i++) {
-    if (this.watchers[i].connected)
-      this.watchers[i].sendUTF(msg);
-  }
-  this.emit('data', buf);
-};
-
-/* For writing to the subprocess's stdin. */
-TermSession.prototype.write = function (data) {
-  this.term.write(data);
-};
-
-TermSession.prototype.input_msg = function (message) {
-  var parsedMsg = getMsgWS(message);
-  if (parsedMsg.t == 'q') {
-    this.close();
-  }
-  else if (parsedMsg.t == 'd') {
-    var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, "");
-    if (hexstr.length % 2 != 0) {
-      hexstr = hexstr.slice(0, -1);
-    }
-    var keybuf = new Buffer(hexstr, "hex");
-    this.write(keybuf);
-  }
-};
-
-/* Teardown. */
-TermSession.prototype.close = function () {
-  if (this.tag() in sessions)
-    this.term.kill('SIGHUP');
-};
-
-TermSession.prototype.destroy = function () {
-  var tag = this.tag();
-  fs.unlink(this.lock);
-  this.record.end();
-  var watchsocks = this.watchers;
-  this.watchers = [];
-  for (var i = 0; i < watchsocks.length; i++) {
-    if (watchsocks[i].connected)
-      watchsocks[i].close();
-  }
-  if (this.playerconn.connected) {
-    this.playerconn.sendUTF(JSON.stringify({"t": "q"}));
-    this.playerconn.close();
-  }
-  this.emit('exit');
-  gamemux.emit('end', this.gname, this.pname);
-  delete sessions[tag];
-  tslog("Game %s ended.", tag);
-};
-
-function DglSession(filename) {
-  BaseGame.call(this);
-  var pathcoms = filename.split('/');
-  this.gname = pathcoms[pathcoms.length - 2];
-  if (!(this.gname in games)) {
-    this.emit('open', false);
-    return;
-  }
-  var basename = pathcoms[pathcoms.length - 1];
-  var firstsep = basename.indexOf(':');
-  this.pname = basename.slice(0, firstsep);
-  var fname = basename.slice(firstsep + 1);
-  this.ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, fname);
-  /* Flag to prevent multiple handlers from reading simultaneously and
-   * getting into a race. */
-  this.reading = false;
-  this.rpos = 0;
-  fs.readFile(filename, {encoding: "utf8"}, (function (err, data) {
-    if (err) {
-      this.emit('open', false);
-      return;
-    }
-    var lines = data.split('\n');
-    this.h = Number(lines[1]);
-    this.w = Number(lines[2]);
-    fs.open(this.ttyrec, "r", (function (err, fd) {
-      if (err) {
-        this.emit('open', false);
-      }
-      else {
-        this.fd = fd;
-        this.emit('open', true);
-        tslog("DGL %s: open", this.tag());
-        gamemux.emit('begin', this.gname, this.pname, 'dgl');
-        this.startchunk();
-        this.recwatcher = fs.watch(this.ttyrec, this.notifier.bind(this));
-      }
-    }).bind(this));
-  }).bind(this));
-}
-DglSession.prototype = new BaseGame();
-
-/* 3 functions to get data from the ttyrec file. */
-DglSession.prototype.startchunk = function () {
-  if (this.reading)
-    return;
-  this.reading = true;
-  var header = new Buffer(12);
-  fs.read(this.fd, header, 0, 12, this.rpos, this.datachunk.bind(this));
-};
-
-DglSession.prototype.datachunk = function (err, n, buf) {
-  /* Stop recursion if end of file has been reached. */
-  if (err || n < 12) {
-    if (!err && n > 0) {
-      tslog("DGL %s: expected 12-byte header, got %d", this.tag(), n);
-    }
-    this.reading = false;
-    return;
-  }
-  this.rpos += 12;
-  /* Update timestamp, to within 1 second. */
-  this.lasttime = new Date(1000 * buf.readUInt32LE(0));
-  var datalen = buf.readUInt32LE(8);
-  if (datalen > 16384) {
-    // Something is probably wrong...
-    tslog("DGL %s: looking for %d bytes", this.tag(), datalen);
-  }
-  var databuf = new Buffer(datalen);
-  fs.read(this.fd, databuf, 0, datalen, this.rpos, this.handledata.bind(this));
-};
-
-DglSession.prototype.handledata = function (err, n, buf) {
-  if (err || n < buf.length) {
-    /* Next time, read the header again. */
-    this.rpos -= 12;
-    this.reading = false;
-    tslog("DGL %s: expected %d bytes, got %d", this.tag(), buf.length, n);
-    return;
-  }
-  this.rpos += n;
-  this.reading = false;
-  /* Process the data */
-  this.framepush(buf);
-  var wmsg = JSON.stringify({"t": "d", "d": buf.toString("hex")});
-  for (var i = 0; i < this.watchers.length; i++) {
-    if (this.watchers[i].connected)
-      this.watchers[i].sendUTF(wmsg);
-  }
-  this.emit("data", buf);
-  /* Recurse. */
-  this.startchunk();
-};
-
-/* Handles events from the ttyrec file watcher. */
-DglSession.prototype.notifier = function (ev, finame) {
-  if (ev == "change")
-    this.startchunk();
-  /* If another kind of event appears, something strange happened. */
-};
-
-DglSession.prototype.close = function () {
-  this.recwatcher.close();
-  /* Ensure all data is handled before quitting. */
-  this.startchunk();
-  var connlist = this.watchers;
-  this.watchers = [];
-  for (var i = 0; i < connlist.length; i++) {
-    if (connlist[i].connected)
-      connlist[i].close();
-  }
-  fs.close(this.fd);
-  this.emit("close");
-  gamemux.emit('end', this.gname, this.pname);
-  tslog("DGL %s: closed", this.tag());
-};
-
-function wsStartGame(wsReq) {
-  var playmatch = wsReq.resourceURL.pathname.match(/^\/play\/([^\/]*)$/);
-  if (!playmatch[1] || !(playmatch[1] in games)) {
-    wsReq.reject(404, errorcodes[2]);
-    return;
-  }
-  var gname = playmatch[1];
-  if (!allowlogin) {
-    wsReq.reject(404, errorcodes[6]);
-    return;
-  }
-  if (!("key" in wsReq.resourceURL.query)) {
-    wsReq.reject(404, "No key given.");
-    return;
-  }
-  var lkey = wsReq.resourceURL.query["key"];
-  if (!(lkey in logins)) {
-    wsReq.reject(404, errorcodes[1]);
-    return;
-  }
-  var pname = logins[lkey].name;
-  function progcallback(err, fname) {
-    if (fname) {
-      wsReq.reject(404, errorcodes[4]);
-      tslog("%s is already playing %s", pname, gname);
-    }
-    else {
-      new TermSession(gname, pname, wsReq);
-    }
-  };
-  checkprogress(pname, games[gname], progcallback, []);
-}
-
-/* Some functions which check whether a player is currently playing or 
- * has a saved game.  Maybe someday they will provide information on 
- * the game. */
-function checkprogress(user, game, callback, args) {
-  var progressdir = path.join("/dgldir/inprogress", game.uname);
-  fs.readdir(progressdir, function(err, files) {
-    if (err) {
-      args.unshift(err, null);
-      callback.apply(null, args);
-      return;
-    }
-    var fre = RegExp("^" + user + ":");
-    for (var i = 0; i < files.length; i++) {
-      if (files[i].match(fre)) {
-        args.unshift(null, files[i]);
-        callback.apply(null, args);
-        return;
-      }
-    }
-    args.unshift(null, false);
-    callback.apply(null, args);
-  });
-}
-
-function checksaved(user, game, callback, args) {
-  var savedirc = game.uname + "save";
-  var basename = String(pwent.uid) + "-" + user + game.suffix;
-  var savefile = path.join("/var/games/roguelike", savedirc, basename);
-  fs.exists(savefile, function (exist) {
-    args.unshift(exist);
-    callback.apply(null, args);
-  });
-}
-
-function playerstatus(user, callback) {
-  var sdata = {};
-  function finishp() {
-    for (var gname in games) {
-      if (!(gname in sdata))
-        return;
-    }
-    callback(sdata);
-  }
-  function regsaved(exists, game) {
-    if (exists)
-      sdata[game.uname] = "s";
-    else
-      sdata[game.uname] = "0";
-    finishp();
-  }
-  function regactive(err, filename, game) {
-    if (!err && filename) {
-      if (filename.match(/^[^:]*:node:/))
-        sdata[game.uname] = "p";
-      else
-        sdata[game.uname] = "d";
-      finishp();
-    }
-    else
-      checksaved(user, game, regsaved, [game]);
-  }
-  for (var gname in games) {
-    checkprogress(user, games[gname], regactive, [games[gname]]);
-  }
-}
-
-/* A few utility functions */
-function timestamp(dd) {
-  if (!(dd instanceof Date)) {
-    dd = new Date();
-  }
-  sd = dd.toISOString();
-  sd = sd.slice(0, sd.indexOf("."));
-  return sd.replace("T", ".");
-}
-
-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;
-}
-
-/* Compares two buffers, returns true for equality up to index n */
-function bufncmp(buf1, buf2, n) {
-  if (!Buffer.isBuffer(buf1) || !Buffer.isBuffer(buf2))
-    return false;
-  for (var i = 0; i < n; i++) {
-    if (i == buf1.length && i == buf2.length)
-      return true;
-    if (i == buf1.length || i == buf2.length)
-      return false;
-    if (buf1[i] != buf2[i])
-      return false;
-  }
-  return true;
-}
-
-function isclear(buf) {
-  for (var i = 0; i < clearbufs.length; i++) {
-    if (bufncmp(buf, clearbufs[i], clearbufs[i].length))
-      return true;
-  }
-  return false;
-}
-
-function tslog() {
-  arguments[0] = new Date().toISOString() + ": " + String(arguments[0]);
-  console.log.apply(console, arguments);
-}
-
-/* Returns a list of the cookies in the request, obviously. */
-function getCookies(req) {
-  cookies = [];
-  if ("cookie" in req.headers) {
-    cookstrs = req.headers["cookie"].split("; ");
-    for (var i = 0; i < cookstrs.length; i++) {
-      eqsign = cookstrs[i].indexOf("=");
-      if (eqsign > 0) {
-        name = cookstrs[i].slice(0, eqsign).toLowerCase();
-        val = cookstrs[i].slice(eqsign + 1);
-        cookies[name] = val;
-      }
-      else if (eqsign < 0)
-        cookies[cookstrs[i]] = null;
-    }
-  }
-  return cookies;
-}
-
-function getMsg(posttext) {
-  var jsonobj;
-  if (!posttext)
-    return {};
-  try {
-    jsonobj = JSON.parse(posttext);
-  }
-  catch (e) {
-    if (e instanceof SyntaxError)
-      return {};
-  }
-  if (typeof(jsonobj) != "object")
-    return {};
-  return jsonobj;
-}
-
-function getMsgWS(msgObj) {
-  if (msgObj.type != "utf8")
-    return {};
-  return getMsg(msgObj.utf8Data);
-}
-
-function login(req, res, formdata) {
-  if (!allowlogin) {
-    sendError(res, 6, null, false);
-    return;
-  }
-  if (!("name" in formdata)) {
-    sendError(res, 2, "Username not given.", false);
-    return;
-  }
-  else if (!("pw" in formdata)) {
-    sendError(res, 2, "Password not given.", false);
-    return;
-  }
-  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", username);
-    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;
-}
-
-/* Sets things up for a new user, like dgamelaunch's commands[register] */
-function regsetup(username) {
-  function regsetup_l2(err) {
-    for (var g in games) {
-      fs.mkdir(path.join("/dgldir/ttyrec", username, games[g].uname), 0755);
-    }
-  }
-  fs.mkdir(path.join("/dgldir/userdata", username), 0755);
-  fs.mkdir(path.join("/dgldir/ttyrec/", username), 0755, regsetup_l2);
-}
-
-function register(req, res, formdata) {
-  var uname, passwd, email;
-  if (typeof (formdata.name) != "string" || formdata.name === "") {
-    sendError(res, 2, "No name given.");
-    return;
-  }
-  else
-    uname = formdata["name"];
-  if (typeof (formdata.pw) != "string" || formdata.pw === "") {
-    sendError(res, 2, "No password given.");
-    return;
-  }
-  else
-    passwd = formdata["pw"];
-  if (typeof (formdata.email) != "string" || formdata.email === "") {
-    /* E-mail is optional */
-    email = "nobody@nowhere.not";
-  }
-  else
-    email = formdata["email"];
-  function checkreg(code, signal) {
-    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);
-    }
-    else if (code == 1) {
-      sendError(res, 2, "Username " + uname + " is already being used.");
-      tslog("Attempted duplicate registration: %s", uname);
-    }
-    else {
-      sendError(res, 0, null);
-      tslog("sqlickrypt register failed with code %d", code);
-    }
-  }
-  var child_adder = child_process.spawn("/bin/sqlickrypt", ["register"]);
-  child_adder.on("exit", checkreg);
-  child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8");
-  return;
-}
-
-/* Stops a running game if the request has the proper key. */
-function stopgame(res, formdata) {
-  if (!("key" in formdata) || !(formdata["key"] in logins)) {
-    sendError(res, 1);
-    return;
-  }
-  var pname = logins[formdata["key"]].name;
-  if (!("g" in formdata) || !(formdata["g"] in games)) {
-    sendError(res, 2, "No such game.");
-    return;
-  }
-  var gname = formdata["g"];
-  function checkback(err, fname) {
-    if (!fname) {
-      sendError(res, 7);
-      return;
-    }
-    var fullfile = path.join("/dgldir/inprogress", gname, fname);
-    fs.readFile(fullfile, "utf8", function(err, fdata) {
-      if (err) {
-        sendError(res, 7);
-        return;
-      }
-      var pid = parseInt(fdata.split('\n')[0], 10);
-      try {
-        process.kill(pid, 'SIGHUP');
-      }
-      catch (err) {
-        /* If the PID is invalid, the lockfile is stale. */
-        if (err.code == "ESRCH") {
-          var nodere = RegExp("^" + pname + ":node:");
-          if (fname.match(nodere)) {
-            fs.unlink(fullfile);
-          }
-        }
-      }
-      /* The response doesn't mean that the game is gone.  The only way
-       * to make sure a dgamelaunch-supervised game is over would be to
-       * poll fname until it disappears. */
-      res.writeHead(200, {'Content-Type': 'application/json'});
-      res.write(JSON.stringify({"t": "q"}));
-      res.end();
-    });
-  }
-  checkprogress(pname, games[gname], checkback, []);
-}
-
-function startProgressWatcher() {
-  var watchdirs = [];
-  for (var gname in games) {
-    watchdirs.push(path.join("/dgldir/inprogress", gname));
-  }
-  var subproc = child_process.spawn("/bin/dglwatcher", watchdirs);
-  subproc.stdout.setEncoding('utf8');
-  subproc.stdout.on('data', function (chunk) {
-    var fname = chunk.slice(2, -1);
-    var filere = /.*\/([^\/]*)\/([^\/:]*):(node:)?(.*)/;
-    var matchresult = fname.match(filere);
-    if (!matchresult || matchresult[3])
-      return;
-    var gname = matchresult[1];
-    var pname = matchresult[2];
-    var tag = gname + "/" + pname;
-    if (chunk[0] == "E") {
-      tslog("DGL: %s is playing %s: %s", pname, gname, fname)
-      dglgames[tag] = new DglSession(fname);
-    }
-    else if (chunk[0] == "C") {
-      tslog("DGL: %s started playing %s: %s", pname, gname, fname)
-      dglgames[tag] = new DglSession(fname);
-    }
-    else if (chunk[0] == "D") {
-      tslog("DGL: %s finished playing %s: %s", pname, gname, fname)
-      dglgames[tag].close();
-      delete dglgames[tag];
-    }
-    else {
-      tslog("Watcher says: %s", chunk)
-    }
-  });
-  subproc.stdout.resume();
-  return subproc;
-}
-
-function serveStatic(req, res, fname) {
-  var nname = path.normalize(fname);
-  if (nname == "" || nname == "/")
-    nname = "index.html";
-  if (nname.match(/\/$/))
-    path.join(nname, "index.html"); /* it was a directory */
-  var realname = path.join(serveStaticRoot, nname);
-  var extension = path.extname(realname);
-  fs.exists(realname, function (exists) {
-    var resheaders = {};
-    if (!exists || !extension || extension == ".html")
-      resheaders["Content-Type"] = "text/html; charset=utf-8";
-    else if (extension == ".png")
-      resheaders["Content-Type"] = "image/png";
-    else if (extension == ".css")
-      resheaders["Content-Type"] = "text/css";
-    else if (extension == ".js")
-      resheaders["Content-Type"] = "text/javascript";
-    else if (extension == ".svg")
-      resheaders["Content-Type"] = "image/svg+xml";
-    else
-      resheaders["Content-Type"] = "application/octet-stream";
-    if (exists) {
-      fs.readFile(realname, function (error, data) {
-        if (error) {
-          res.writeHead(500, {});
-          res.end();
-        }
-        else {
-          res.writeHead(200, resheaders);
-          if (req.method != 'HEAD')
-            res.write(data);
-          res.end();
-        }
-      });
-    }
-    else {
-      send404(res, nname, req.method == 'HEAD');
-    }
-  });
-  return;
-}
-
-/* Currently, this doesn't do anything blocking, but keep the callback */
-function getStatus(callback) {
-  var now = new Date();
-  var statusinfo = {"s": allowlogin, "g": []};
-  for (var tag in sessions) {
-    var gamedesc = {"c": "rlg"};
-    gamedesc["p"] = sessions[tag].pname;
-    gamedesc["g"] = sessions[tag].game.uname;
-    gamedesc["i"] = now - sessions[tag].lasttime;
-    gamedesc["w"] = sessions[tag].watchers.length;
-    statusinfo["g"].push(gamedesc);
-  }
-  for (var tag in dglgames) {
-    var dglinfo = {"c": "dgl"};
-    var slash = tag.search('/');
-    dglinfo["g"] = tag.slice(0, slash);
-    dglinfo["p"] = tag.slice(slash + 1);
-    dglinfo["i"] = now - dglgames[tag].lasttime;
-    dglinfo["w"] = dglgames[tag].watchers.length;
-    statusinfo["g"].push(dglinfo);
-  }
-  callback(statusinfo);
-}
-
-function statusmsg(req, res) {
-  function respond(info) {
-    res.writeHead(200, { "Content-Type": "application/json" });
-    if (req.method != 'HEAD')
-      res.write(JSON.stringify(info));
-    res.end();
-  }
-  getStatus(respond);
-}
-
-function pstatusmsg(req, res) {
-  if (req.method == 'HEAD') {
-    res.writeHead(200, { "Content-Type": "application/json" });
-    res.end();
-    return;
-  }
-  var target = url.parse(req.url).pathname;
-  var pmatch = target.match(/^\/pstatus\/(.*)/);
-  if (pmatch && pmatch[1])
-    var pname = pmatch[1];
-  else {
-    sendError(res, 2, "No name given.");
-    return;
-  }
-  var reply = {"name": pname};
-  playerstatus(pname, function (pdata) {
-    reply["stat"] = pdata;
-    res.writeHead(200, { "Content-Type": "application/json" });
-    res.write(JSON.stringify(reply));
-    res.end();
-  });
-}
-
-function getuinfo(req, res) {
-  var urlobj = url.parse(req.url, true);
-  var query = urlobj.query;
-  if (!("key" in query) || !(query["key"] in logins)) {
-    sendError(res, 1);
-    return;
-  }
-  var match = urlobj.pathname.match(/^\/[^\/]*\/(.*)/);
-  if (!match || !match[1]) {
-    send404(res, urlobj.pathname, req.method == 'HEAD');
-    return;
-  }
-  var which = match[1];
-  var name = logins[query["key"]].name;
-  var reply = { "u": name };
-  function send() {
-    res.writeHead(200, { "Content-Type": "application/json" });
-    res.write(JSON.stringify(reply));
-    res.end();
-  }
-  if (which == "pw") {
-    /* Don't actually divulge passwords. */
-    reply["pw"] = "";
-    send();
-  }
-  else if (which == "email") {
-    var address;
-    function finish(code, signal) {
-      if (code != 0) {
-        tslog("sqlickrypt: %d with name %s", code, name);
-        sendError(res, 2);
-      }
-      else {
-        reply["email"] = address;
-        send();
-      }
-    }
-    var subproc = child_process.spawn("/bin/sqlickrypt", ["getmail"]);
-    subproc.stdout.on("data", function (data) {
-      address = data.toString().replace(/\n/g, "");
-    });
-    subproc.on("exit", finish);
-    subproc.stdin.end(name + '\n', "utf8");
-  }
-  else {
-    send404(res, urlobj.pathname, req.method == 'HEAD');
-    return;
-  }
-}
-
-function setuinfo(req, res, postdata) {
-  var urlobj = url.parse(req.url, true);
-  var query = urlobj.query;
-  if (!("key" in query) || !(query["key"] in logins)) {
-    sendError(res, 1);
-    return;
-  }
-  var name = logins[query["key"]].name;
-  var match = urlobj.pathname.match(/^\/[^\/]*\/(.*)/);
-  if (!match || !match[1]) {
-    send404(res, urlobj.pathname, true);
-    return;
-  }
-  var which = match[1];
-  if (!("v" in postdata)) {
-    sendError(res, 2, "No value provided");
-    return;
-  }
-  if (which == "email" || which == "pw") {
-    var args;
-    if (which == "email")
-      args = ["setmail"];
-    else
-      args = ["setpw"];
-    var child = child_process.execFile("/bin/sqlickrypt", args, 
-                  function (err, stdout, stderr) {
-      if (err) {
-        tslog("Could not set %s: sqlickrypt error %d", which, err.code);
-        sendError(res, 2);
-      }
-      else {
-        tslog("User %s has changed %s", name, which);
-        res.writeHead(200, { "Content-Type": "application/json" });
-        res.end(JSON.stringify({"t": "t"}));
-      }
-    });
-    child.stdin.end(name + "\n" + postdata.v + "\n", "utf8");
-  }
-  else {
-    send404(res, urlobj.pathname, true);
-  }
-}
-
-var errorcodes = [ "Generic Error", "Not logged in", "Invalid data", 
-        "Login failed", "Already playing", "Game launch failed",
-        "Server shutting down", "Game not in progress" ];
-
-function sendError(res, ecode, msg, box) {
-  res.writeHead(200, { "Content-Type": "application/json" });
-  var edict = {"t": "E"};
-  if (!(ecode < errorcodes.length && ecode > 0))
-    ecode = 0;
-  edict["c"] = ecode;
-  edict["s"] = errorcodes[ecode];
-  if (msg)
-    edict["s"] += ": " + msg;
-  if (box)
-    res.write(JSON.stringify([edict]));
-  else
-    res.write(JSON.stringify(edict));
-  res.end();
-}
-
-function send404(res, path, nopage) {
-  res.writeHead(404, {"Content-Type": "text/html; charset=utf-8"});
-  if (!nopage) {
-    res.write("<html><head><title>" + path + "</title></head>\n<body><h1>"
-              + path + " Not Found</h1></body></html>\n");
-  }
-  res.end();
-}
-
-function webHandler(req, res) {
-  /* default headers for the response */
-  var resheaders = {'Content-Type': 'text/html'};
-  /* The request body will be added to this as it arrives. */
-  var reqbody = "";
-  var formdata;
-
-  /* Register a listener to get the body. */
-  function moredata(chunk) {
-    reqbody += chunk;
-  }
-  req.on('data', moredata);
-
-  /* This will send the response once the whole request is here. */
-  function respond() {
-    formdata = getMsg(reqbody);
-    var target = url.parse(req.url).pathname;
-    /* First figure out if the client is POSTing to a command interface. */
-    if (req.method == 'POST') {
-      if (target == "/login") {
-        login(req, res, formdata);
-      }
-      else if (target == "/addacct") {
-        register(req, res, formdata);
-      }
-      else if (target == "/quit") {
-        stopgame(res, formdata);
-      }
-      else if (target.match(/^\/uinfo\//)) {
-        setuinfo(req, res, formdata);
-      }
-      else {
-        res.writeHead(405, resheaders);
-        res.end();
-      }
-    }
-    else if (req.method == 'GET' || req.method == 'HEAD') {
-      if (target == '/status') {
-        statusmsg(req, res);
-      }
-      else if (target.match(/^\/uinfo\//)) {
-        getuinfo(req, res);
-      }
-      else if (target.match(/^\/pstatus\//)) {
-        pstatusmsg(req, res);
-      }
-      else /* Go look for it in the filesystem */
-        serveStatic(req, res, target);
-    }
-    else { /* Some other method */
-      res.writeHead(501, resheaders);
-      res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n");
-      res.end();
-    }
-    return;
-  }
-  req.on('end', respond);
-}
-
-function wsHandler(wsRequest) {
-  var watchmatch = wsRequest.resource.match(/^\/watch\/(.*)$/);
-  var playmatch = wsRequest.resource.match(/^\/play\//);
-  if (watchmatch !== null) {
-    if (watchmatch[1] in sessions) {
-      var tsession = sessions[watchmatch[1]];
-      tsession.attach(wsRequest);
-      tslog("Game %s is being watched via WebSockets", tsession.tag());
-    }
-    else if (watchmatch[1] in dglgames) {
-      var dsession = dglgames[watchmatch[1]];
-      dsession.attach(wsRequest);
-      tslog("DGL game %s is being watched via WebSockets", dsession.tag());
-    }
-    else {
-      wsRequest.reject(404, errorcodes[7]);
-      return;
-    }
-  }
-  else if (playmatch !== null) {
-    wsStartGame(wsRequest);
-  }
-  else if (wsRequest.resourceURL.pathname == "/status") {
-    var conn = wsRequest.accept(null, wsRequest.origin);
-    var tell = function () {
-      getStatus(function (info) {
-        info["t"] = "t";
-        conn.sendUTF(JSON.stringify(info));
-      });
-    }
-    var beginH = function (gname, pname, client) {
-      conn.sendUTF(JSON.stringify({"t": "b", "c": client, "p": pname, 
-                   "g": gname}));
-    };
-    var listH = function (list) {
-      conn.sendUTF(JSON.stringify(list));
-    };
-    var endH = function (gname, pname) {
-      conn.sendUTF(JSON.stringify({"t": "e", "p": pname, "g": gname}));
-    };
-    gamemux.on('begin', beginH);
-    gamemux.on('list', listH);
-    gamemux.on('end', endH);
-    conn.on('message', tell);
-    conn.on('close', function () {
-      gamemux.removeListener('begin', beginH);
-      gamemux.removeListener('list', listH);
-      gamemux.removeListener('end', endH);
-    });
-    tell();
-  }
-  else
-    wsRequest.reject(404, "No such resource.");
-}
-
-/* Only games with low idle time are included.  Use getStatus() for the
- * complete list. */
-function pushStatus() {
-  var now = new Date();
-  var statusinfo = {"t": "p", "s": allowlogin, "g": []};
-  for (var tag in sessions) {
-    var delta = now - sessions[tag].lasttime;
-    if (delta < 60000) {
-      var gamedesc = {"c": "rlg"};
-      gamedesc["p"] = sessions[tag].pname;
-      gamedesc["g"] = sessions[tag].game.uname;
-      gamedesc["i"] = delta;
-      gamedesc["w"] = sessions[tag].watchers.length;
-      statusinfo["g"].push(gamedesc);
-    }
-  }
-  for (var tag in dglgames) {
-    var delta = now - dglgames[tag].lasttime;
-    if (delta < 60000) {
-      var dglinfo = {"c": "dgl"};
-      var slash = tag.search('/');
-      dglinfo["g"] = tag.slice(0, slash);
-      dglinfo["p"] = tag.slice(slash + 1);
-      dglinfo["i"] = delta;
-      dglinfo["w"] = dglgames[tag].watchers.length;
-      statusinfo["g"].push(dglinfo);
-    }
-  }
-  gamemux.emit('list', statusinfo);
-}
-
-function shutdown () {
-  httpServer.close();
-  httpServer.removeAllListeners('request');
-  ctlServer.close();
-  tslog("Shutting down...");
-  process.exit();
-}
-
-function consoleHandler(chunk) {
-  var msg = chunk.toString().split('\n')[0];
-  if (msg == "quit") {
-    allowlogin = false;
-    tslog("Disconnecting...");
-    for (var tag in sessions) {
-      sessions[tag].close();
-    }
-    progressWatcher.stdin.end("\n");
-    setTimeout(shutdown, 2000);
-  }
-}
-
-process.on("exit", function () {
-  for (var tag in sessions) {
-    sessions[tag].term.kill('SIGHUP');
-  }
-  tslog("Quitting...");
-  return;
-});
-
-/* Initialization STARTS HERE */
-process.env["TERM"] = "xterm-256color";
-
-if (process.getuid() != 0) {
-  tslog("Not running as root, cannot chroot.");
-  process.exit(1);
-}
-
-var httpServer; // declare here so shutdown() can find it
-var wsServer;
-var progressWatcher;
-
-var pwent; 
-try {
-  pwent = posix.getpwnam(dropToUser);
-}
-catch (err) {
-  tslog("Could not drop to user %s: user does not exist", dropToUser);
-  process.exit(1);
-}
-
-/* This could be nonblocking, but nothing else can start yet anyway. */
-if (fs.existsSync(ctlsocket)) {
-  fs.unlinkSync(ctlsocket);
-}
-
-/* Open the control socket before chrooting where it can't be found */
-var ctlServer = net.createServer(function (sock) {
-  sock.on('data', consoleHandler);
-});
-ctlServer.listen(ctlsocket, function () {
-  /* rlgwebd.js now assumes that it has been started by the rlgwebd shell
-   * script, or some other method that detaches it and sets up stdio. */
-  /* chroot and drop permissions.  posix.chroot() does chdir() itself. */
-  try {
-    posix.chroot(chrootDir);
-  }
-  catch (err) {
-    tslog("chroot to %s failed: %s", chrootDir, err);
-    process.exit(1);
-  }
-  try {
-    // drop gid first, that requires UID=0
-    process.setgid(pwent.gid);
-    process.setuid(pwent.uid);
-  }
-  catch (err) {
-    tslog("Could not drop permissions: %s", err);
-    process.exit(1);
-  }
-  httpServer = http.createServer(webHandler);
-  httpServer.listen(httpPort);
-  tslog('rlgwebd running on port %d', httpPort); 
-  wsServer = new WebSocketServer({"httpServer": httpServer});
-  wsServer.on("request", wsHandler);
-  tslog('WebSockets are online'); 
-  progressWatcher = startProgressWatcher();
-  setInterval(pushStatus, 40000);
-});
-
--- a/rlgwebd.service	Thu Jan 14 19:10:46 2016 -0500
+++ b/rlgwebd.service	Thu Jan 14 20:52:29 2016 -0500
@@ -5,8 +5,8 @@
 [Service]
 Type=simple
 Environment=NODE_PATH=/usr/lib/node_modules
-ExecStart=/usr/bin/node /usr/local/bin/rlgwebd.js
-ExecStop=/usr/bin/node /usr/local/bin/rlgwebd-stop.js
+ExecStart=/usr/local/bin/rlgwebd
+ExecStop=/usr/local/bin/rlgwebd-stop
 
 [Install]
 WantedBy=multi-user.target
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webtty	Thu Jan 14 20:52:29 2016 -0500
@@ -0,0 +1,278 @@
+#!/usr/bin/env node
+
+var localModules = '/usr/lib/node_modules/';
+var http = require('http');
+var url = require('url');
+var path = require('path');
+var fs = require('fs');
+var pty = require(path.join(localModules, "pty.js"));
+var child_process = require("child_process");
+var webSocketServer = require(path.join(localModules, "websocket")).server;
+
+var serveStaticRoot = fs.realpathSync(".");
+var sessions = {};
+var nsessid = 0;
+
+var env_dontuse = {"TMUX": true, "TMUX_PANE": true};
+
+/* Constructor for TermSessions.  Note that it opens the terminal and 
+ * adds itself to the sessions dict. 
+ */
+function TermSessionWS(conn, h, w) {
+  var ss = this;
+  /* Set up the sizes. */
+  w = Math.floor(Number(w));
+  if (!(w > 0 && w < 256))
+    w = 80;
+  this.w = w;
+  h = Math.floor(Number(h));
+  if (!(h > 0 && h < 256))
+    h = 25;
+  this.h = h;
+  this.conn = conn;
+  /* Customize the environment. */
+  var childenv = {};
+  for (var key in process.env) {
+    if (!(key in env_dontuse))
+      childenv[key] = process.env[key];
+  }
+  var spawnopts = {"env": childenv, "cwd": process.env["HOME"], 
+                   "rows": this.h, "cols": this.w};
+  this.term = pty.spawn("bash", [], spawnopts);
+  this.alive = true;
+  this.term.on("data", function (datastr) {
+    var buf = new Buffer(datastr);
+    if (ss.conn.connected)
+      ss.conn.sendUTF(JSON.stringify({"t": "d", "d": buf.toString("hex")}));
+  });
+  this.term.on("exit", function () {
+    ss.alive = false;
+    /* Wait for all the data to get collected */
+    setTimeout(ss.cleanup, 1000);
+  });
+  this.conn.on("message", function (msg) {
+    try {
+      var msgObj = JSON.parse(msg.utf8Data);
+    }
+    catch (e) {
+      return;
+    }
+    if (msgObj.t == "d") {
+      var hexstr = msgObj["d"].replace(/[^0-9a-f]/gi, "");
+      if (hexstr.length % 2 != 0) {
+        return;
+      }
+      var keybuf = new Buffer(hexstr, "hex");
+      ss.term.write(keybuf);
+    }
+  });
+  this.conn.on("close", function (msg) {
+    if (ss.alive)
+      ss.term.kill('SIGHUP');
+    console.log("WebSocket connection closed.");
+  });
+  this.cleanup = function () {
+    /* Call this when the child is dead. */
+    if (ss.alive)
+      return;
+    if (ss.conn.connected) {
+      ss.conn.sendUTF(JSON.stringify({"t": "q"}));
+    }
+  };
+  sessions[nsessid++] = this;
+  this.conn.sendUTF(JSON.stringify({"t": "l", "w": w, "h": h}));
+  console.log("New WebSocket connection.");
+}
+
+function randkey() {
+  rnum = Math.floor(Math.random() * 65536 * 65536);
+  hexstr = rnum.toString(16);
+  while (hexstr.length < 8)
+    hexstr = "0" + hexstr;
+  return hexstr;
+}
+
+/* Returns a list of the cookies in the request, obviously. */
+function getCookies(req) {
+  cookies = [];
+  if ("cookie" in req.headers) {
+    cookstrs = req.headers["cookie"].split("; ");
+    for (var i = 0; i < cookstrs.length; i++) {
+      eqsign = cookstrs[i].indexOf("=");
+      if (eqsign > 0) {
+        name = cookstrs[i].slice(0, eqsign).toLowerCase();
+        val = cookstrs[i].slice(eqsign + 1);
+        cookies[name] = val;
+      }
+      else if (eqsign < 0)
+        cookies[cookstrs[i]] = null;
+    }
+  }
+  return cookies;
+}
+
+function urlDec(encstr) {
+  var decstr = "";
+  var tnum;
+  for (var i = 0; i < encstr.length; i++)
+  {
+    if (encstr.charAt(i) == "+")
+      decstr += " ";
+    else if (encstr.charAt(i) == "%")
+    {
+      tnum = Number("0x" + encstr.slice(i + 1, 2));
+      if (!isNaN(tnum) && tnum >= 0)
+        decstr += String.fromCharCode(tnum);
+      i += 2;
+    }
+    else
+      decstr += encstr.charAt(i);
+  }
+  return decstr;
+}
+
+/* Returns the contents of a form */
+function getFormValues(formtext) {
+  var jsonobj;
+  try {
+    jsonobj = JSON.parse(formtext);
+  } catch (e) {
+    if (e instanceof SyntaxError)
+      return null;
+  }
+  return jsonobj;
+}
+
+function serveStatic(req, res, fname) {
+  var nname = path.normalize(fname);
+  if (nname == "" || nname == "/")
+    nname = "index-sh.html";
+  if (nname.match(/\/$/))
+    path.join(nname, "index.html"); /* it was a directory */
+  var realname = path.join(serveStaticRoot, nname);
+  var extension = path.extname(realname);
+  fs.exists(realname, function (exists) {
+    var resheaders = {};
+    if (!exists || !extension || extension == ".html")
+      resheaders["Content-Type"] = "text/html";
+    else if (extension == ".png")
+      resheaders["Content-Type"] = "image/png";
+    else if (extension == ".css")
+      resheaders["Content-Type"] = "text/css";
+    else if (extension == ".js")
+      resheaders["Content-Type"] = "text/javascript";
+    else if (extension == ".svg")
+      resheaders["Content-Type"] = "image/svg+xml";
+    else
+      resheaders["Content-Type"] = "application/octet-stream";
+    if (exists) {
+      /* Not nice, not sensible.  First see if it's readable, then respond
+       * 200 or 500.  Don't throw nasty errors. */
+      res.writeHead(200, resheaders);
+      fs.readFile(realname, function (error, data) {
+        if (error) throw error;
+        res.write(data);
+        res.end();
+      });
+    }
+    else {
+      res.writeHead(404, resheaders);
+      res.write("<html><head><title>" + nname + "</title></head>\n<body><h1>" + nname + " Not Found</h1></body></html>\n");
+      res.end();
+    }
+  });
+  return;
+}
+
+var errorcodes = [ "Generic Error", "Not logged in", "Invalid data" ];
+
+function sendError(res, ecode) {
+  res.writeHead(200, { "Content-Type": "text/plain" });
+  if (!(ecode >= 0 && ecode < errorcodes.length))
+    ecode = 0;
+  res.write(JSON.stringify({"t": "E", "c": ecode, "s": errorcodes[ecode]}));
+  res.end();
+}
+
+function handler(req, res) {
+  /* default headers for the response */
+  var resheaders = {'Content-Type': 'text/html'};
+  /* The request body will be added to this as it arrives. */
+  var reqbody = "";
+  var formdata;
+
+  /* Register a listener to get the body. */
+  function moredata(chunk) {
+    reqbody += chunk;
+  }
+  req.on('data', moredata);
+
+  /* This will send the response once the whole request is here. */
+  function respond() {
+    var target = url.parse(req.url).pathname;
+    /* Currently only static files and WebSockets are needed. */
+    if (req.method == 'POST') {
+      formdata = getFormValues(reqbody);
+      res.writeHead(405, resheaders);
+      res.end();
+    }
+    else if (req.method == 'GET' || req.method == 'HEAD') {
+      serveStatic(req, res, target);
+    }
+    else { /* Some other method */
+      res.writeHead(501, resheaders);
+      res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n");
+      res.end();
+    }
+    return;
+  }
+  req.on('end', respond);
+}
+
+process.on("exit", function () {
+  for (var sessid in sessions) {
+    if (sessions[sessid].alive)
+      sessions[sessid].term.kill('SIGHUP');
+  }
+  console.log("Quitting...");
+  return;
+});
+
+function wsRespond(req) {
+  var w, h, conn;
+  if (req.resourceURL.pathname == "/sock") {
+    w = parseInt(req.resourceURL.query.w);
+    if (isNaN(w) || w <= 0 || w > 256)
+      w = 80;
+    h = parseInt(req.resourceURL.query.h);
+    if (isNaN(h) || h <= 0 || h > 256)
+      h = 25;
+    conn = req.accept(null, req.origin);
+    new TermSessionWS(conn, h, w);
+  }
+  else {
+    req.reject(404, "No such resource.");
+  }
+}
+
+/* The pty.js module doesn't wait for the processes it spawns, so they 
+ * become zombies, which leads to unpleasantness when the system runs
+ * out of process table entries.  But if the child_process module is 
+ * initialized and a child spawned, node will continue waiting for any
+ * children.
+ * Someday, some developer will get the bright idea of tracking how many
+ * processes the child_process module has spawned, and not waiting if 
+ * it's zero.  Until then, the following useless line will protect us 
+ * from the zombie hordes.
+ * Figuring this out was almost as interesting as the Rogue bug where 
+ * printf debugging altered whether the high score list was checked.
+ */
+child_process.spawn("/bin/true");
+
+process.env["TERM"] = "xterm-256color";
+var webServer = http.createServer(handler);
+webServer.listen(8080, "127.0.0.1");
+console.log('Server running at http://127.0.0.1:8080/'); 
+var wsServer = new webSocketServer({"httpServer": webServer});
+wsServer.on("request", wsRespond);
+console.log('WebSockets online'); 
--- a/webtty.js	Thu Jan 14 19:10:46 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,278 +0,0 @@
-#!/usr/bin/env node
-
-var localModules = '/usr/lib/node_modules/';
-var http = require('http');
-var url = require('url');
-var path = require('path');
-var fs = require('fs');
-var pty = require(path.join(localModules, "pty.js"));
-var child_process = require("child_process");
-var webSocketServer = require(path.join(localModules, "websocket")).server;
-
-var serveStaticRoot = fs.realpathSync(".");
-var sessions = {};
-var nsessid = 0;
-
-var env_dontuse = {"TMUX": true, "TMUX_PANE": true};
-
-/* Constructor for TermSessions.  Note that it opens the terminal and 
- * adds itself to the sessions dict. 
- */
-function TermSessionWS(conn, h, w) {
-  var ss = this;
-  /* Set up the sizes. */
-  w = Math.floor(Number(w));
-  if (!(w > 0 && w < 256))
-    w = 80;
-  this.w = w;
-  h = Math.floor(Number(h));
-  if (!(h > 0 && h < 256))
-    h = 25;
-  this.h = h;
-  this.conn = conn;
-  /* Customize the environment. */
-  var childenv = {};
-  for (var key in process.env) {
-    if (!(key in env_dontuse))
-      childenv[key] = process.env[key];
-  }
-  var spawnopts = {"env": childenv, "cwd": process.env["HOME"], 
-                   "rows": this.h, "cols": this.w};
-  this.term = pty.spawn("bash", [], spawnopts);
-  this.alive = true;
-  this.term.on("data", function (datastr) {
-    var buf = new Buffer(datastr);
-    if (ss.conn.connected)
-      ss.conn.sendUTF(JSON.stringify({"t": "d", "d": buf.toString("hex")}));
-  });
-  this.term.on("exit", function () {
-    ss.alive = false;
-    /* Wait for all the data to get collected */
-    setTimeout(ss.cleanup, 1000);
-  });
-  this.conn.on("message", function (msg) {
-    try {
-      var msgObj = JSON.parse(msg.utf8Data);
-    }
-    catch (e) {
-      return;
-    }
-    if (msgObj.t == "d") {
-      var hexstr = msgObj["d"].replace(/[^0-9a-f]/gi, "");
-      if (hexstr.length % 2 != 0) {
-        return;
-      }
-      var keybuf = new Buffer(hexstr, "hex");
-      ss.term.write(keybuf);
-    }
-  });
-  this.conn.on("close", function (msg) {
-    if (ss.alive)
-      ss.term.kill('SIGHUP');
-    console.log("WebSocket connection closed.");
-  });
-  this.cleanup = function () {
-    /* Call this when the child is dead. */
-    if (ss.alive)
-      return;
-    if (ss.conn.connected) {
-      ss.conn.sendUTF(JSON.stringify({"t": "q"}));
-    }
-  };
-  sessions[nsessid++] = this;
-  this.conn.sendUTF(JSON.stringify({"t": "l", "w": w, "h": h}));
-  console.log("New WebSocket connection.");
-}
-
-function randkey() {
-  rnum = Math.floor(Math.random() * 65536 * 65536);
-  hexstr = rnum.toString(16);
-  while (hexstr.length < 8)
-    hexstr = "0" + hexstr;
-  return hexstr;
-}
-
-/* Returns a list of the cookies in the request, obviously. */
-function getCookies(req) {
-  cookies = [];
-  if ("cookie" in req.headers) {
-    cookstrs = req.headers["cookie"].split("; ");
-    for (var i = 0; i < cookstrs.length; i++) {
-      eqsign = cookstrs[i].indexOf("=");
-      if (eqsign > 0) {
-        name = cookstrs[i].slice(0, eqsign).toLowerCase();
-        val = cookstrs[i].slice(eqsign + 1);
-        cookies[name] = val;
-      }
-      else if (eqsign < 0)
-        cookies[cookstrs[i]] = null;
-    }
-  }
-  return cookies;
-}
-
-function urlDec(encstr) {
-  var decstr = "";
-  var tnum;
-  for (var i = 0; i < encstr.length; i++)
-  {
-    if (encstr.charAt(i) == "+")
-      decstr += " ";
-    else if (encstr.charAt(i) == "%")
-    {
-      tnum = Number("0x" + encstr.slice(i + 1, 2));
-      if (!isNaN(tnum) && tnum >= 0)
-        decstr += String.fromCharCode(tnum);
-      i += 2;
-    }
-    else
-      decstr += encstr.charAt(i);
-  }
-  return decstr;
-}
-
-/* Returns the contents of a form */
-function getFormValues(formtext) {
-  var jsonobj;
-  try {
-    jsonobj = JSON.parse(formtext);
-  } catch (e) {
-    if (e instanceof SyntaxError)
-      return null;
-  }
-  return jsonobj;
-}
-
-function serveStatic(req, res, fname) {
-  var nname = path.normalize(fname);
-  if (nname == "" || nname == "/")
-    nname = "index-sh.html";
-  if (nname.match(/\/$/))
-    path.join(nname, "index.html"); /* it was a directory */
-  var realname = path.join(serveStaticRoot, nname);
-  var extension = path.extname(realname);
-  fs.exists(realname, function (exists) {
-    var resheaders = {};
-    if (!exists || !extension || extension == ".html")
-      resheaders["Content-Type"] = "text/html";
-    else if (extension == ".png")
-      resheaders["Content-Type"] = "image/png";
-    else if (extension == ".css")
-      resheaders["Content-Type"] = "text/css";
-    else if (extension == ".js")
-      resheaders["Content-Type"] = "text/javascript";
-    else if (extension == ".svg")
-      resheaders["Content-Type"] = "image/svg+xml";
-    else
-      resheaders["Content-Type"] = "application/octet-stream";
-    if (exists) {
-      /* Not nice, not sensible.  First see if it's readable, then respond
-       * 200 or 500.  Don't throw nasty errors. */
-      res.writeHead(200, resheaders);
-      fs.readFile(realname, function (error, data) {
-        if (error) throw error;
-        res.write(data);
-        res.end();
-      });
-    }
-    else {
-      res.writeHead(404, resheaders);
-      res.write("<html><head><title>" + nname + "</title></head>\n<body><h1>" + nname + " Not Found</h1></body></html>\n");
-      res.end();
-    }
-  });
-  return;
-}
-
-var errorcodes = [ "Generic Error", "Not logged in", "Invalid data" ];
-
-function sendError(res, ecode) {
-  res.writeHead(200, { "Content-Type": "text/plain" });
-  if (!(ecode >= 0 && ecode < errorcodes.length))
-    ecode = 0;
-  res.write(JSON.stringify({"t": "E", "c": ecode, "s": errorcodes[ecode]}));
-  res.end();
-}
-
-function handler(req, res) {
-  /* default headers for the response */
-  var resheaders = {'Content-Type': 'text/html'};
-  /* The request body will be added to this as it arrives. */
-  var reqbody = "";
-  var formdata;
-
-  /* Register a listener to get the body. */
-  function moredata(chunk) {
-    reqbody += chunk;
-  }
-  req.on('data', moredata);
-
-  /* This will send the response once the whole request is here. */
-  function respond() {
-    var target = url.parse(req.url).pathname;
-    /* Currently only static files and WebSockets are needed. */
-    if (req.method == 'POST') {
-      formdata = getFormValues(reqbody);
-      res.writeHead(405, resheaders);
-      res.end();
-    }
-    else if (req.method == 'GET' || req.method == 'HEAD') {
-      serveStatic(req, res, target);
-    }
-    else { /* Some other method */
-      res.writeHead(501, resheaders);
-      res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n");
-      res.end();
-    }
-    return;
-  }
-  req.on('end', respond);
-}
-
-process.on("exit", function () {
-  for (var sessid in sessions) {
-    if (sessions[sessid].alive)
-      sessions[sessid].term.kill('SIGHUP');
-  }
-  console.log("Quitting...");
-  return;
-});
-
-function wsRespond(req) {
-  var w, h, conn;
-  if (req.resourceURL.pathname == "/sock") {
-    w = parseInt(req.resourceURL.query.w);
-    if (isNaN(w) || w <= 0 || w > 256)
-      w = 80;
-    h = parseInt(req.resourceURL.query.h);
-    if (isNaN(h) || h <= 0 || h > 256)
-      h = 25;
-    conn = req.accept(null, req.origin);
-    new TermSessionWS(conn, h, w);
-  }
-  else {
-    req.reject(404, "No such resource.");
-  }
-}
-
-/* The pty.js module doesn't wait for the processes it spawns, so they 
- * become zombies, which leads to unpleasantness when the system runs
- * out of process table entries.  But if the child_process module is 
- * initialized and a child spawned, node will continue waiting for any
- * children.
- * Someday, some developer will get the bright idea of tracking how many
- * processes the child_process module has spawned, and not waiting if 
- * it's zero.  Until then, the following useless line will protect us 
- * from the zombie hordes.
- * Figuring this out was almost as interesting as the Rogue bug where 
- * printf debugging altered whether the high score list was checked.
- */
-child_process.spawn("/bin/true");
-
-process.env["TERM"] = "xterm-256color";
-var webServer = http.createServer(handler);
-webServer.listen(8080, "127.0.0.1");
-console.log('Server running at http://127.0.0.1:8080/'); 
-var wsServer = new webSocketServer({"httpServer": webServer});
-wsServer.on("request", wsRespond);
-console.log('WebSockets online');