Wrap console.log() with the tslog() function, which prepends timestamps. Clean up some of the messages as well.
679 lines
19 KiB
JavaScript
Executable file
679 lines
19 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
// If you can't quite trust node to find it on its own
|
|
var localModules = '/usr/local/lib/node_modules/';
|
|
var http = require('http');
|
|
var url = require('url');
|
|
var path = require('path');
|
|
var fs = require('fs');
|
|
var child_process = require('child_process');
|
|
var daemon = require(path.join(localModules, "daemon"));
|
|
|
|
var chrootDir = "/var/dgl/";
|
|
var dropToUID = 501;
|
|
var dropToGID = 501;
|
|
var serveStaticRoot = "/var/www/"; // inside the chroot
|
|
var passwdfile = "/dgldir/dgl-login";
|
|
var sessions = {};
|
|
|
|
var games = {
|
|
"rogue3": {
|
|
"name": "Rogue V3",
|
|
"uname": "rogue3",
|
|
"path": "/bin/rogue3"
|
|
},
|
|
"rogue4": {
|
|
"name": "Rogue V4",
|
|
"uname": "rogue4",
|
|
"path": "/bin/rogue4"
|
|
},
|
|
"rogue5": {
|
|
"name": "Rogue V5",
|
|
"uname": "rogue5",
|
|
"path": "/bin/rogue5"
|
|
},
|
|
"srogue": {
|
|
"name": "Super-Rogue",
|
|
"uname": "srogue",
|
|
"path": "/bin/srogue"
|
|
}
|
|
};
|
|
|
|
/* Constructor for TermSessions. Note that it opens the terminal and
|
|
* adds itself to the sessions dict. It currently assumes the user has
|
|
* been authenticated.
|
|
*/
|
|
function TermSession(game, user, files, dims) {
|
|
/* First make sure starting the game will work. */
|
|
if (!(game in games)) {
|
|
// TODO: throw an exception instead
|
|
return null;
|
|
}
|
|
/* This order seems to best avoid race conditions... */
|
|
this.alive = false;
|
|
this.sessid = randkey();
|
|
while (this.sessid in sessions) {
|
|
this.sessid = randkey();
|
|
}
|
|
/* Grab a spot in the sessions table. */
|
|
sessions[this.sessid] = this;
|
|
/* State for messaging. */
|
|
this.nsend = 0;
|
|
this.nrecv = 0;
|
|
this.msgQ = []
|
|
this.Qtimeout = null;
|
|
/* Set up the sizes. */
|
|
this.w = Math.floor(Number(dims[1]));
|
|
if (!(this.w > 0 && this.w < 256))
|
|
this.w = 80;
|
|
this.h = Math.floor(Number(dims[0]));
|
|
if (!(this.h > 0 && this.h < 256))
|
|
this.h = 24;
|
|
/* Environment. */
|
|
var childenv = {};
|
|
for (var key in process.env) {
|
|
childenv[key] = process.env[key];
|
|
}
|
|
childenv["PTYHELPER"] = String(this.h) + "x" + String(this.w);
|
|
/* TODO handle tty-opening errors */
|
|
/* TODO make argument-finding into a method */
|
|
args = [games[game].path, "-n", user.toString()];
|
|
this.child = child_process.spawn("/bin/ptyhelper", args, {"env": childenv});
|
|
var ss = this;
|
|
this.alive = true;
|
|
this.data = [];
|
|
this.lock = files[0];
|
|
fs.writeFile(this.lock, this.child.pid.toString() + '\n' + this.w + '\n' +
|
|
this.h + '\n', "utf8");
|
|
this.record = fs.createWriteStream(files[1], { mode: 0664 });
|
|
/* END setup */
|
|
function ttyrec_chunk(buf) {
|
|
var ts = new Date();
|
|
var chunk = new Buffer(buf.length + 12);
|
|
/* TTYREC headers */
|
|
chunk.writeUInt32LE(Math.floor(ts.getTime() / 1000), 0);
|
|
chunk.writeUInt32LE(1000 * (ts.getTime() % 1000), 4);
|
|
chunk.writeUInt32LE(buf.length, 8);
|
|
buf.copy(chunk, 12);
|
|
ss.data.push(chunk);
|
|
ss.record.write(chunk);
|
|
}
|
|
this.child.stdout.on("data", ttyrec_chunk);
|
|
this.child.stderr.on("data", ttyrec_chunk);
|
|
this.child.on("exit", function (code, signal) {
|
|
ss.exitcode = (code != null ? code : 255);
|
|
ss.alive = false;
|
|
fs.unlink(ss.lock);
|
|
/* Wait for all the data to get collected */
|
|
setTimeout(ss.cleanup, 1000);
|
|
});
|
|
this.write = function (data, n) {
|
|
if (!this.alive || typeof (n) != "number") {
|
|
return;
|
|
}
|
|
//console.log("Got message " + n);
|
|
var oindex = n - this.nrecv;
|
|
if (oindex === 0) {
|
|
//console.log("Writing message " + n);
|
|
this.child.stdin.write(data);
|
|
this.nrecv++;
|
|
var next;
|
|
while ((next = this.msgQ.shift()) !== undefined) {
|
|
//console.log("Writing message " + this.nrecv);
|
|
this.child.stdin.write(next);
|
|
this.nrecv++;
|
|
}
|
|
if (this.msgQ.length == 0 && this.Qtimeout) {
|
|
clearTimeout(this.Qtimeout);
|
|
this.Qtimeout = null;
|
|
}
|
|
}
|
|
else if (oindex > 0 && oindex <= 1024) {
|
|
tslog("Stashing message %d at %d", n, oindex - 1);
|
|
this.msgQ[oindex - 1] = data;
|
|
if (!this.Qtimeout) {
|
|
var nextn = this.nrecv + this.msgQ.length + 1;
|
|
this.Qtimeout = setTimeout(this.flushQ, 30000, this, nextn);
|
|
}
|
|
}
|
|
/* Otherwise, discard it */
|
|
return;
|
|
};
|
|
this.flushQ = function (session, n) {
|
|
/* Callback for when an unreceived message times out.
|
|
* n is the first empty space that will not be given up on. */
|
|
if (!session.alive)
|
|
return;
|
|
session.nrecv++;
|
|
var next;
|
|
/* Clear the queue up to n */
|
|
while (session.nrecv < n) {
|
|
next = session.msgQ.shift();
|
|
if (next !== undefined)
|
|
session.child.stdin.write(next);
|
|
session.nrecv++;
|
|
}
|
|
/* Clear out anything that's ready. */
|
|
while ((next = session.msgQ.shift()) !== undefined) {
|
|
session.child.stdin.write(next);
|
|
session.nrecv++;
|
|
}
|
|
/* Now set another timeout if necessary. */
|
|
if (session.msgQ.length != 0) {
|
|
var nextn = session.nrecv + session.msgQ.length + 1;
|
|
session.Qtimeout = setTimeout(session.flushQ, 30000, session, nextn);
|
|
}
|
|
tslog("Flushing queue for session %s", session.sessid);
|
|
};
|
|
this.read = function () {
|
|
if (this.data.length == 0)
|
|
return null;
|
|
var pos = 0;
|
|
var i = 0;
|
|
for (i = 0; i < this.data.length; i++)
|
|
pos += this.data[i].length - 12;
|
|
var nbuf = new Buffer(pos);
|
|
var tptr;
|
|
pos = 0;
|
|
while (this.data.length > 0) {
|
|
tptr = this.data.shift();
|
|
tptr.copy(nbuf, pos, 12);
|
|
pos += tptr.length - 12;
|
|
}
|
|
return nbuf;
|
|
};
|
|
this.close = function () {
|
|
if (this.alive)
|
|
this.child.kill('SIGHUP');
|
|
};
|
|
this.cleanup = function () {
|
|
/* Call this when the child is dead. */
|
|
if (this.alive)
|
|
return;
|
|
ss.record.end();
|
|
/* Give the client a chance to read any leftover data. */
|
|
if (ss.data.length > 0)
|
|
setTimeout(ss.remove, 8000);
|
|
else
|
|
ss.remove();
|
|
};
|
|
this.remove = function () {
|
|
delete sessions[ss.sessid];
|
|
tslog("Session %s removed.", this.sessid);
|
|
};
|
|
}
|
|
|
|
/* A few utility functions */
|
|
function timestamp() {
|
|
dd = new Date();
|
|
sd = dd.toISOString();
|
|
sd = sd.slice(0, sd.indexOf("."));
|
|
return sd.replace("T", ".");
|
|
}
|
|
|
|
function randkey() {
|
|
rnum = Math.floor(Math.random() * 65536 * 65536);
|
|
hexstr = rnum.toString(16);
|
|
while (hexstr.length < 8)
|
|
hexstr = "0" + hexstr;
|
|
return hexstr;
|
|
}
|
|
|
|
function tslog() {
|
|
arguments[0] = new Date().toISOString() + ": " + String(arguments[0]);
|
|
console.log.apply(console, arguments);
|
|
}
|
|
|
|
/* 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 pairstrs = formtext.split("&");
|
|
var data = {};
|
|
for (var i = 0; i < pairstrs.length; i++)
|
|
{
|
|
var eqsign = pairstrs[i].indexOf("=");
|
|
if (eqsign > 0) {
|
|
rawname = pairstrs[i].slice(0, eqsign);
|
|
rawval = pairstrs[i].slice(eqsign + 1);
|
|
name = urlDec(rawname);
|
|
val = urlDec(rawval);
|
|
if (!(name in data))
|
|
data[name] = [];
|
|
data[name].push(val);
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function getMsg(posttext) {
|
|
var jsonobj;
|
|
if (!posttext)
|
|
return {};
|
|
try {
|
|
jsonobj = JSON.parse(posttext);
|
|
}
|
|
catch (e) {
|
|
if (e instanceof SyntaxError)
|
|
return {};
|
|
}
|
|
if (typeof(jsonobj) != "object")
|
|
return {};
|
|
return jsonobj;
|
|
}
|
|
|
|
function auth(username, password) {
|
|
// Real authentication not implemented
|
|
return true;
|
|
}
|
|
|
|
function login(req, res, formdata) {
|
|
if (!("game" in formdata)) {
|
|
sendError(res, 2, "No game specified.");
|
|
return;
|
|
}
|
|
else if (!("name" in formdata)) {
|
|
sendError(res, 2, "Username not given.");
|
|
return;
|
|
}
|
|
else if (!("pw" in formdata)) {
|
|
sendError(res, 2, "Password not given.");
|
|
return;
|
|
}
|
|
var username = formdata["name"];
|
|
var password = formdata["pw"];
|
|
var gname = formdata["game"];
|
|
var dims = [formdata["h"], formdata["w"]];
|
|
if (!(gname in games)) {
|
|
sendError(res, 2, "No such game: " + gname);
|
|
tslog("Request for nonexistant game \"%s\"", gname);
|
|
return;
|
|
}
|
|
var progressdir = "/dgldir/inprogress-" + games[gname].uname;
|
|
|
|
// This sets up the game once starting is approved.
|
|
function startgame() {
|
|
var ts = timestamp();
|
|
var lockfile = path.join(progressdir, username + ":node:" + ts + ".ttyrec");
|
|
var ttyrec = path.join("/dgldir/ttyrec", username, gname, ts + ".ttyrec");
|
|
var nsession = new TermSession(gname, username, [lockfile, ttyrec], dims);
|
|
if (nsession) {
|
|
/* Technically there's a race condition for the "lock"file, but since
|
|
* it requires the user deliberately starting two games at similar times,
|
|
* it's not too serious. We can't get O_EXCL in Node anyway. */
|
|
res.writeHead(200, {'Content-Type': 'text/plain'});
|
|
var reply = {"t": "l", "id": nsession.sessid, "w": nsession.w, "h":
|
|
nsession.h};
|
|
res.write(JSON.stringify(reply));
|
|
res.end();
|
|
tslog("%s playing %s (key %s, pid %d)", username, gname,
|
|
nsession.sessid, nsession.child.pid);
|
|
}
|
|
else {
|
|
sendError(res, 5, "Failed to open TTY");
|
|
tslog("Unable to allocate TTY for %s", gname);
|
|
}
|
|
}
|
|
function checkit(code, signal) {
|
|
// check the password
|
|
if (code != 0) {
|
|
sendError(res, 3);
|
|
if (code == 1)
|
|
tslog("Password check failed for user %s", username);
|
|
else if (code == 2)
|
|
tslog("Attempted login by nonexistent user %s", username);
|
|
else
|
|
tslog("Login failed: sqlickrypt error %d", code);
|
|
return;
|
|
}
|
|
// check for an existing game
|
|
fs.readdir(progressdir, function(err, files) {
|
|
if (!err) {
|
|
var fre = RegExp("^" + username + ":");
|
|
for (var i = 0; i < files.length; i++) {
|
|
if (files[i].match(fre)) {
|
|
sendError(res, 4, null);
|
|
tslog("%s is already playing %s", username, gname);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// If progressdir isn't readable, start a new game anyway.
|
|
startgame();
|
|
});
|
|
}
|
|
/* Launch the sqlickrypt utility to check the password. */
|
|
var checker = child_process.spawn("/bin/sqlickrypt", ["check"]);
|
|
checker.on("exit", checkit);
|
|
checker.stdin.end(username + '\n' + password + '\n', "utf8");
|
|
return;
|
|
}
|
|
|
|
/* Sets things up for a new user, like dgamelaunch's commands[register] */
|
|
function regsetup(username) {
|
|
function regsetup_l2(err) {
|
|
for (var g in games) {
|
|
fs.mkdir(path.join("/dgldir/ttyrec", username, games[g].uname), 0755);
|
|
}
|
|
}
|
|
fs.mkdir(path.join("/dgldir/userdata", username), 0755);
|
|
fs.mkdir(path.join("/dgldir/ttyrec/", username), 0755, regsetup_l2);
|
|
}
|
|
|
|
function register(req, res, formdata) {
|
|
var uname, passwd, email;
|
|
if (typeof (formdata.name) != "string" || formdata.name === "") {
|
|
sendError(res, 2, "No name given.");
|
|
return;
|
|
}
|
|
else
|
|
uname = formdata["name"];
|
|
if (typeof (formdata.pw) != "string" || formdata.pw === "") {
|
|
sendError(res, 2, "No password given.");
|
|
return;
|
|
}
|
|
else
|
|
passwd = formdata["pw"];
|
|
if (typeof (formdata.email) != "string" || formdata.email === "") {
|
|
/* E-mail is optional */
|
|
email = "nobody@nowhere.not";
|
|
}
|
|
else
|
|
email = formdata["email"];
|
|
function checkreg(code, signal) {
|
|
if (code == 4) {
|
|
sendError(res, 2, "Invalid characters in name or email.");
|
|
tslog("Attempted registration: %s %s", uname, email);
|
|
}
|
|
else if (code == 1) {
|
|
sendError(res, 2, "Username " + uname + " is already being used.");
|
|
tslog("Attempted duplicate registration: %s", uname);
|
|
}
|
|
else if (code != 0) {
|
|
sendError(res, 0, null);
|
|
tslog("sqlickrypt register failed with code %d", code);
|
|
}
|
|
else {
|
|
res.writeHead(200, {'Content-Type': 'text/plain'});
|
|
var reply = {"t": "r", "d": uname};
|
|
res.write(JSON.stringify(reply));
|
|
res.end();
|
|
tslog("Added new user: %s", uname);
|
|
regsetup(uname);
|
|
}
|
|
}
|
|
var child_adder = child_process.spawn("/bin/sqlickrypt", ["register"]);
|
|
child_adder.on("exit", checkreg);
|
|
child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8");
|
|
return;
|
|
}
|
|
|
|
function logout(term, res) {
|
|
if (!term.alive) {
|
|
sendError(res, 1, null);
|
|
return;
|
|
}
|
|
term.close();
|
|
var resheaders = {'Content-Type': 'text/plain'};
|
|
res.writeHead(200, resheaders);
|
|
res.write(JSON.stringify({"t": "q"}));
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
function findTermSession(formdata) {
|
|
if (typeof(formdata) != "object")
|
|
return null;
|
|
if ("id" in formdata) {
|
|
var sessid = formdata["id"];
|
|
if (sessid in sessions) {
|
|
return sessions[sessid];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function serveStatic(req, res, fname) {
|
|
var nname = path.normalize(fname);
|
|
if (nname == "" || nname == "/")
|
|
nname = "index.html";
|
|
if (nname.match(/\/$/))
|
|
path.join(nname, "index.html"); /* it was a directory */
|
|
var realname = path.join(serveStaticRoot, nname);
|
|
var extension = path.extname(realname);
|
|
path.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) {
|
|
fs.readFile(realname, function (error, data) {
|
|
if (error) {
|
|
res.writeHead(500, {});
|
|
res.end();
|
|
}
|
|
else {
|
|
res.writeHead(200, resheaders);
|
|
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;
|
|
}
|
|
|
|
function readFeed(res, term) {
|
|
if (term) {
|
|
var reply = {};
|
|
var result = term.read();
|
|
if (result == null) {
|
|
if (term.alive)
|
|
reply.t = "n";
|
|
else
|
|
reply.t = "q";
|
|
}
|
|
else {
|
|
reply.t = "d";
|
|
reply.n = term.nsend++;
|
|
reply.d = result.toString("hex");
|
|
}
|
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
res.write(JSON.stringify(reply));
|
|
res.end();
|
|
}
|
|
else {
|
|
sendError(res, 1, null);
|
|
}
|
|
}
|
|
|
|
var errorcodes = [ "Generic Error", "Not logged in", "Invalid data",
|
|
"Login failed", "Already playing", "Game launch failed" ];
|
|
|
|
function sendError(res, ecode, msg) {
|
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
var edict = {"t": "E"};
|
|
if (!(ecode < errorcodes.length && ecode > 0))
|
|
ecode = 0;
|
|
edict["c"] = ecode;
|
|
edict["s"] = errorcodes[ecode];
|
|
if (msg)
|
|
edict["s"] += ": " + msg;
|
|
res.write(JSON.stringify(edict));
|
|
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() {
|
|
//formdata = getFormValues(reqbody);
|
|
formdata = getMsg(reqbody);
|
|
var target = url.parse(req.url).pathname;
|
|
var cterm = findTermSession(formdata);
|
|
/* First figure out if the client is POSTing to a command interface. */
|
|
if (req.method == 'POST') {
|
|
if (target == '/feed') {
|
|
if (!cterm) {
|
|
sendError(res, 1, null);
|
|
return;
|
|
}
|
|
if (formdata.t == "q") {
|
|
/* The client wants to terminate the process. */
|
|
logout(cterm, res);
|
|
}
|
|
else if (formdata.t == "d" && typeof(formdata.d) == "string") {
|
|
/* process the keys */
|
|
hexstr = formdata.d.replace(/[^0-9a-f]/gi, "");
|
|
if (hexstr.length % 2 != 0) {
|
|
sendError(res, 2, "incomplete byte");
|
|
return;
|
|
}
|
|
keybuf = new Buffer(hexstr, "hex");
|
|
/* TODO OoO correction */
|
|
cterm.write(keybuf, formdata.n);
|
|
}
|
|
readFeed(res, cterm);
|
|
}
|
|
else if (target == "/login") {
|
|
login(req, res, formdata);
|
|
}
|
|
else if (target == "/addacct") {
|
|
register(req, res, formdata);
|
|
}
|
|
else {
|
|
res.writeHead(405, resheaders);
|
|
res.end();
|
|
}
|
|
}
|
|
else if (req.method == 'GET' || req.method == 'HEAD') {
|
|
if (target == '/feed') {
|
|
if (!cterm) {
|
|
sendError(res, 1, null);
|
|
return;
|
|
}
|
|
readFeed(res, cterm);
|
|
}
|
|
/* Default page, create a new term */
|
|
/* FIXME New term not created anymore, is a special case still needed? */
|
|
else if (target == '/') {
|
|
serveStatic(req, res, "/");
|
|
}
|
|
else /* Go look for it in the filesystem */
|
|
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].child.kill('SIGHUP');
|
|
}
|
|
tslog("Quitting...");
|
|
return;
|
|
});
|
|
|
|
/* Initialization STARTS HERE */
|
|
process.env["TERM"] = "xterm-256color";
|
|
|
|
if (process.getuid() != 0) {
|
|
tslog("Not running as root, cannot chroot.");
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
process.chdir(chrootDir);
|
|
}
|
|
catch (err) {
|
|
tslog("Cannot enter %s: %s", chrootDir, err);
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
daemon.chroot(chrootDir);
|
|
}
|
|
catch (err) {
|
|
tslog("chroot to %s failed: %s", chrootDir, err);
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
// drop gid first, that requires UID=0
|
|
process.setgid(dropToGID);
|
|
process.setuid(dropToUID);
|
|
}
|
|
catch (err) {
|
|
tslog("Could not drop permissions: %s", err);
|
|
process.exit(1);
|
|
}
|
|
|
|
http.createServer(handler).listen(8080, "127.0.0.1");
|
|
tslog('rlgwebd running at http://127.0.0.1:8080/');
|