view webtty.js @ 170:50e4c9feeac2

RLGWebD: fix simultaneous player bug. Multiple games can now run at the same time, and data will be sent to the proper place. The interaction of multiple players with watchers has not yet been tested.
author John "Elwin" Edwards
date Fri, 09 Jan 2015 13:06:41 -0500
parents c4a32007d2dc
children 5372f1f97cf5
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.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');