comparison webtty.js @ 0:bd412f63ce0d

Put this project under version control, finally.
author John "Elwin" Edwards <elwin@sdf.org>
date Sun, 06 May 2012 08:45:40 -0700
parents
children 98bf7c94c954
comparison
equal deleted inserted replaced
-1:000000000000 0:bd412f63ce0d
1 #!/usr/bin/env node
2 var http = require('http');
3 var url = require('url');
4 var path = require('path');
5 var fs = require('fs');
6 //var tty = require("tty");
7 var child_process = require("child_process");
8
9 var serveStaticRoot = "/home/elwin/hk/nodejs/rlg/s/";
10 var sessions = {};
11
12 /* Constructor for TermSessions. Note that it opens the terminal and
13 * adds itself to the sessions dict.
14 */
15 function TermSession(sessid) {
16 //var pterm = tty.open("/bin/bash");
17 this.child = child_process.spawn("./ptywreck/ptyhelperC", ["bash"]);
18 var ss = this;
19 /* Eventually we'll need to make sure the sessid isn't in use yet. */
20 this.sessid = sessid;
21 //this.ptmx = pterm[0];
22 //this.child = pterm[1];
23 this.alive = true;
24 this.data = [];
25 this.child.stdout.on("data", function (buf) {
26 ss.data.push(buf);
27 });
28 this.child.stderr.on("data", function (buf) {
29 ss.data.push(buf);
30 });
31 this.child.on("exit", function (code, signal) {
32 ss.exitcode = (code != null ? code : 255);
33 ss.alive = false;
34 /* Wait for all the data to get collected */
35 setTimeout(ss.cleanup, 1000);
36 });
37 this.write = function (data) {
38 if (this.alive)
39 this.child.stdin.write(data);
40 /* Otherwise, throw some kind of exception? */
41 };
42 this.read = function () {
43 if (this.data.length == 0)
44 return null;
45 var pos = 0;
46 var i = 0;
47 for (i = 0; i < this.data.length; i++)
48 pos += this.data[i].length;
49 var nbuf = new Buffer(pos);
50 var tptr;
51 pos = 0;
52 while (this.data.length > 0) {
53 tptr = this.data.shift();
54 tptr.copy(nbuf, pos);
55 pos += tptr.length;
56 }
57 return nbuf;
58 };
59 this.close = function () {
60 if (this.alive)
61 this.child.kill('SIGHUP');
62 };
63 this.cleanup = function () {
64 /* Call this when the child is dead. */
65 if (this.alive)
66 return;
67 //ss.ptmx.destroy();
68 /* Give the client a chance to read any leftover data. */
69 if (ss.data.length > 0)
70 setTimeout(ss.remove, 8000);
71 else
72 ss.remove();
73 };
74 this.remove = function () {
75 delete sessions[ss.sessid];
76 console.log("Session " + this.sessid + " removed.");
77 };
78 sessions[sessid] = this;
79 }
80
81 function randkey() {
82 rnum = Math.floor(Math.random() * 65536 * 65536);
83 hexstr = rnum.toString(16);
84 while (hexstr.length < 8)
85 hexstr = "0" + hexstr;
86 return hexstr;
87 }
88
89 /* Returns a list of the cookies in the request, obviously. */
90 function getCookies(req) {
91 cookies = [];
92 if ("cookie" in req.headers) {
93 cookstrs = req.headers["cookie"].split("; ");
94 for (var i = 0; i < cookstrs.length; i++) {
95 eqsign = cookstrs[i].indexOf("=");
96 if (eqsign > 0) {
97 name = cookstrs[i].slice(0, eqsign).toLowerCase();
98 val = cookstrs[i].slice(eqsign + 1);
99 cookies[name] = val;
100 }
101 else if (eqsign < 0)
102 cookies[cookstrs[i]] = null;
103 }
104 }
105 return cookies;
106 }
107
108 function urlDec(encstr) {
109 var decstr = "";
110 var tnum;
111 for (var i = 0; i < encstr.length; i++)
112 {
113 if (encstr.charAt(i) == "+")
114 decstr += " ";
115 else if (encstr.charAt(i) == "%")
116 {
117 tnum = Number("0x" + encstr.slice(i + 1, 2));
118 if (!isNaN(tnum) && tnum >= 0)
119 decstr += String.fromCharCode(tnum);
120 i += 2;
121 }
122 else
123 decstr += encstr.charAt(i);
124 }
125 return decstr;
126 }
127
128 /* Returns the contents of a form */
129 function getFormValues(formtext) {
130 var pairstrs = formtext.split("&");
131 var data = {};
132 for (var i = 0; i < pairstrs.length; i++)
133 {
134 var eqsign = pairstrs[i].indexOf("=");
135 if (eqsign > 0) {
136 rawname = pairstrs[i].slice(0, eqsign);
137 rawval = pairstrs[i].slice(eqsign + 1);
138 name = urlDec(rawname);
139 val = urlDec(rawval);
140 if (!(name in data))
141 data[name] = [];
142 data[name].push(val);
143 }
144 }
145 return data;
146 }
147
148 function login(req, res, formdata) {
149 var resheaders = {'Content-Type': 'text/plain'};
150 var sessid = randkey();
151 var nsession = new TermSession(sessid);
152 resheaders["Set-Cookie"] = "ID=" + sessid;
153 res.writeHead(200, resheaders);
154 res.write("l1\n" + sessid + "\n");
155 res.end();
156 console.log("Started new session with key " + sessid + ", pid " + nsession.child.pid);
157 return;
158 }
159
160 function findTermSession(req) {
161 var cookies = getCookies(req);
162 if ("id" in cookies) {
163 var sessid = cookies["id"];
164 if (sessid in sessions) {
165 return sessions[sessid];
166 }
167 }
168 return null;
169 }
170
171 function serveStatic(req, res, fname) {
172 var nname = path.normalize(fname);
173 if (nname == "" || nname == "/")
174 nname = "index.html";
175 if (nname.match(/\/$/))
176 path.join(nname, "index.html"); /* it was a directory */
177 var realname = path.join(serveStaticRoot, nname);
178 var extension = path.extname(realname);
179 path.exists(realname, function (exists) {
180 var resheaders = {};
181 if (!exists || !extension || extension == ".html")
182 resheaders["Content-Type"] = "text/html";
183 else if (extension == ".png")
184 resheaders["Content-Type"] = "image/png";
185 else if (extension == ".css")
186 resheaders["Content-Type"] = "text/css";
187 else if (extension == ".js")
188 resheaders["Content-Type"] = "text/javascript";
189 else if (extension == ".svg")
190 resheaders["Content-Type"] = "image/svg+xml";
191 else
192 resheaders["Content-Type"] = "application/octet-stream";
193 if (exists) {
194 /* Not nice, not sensible. First see if it's readable, then respond
195 * 200 or 500. Don't throw nasty errors. */
196 res.writeHead(200, resheaders);
197 fs.readFile(realname, function (error, data) {
198 if (error) throw error;
199 res.write(data);
200 res.end();
201 });
202 }
203 else {
204 res.writeHead(404, resheaders);
205 res.write("<html><head><title>" + nname + "</title></head>\n<body><h1>" + nname + " Not Found</h1></body></html>\n");
206 res.end();
207 }
208 });
209 return;
210 }
211
212 function readFeed(res, term) {
213 res.writeHead(200, { "Content-Type": "text/plain" });
214 if (term) {
215 var result = term.read();
216 if (result == null)
217 resultstr = "";
218 else
219 resultstr = result.toString("hex");
220 res.write("d" + resultstr.length.toString() + "\n" + resultstr + "\n");
221 }
222 else {
223 //console.log("Where's the term?");
224 res.write("d0\n\n");
225 }
226 }
227
228 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data" ];
229
230 function sendError(res, ecode) {
231 res.writeHead(200, { "Content-Type": "text/plain" });
232 if (ecode < errorcodes.length && ecode > 0)
233 res.write("E" + ecode + '\n' + errorcodes[ecode] + '\n');
234 else
235 res.write("E0\nGeneric Error\n");
236 res.end();
237 }
238
239 function handler(req, res) {
240 /* default headers for the response */
241 var resheaders = {'Content-Type': 'text/html'};
242 /* The request body will be added to this as it arrives. */
243 var reqbody = "";
244 var formdata;
245
246 /* Register a listener to get the body. */
247 function moredata(chunk) {
248 reqbody += chunk;
249 }
250 req.on('data', moredata);
251
252 /* This will send the response once the whole request is here. */
253 function respond() {
254 var target = url.parse(req.url).pathname;
255 var cterm = findTermSession(req);
256 /* First figure out if the client is POSTing to a command interface. */
257 if (req.method == 'POST') {
258 formdata = getFormValues(reqbody);
259 if (target == '/feed') {
260 if (!cterm) {
261 sendError(res, 1);
262 return;
263 }
264 if (formdata["quit"] == "quit") {
265 /* The client wants to quit. */
266 // FIXME need to send a message back to the client
267 cterm.close();
268 }
269 else if (formdata["keys"]) {
270 /* process the keys */
271 hexstr = formdata["keys"][0].replace(/[^0-9a-f]/gi, "");
272 if (hexstr.length % 2 != 0) {
273 sendError(res, 2);
274 return;
275 }
276 keybuf = new Buffer(hexstr, "hex");
277 cterm.write(keybuf);
278 }
279 readFeed(res, cterm);
280 res.end();
281 }
282 else if (target == "/login") {
283 login(req, res, formdata);
284 }
285 else {
286 res.writeHead(405, resheaders);
287 res.end();
288 }
289 }
290 else if (req.method == 'GET' || req.method == 'HEAD') {
291 if (target == '/feed') {
292 if (!cterm) {
293 sendError(res, 1);
294 return;
295 }
296 readFeed(res, cterm);
297 res.end();
298 }
299 /* Default page, create a new term */
300 /* FIXME New term not created anymore, is a special case still needed? */
301 else if (target == '/') {
302 serveStatic(req, res, "/");
303 }
304 else /* Go look for it in the filesystem */
305 serveStatic(req, res, target);
306 }
307 else { /* Some other method */
308 res.writeHead(501, resheaders);
309 res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n");
310 res.end();
311 }
312 return;
313 }
314 req.on('end', respond);
315
316 }
317
318 process.on("exit", function () {
319 for (var sessid in sessions) {
320 if (sessions[sessid].alive)
321 sessions[sessid].child.kill('SIGHUP');
322 }
323 console.log("Quitting...");
324 return;
325 });
326
327 process.env["TERM"] = "xterm-256color";
328 http.createServer(handler).listen(8080, "127.0.0.1");
329 console.log('Server running at http://127.0.0.1:8080/');