Mercurial > hg > rlgwebd
comparison rlgwebd.js @ 16:ef6127ed6da3
RLGWeb: switch to JSON protocol.
Port the JSON communication from WebTTY to RLGWeb. Fixing out-of-order
messages is still not implemented on the server side. Terminal size is
still hard-coded. Unused code is still lying around.
author | John "Elwin" Edwards <elwin@sdf.org> |
---|---|
date | Thu, 17 May 2012 09:32:19 -0700 |
parents | ad0a31e52007 |
children | d3e3d6b4016b |
comparison
equal
deleted
inserted
replaced
15:7466927c17a5 | 16:ef6127ed6da3 |
---|---|
41 | 41 |
42 /* Constructor for TermSessions. Note that it opens the terminal and | 42 /* Constructor for TermSessions. Note that it opens the terminal and |
43 * adds itself to the sessions dict. It currently assumes the user has | 43 * adds itself to the sessions dict. It currently assumes the user has |
44 * been authenticated. | 44 * been authenticated. |
45 */ | 45 */ |
46 function TermSession(game, user, files) { | 46 function TermSession(game, user, files, dims) { |
47 /* First make sure starting the game will work. */ | 47 /* First make sure starting the game will work. */ |
48 if (!(game in games)) { | 48 if (!(game in games)) { |
49 // TODO: throw an exception instead | 49 // TODO: throw an exception instead |
50 return null; | 50 return null; |
51 } | 51 } |
55 while (this.sessid in sessions) { | 55 while (this.sessid in sessions) { |
56 this.sessid = randkey(); | 56 this.sessid = randkey(); |
57 } | 57 } |
58 /* Grab a spot in the sessions table. */ | 58 /* Grab a spot in the sessions table. */ |
59 sessions[this.sessid] = this; | 59 sessions[this.sessid] = this; |
60 /* State for messaging. */ | |
61 this.nsend = 0; | |
62 this.nrecv = 0; | |
63 this.msgQ = [] | |
64 /* Set up the sizes. */ | |
65 this.w = Math.floor(Number(dims[1])); | |
66 if (!(this.w > 0 && this.w < 256)) | |
67 this.w = 80; | |
68 this.h = Math.floor(Number(dims[0])); | |
69 if (!(this.h > 0 && this.h < 256)) | |
70 this.h = 24; | |
71 /* Environment. */ | |
72 var childenv = {}; | |
73 for (var key in process.env) { | |
74 childenv[key] = process.env[key]; | |
75 } | |
76 childenv["PTYHELPER"] = String(this.h) + "x" + String(this.w); | |
60 /* TODO handle tty-opening errors */ | 77 /* TODO handle tty-opening errors */ |
61 /* TODO make argument-finding into a method */ | 78 /* TODO make argument-finding into a method */ |
62 args = [games[game].path, "-n", user.toString()]; | 79 args = [games[game].path, "-n", user.toString()]; |
63 this.child = child_process.spawn("/bin/ptyhelper", args); | 80 this.child = child_process.spawn("/bin/ptyhelper", args, {"env": childenv}); |
64 var ss = this; | 81 var ss = this; |
65 this.alive = true; | 82 this.alive = true; |
66 this.data = []; | 83 this.data = []; |
67 this.lock = files[0]; | 84 this.lock = files[0]; |
68 fs.writeFile(this.lock, this.child.pid.toString() + '\n80\n24\n', "utf8"); | 85 fs.writeFile(this.lock, this.child.pid.toString() + '\n' + this.w + '\n' + |
86 this.h + '\n', "utf8"); | |
69 this.record = fs.createWriteStream(files[1], { mode: 0664 }); | 87 this.record = fs.createWriteStream(files[1], { mode: 0664 }); |
70 /* END setup */ | 88 /* END setup */ |
71 function ttyrec_chunk(buf) { | 89 function ttyrec_chunk(buf) { |
72 var ts = new Date(); | 90 var ts = new Date(); |
73 var chunk = new Buffer(buf.length + 12); | 91 var chunk = new Buffer(buf.length + 12); |
204 } | 222 } |
205 } | 223 } |
206 return data; | 224 return data; |
207 } | 225 } |
208 | 226 |
227 function getMsg(posttext) { | |
228 var jsonobj; | |
229 if (!posttext) | |
230 return {}; | |
231 try { | |
232 jsonobj = JSON.parse(posttext); | |
233 } | |
234 catch (e) { | |
235 if (e instanceof SyntaxError) | |
236 return {}; | |
237 } | |
238 if (typeof(jsonobj) != "object") | |
239 return {}; | |
240 return jsonobj; | |
241 } | |
242 | |
209 function auth(username, password) { | 243 function auth(username, password) { |
210 // Real authentication not implemented | 244 // Real authentication not implemented |
211 return true; | 245 return true; |
212 } | 246 } |
213 | 247 |
222 } | 256 } |
223 else if (!("pw" in formdata)) { | 257 else if (!("pw" in formdata)) { |
224 sendError(res, 2, "Password not given."); | 258 sendError(res, 2, "Password not given."); |
225 return; | 259 return; |
226 } | 260 } |
227 var username = formdata["name"][0]; | 261 var username = formdata["name"]; |
228 var password = formdata["pw"][0]; | 262 var password = formdata["pw"]; |
229 var gname = formdata["game"][0]; | 263 var gname = formdata["game"]; |
264 var dims = [formdata["h"], formdata["w"]]; | |
230 if (!(gname in games)) { | 265 if (!(gname in games)) { |
231 sendError(res, 2, "No such game: " + gname); | 266 sendError(res, 2, "No such game: " + gname); |
232 console.log("Request for nonexistant game \"" + gname + "\""); | 267 console.log("Request for nonexistant game \"" + gname + "\""); |
233 return; | 268 return; |
234 } | 269 } |
237 // This sets up the game once starting is approved. | 272 // This sets up the game once starting is approved. |
238 function startgame() { | 273 function startgame() { |
239 var ts = timestamp(); | 274 var ts = timestamp(); |
240 var lockfile = path.join(progressdir, username + ":node:" + ts + ".ttyrec"); | 275 var lockfile = path.join(progressdir, username + ":node:" + ts + ".ttyrec"); |
241 var ttyrec = path.join("/dgldir/ttyrec", username, gname, ts + ".ttyrec"); | 276 var ttyrec = path.join("/dgldir/ttyrec", username, gname, ts + ".ttyrec"); |
242 var nsession = new TermSession(gname, username, [lockfile, ttyrec]); | 277 var nsession = new TermSession(gname, username, [lockfile, ttyrec], dims); |
243 if (nsession) { | 278 if (nsession) { |
244 /* Technically there's a race condition for the "lock"file, but since | 279 /* Technically there's a race condition for the "lock"file, but since |
245 * it requires the user deliberately starting two games at similar times, | 280 * it requires the user deliberately starting two games at similar times, |
246 * it's not too serious. We can't get O_EXCL in Node anyway. */ | 281 * it's not too serious. We can't get O_EXCL in Node anyway. */ |
247 res.writeHead(200, {'Content-Type': 'text/plain'}); | 282 res.writeHead(200, {'Content-Type': 'text/plain'}); |
248 res.write("l1\n" + nsession.sessid + "\n"); | 283 var reply = {"t": "l", "id": nsession.sessid, "w": nsession.w, "h": |
284 nsession.h}; | |
285 res.write(JSON.stringify(reply)); | |
249 res.end(); | 286 res.end(); |
250 console.log("%s playing %s (key %s, pid %d)", username, gname, | 287 console.log("%s playing %s (key %s, pid %d)", username, gname, |
251 nsession.sessid, nsession.child.pid); | 288 nsession.sessid, nsession.child.pid); |
252 } | 289 } |
253 else { | 290 else { |
307 return; | 344 return; |
308 } | 345 } |
309 cterm.close(); | 346 cterm.close(); |
310 var resheaders = {'Content-Type': 'text/plain'}; | 347 var resheaders = {'Content-Type': 'text/plain'}; |
311 res.writeHead(200, resheaders); | 348 res.writeHead(200, resheaders); |
312 res.write("q1\n\n"); | 349 res.write(JSON.stringify({"t": "q"})); |
313 res.end(); | 350 res.end(); |
314 return; | 351 return; |
315 } | 352 } |
316 | 353 |
317 function findTermSession(formdata) { | 354 function findTermSession(formdata) { |
355 if (typeof(formdata) != "object") | |
356 return null; | |
318 if ("id" in formdata) { | 357 if ("id" in formdata) { |
319 var sessid = formdata["id"][0]; | 358 var sessid = formdata["id"]; |
320 if (sessid in sessions) { | 359 if (sessid in sessions) { |
321 return sessions[sessid]; | 360 return sessions[sessid]; |
322 } | 361 } |
323 } | 362 } |
324 return null; | 363 return null; |
368 return; | 407 return; |
369 } | 408 } |
370 | 409 |
371 function readFeed(res, term) { | 410 function readFeed(res, term) { |
372 if (term) { | 411 if (term) { |
412 var reply = {}; | |
373 var result = term.read(); | 413 var result = term.read(); |
414 if (result == null) { | |
415 if (term.alive) | |
416 reply.t = "n"; | |
417 else | |
418 reply.t = "q"; | |
419 } | |
420 else { | |
421 reply.t = "d"; | |
422 reply.n = term.nsend++; | |
423 reply.d = result.toString("hex"); | |
424 } | |
374 res.writeHead(200, { "Content-Type": "text/plain" }); | 425 res.writeHead(200, { "Content-Type": "text/plain" }); |
375 if (result == null) | 426 res.write(JSON.stringify(reply)); |
376 resultstr = ""; | |
377 else | |
378 resultstr = result.toString("hex"); | |
379 if (result == null && !term.alive) { | |
380 /* Child has terminated and data is flushed. */ | |
381 res.write("q1\n\n"); | |
382 } | |
383 else | |
384 res.write("d" + resultstr.length.toString() + "\n" + resultstr + "\n"); | |
385 res.end(); | 427 res.end(); |
386 } | 428 } |
387 else { | 429 else { |
388 //console.log("Where's the term?"); | |
389 sendError(res, 1, null); | 430 sendError(res, 1, null); |
390 } | 431 } |
391 } | 432 } |
392 | 433 |
393 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data", | 434 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data", |
394 "Login failed", "Already playing", "Game launch failed" ]; | 435 "Login failed", "Already playing", "Game launch failed" ]; |
395 | 436 |
396 function sendError(res, ecode, msg) { | 437 function sendError(res, ecode, msg) { |
397 res.writeHead(200, { "Content-Type": "text/plain" }); | 438 res.writeHead(200, { "Content-Type": "text/plain" }); |
398 if (ecode < errorcodes.length && ecode > 0) { | 439 var edict = {"t": "E"}; |
399 var emsg = errorcodes[ecode]; | 440 if (!(ecode < errorcodes.length && ecode > 0)) |
400 if (msg) | 441 ecode = 0; |
401 emsg += ": " + msg; | 442 edict["c"] = ecode; |
402 res.write("E" + ecode + '\n' + emsg + '\n'); | 443 edict["s"] = errorcodes[ecode]; |
403 } | 444 if (msg) |
404 else | 445 edict["s"] += ": " + msg; |
405 res.write("E0\nGeneric Error\n"); | 446 res.write(JSON.stringify(edict)); |
406 res.end(); | 447 res.end(); |
407 } | 448 } |
408 | 449 |
409 function handler(req, res) { | 450 function handler(req, res) { |
410 /* default headers for the response */ | 451 /* default headers for the response */ |
419 } | 460 } |
420 req.on('data', moredata); | 461 req.on('data', moredata); |
421 | 462 |
422 /* This will send the response once the whole request is here. */ | 463 /* This will send the response once the whole request is here. */ |
423 function respond() { | 464 function respond() { |
424 formdata = getFormValues(reqbody); | 465 //formdata = getFormValues(reqbody); |
466 formdata = getMsg(reqbody); | |
425 var target = url.parse(req.url).pathname; | 467 var target = url.parse(req.url).pathname; |
426 var cterm = findTermSession(formdata); | 468 var cterm = findTermSession(formdata); |
427 /* First figure out if the client is POSTing to a command interface. */ | 469 /* First figure out if the client is POSTing to a command interface. */ |
428 if (req.method == 'POST') { | 470 if (req.method == 'POST') { |
429 if (target == '/feed') { | 471 if (target == '/feed') { |
430 if (!cterm) { | 472 if (!cterm) { |
431 sendError(res, 1, null); | 473 sendError(res, 1, null); |
432 return; | 474 return; |
433 } | 475 } |
434 if ("quit" in formdata) { | 476 if (formdata.t == "q") { |
435 /* The client wants to terminate the process. */ | 477 /* The client wants to terminate the process. */ |
436 logout(cterm, res); | 478 logout(cterm, res); |
437 } | 479 } |
438 else if (formdata["keys"]) { | 480 else if (formdata.t == "d" && typeof(formdata.d) == "string") { |
439 /* process the keys */ | 481 /* process the keys */ |
440 hexstr = formdata["keys"][0].replace(/[^0-9a-f]/gi, ""); | 482 hexstr = formdata.d.replace(/[^0-9a-f]/gi, ""); |
441 if (hexstr.length % 2 != 0) { | 483 if (hexstr.length % 2 != 0) { |
442 sendError(res, 2, "incomplete byte"); | 484 sendError(res, 2, "incomplete byte"); |
443 return; | 485 return; |
444 } | 486 } |
445 keybuf = new Buffer(hexstr, "hex"); | 487 keybuf = new Buffer(hexstr, "hex"); |
488 /* TODO OoO correction */ | |
446 cterm.write(keybuf); | 489 cterm.write(keybuf); |
447 } | 490 } |
448 readFeed(res, cterm); | 491 readFeed(res, cterm); |
449 } | 492 } |
450 else if (target == "/login") { | 493 else if (target == "/login") { |