comparison 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
comparison
equal deleted inserted replaced
194:5483d413a45b 195:3bdee6371c3f
1 #!/usr/bin/env node
2
3 var localModules = '/usr/lib/node_modules/';
4 var http = require('http');
5 var url = require('url');
6 var path = require('path');
7 var fs = require('fs');
8 var pty = require(path.join(localModules, "pty.js"));
9 var child_process = require("child_process");
10 var webSocketServer = require(path.join(localModules, "websocket")).server;
11
12 var serveStaticRoot = fs.realpathSync(".");
13 var sessions = {};
14 var nsessid = 0;
15
16 var env_dontuse = {"TMUX": true, "TMUX_PANE": true};
17
18 /* Constructor for TermSessions. Note that it opens the terminal and
19 * adds itself to the sessions dict.
20 */
21 function TermSessionWS(conn, h, w) {
22 var ss = this;
23 /* Set up the sizes. */
24 w = Math.floor(Number(w));
25 if (!(w > 0 && w < 256))
26 w = 80;
27 this.w = w;
28 h = Math.floor(Number(h));
29 if (!(h > 0 && h < 256))
30 h = 25;
31 this.h = h;
32 this.conn = conn;
33 /* Customize the environment. */
34 var childenv = {};
35 for (var key in process.env) {
36 if (!(key in env_dontuse))
37 childenv[key] = process.env[key];
38 }
39 var spawnopts = {"env": childenv, "cwd": process.env["HOME"],
40 "rows": this.h, "cols": this.w};
41 this.term = pty.spawn("bash", [], spawnopts);
42 this.alive = true;
43 this.term.on("data", function (datastr) {
44 var buf = new Buffer(datastr);
45 if (ss.conn.connected)
46 ss.conn.sendUTF(JSON.stringify({"t": "d", "d": buf.toString("hex")}));
47 });
48 this.term.on("exit", function () {
49 ss.alive = false;
50 /* Wait for all the data to get collected */
51 setTimeout(ss.cleanup, 1000);
52 });
53 this.conn.on("message", function (msg) {
54 try {
55 var msgObj = JSON.parse(msg.utf8Data);
56 }
57 catch (e) {
58 return;
59 }
60 if (msgObj.t == "d") {
61 var hexstr = msgObj["d"].replace(/[^0-9a-f]/gi, "");
62 if (hexstr.length % 2 != 0) {
63 return;
64 }
65 var keybuf = new Buffer(hexstr, "hex");
66 ss.term.write(keybuf);
67 }
68 });
69 this.conn.on("close", function (msg) {
70 if (ss.alive)
71 ss.term.kill('SIGHUP');
72 console.log("WebSocket connection closed.");
73 });
74 this.cleanup = function () {
75 /* Call this when the child is dead. */
76 if (ss.alive)
77 return;
78 if (ss.conn.connected) {
79 ss.conn.sendUTF(JSON.stringify({"t": "q"}));
80 }
81 };
82 sessions[nsessid++] = this;
83 this.conn.sendUTF(JSON.stringify({"t": "l", "w": w, "h": h}));
84 console.log("New WebSocket connection.");
85 }
86
87 function randkey() {
88 rnum = Math.floor(Math.random() * 65536 * 65536);
89 hexstr = rnum.toString(16);
90 while (hexstr.length < 8)
91 hexstr = "0" + hexstr;
92 return hexstr;
93 }
94
95 /* Returns a list of the cookies in the request, obviously. */
96 function getCookies(req) {
97 cookies = [];
98 if ("cookie" in req.headers) {
99 cookstrs = req.headers["cookie"].split("; ");
100 for (var i = 0; i < cookstrs.length; i++) {
101 eqsign = cookstrs[i].indexOf("=");
102 if (eqsign > 0) {
103 name = cookstrs[i].slice(0, eqsign).toLowerCase();
104 val = cookstrs[i].slice(eqsign + 1);
105 cookies[name] = val;
106 }
107 else if (eqsign < 0)
108 cookies[cookstrs[i]] = null;
109 }
110 }
111 return cookies;
112 }
113
114 function urlDec(encstr) {
115 var decstr = "";
116 var tnum;
117 for (var i = 0; i < encstr.length; i++)
118 {
119 if (encstr.charAt(i) == "+")
120 decstr += " ";
121 else if (encstr.charAt(i) == "%")
122 {
123 tnum = Number("0x" + encstr.slice(i + 1, 2));
124 if (!isNaN(tnum) && tnum >= 0)
125 decstr += String.fromCharCode(tnum);
126 i += 2;
127 }
128 else
129 decstr += encstr.charAt(i);
130 }
131 return decstr;
132 }
133
134 /* Returns the contents of a form */
135 function getFormValues(formtext) {
136 var jsonobj;
137 try {
138 jsonobj = JSON.parse(formtext);
139 } catch (e) {
140 if (e instanceof SyntaxError)
141 return null;
142 }
143 return jsonobj;
144 }
145
146 function serveStatic(req, res, fname) {
147 var nname = path.normalize(fname);
148 if (nname == "" || nname == "/")
149 nname = "index-sh.html";
150 if (nname.match(/\/$/))
151 path.join(nname, "index.html"); /* it was a directory */
152 var realname = path.join(serveStaticRoot, nname);
153 var extension = path.extname(realname);
154 fs.exists(realname, function (exists) {
155 var resheaders = {};
156 if (!exists || !extension || extension == ".html")
157 resheaders["Content-Type"] = "text/html";
158 else if (extension == ".png")
159 resheaders["Content-Type"] = "image/png";
160 else if (extension == ".css")
161 resheaders["Content-Type"] = "text/css";
162 else if (extension == ".js")
163 resheaders["Content-Type"] = "text/javascript";
164 else if (extension == ".svg")
165 resheaders["Content-Type"] = "image/svg+xml";
166 else
167 resheaders["Content-Type"] = "application/octet-stream";
168 if (exists) {
169 /* Not nice, not sensible. First see if it's readable, then respond
170 * 200 or 500. Don't throw nasty errors. */
171 res.writeHead(200, resheaders);
172 fs.readFile(realname, function (error, data) {
173 if (error) throw error;
174 res.write(data);
175 res.end();
176 });
177 }
178 else {
179 res.writeHead(404, resheaders);
180 res.write("<html><head><title>" + nname + "</title></head>\n<body><h1>" + nname + " Not Found</h1></body></html>\n");
181 res.end();
182 }
183 });
184 return;
185 }
186
187 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data" ];
188
189 function sendError(res, ecode) {
190 res.writeHead(200, { "Content-Type": "text/plain" });
191 if (!(ecode >= 0 && ecode < errorcodes.length))
192 ecode = 0;
193 res.write(JSON.stringify({"t": "E", "c": ecode, "s": errorcodes[ecode]}));
194 res.end();
195 }
196
197 function handler(req, res) {
198 /* default headers for the response */
199 var resheaders = {'Content-Type': 'text/html'};
200 /* The request body will be added to this as it arrives. */
201 var reqbody = "";
202 var formdata;
203
204 /* Register a listener to get the body. */
205 function moredata(chunk) {
206 reqbody += chunk;
207 }
208 req.on('data', moredata);
209
210 /* This will send the response once the whole request is here. */
211 function respond() {
212 var target = url.parse(req.url).pathname;
213 /* Currently only static files and WebSockets are needed. */
214 if (req.method == 'POST') {
215 formdata = getFormValues(reqbody);
216 res.writeHead(405, resheaders);
217 res.end();
218 }
219 else if (req.method == 'GET' || req.method == 'HEAD') {
220 serveStatic(req, res, target);
221 }
222 else { /* Some other method */
223 res.writeHead(501, resheaders);
224 res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n");
225 res.end();
226 }
227 return;
228 }
229 req.on('end', respond);
230 }
231
232 process.on("exit", function () {
233 for (var sessid in sessions) {
234 if (sessions[sessid].alive)
235 sessions[sessid].term.kill('SIGHUP');
236 }
237 console.log("Quitting...");
238 return;
239 });
240
241 function wsRespond(req) {
242 var w, h, conn;
243 if (req.resourceURL.pathname == "/sock") {
244 w = parseInt(req.resourceURL.query.w);
245 if (isNaN(w) || w <= 0 || w > 256)
246 w = 80;
247 h = parseInt(req.resourceURL.query.h);
248 if (isNaN(h) || h <= 0 || h > 256)
249 h = 25;
250 conn = req.accept(null, req.origin);
251 new TermSessionWS(conn, h, w);
252 }
253 else {
254 req.reject(404, "No such resource.");
255 }
256 }
257
258 /* The pty.js module doesn't wait for the processes it spawns, so they
259 * become zombies, which leads to unpleasantness when the system runs
260 * out of process table entries. But if the child_process module is
261 * initialized and a child spawned, node will continue waiting for any
262 * children.
263 * Someday, some developer will get the bright idea of tracking how many
264 * processes the child_process module has spawned, and not waiting if
265 * it's zero. Until then, the following useless line will protect us
266 * from the zombie hordes.
267 * Figuring this out was almost as interesting as the Rogue bug where
268 * printf debugging altered whether the high score list was checked.
269 */
270 child_process.spawn("/bin/true");
271
272 process.env["TERM"] = "xterm-256color";
273 var webServer = http.createServer(handler);
274 webServer.listen(8080, "127.0.0.1");
275 console.log('Server running at http://127.0.0.1:8080/');
276 var wsServer = new webSocketServer({"httpServer": webServer});
277 wsServer.on("request", wsRespond);
278 console.log('WebSockets online');