view webtty @ 198:ea28353d620a

Make sure games have saved and exited before stopping the server. This should make the systemd version safe for production, though it can't yet deal with a game that hangs and doesn't exit.
author John "Elwin" Edwards
date Thu, 28 Jan 2016 21:17:06 -0500
parents 3bdee6371c3f
children
line wrap: on
line source

#!/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');