view webtty @ 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 webtty.js@5372f1f97cf5
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');