view webtty.js @ 77:f8bb37f48d58

Make font resizing affect the keyboard too. The keyboard font size now changes along with the terminal font size. This allows the keyboard to work at page widths down at least to 600px.
author John "Elwin" Edwards <elwin@sdf.org>
date Sat, 23 Jun 2012 17:41:27 -0700
parents 7466927c17a5
children e4773ac5d4d5
line wrap: on
line source

#!/usr/bin/env node
var http = require('http');
var url = require('url');
var path = require('path');
var fs = require('fs');
var child_process = require("child_process");

var serveStaticRoot = "/home/elwin/hk/nodejs/rlg/s/";
var ptyhelp = "/home/elwin/hk/nodejs/rlg/ptyhelper";
var sessions = {};

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 TermSession(sessid, h, w) {
  /* 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;
  /* Customize the environment. */
  var childenv = {};
  for (var key in process.env) {
    if (!(key in env_dontuse))
      childenv[key] = process.env[key];
  }
  childenv["PTYHELPER"] = String(this.h) + "x" + String(this.w);
  // Should setsid get set?
  var spawnopts = {"env": childenv, "cwd": process.env["HOME"]};
  this.child = child_process.spawn(ptyhelp, ["bash"], spawnopts);
  var ss = this;
  /* Eventually we'll need to make sure the sessid isn't in use yet. */
  this.sessid = sessid;
  this.alive = true;
  this.data = []; // Buffer for the process' output.
  this.nsend = 0; // Number to use for the next message sent.
  this.nrecv = 0; // Number expected on the next message received.
  this.msgQ = []; // Queue for messages that arrived out of order.
  this.child.stdout.on("data", function (buf) {
    ss.data.push(buf);
  });
  this.child.stderr.on("data", function (buf) {
    ss.data.push(buf);
  });
  this.child.on("exit", function (code, signal) {
    ss.exitcode = (code != null ? code : 255);
    ss.alive = false;
    /* Wait for all the data to get collected */
    setTimeout(ss.cleanup, 1000);
  });
  this.write = function (data, n) {
    if (!this.alive) {
      /* Throw some kind of exception? */
      return;
    }
    if (n !== this.nrecv) {
      console.log("Session " + this.sessid + ": Expected message " + this.nrecv + ", got " + n);
    }
    this.nrecv = n + 1;
    this.child.stdin.write(data);
  };
  this.read = function () {
    if (this.data.length == 0)
      return null;
    var pos = 0;
    var i = 0;
    for (i = 0; i < this.data.length; i++)
      pos += this.data[i].length;
    var nbuf = new Buffer(pos);
    var tptr;
    pos = 0;
    while (this.data.length > 0) {
      tptr = this.data.shift();
      tptr.copy(nbuf, pos);
      pos += tptr.length;
    }
    return nbuf;
  };
  this.close = function () {
    if (this.alive)
      this.child.kill('SIGHUP');
  };
  this.cleanup = function () {
    /* Call this when the child is dead. */
    if (this.alive)
      return;
    /* Give the client a chance to read any leftover data. */
    if (ss.data.length > 0)
      setTimeout(ss.remove, 8000);
    else
      ss.remove();
  };
  this.remove = function () {
    delete sessions[ss.sessid];
    console.log("Session " + this.sessid + " removed.");
  };
  sessions[sessid] = this;
}

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 login(req, res, formdata) {
  var resheaders = {'Content-Type': 'text/plain'};
  var sessid = randkey();
  /* The TermSession constructor will check these thoroughly too, but 
   * you can't be too suspicious of client-supplied data. */
  var w = 80;
  var h = 25;
  var t;
  if ("w" in formdata) {
    t = Math.floor(Number(formdata["w"]));
    if (t > 0 && t < 256)
      w = t;
  }
  if ("h" in formdata) {
    t = Math.floor(Number(formdata["h"]));
    if (t > 0 && t < 256)
      h = t;
  }
  var nsession = new TermSession(sessid, h, w);
  resheaders["Set-Cookie"] = "ID=" + sessid;
  res.writeHead(200, resheaders);
  var logindict = {"login": true, "id": sessid, "w": w, "h": h};
  res.write(JSON.stringify(logindict));
  res.end();
  console.log("Started new session with key " + sessid + ", pid " + nsession.child.pid);
  return;
}

function findTermSession(req) {
  var cookies = getCookies(req);
  if ("id" in cookies) {
    var sessid = cookies["id"];
    if (sessid in sessions) {
      return sessions[sessid];
    }
  }
  return null;
}

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);
  path.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;
}

function readFeed(res, term) {
  res.writeHead(200, { "Content-Type": "text/plain" });
  if (term) {
    var answer = {};
    var result = term.read();
    if (result == null) {
      answer["t"] = "n";
    }
    else {
      answer["t"] = "d";
      answer["d"] = result.toString("hex");
      answer["n"] = term.nsend++;
    }
    res.write(JSON.stringify(answer));
    res.end();
  }
  else {
    sendError(res, 1);
  }
}

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;
    var cterm = findTermSession(req);
    /* First figure out if the client is POSTing to a command interface. */
    if (req.method == 'POST') {
      formdata = getFormValues(reqbody);
      if (target == '/feed') {
        if (!cterm) {
          sendError(res, 1);
          return;
        }
        if (formdata["t"] == "q") {
          /* The client wants to quit. */
          // FIXME need to send a message back to the client
          cterm.close();
        }
        else if (formdata["t"] == "d" && typeof(formdata["d"]) == "string") {
          /* process the keys */
          hexstr = formdata["d"].replace(/[^0-9a-f]/gi, "");
          if (hexstr.length % 2 != 0) {
            sendError(res, 2);
            return;
          }
          keybuf = new Buffer(hexstr, "hex");
          cterm.write(keybuf, formdata["n"]);
        }
        readFeed(res, cterm);
      }
      else if (target == "/login") {
        login(req, res, formdata);
      }
      else {
        res.writeHead(405, resheaders);
        res.end();
      }
    }
    else if (req.method == 'GET' || req.method == 'HEAD') {
      if (target == '/feed') {
        if (!cterm) {
          sendError(res, 1);
          return;
        }
        readFeed(res, cterm);
      }
      /* Default page, create a new term */
      /* FIXME New term not created anymore, is a special case still needed? */
      else if (target == '/') {
        serveStatic(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);

}

process.on("exit", function () {
  for (var sessid in sessions) {
    if (sessions[sessid].alive)
      sessions[sessid].child.kill('SIGHUP');
  }
  console.log("Quitting...");
  return;
});

process.env["TERM"] = "xterm-256color";
http.createServer(handler).listen(8080, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8080/');