Implement checking the numbers of the client's messages on the server. Fixing out-of-ordering isn't implemented because the problem hasn't been observed yet, though it likely will once actual network transit is involved.
364 lines
10 KiB
JavaScript
Executable file
364 lines
10 KiB
JavaScript
Executable file
#!/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/');
|