Mercurial > hg > rlgwebd
comparison rlgwebd @ 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 | rlgwebd.js@5483d413a45b |
children | ea28353d620a |
comparison
equal
deleted
inserted
replaced
194:5483d413a45b | 195:3bdee6371c3f |
---|---|
1 #!/bin/sh | 1 #!/usr/bin/env node |
2 | 2 |
3 NODE_PATH=/usr/lib/node_modules | 3 var http = require('http'); |
4 LOGFILE=/var/local/rlgwebd/log | 4 var net = require('net'); |
5 CTLSOCKET=/var/run/rlgwebd.sock | 5 var url = require('url'); |
6 RLGWEBDJS=./rlgwebd.js | 6 var path = require('path'); |
7 | 7 var fs = require('fs'); |
8 export NODE_PATH | 8 var events = require('events'); |
9 | 9 var child_process = require('child_process'); |
10 if [ $UID != 0 ] | 10 // Dependencies |
11 then | 11 var posix = require("posix"); |
12 echo "$0 needs to run as root." >&2 | 12 var pty = require("pty.js"); |
13 exit 1 | 13 var WebSocketServer = require("websocket").server; |
14 fi | 14 |
15 | 15 /* Configuration variables */ |
16 if [ $# -gt 0 ] && [ $1 = stop ] | 16 // The first file is NOT in the chroot. |
17 then | 17 var ctlsocket = "/var/run/rlgwebd.sock"; |
18 socat "EXEC:echo quit" "$CTLSOCKET" | 18 var httpPort = 8080; |
19 else | 19 var chrootDir = "/var/dgl/"; |
20 # Start | 20 var dropToUser = "rodney"; |
21 setsid node "$RLGWEBDJS" </dev/null &>>$LOGFILE & | 21 var serveStaticRoot = "/var/www/"; // inside the chroot |
22 fi | 22 |
23 | 23 var clearbufs = [ |
24 exit | 24 new Buffer([27, 91, 72, 27, 91, 50, 74]), // xterm: CSI H CSI 2J |
25 | 25 new Buffer([27, 91, 72, 27, 91, 74]) // screen: CSI H CSI J |
26 ]; | |
27 | |
28 /* Data on the games available. */ | |
29 var games = { | |
30 "rogue3": { | |
31 "name": "Rogue V3", | |
32 "uname": "rogue3", | |
33 "suffix": ".r3sav", | |
34 "path": "/usr/bin/rogue3" | |
35 }, | |
36 "rogue4": { | |
37 "name": "Rogue V4", | |
38 "uname": "rogue4", | |
39 "suffix": ".r4sav", | |
40 "path": "/usr/bin/rogue4" | |
41 }, | |
42 "rogue5": { | |
43 "name": "Rogue V5", | |
44 "uname": "rogue5", | |
45 "suffix": ".r5sav", | |
46 "path": "/usr/bin/rogue5" | |
47 }, | |
48 "srogue": { | |
49 "name": "Super-Rogue", | |
50 "uname": "srogue", | |
51 "suffix": ".srsav", | |
52 "path": "/usr/bin/srogue" | |
53 }, | |
54 "arogue5": { | |
55 "name": "Advanced Rogue 5", | |
56 "uname": "arogue5", | |
57 "suffix": ".ar5sav", | |
58 "path": "/usr/bin/arogue5" | |
59 }, | |
60 "arogue7": { | |
61 "name": "Advanced Rogue 7", | |
62 "uname": "arogue7", | |
63 "suffix": ".ar7sav", | |
64 "path": "/usr/bin/arogue7" | |
65 }, | |
66 "xrogue": { | |
67 "name": "XRogue", | |
68 "uname": "xrogue", | |
69 "suffix": ".xrsav", | |
70 "path": "/usr/bin/xrogue" | |
71 } | |
72 }; | |
73 | |
74 /* Global state */ | |
75 var logins = {}; | |
76 var sessions = {}; | |
77 var dglgames = {}; | |
78 var allowlogin = true; | |
79 var gamemux = new events.EventEmitter(); | |
80 | |
81 /* A base class. TermSession and DglSession inherit from it. */ | |
82 function BaseGame() { | |
83 /* Games subclass EventEmitter, though there are few listeners. */ | |
84 events.EventEmitter.call(this); | |
85 /* Array of watching WebSockets. */ | |
86 this.watchers = []; | |
87 /* replaybuf holds the output since the last screen clear, so watchers can | |
88 * begin with a complete screen. replaylen is the number of bytes stored. */ | |
89 this.replaybuf = new Buffer(1024); | |
90 this.replaylen = 0; | |
91 /* Time of last activity. */ | |
92 this.lasttime = new Date(); | |
93 } | |
94 BaseGame.prototype = new events.EventEmitter(); | |
95 | |
96 BaseGame.prototype.tag = function () { | |
97 if (this.pname === undefined || this.gname === undefined) | |
98 return ""; | |
99 return this.gname + "/" + this.pname; | |
100 }; | |
101 | |
102 BaseGame.prototype.framepush = function(chunk) { | |
103 /* If this chunk resets the screen, discard what preceded it. */ | |
104 if (isclear(chunk)) { | |
105 this.replaybuf = new Buffer(1024); | |
106 this.replaylen = 0; | |
107 } | |
108 /* Make sure there's space. */ | |
109 while (this.replaybuf.length < chunk.length + this.replaylen) { | |
110 var nbuf = new Buffer(this.replaybuf.length * 2); | |
111 this.replaybuf.copy(nbuf, 0, 0, this.replaylen); | |
112 this.replaybuf = nbuf; | |
113 if (this.replaybuf.length > 65536) { | |
114 tslog("Warning: %s frame buffer at %d bytes", this.tag(), | |
115 this.replaybuf.length); | |
116 } | |
117 } | |
118 chunk.copy(this.replaybuf, this.replaylen); | |
119 this.replaylen += chunk.length; | |
120 }; | |
121 | |
122 /* Adds a watcher. */ | |
123 BaseGame.prototype.attach = function (wsReq) { | |
124 var conn = wsReq.accept(null, wsReq.origin); | |
125 conn.sendUTF(JSON.stringify({ | |
126 "t": "w", "w": this.w, "h": this.h, "p": this.pname, "g": this.gname | |
127 })); | |
128 conn.sendUTF(JSON.stringify({"t": "d", | |
129 "d": this.replaybuf.toString("hex", 0, this.replaylen)})); | |
130 conn.on('close', this.detach.bind(this, conn)); | |
131 this.watchers.push(conn); | |
132 }; | |
133 | |
134 BaseGame.prototype.detach = function (socket) { | |
135 var n = this.watchers.indexOf(socket); | |
136 if (n >= 0) { | |
137 this.watchers.splice(n, 1); | |
138 tslog("A WebSocket watcher has left game %s", this.tag()); | |
139 } | |
140 }; | |
141 | |
142 /* Constructor. A TermSession handles a pty and the game running on it. | |
143 * gname: (String) Name of the game to launch. | |
144 * pname: (String) The player's name. | |
145 * wsReq: (WebSocketRequest) The request from the client. | |
146 * | |
147 * Events: | |
148 * "data": Data generated by child. Parameters: buf (Buffer) | |
149 * "exit": Child terminated. Parameters: none | |
150 */ | |
151 function TermSession(gname, pname, wsReq) { | |
152 BaseGame.call(this); | |
153 /* Don't launch anything that's not a real game. */ | |
154 if (gname in games) { | |
155 this.game = games[gname]; | |
156 this.gname = gname; | |
157 } | |
158 else { | |
159 this.failed = true; | |
160 wsReq.reject(404, errorcodes[2], "No such game"); | |
161 tslog("Game %s is not available", game); | |
162 return; | |
163 } | |
164 this.pname = pname; | |
165 /* Set up the sizes. */ | |
166 this.w = Math.floor(Number(wsReq.resourceURL.query.w)); | |
167 if (!(this.w > 0 && this.w < 256)) | |
168 this.w = 80; | |
169 this.h = Math.floor(Number(wsReq.resourceURL.query.h)); | |
170 if (!(this.h > 0 && this.h < 256)) | |
171 this.h = 24; | |
172 /* Environment. */ | |
173 var childenv = {}; | |
174 for (var key in process.env) { | |
175 childenv[key] = process.env[key]; | |
176 } | |
177 var args = ["-n", this.pname]; | |
178 var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w, | |
179 "name": "xterm-256color"}; | |
180 this.term = pty.spawn(this.game.path, args, spawnopts); | |
181 tslog("%s playing %s (pid %d)", this.pname, this.gname, this.term.pid); | |
182 this.failed = false; | |
183 sessions[this.gname + "/" + this.pname] = this; | |
184 gamemux.emit('begin', this.gname, this.pname, 'rlg'); | |
185 /* Set up the lockfile and ttyrec */ | |
186 var ts = timestamp(this.lasttime); | |
187 var progressdir = path.join("/dgldir/inprogress", this.gname); | |
188 this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec"); | |
189 var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n'; | |
190 fs.writeFile(this.lock, lmsg, "utf8"); | |
191 var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, | |
192 ts + ".ttyrec"); | |
193 this.record = fs.createWriteStream(ttyrec, { mode: 0664 }); | |
194 /* The player's WebSocket and its handlers. */ | |
195 this.playerconn = wsReq.accept(null, wsReq.origin); | |
196 this.playerconn.on('message', this.input_msg.bind(this)); | |
197 this.playerconn.on('close', this.close.bind(this)); | |
198 /* Send initial data. */ | |
199 this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h, | |
200 "p": this.pname, "g": this.gname})); | |
201 /* Begin the ttyrec with some metadata, like dgamelaunch does. */ | |
202 var descstr = "\x1b[2J\x1b[1;1H\r\n"; | |
203 descstr += "Player: " + this.pname + "\r\nGame: " + this.game.name + "\r\n"; | |
204 descstr += "Server: Roguelike Gallery - rlgallery.org\r\n"; | |
205 descstr += "Filename: " + ts + ".ttyrec\r\n"; | |
206 descstr += "Time: (" + Math.floor(this.lasttime.getTime() / 1000) + ") "; | |
207 descstr += this.lasttime.toUTCString().slice(0, -4) + "\r\n"; | |
208 descstr += "Size: " + this.w + "x" + this.h + "\r\n\x1b[2J"; | |
209 this.write_ttyrec(descstr); | |
210 this.term.on("data", this.write_ttyrec.bind(this)); | |
211 this.term.on("exit", this.destroy.bind(this)); | |
212 } | |
213 TermSession.prototype = new BaseGame(); | |
214 | |
215 /* Currently this also sends to the player and any watchers. */ | |
216 TermSession.prototype.write_ttyrec = function (datastr) { | |
217 this.lasttime = new Date(); | |
218 var buf = new Buffer(datastr); | |
219 var chunk = new Buffer(buf.length + 12); | |
220 /* TTYREC headers */ | |
221 chunk.writeUInt32LE(Math.floor(this.lasttime.getTime() / 1000), 0); | |
222 chunk.writeUInt32LE(1000 * (this.lasttime.getTime() % 1000), 4); | |
223 chunk.writeUInt32LE(buf.length, 8); | |
224 buf.copy(chunk, 12); | |
225 this.record.write(chunk); | |
226 this.framepush(buf); | |
227 /* Send to the player. */ | |
228 var msg = JSON.stringify({"t": "d", "d": buf.toString("hex")}); | |
229 this.playerconn.sendUTF(msg); | |
230 /* Send to any watchers. */ | |
231 for (var i = 0; i < this.watchers.length; i++) { | |
232 if (this.watchers[i].connected) | |
233 this.watchers[i].sendUTF(msg); | |
234 } | |
235 this.emit('data', buf); | |
236 }; | |
237 | |
238 /* For writing to the subprocess's stdin. */ | |
239 TermSession.prototype.write = function (data) { | |
240 this.term.write(data); | |
241 }; | |
242 | |
243 TermSession.prototype.input_msg = function (message) { | |
244 var parsedMsg = getMsgWS(message); | |
245 if (parsedMsg.t == 'q') { | |
246 this.close(); | |
247 } | |
248 else if (parsedMsg.t == 'd') { | |
249 var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, ""); | |
250 if (hexstr.length % 2 != 0) { | |
251 hexstr = hexstr.slice(0, -1); | |
252 } | |
253 var keybuf = new Buffer(hexstr, "hex"); | |
254 this.write(keybuf); | |
255 } | |
256 }; | |
257 | |
258 /* Teardown. */ | |
259 TermSession.prototype.close = function () { | |
260 if (this.tag() in sessions) | |
261 this.term.kill('SIGHUP'); | |
262 }; | |
263 | |
264 TermSession.prototype.destroy = function () { | |
265 var tag = this.tag(); | |
266 fs.unlink(this.lock); | |
267 this.record.end(); | |
268 var watchsocks = this.watchers; | |
269 this.watchers = []; | |
270 for (var i = 0; i < watchsocks.length; i++) { | |
271 if (watchsocks[i].connected) | |
272 watchsocks[i].close(); | |
273 } | |
274 if (this.playerconn.connected) { | |
275 this.playerconn.sendUTF(JSON.stringify({"t": "q"})); | |
276 this.playerconn.close(); | |
277 } | |
278 this.emit('exit'); | |
279 gamemux.emit('end', this.gname, this.pname); | |
280 delete sessions[tag]; | |
281 tslog("Game %s ended.", tag); | |
282 }; | |
283 | |
284 function DglSession(filename) { | |
285 BaseGame.call(this); | |
286 var pathcoms = filename.split('/'); | |
287 this.gname = pathcoms[pathcoms.length - 2]; | |
288 if (!(this.gname in games)) { | |
289 this.emit('open', false); | |
290 return; | |
291 } | |
292 var basename = pathcoms[pathcoms.length - 1]; | |
293 var firstsep = basename.indexOf(':'); | |
294 this.pname = basename.slice(0, firstsep); | |
295 var fname = basename.slice(firstsep + 1); | |
296 this.ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, fname); | |
297 /* Flag to prevent multiple handlers from reading simultaneously and | |
298 * getting into a race. */ | |
299 this.reading = false; | |
300 this.rpos = 0; | |
301 fs.readFile(filename, {encoding: "utf8"}, (function (err, data) { | |
302 if (err) { | |
303 this.emit('open', false); | |
304 return; | |
305 } | |
306 var lines = data.split('\n'); | |
307 this.h = Number(lines[1]); | |
308 this.w = Number(lines[2]); | |
309 fs.open(this.ttyrec, "r", (function (err, fd) { | |
310 if (err) { | |
311 this.emit('open', false); | |
312 } | |
313 else { | |
314 this.fd = fd; | |
315 this.emit('open', true); | |
316 tslog("DGL %s: open", this.tag()); | |
317 gamemux.emit('begin', this.gname, this.pname, 'dgl'); | |
318 this.startchunk(); | |
319 this.recwatcher = fs.watch(this.ttyrec, this.notifier.bind(this)); | |
320 } | |
321 }).bind(this)); | |
322 }).bind(this)); | |
323 } | |
324 DglSession.prototype = new BaseGame(); | |
325 | |
326 /* 3 functions to get data from the ttyrec file. */ | |
327 DglSession.prototype.startchunk = function () { | |
328 if (this.reading) | |
329 return; | |
330 this.reading = true; | |
331 var header = new Buffer(12); | |
332 fs.read(this.fd, header, 0, 12, this.rpos, this.datachunk.bind(this)); | |
333 }; | |
334 | |
335 DglSession.prototype.datachunk = function (err, n, buf) { | |
336 /* Stop recursion if end of file has been reached. */ | |
337 if (err || n < 12) { | |
338 if (!err && n > 0) { | |
339 tslog("DGL %s: expected 12-byte header, got %d", this.tag(), n); | |
340 } | |
341 this.reading = false; | |
342 return; | |
343 } | |
344 this.rpos += 12; | |
345 /* Update timestamp, to within 1 second. */ | |
346 this.lasttime = new Date(1000 * buf.readUInt32LE(0)); | |
347 var datalen = buf.readUInt32LE(8); | |
348 if (datalen > 16384) { | |
349 // Something is probably wrong... | |
350 tslog("DGL %s: looking for %d bytes", this.tag(), datalen); | |
351 } | |
352 var databuf = new Buffer(datalen); | |
353 fs.read(this.fd, databuf, 0, datalen, this.rpos, this.handledata.bind(this)); | |
354 }; | |
355 | |
356 DglSession.prototype.handledata = function (err, n, buf) { | |
357 if (err || n < buf.length) { | |
358 /* Next time, read the header again. */ | |
359 this.rpos -= 12; | |
360 this.reading = false; | |
361 tslog("DGL %s: expected %d bytes, got %d", this.tag(), buf.length, n); | |
362 return; | |
363 } | |
364 this.rpos += n; | |
365 this.reading = false; | |
366 /* Process the data */ | |
367 this.framepush(buf); | |
368 var wmsg = JSON.stringify({"t": "d", "d": buf.toString("hex")}); | |
369 for (var i = 0; i < this.watchers.length; i++) { | |
370 if (this.watchers[i].connected) | |
371 this.watchers[i].sendUTF(wmsg); | |
372 } | |
373 this.emit("data", buf); | |
374 /* Recurse. */ | |
375 this.startchunk(); | |
376 }; | |
377 | |
378 /* Handles events from the ttyrec file watcher. */ | |
379 DglSession.prototype.notifier = function (ev, finame) { | |
380 if (ev == "change") | |
381 this.startchunk(); | |
382 /* If another kind of event appears, something strange happened. */ | |
383 }; | |
384 | |
385 DglSession.prototype.close = function () { | |
386 this.recwatcher.close(); | |
387 /* Ensure all data is handled before quitting. */ | |
388 this.startchunk(); | |
389 var connlist = this.watchers; | |
390 this.watchers = []; | |
391 for (var i = 0; i < connlist.length; i++) { | |
392 if (connlist[i].connected) | |
393 connlist[i].close(); | |
394 } | |
395 fs.close(this.fd); | |
396 this.emit("close"); | |
397 gamemux.emit('end', this.gname, this.pname); | |
398 tslog("DGL %s: closed", this.tag()); | |
399 }; | |
400 | |
401 function wsStartGame(wsReq) { | |
402 var playmatch = wsReq.resourceURL.pathname.match(/^\/play\/([^\/]*)$/); | |
403 if (!playmatch[1] || !(playmatch[1] in games)) { | |
404 wsReq.reject(404, errorcodes[2]); | |
405 return; | |
406 } | |
407 var gname = playmatch[1]; | |
408 if (!allowlogin) { | |
409 wsReq.reject(404, errorcodes[6]); | |
410 return; | |
411 } | |
412 if (!("key" in wsReq.resourceURL.query)) { | |
413 wsReq.reject(404, "No key given."); | |
414 return; | |
415 } | |
416 var lkey = wsReq.resourceURL.query["key"]; | |
417 if (!(lkey in logins)) { | |
418 wsReq.reject(404, errorcodes[1]); | |
419 return; | |
420 } | |
421 var pname = logins[lkey].name; | |
422 function progcallback(err, fname) { | |
423 if (fname) { | |
424 wsReq.reject(404, errorcodes[4]); | |
425 tslog("%s is already playing %s", pname, gname); | |
426 } | |
427 else { | |
428 new TermSession(gname, pname, wsReq); | |
429 } | |
430 }; | |
431 checkprogress(pname, games[gname], progcallback, []); | |
432 } | |
433 | |
434 /* Some functions which check whether a player is currently playing or | |
435 * has a saved game. Maybe someday they will provide information on | |
436 * the game. */ | |
437 function checkprogress(user, game, callback, args) { | |
438 var progressdir = path.join("/dgldir/inprogress", game.uname); | |
439 fs.readdir(progressdir, function(err, files) { | |
440 if (err) { | |
441 args.unshift(err, null); | |
442 callback.apply(null, args); | |
443 return; | |
444 } | |
445 var fre = RegExp("^" + user + ":"); | |
446 for (var i = 0; i < files.length; i++) { | |
447 if (files[i].match(fre)) { | |
448 args.unshift(null, files[i]); | |
449 callback.apply(null, args); | |
450 return; | |
451 } | |
452 } | |
453 args.unshift(null, false); | |
454 callback.apply(null, args); | |
455 }); | |
456 } | |
457 | |
458 function checksaved(user, game, callback, args) { | |
459 var savedirc = game.uname + "save"; | |
460 var basename = String(pwent.uid) + "-" + user + game.suffix; | |
461 var savefile = path.join("/var/games/roguelike", savedirc, basename); | |
462 fs.exists(savefile, function (exist) { | |
463 args.unshift(exist); | |
464 callback.apply(null, args); | |
465 }); | |
466 } | |
467 | |
468 function playerstatus(user, callback) { | |
469 var sdata = {}; | |
470 function finishp() { | |
471 for (var gname in games) { | |
472 if (!(gname in sdata)) | |
473 return; | |
474 } | |
475 callback(sdata); | |
476 } | |
477 function regsaved(exists, game) { | |
478 if (exists) | |
479 sdata[game.uname] = "s"; | |
480 else | |
481 sdata[game.uname] = "0"; | |
482 finishp(); | |
483 } | |
484 function regactive(err, filename, game) { | |
485 if (!err && filename) { | |
486 if (filename.match(/^[^:]*:node:/)) | |
487 sdata[game.uname] = "p"; | |
488 else | |
489 sdata[game.uname] = "d"; | |
490 finishp(); | |
491 } | |
492 else | |
493 checksaved(user, game, regsaved, [game]); | |
494 } | |
495 for (var gname in games) { | |
496 checkprogress(user, games[gname], regactive, [games[gname]]); | |
497 } | |
498 } | |
499 | |
500 /* A few utility functions */ | |
501 function timestamp(dd) { | |
502 if (!(dd instanceof Date)) { | |
503 dd = new Date(); | |
504 } | |
505 sd = dd.toISOString(); | |
506 sd = sd.slice(0, sd.indexOf(".")); | |
507 return sd.replace("T", "."); | |
508 } | |
509 | |
510 function randkey(words) { | |
511 if (!words || !(words > 0)) | |
512 words = 1; | |
513 function rand32() { | |
514 rnum = Math.floor(Math.random() * 65536 * 65536); | |
515 hexstr = rnum.toString(16); | |
516 while (hexstr.length < 8) | |
517 hexstr = "0" + hexstr; | |
518 return hexstr; | |
519 } | |
520 var key = ""; | |
521 for (var i = 0; i < words; i++) | |
522 key += rand32(); | |
523 return key; | |
524 } | |
525 | |
526 /* Compares two buffers, returns true for equality up to index n */ | |
527 function bufncmp(buf1, buf2, n) { | |
528 if (!Buffer.isBuffer(buf1) || !Buffer.isBuffer(buf2)) | |
529 return false; | |
530 for (var i = 0; i < n; i++) { | |
531 if (i == buf1.length && i == buf2.length) | |
532 return true; | |
533 if (i == buf1.length || i == buf2.length) | |
534 return false; | |
535 if (buf1[i] != buf2[i]) | |
536 return false; | |
537 } | |
538 return true; | |
539 } | |
540 | |
541 function isclear(buf) { | |
542 for (var i = 0; i < clearbufs.length; i++) { | |
543 if (bufncmp(buf, clearbufs[i], clearbufs[i].length)) | |
544 return true; | |
545 } | |
546 return false; | |
547 } | |
548 | |
549 function tslog() { | |
550 arguments[0] = new Date().toISOString() + ": " + String(arguments[0]); | |
551 console.log.apply(console, arguments); | |
552 } | |
553 | |
554 /* Returns a list of the cookies in the request, obviously. */ | |
555 function getCookies(req) { | |
556 cookies = []; | |
557 if ("cookie" in req.headers) { | |
558 cookstrs = req.headers["cookie"].split("; "); | |
559 for (var i = 0; i < cookstrs.length; i++) { | |
560 eqsign = cookstrs[i].indexOf("="); | |
561 if (eqsign > 0) { | |
562 name = cookstrs[i].slice(0, eqsign).toLowerCase(); | |
563 val = cookstrs[i].slice(eqsign + 1); | |
564 cookies[name] = val; | |
565 } | |
566 else if (eqsign < 0) | |
567 cookies[cookstrs[i]] = null; | |
568 } | |
569 } | |
570 return cookies; | |
571 } | |
572 | |
573 function getMsg(posttext) { | |
574 var jsonobj; | |
575 if (!posttext) | |
576 return {}; | |
577 try { | |
578 jsonobj = JSON.parse(posttext); | |
579 } | |
580 catch (e) { | |
581 if (e instanceof SyntaxError) | |
582 return {}; | |
583 } | |
584 if (typeof(jsonobj) != "object") | |
585 return {}; | |
586 return jsonobj; | |
587 } | |
588 | |
589 function getMsgWS(msgObj) { | |
590 if (msgObj.type != "utf8") | |
591 return {}; | |
592 return getMsg(msgObj.utf8Data); | |
593 } | |
594 | |
595 function login(req, res, formdata) { | |
596 if (!allowlogin) { | |
597 sendError(res, 6, null, false); | |
598 return; | |
599 } | |
600 if (!("name" in formdata)) { | |
601 sendError(res, 2, "Username not given.", false); | |
602 return; | |
603 } | |
604 else if (!("pw" in formdata)) { | |
605 sendError(res, 2, "Password not given.", false); | |
606 return; | |
607 } | |
608 var username = String(formdata["name"]); | |
609 var password = String(formdata["pw"]); | |
610 function checkit(code, signal) { | |
611 /* Checks the exit status, see sqlickrypt.c for details. */ | |
612 if (code != 0) { | |
613 sendError(res, 3); | |
614 if (code == 1) | |
615 tslog("Password check failed for user %s", username); | |
616 else if (code == 2) | |
617 tslog("Attempted login by nonexistent user %s", username); | |
618 else | |
619 tslog("Login failed: sqlickrypt error %d", code); | |
620 return; | |
621 } | |
622 var lkey = randkey(2); | |
623 while (lkey in logins) | |
624 lkey = randkey(2); | |
625 logins[lkey] = {"name": username, "ts": new Date()}; | |
626 res.writeHead(200, {'Content-Type': 'application/json'}); | |
627 var reply = {"t": "l", "k": lkey, "u": username}; | |
628 res.write(JSON.stringify(reply)); | |
629 res.end(); | |
630 tslog("%s has logged in", username); | |
631 return; | |
632 } | |
633 /* Launch the sqlickrypt utility to check the password. */ | |
634 var pwchecker = child_process.spawn("/bin/sqlickrypt", ["check"]); | |
635 pwchecker.on("exit", checkit); | |
636 pwchecker.stdin.end(username + '\n' + password + '\n', "utf8"); | |
637 return; | |
638 } | |
639 | |
640 /* Sets things up for a new user, like dgamelaunch's commands[register] */ | |
641 function regsetup(username) { | |
642 function regsetup_l2(err) { | |
643 for (var g in games) { | |
644 fs.mkdir(path.join("/dgldir/ttyrec", username, games[g].uname), 0755); | |
645 } | |
646 } | |
647 fs.mkdir(path.join("/dgldir/userdata", username), 0755); | |
648 fs.mkdir(path.join("/dgldir/ttyrec/", username), 0755, regsetup_l2); | |
649 } | |
650 | |
651 function register(req, res, formdata) { | |
652 var uname, passwd, email; | |
653 if (typeof (formdata.name) != "string" || formdata.name === "") { | |
654 sendError(res, 2, "No name given."); | |
655 return; | |
656 } | |
657 else | |
658 uname = formdata["name"]; | |
659 if (typeof (formdata.pw) != "string" || formdata.pw === "") { | |
660 sendError(res, 2, "No password given."); | |
661 return; | |
662 } | |
663 else | |
664 passwd = formdata["pw"]; | |
665 if (typeof (formdata.email) != "string" || formdata.email === "") { | |
666 /* E-mail is optional */ | |
667 email = "nobody@nowhere.not"; | |
668 } | |
669 else | |
670 email = formdata["email"]; | |
671 function checkreg(code, signal) { | |
672 if (code === 0) { | |
673 var lkey = randkey(2); | |
674 while (lkey in logins) | |
675 lkey = randkey(2); | |
676 logins[lkey] = {"name": uname, "ts": new Date()}; | |
677 var reply = {"t": "r", "k": lkey, "u": uname}; | |
678 res.writeHead(200, {'Content-Type': 'application/json'}); | |
679 res.write(JSON.stringify(reply)); | |
680 res.end(); | |
681 tslog("Added new user: %s", uname); | |
682 regsetup(uname); | |
683 } | |
684 else if (code == 4) { | |
685 sendError(res, 2, "Invalid characters in name or email."); | |
686 tslog("Attempted registration: %s %s", uname, email); | |
687 } | |
688 else if (code == 1) { | |
689 sendError(res, 2, "Username " + uname + " is already being used."); | |
690 tslog("Attempted duplicate registration: %s", uname); | |
691 } | |
692 else { | |
693 sendError(res, 0, null); | |
694 tslog("sqlickrypt register failed with code %d", code); | |
695 } | |
696 } | |
697 var child_adder = child_process.spawn("/bin/sqlickrypt", ["register"]); | |
698 child_adder.on("exit", checkreg); | |
699 child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8"); | |
700 return; | |
701 } | |
702 | |
703 /* Stops a running game if the request has the proper key. */ | |
704 function stopgame(res, formdata) { | |
705 if (!("key" in formdata) || !(formdata["key"] in logins)) { | |
706 sendError(res, 1); | |
707 return; | |
708 } | |
709 var pname = logins[formdata["key"]].name; | |
710 if (!("g" in formdata) || !(formdata["g"] in games)) { | |
711 sendError(res, 2, "No such game."); | |
712 return; | |
713 } | |
714 var gname = formdata["g"]; | |
715 function checkback(err, fname) { | |
716 if (!fname) { | |
717 sendError(res, 7); | |
718 return; | |
719 } | |
720 var fullfile = path.join("/dgldir/inprogress", gname, fname); | |
721 fs.readFile(fullfile, "utf8", function(err, fdata) { | |
722 if (err) { | |
723 sendError(res, 7); | |
724 return; | |
725 } | |
726 var pid = parseInt(fdata.split('\n')[0], 10); | |
727 try { | |
728 process.kill(pid, 'SIGHUP'); | |
729 } | |
730 catch (err) { | |
731 /* If the PID is invalid, the lockfile is stale. */ | |
732 if (err.code == "ESRCH") { | |
733 var nodere = RegExp("^" + pname + ":node:"); | |
734 if (fname.match(nodere)) { | |
735 fs.unlink(fullfile); | |
736 } | |
737 } | |
738 } | |
739 /* The response doesn't mean that the game is gone. The only way | |
740 * to make sure a dgamelaunch-supervised game is over would be to | |
741 * poll fname until it disappears. */ | |
742 res.writeHead(200, {'Content-Type': 'application/json'}); | |
743 res.write(JSON.stringify({"t": "q"})); | |
744 res.end(); | |
745 }); | |
746 } | |
747 checkprogress(pname, games[gname], checkback, []); | |
748 } | |
749 | |
750 function startProgressWatcher() { | |
751 var watchdirs = []; | |
752 for (var gname in games) { | |
753 watchdirs.push(path.join("/dgldir/inprogress", gname)); | |
754 } | |
755 var subproc = child_process.spawn("/bin/dglwatcher", watchdirs); | |
756 subproc.stdout.setEncoding('utf8'); | |
757 subproc.stdout.on('data', function (chunk) { | |
758 var fname = chunk.slice(2, -1); | |
759 var filere = /.*\/([^\/]*)\/([^\/:]*):(node:)?(.*)/; | |
760 var matchresult = fname.match(filere); | |
761 if (!matchresult || matchresult[3]) | |
762 return; | |
763 var gname = matchresult[1]; | |
764 var pname = matchresult[2]; | |
765 var tag = gname + "/" + pname; | |
766 if (chunk[0] == "E") { | |
767 tslog("DGL: %s is playing %s: %s", pname, gname, fname) | |
768 dglgames[tag] = new DglSession(fname); | |
769 } | |
770 else if (chunk[0] == "C") { | |
771 tslog("DGL: %s started playing %s: %s", pname, gname, fname) | |
772 dglgames[tag] = new DglSession(fname); | |
773 } | |
774 else if (chunk[0] == "D") { | |
775 tslog("DGL: %s finished playing %s: %s", pname, gname, fname) | |
776 dglgames[tag].close(); | |
777 delete dglgames[tag]; | |
778 } | |
779 else { | |
780 tslog("Watcher says: %s", chunk) | |
781 } | |
782 }); | |
783 subproc.stdout.resume(); | |
784 return subproc; | |
785 } | |
786 | |
787 function serveStatic(req, res, fname) { | |
788 var nname = path.normalize(fname); | |
789 if (nname == "" || nname == "/") | |
790 nname = "index.html"; | |
791 if (nname.match(/\/$/)) | |
792 path.join(nname, "index.html"); /* it was a directory */ | |
793 var realname = path.join(serveStaticRoot, nname); | |
794 var extension = path.extname(realname); | |
795 fs.exists(realname, function (exists) { | |
796 var resheaders = {}; | |
797 if (!exists || !extension || extension == ".html") | |
798 resheaders["Content-Type"] = "text/html; charset=utf-8"; | |
799 else if (extension == ".png") | |
800 resheaders["Content-Type"] = "image/png"; | |
801 else if (extension == ".css") | |
802 resheaders["Content-Type"] = "text/css"; | |
803 else if (extension == ".js") | |
804 resheaders["Content-Type"] = "text/javascript"; | |
805 else if (extension == ".svg") | |
806 resheaders["Content-Type"] = "image/svg+xml"; | |
807 else | |
808 resheaders["Content-Type"] = "application/octet-stream"; | |
809 if (exists) { | |
810 fs.readFile(realname, function (error, data) { | |
811 if (error) { | |
812 res.writeHead(500, {}); | |
813 res.end(); | |
814 } | |
815 else { | |
816 res.writeHead(200, resheaders); | |
817 if (req.method != 'HEAD') | |
818 res.write(data); | |
819 res.end(); | |
820 } | |
821 }); | |
822 } | |
823 else { | |
824 send404(res, nname, req.method == 'HEAD'); | |
825 } | |
826 }); | |
827 return; | |
828 } | |
829 | |
830 /* Currently, this doesn't do anything blocking, but keep the callback */ | |
831 function getStatus(callback) { | |
832 var now = new Date(); | |
833 var statusinfo = {"s": allowlogin, "g": []}; | |
834 for (var tag in sessions) { | |
835 var gamedesc = {"c": "rlg"}; | |
836 gamedesc["p"] = sessions[tag].pname; | |
837 gamedesc["g"] = sessions[tag].game.uname; | |
838 gamedesc["i"] = now - sessions[tag].lasttime; | |
839 gamedesc["w"] = sessions[tag].watchers.length; | |
840 statusinfo["g"].push(gamedesc); | |
841 } | |
842 for (var tag in dglgames) { | |
843 var dglinfo = {"c": "dgl"}; | |
844 var slash = tag.search('/'); | |
845 dglinfo["g"] = tag.slice(0, slash); | |
846 dglinfo["p"] = tag.slice(slash + 1); | |
847 dglinfo["i"] = now - dglgames[tag].lasttime; | |
848 dglinfo["w"] = dglgames[tag].watchers.length; | |
849 statusinfo["g"].push(dglinfo); | |
850 } | |
851 callback(statusinfo); | |
852 } | |
853 | |
854 function statusmsg(req, res) { | |
855 function respond(info) { | |
856 res.writeHead(200, { "Content-Type": "application/json" }); | |
857 if (req.method != 'HEAD') | |
858 res.write(JSON.stringify(info)); | |
859 res.end(); | |
860 } | |
861 getStatus(respond); | |
862 } | |
863 | |
864 function pstatusmsg(req, res) { | |
865 if (req.method == 'HEAD') { | |
866 res.writeHead(200, { "Content-Type": "application/json" }); | |
867 res.end(); | |
868 return; | |
869 } | |
870 var target = url.parse(req.url).pathname; | |
871 var pmatch = target.match(/^\/pstatus\/(.*)/); | |
872 if (pmatch && pmatch[1]) | |
873 var pname = pmatch[1]; | |
874 else { | |
875 sendError(res, 2, "No name given."); | |
876 return; | |
877 } | |
878 var reply = {"name": pname}; | |
879 playerstatus(pname, function (pdata) { | |
880 reply["stat"] = pdata; | |
881 res.writeHead(200, { "Content-Type": "application/json" }); | |
882 res.write(JSON.stringify(reply)); | |
883 res.end(); | |
884 }); | |
885 } | |
886 | |
887 function getuinfo(req, res) { | |
888 var urlobj = url.parse(req.url, true); | |
889 var query = urlobj.query; | |
890 if (!("key" in query) || !(query["key"] in logins)) { | |
891 sendError(res, 1); | |
892 return; | |
893 } | |
894 var match = urlobj.pathname.match(/^\/[^\/]*\/(.*)/); | |
895 if (!match || !match[1]) { | |
896 send404(res, urlobj.pathname, req.method == 'HEAD'); | |
897 return; | |
898 } | |
899 var which = match[1]; | |
900 var name = logins[query["key"]].name; | |
901 var reply = { "u": name }; | |
902 function send() { | |
903 res.writeHead(200, { "Content-Type": "application/json" }); | |
904 res.write(JSON.stringify(reply)); | |
905 res.end(); | |
906 } | |
907 if (which == "pw") { | |
908 /* Don't actually divulge passwords. */ | |
909 reply["pw"] = ""; | |
910 send(); | |
911 } | |
912 else if (which == "email") { | |
913 var address; | |
914 function finish(code, signal) { | |
915 if (code != 0) { | |
916 tslog("sqlickrypt: %d with name %s", code, name); | |
917 sendError(res, 2); | |
918 } | |
919 else { | |
920 reply["email"] = address; | |
921 send(); | |
922 } | |
923 } | |
924 var subproc = child_process.spawn("/bin/sqlickrypt", ["getmail"]); | |
925 subproc.stdout.on("data", function (data) { | |
926 address = data.toString().replace(/\n/g, ""); | |
927 }); | |
928 subproc.on("exit", finish); | |
929 subproc.stdin.end(name + '\n', "utf8"); | |
930 } | |
931 else { | |
932 send404(res, urlobj.pathname, req.method == 'HEAD'); | |
933 return; | |
934 } | |
935 } | |
936 | |
937 function setuinfo(req, res, postdata) { | |
938 var urlobj = url.parse(req.url, true); | |
939 var query = urlobj.query; | |
940 if (!("key" in query) || !(query["key"] in logins)) { | |
941 sendError(res, 1); | |
942 return; | |
943 } | |
944 var name = logins[query["key"]].name; | |
945 var match = urlobj.pathname.match(/^\/[^\/]*\/(.*)/); | |
946 if (!match || !match[1]) { | |
947 send404(res, urlobj.pathname, true); | |
948 return; | |
949 } | |
950 var which = match[1]; | |
951 if (!("v" in postdata)) { | |
952 sendError(res, 2, "No value provided"); | |
953 return; | |
954 } | |
955 if (which == "email" || which == "pw") { | |
956 var args; | |
957 if (which == "email") | |
958 args = ["setmail"]; | |
959 else | |
960 args = ["setpw"]; | |
961 var child = child_process.execFile("/bin/sqlickrypt", args, | |
962 function (err, stdout, stderr) { | |
963 if (err) { | |
964 tslog("Could not set %s: sqlickrypt error %d", which, err.code); | |
965 sendError(res, 2); | |
966 } | |
967 else { | |
968 tslog("User %s has changed %s", name, which); | |
969 res.writeHead(200, { "Content-Type": "application/json" }); | |
970 res.end(JSON.stringify({"t": "t"})); | |
971 } | |
972 }); | |
973 child.stdin.end(name + "\n" + postdata.v + "\n", "utf8"); | |
974 } | |
975 else { | |
976 send404(res, urlobj.pathname, true); | |
977 } | |
978 } | |
979 | |
980 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data", | |
981 "Login failed", "Already playing", "Game launch failed", | |
982 "Server shutting down", "Game not in progress" ]; | |
983 | |
984 function sendError(res, ecode, msg, box) { | |
985 res.writeHead(200, { "Content-Type": "application/json" }); | |
986 var edict = {"t": "E"}; | |
987 if (!(ecode < errorcodes.length && ecode > 0)) | |
988 ecode = 0; | |
989 edict["c"] = ecode; | |
990 edict["s"] = errorcodes[ecode]; | |
991 if (msg) | |
992 edict["s"] += ": " + msg; | |
993 if (box) | |
994 res.write(JSON.stringify([edict])); | |
995 else | |
996 res.write(JSON.stringify(edict)); | |
997 res.end(); | |
998 } | |
999 | |
1000 function send404(res, path, nopage) { | |
1001 res.writeHead(404, {"Content-Type": "text/html; charset=utf-8"}); | |
1002 if (!nopage) { | |
1003 res.write("<html><head><title>" + path + "</title></head>\n<body><h1>" | |
1004 + path + " Not Found</h1></body></html>\n"); | |
1005 } | |
1006 res.end(); | |
1007 } | |
1008 | |
1009 function webHandler(req, res) { | |
1010 /* default headers for the response */ | |
1011 var resheaders = {'Content-Type': 'text/html'}; | |
1012 /* The request body will be added to this as it arrives. */ | |
1013 var reqbody = ""; | |
1014 var formdata; | |
1015 | |
1016 /* Register a listener to get the body. */ | |
1017 function moredata(chunk) { | |
1018 reqbody += chunk; | |
1019 } | |
1020 req.on('data', moredata); | |
1021 | |
1022 /* This will send the response once the whole request is here. */ | |
1023 function respond() { | |
1024 formdata = getMsg(reqbody); | |
1025 var target = url.parse(req.url).pathname; | |
1026 /* First figure out if the client is POSTing to a command interface. */ | |
1027 if (req.method == 'POST') { | |
1028 if (target == "/login") { | |
1029 login(req, res, formdata); | |
1030 } | |
1031 else if (target == "/addacct") { | |
1032 register(req, res, formdata); | |
1033 } | |
1034 else if (target == "/quit") { | |
1035 stopgame(res, formdata); | |
1036 } | |
1037 else if (target.match(/^\/uinfo\//)) { | |
1038 setuinfo(req, res, formdata); | |
1039 } | |
1040 else { | |
1041 res.writeHead(405, resheaders); | |
1042 res.end(); | |
1043 } | |
1044 } | |
1045 else if (req.method == 'GET' || req.method == 'HEAD') { | |
1046 if (target == '/status') { | |
1047 statusmsg(req, res); | |
1048 } | |
1049 else if (target.match(/^\/uinfo\//)) { | |
1050 getuinfo(req, res); | |
1051 } | |
1052 else if (target.match(/^\/pstatus\//)) { | |
1053 pstatusmsg(req, res); | |
1054 } | |
1055 else /* Go look for it in the filesystem */ | |
1056 serveStatic(req, res, target); | |
1057 } | |
1058 else { /* Some other method */ | |
1059 res.writeHead(501, resheaders); | |
1060 res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n"); | |
1061 res.end(); | |
1062 } | |
1063 return; | |
1064 } | |
1065 req.on('end', respond); | |
1066 } | |
1067 | |
1068 function wsHandler(wsRequest) { | |
1069 var watchmatch = wsRequest.resource.match(/^\/watch\/(.*)$/); | |
1070 var playmatch = wsRequest.resource.match(/^\/play\//); | |
1071 if (watchmatch !== null) { | |
1072 if (watchmatch[1] in sessions) { | |
1073 var tsession = sessions[watchmatch[1]]; | |
1074 tsession.attach(wsRequest); | |
1075 tslog("Game %s is being watched via WebSockets", tsession.tag()); | |
1076 } | |
1077 else if (watchmatch[1] in dglgames) { | |
1078 var dsession = dglgames[watchmatch[1]]; | |
1079 dsession.attach(wsRequest); | |
1080 tslog("DGL game %s is being watched via WebSockets", dsession.tag()); | |
1081 } | |
1082 else { | |
1083 wsRequest.reject(404, errorcodes[7]); | |
1084 return; | |
1085 } | |
1086 } | |
1087 else if (playmatch !== null) { | |
1088 wsStartGame(wsRequest); | |
1089 } | |
1090 else if (wsRequest.resourceURL.pathname == "/status") { | |
1091 var conn = wsRequest.accept(null, wsRequest.origin); | |
1092 var tell = function () { | |
1093 getStatus(function (info) { | |
1094 info["t"] = "t"; | |
1095 conn.sendUTF(JSON.stringify(info)); | |
1096 }); | |
1097 } | |
1098 var beginH = function (gname, pname, client) { | |
1099 conn.sendUTF(JSON.stringify({"t": "b", "c": client, "p": pname, | |
1100 "g": gname})); | |
1101 }; | |
1102 var listH = function (list) { | |
1103 conn.sendUTF(JSON.stringify(list)); | |
1104 }; | |
1105 var endH = function (gname, pname) { | |
1106 conn.sendUTF(JSON.stringify({"t": "e", "p": pname, "g": gname})); | |
1107 }; | |
1108 gamemux.on('begin', beginH); | |
1109 gamemux.on('list', listH); | |
1110 gamemux.on('end', endH); | |
1111 conn.on('message', tell); | |
1112 conn.on('close', function () { | |
1113 gamemux.removeListener('begin', beginH); | |
1114 gamemux.removeListener('list', listH); | |
1115 gamemux.removeListener('end', endH); | |
1116 }); | |
1117 tell(); | |
1118 } | |
1119 else | |
1120 wsRequest.reject(404, "No such resource."); | |
1121 } | |
1122 | |
1123 /* Only games with low idle time are included. Use getStatus() for the | |
1124 * complete list. */ | |
1125 function pushStatus() { | |
1126 var now = new Date(); | |
1127 var statusinfo = {"t": "p", "s": allowlogin, "g": []}; | |
1128 for (var tag in sessions) { | |
1129 var delta = now - sessions[tag].lasttime; | |
1130 if (delta < 60000) { | |
1131 var gamedesc = {"c": "rlg"}; | |
1132 gamedesc["p"] = sessions[tag].pname; | |
1133 gamedesc["g"] = sessions[tag].game.uname; | |
1134 gamedesc["i"] = delta; | |
1135 gamedesc["w"] = sessions[tag].watchers.length; | |
1136 statusinfo["g"].push(gamedesc); | |
1137 } | |
1138 } | |
1139 for (var tag in dglgames) { | |
1140 var delta = now - dglgames[tag].lasttime; | |
1141 if (delta < 60000) { | |
1142 var dglinfo = {"c": "dgl"}; | |
1143 var slash = tag.search('/'); | |
1144 dglinfo["g"] = tag.slice(0, slash); | |
1145 dglinfo["p"] = tag.slice(slash + 1); | |
1146 dglinfo["i"] = delta; | |
1147 dglinfo["w"] = dglgames[tag].watchers.length; | |
1148 statusinfo["g"].push(dglinfo); | |
1149 } | |
1150 } | |
1151 gamemux.emit('list', statusinfo); | |
1152 } | |
1153 | |
1154 function shutdown () { | |
1155 httpServer.close(); | |
1156 httpServer.removeAllListeners('request'); | |
1157 ctlServer.close(); | |
1158 tslog("Shutting down..."); | |
1159 process.exit(); | |
1160 } | |
1161 | |
1162 function consoleHandler(chunk) { | |
1163 var msg = chunk.toString().split('\n')[0]; | |
1164 if (msg == "quit") { | |
1165 allowlogin = false; | |
1166 tslog("Disconnecting..."); | |
1167 for (var tag in sessions) { | |
1168 sessions[tag].close(); | |
1169 } | |
1170 progressWatcher.stdin.end("\n"); | |
1171 setTimeout(shutdown, 2000); | |
1172 } | |
1173 } | |
1174 | |
1175 process.on("exit", function () { | |
1176 for (var tag in sessions) { | |
1177 sessions[tag].term.kill('SIGHUP'); | |
1178 } | |
1179 tslog("Quitting..."); | |
1180 return; | |
1181 }); | |
1182 | |
1183 /* Initialization STARTS HERE */ | |
1184 process.env["TERM"] = "xterm-256color"; | |
1185 | |
1186 if (process.getuid() != 0) { | |
1187 tslog("Not running as root, cannot chroot."); | |
1188 process.exit(1); | |
1189 } | |
1190 | |
1191 var httpServer; // declare here so shutdown() can find it | |
1192 var wsServer; | |
1193 var progressWatcher; | |
1194 | |
1195 var pwent; | |
1196 try { | |
1197 pwent = posix.getpwnam(dropToUser); | |
1198 } | |
1199 catch (err) { | |
1200 tslog("Could not drop to user %s: user does not exist", dropToUser); | |
1201 process.exit(1); | |
1202 } | |
1203 | |
1204 /* This could be nonblocking, but nothing else can start yet anyway. */ | |
1205 if (fs.existsSync(ctlsocket)) { | |
1206 fs.unlinkSync(ctlsocket); | |
1207 } | |
1208 | |
1209 /* Open the control socket before chrooting where it can't be found */ | |
1210 var ctlServer = net.createServer(function (sock) { | |
1211 sock.on('data', consoleHandler); | |
1212 }); | |
1213 ctlServer.listen(ctlsocket, function () { | |
1214 /* rlgwebd.js now assumes that it has been started by the rlgwebd shell | |
1215 * script, or some other method that detaches it and sets up stdio. */ | |
1216 /* chroot and drop permissions. posix.chroot() does chdir() itself. */ | |
1217 try { | |
1218 posix.chroot(chrootDir); | |
1219 } | |
1220 catch (err) { | |
1221 tslog("chroot to %s failed: %s", chrootDir, err); | |
1222 process.exit(1); | |
1223 } | |
1224 try { | |
1225 // drop gid first, that requires UID=0 | |
1226 process.setgid(pwent.gid); | |
1227 process.setuid(pwent.uid); | |
1228 } | |
1229 catch (err) { | |
1230 tslog("Could not drop permissions: %s", err); | |
1231 process.exit(1); | |
1232 } | |
1233 httpServer = http.createServer(webHandler); | |
1234 httpServer.listen(httpPort); | |
1235 tslog('rlgwebd running on port %d', httpPort); | |
1236 wsServer = new WebSocketServer({"httpServer": httpServer}); | |
1237 wsServer.on("request", wsHandler); | |
1238 tslog('WebSockets are online'); | |
1239 progressWatcher = startProgressWatcher(); | |
1240 setInterval(pushStatus, 40000); | |
1241 }); | |
1242 |