Mercurial > hg > rlgwebd
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/webtty Thu Jan 14 20:52:29 2016 -0500 @@ -0,0 +1,278 @@ +#!/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');