rlgwebd/rlgterm.js
John "Elwin" Edwards 37bd2e8c28 Initial support for TLS.
RLGWebD now serves via encrypted connections on port 8081.

The client-side script now uses secure WebSockets if the page is being
accessed via HTTPS.
2017-01-01 20:18:01 -05:00

943 lines
25 KiB
JavaScript

/* rlgterm.js: Roguelike Gallery's driver for termemu.js */
/* Data on the available games. */
var games = {
"rogue3": {
"name": "Rogue V3",
"uname": "rogue3"
},
"rogue4": {
"name": "Rogue V4",
"uname": "rogue4"
},
"rogue5": {
"name": "Rogue V5",
"uname": "rogue5"
},
"srogue": {
"name": "Super-Rogue",
"uname": "srogue"
},
"arogue5": {
"name": "Advanced Rogue 5",
"uname": "arogue5"
},
"arogue7": {
"name": "Advanced Rogue 7",
"uname": "arogue7"
},
"xrogue": {
"name": "XRogue",
"uname": "xrogue"
}
};
var session = {
/* The session id assigned by the server. */
connect: false,
/* Login name and key are now in sessionStorage. */
/* Whether the game is being played or just watched. */
playing: false,
/* If watching, the name of the player. */
playername: null,
/* Whether the Stop button was pushed. */
leaving: false,
/* WebSocket for communication */
sock: null
};
/* The interval ID for checking the status of current games. */
var statInterval = null;
/* How frequently to check. */
var statDelta = 8000;
/* A WebSocket to listen for status events. */
var statsock = null;
/* List of currently active games. */
var currentList = [];
/* Last time the list was updated. */
var currentTS = null;
function writeData(hexstr) {
var codenum;
var codes = [];
var nc;
var u8wait = 0; /* Stores bits from previous bytes of multibyte sequences. */
var expect = 0; /* The number of 10------ bytes expected. */
/* UTF-8 translation. */
for (var i = 0; i < hexstr.length; i += 2) {
nc = Number("0x" + hexstr.substr(i, 2));
if (nc < 0x7F) {
/* 0------- */
codes.push(nc);
/* Any incomplete sequence will be discarded. */
u8wait = 0;
expect = 0;
}
else if (nc < 0xC0) {
/* 10------ : part of a multibyte sequence */
if (expect > 0) {
u8wait <<= 6;
u8wait += (nc & 0x3F);
expect--;
if (expect == 0) {
codes.push(u8wait);
u8wait = 0;
}
}
else {
/* Assume an initial byte was missed. */
u8wait = 0;
}
}
/* These will all discard any incomplete sequence. */
else if (nc < 0xE0) {
/* 110----- : introduces 2-byte sequence */
u8wait = (nc & 0x1F);
expect = 1;
}
else if (nc < 0xF0) {
/* 1110---- : introduces 3-byte sequence */
u8wait = (nc & 0x0F);
expect = 2;
}
else if (nc < 0xF8) {
/* 11110--- : introduces 4-byte sequence */
u8wait = (nc & 0x07);
expect = 3;
}
else if (nc < 0xFC) {
/* 111110-- : introduces 5-byte sequence */
u8wait = (nc & 0x03);
expect = 4;
}
else if (nc < 0xFE) {
/* 1111110- : introduces 6-byte sequence */
u8wait = (nc & 0x01);
expect = 5;
}
else {
/* 1111111- : should never appear */
u8wait = 0;
expect = 0;
}
/* Supporting all 31 bits is probably overkill... */
}
termemu.write(codes);
return;
}
function errHandler() {
message("Unable to connect to the server.", "warn");
}
function sendback(str) {
/* For responding to terminal queries. */
if (session.sock) {
session.sock.send(JSON.stringify({"t": "d", "d": str}));
}
return;
}
function sendkey(ev) {
if (!session.playing)
return;
var keynum = ev.keyCode;
var code;
if (keynum >= 65 && keynum <= 90) {
/* Letters. */
if (ev.ctrlKey)
keynum -= 64;
else if (!ev.shiftKey)
keynum += 32;
code = keynum.toString(16);
if (code.length < 2)
code = "0" + code;
}
else if (keynum >= 48 && keynum <= 57) {
/* The number row, NOT the numpad. */
if (ev.shiftKey) {
code = numShifts[keynum - 48].toString(16);
}
else {
code = keynum.toString(16);
}
}
else if (keynum in keyHexCodes) {
if (ev.shiftKey)
code = keyHexCodes[keynum][1];
else
code = keyHexCodes[keynum][0];
}
else if (keynum >= 16 && keynum <= 20) {
/* Shift, Cntl, Alt, CAPSLOCK */
return;
}
else {
debug(1, "Ignoring keycode " + keynum);
return;
}
ev.preventDefault();
if (session.sock) {
session.sock.send(JSON.stringify({"t": "d", "d": code}));
}
/* Otherwise it is disconnected */
return;
}
var charshifts = { '-': "5f", '=': "2b", '[': "7b", ']': "7d", '\\': "7c",
';': "3a", '\'': "22", ',': "3c", '.': "3e", '/': "3f", '`': "7e"
}
var kpkeys = { "KP1": "1b4f46", "KP2": "1b4f42", "KP3": "1b5b367e",
"KP4": "1b4f44", "KP5": "1b5b45", "KP6": "1b4f43",
"KP7": "1b4f48", "KP8": "1b4f41", "KP9": "1b5b357e" };
function vkey(c) {
if (!session.playing)
return;
var keystr;
if (c.match(/^[a-z]$/)) {
if (termemu.ctrlp()) {
var n = c.charCodeAt(0) - 96;
keystr = n.toString(16);
if (keystr.length < 2)
keystr = "0" + keystr;
}
else if (termemu.shiftp())
keystr = c.toUpperCase().charCodeAt(0).toString(16);
else
keystr = c.charCodeAt(0).toString(16);
}
else if (c.match(/^[0-9]$/)) {
if (termemu.shiftp())
keystr = numShifts[c.charCodeAt(0) - 48].toString(16);
else
keystr = c.charCodeAt(0).toString(16);
}
else if (c == '\n')
keystr = "0a";
else if (c == '\t')
keystr = "09";
else if (c == '\b')
keystr = "08";
else if (c == ' ')
keystr = "20";
else if (c in charshifts) {
if (termemu.shiftp())
keystr = charshifts[c];
else
keystr = c.charCodeAt(0).toString(16);
}
else if (c in kpkeys) {
keystr = kpkeys[c];
}
else
return;
if (session.sock) {
session.sock.send(JSON.stringify({"t": "d", "d": keystr}));
}
return;
}
function setup() {
keyHexCodes.init();
termemu.init("termwrap", 24, 80);
/* Is someone already logged in? */
if ("lcred" in sessionStorage) {
setmode("choose");
message("You are logged in as " + sessionStorage.getItem("lname") + ".");
}
else
setmode("login");
/* Set up the text size. */
var cssSize = termemu.view.style.fontSize;
var match = cssSize.match(/\d*/);
if (!match) {
return;
}
var csize = Number(match[0]);
var allscreen = document.getElementById("termwrap");
while (csize > 9 && csize < 48) {
if (allscreen.scrollWidth * 1.2 > window.innerWidth) {
csize = textsize(false);
}
else if (allscreen.scrollWidth * 2 < window.innerWidth) {
csize = textsize(true);
}
else
break;
}
if (!window.WebSocket) {
message("Your browser does not support WebSockets. " +
"This Web app will not work.", "warn");
}
return;
}
function toggleshift() {
termemu.toggleshift();
keydiv = document.getElementById("shiftkey");
if (termemu.shiftp())
keydiv.className = "keysel";
else
keydiv.className = "key";
return;
}
function togglectrl() {
termemu.togglectrl();
keydiv = document.getElementById("ctrlkey");
if (termemu.ctrlp())
keydiv.className = "keysel";
else
keydiv.className = "key";
return;
}
function formlogin(ev) {
ev.preventDefault();
/* What to do if logged in already? */
var loginmsg = {};
loginmsg["name"] = document.getElementById("input_name").value;
loginmsg["pw"] = document.getElementById("input_pw").value;
var req = new XMLHttpRequest();
req.onerror = errHandler;
req.onreadystatechange = function () {
if (req.readyState != 4 || req.status != 200)
return;
var reply = JSON.parse(req.responseText);
if (reply.t == 'l') {
/* Success */
sessionStorage.setItem("lcred", reply.k);
sessionStorage.setItem("lname", reply.u);
message("You are now logged in as " + reply.u + ".");
setmode("choose");
}
else if (reply.t == 'E') {
var failmsg = "Logging in failed. ";
if (reply.c == 2)
failmsg += reply.s.match(/Invalid data: (.*)/)[1];
else if (reply.c == 3)
failmsg += "The username or password was incorrect.";
else if (reply.c == 6)
failmsg += "The server is shutting down.";
message(failmsg, "warn");
document.getElementById("input_name").value = "";
document.getElementById("input_pw").value = "";
}
};
req.open('POST', '/login', true);
req.send(JSON.stringify(loginmsg));
return;
}
function tableCurrent(gamelist) {
var gamediv = document.getElementById("gametable");
while (gamediv.children.length > 2)
gamediv.removeChild(gamediv.children[2]);
if (gamelist.length === 0) {
gamediv.style.display = "none";
document.getElementById("nogames").style.display = "block";
}
else {
gamediv.style.display = "table";
document.getElementById("nogames").style.display = "none";
}
for (var i = 0; i < gamelist.length; i++) {
var row = document.createElement("div");
var cell1 = document.createElement("div");
var cell2 = document.createElement("div");
var cell3 = document.createElement("div");
var cell4 = document.createElement("div");
var cell5 = document.createElement("div");
cell1.appendChild(document.createTextNode(gamelist[i].p));
var uname = gamelist[i].g;
if (uname in games)
cell2.appendChild(document.createTextNode(games[uname].name));
else {
continue;
}
var srcstr = "VR";
if (gamelist[i].c == "rlg")
srcstr = "Web";
else if (gamelist[i].c == "dgl")
srcstr = "SSH";
cell3.appendChild(document.createTextNode(srcstr));
cell4.appendChild(document.createTextNode(idlestr(gamelist[i].i)));
var button = document.createElement("span");
button.appendChild(document.createTextNode("Watch"));
button.onclick = makeWatcher(uname + "/" + gamelist[i].p);
button.className = "ibutton";
cell5.appendChild(button);
row.appendChild(cell1);
row.appendChild(cell2);
row.appendChild(cell3);
row.appendChild(cell4);
row.appendChild(cell5);
gamediv.appendChild(row);
}
}
/* Handles the status socket, opening and closing it when necessary. */
function wsCurrent() {
if (!window.WebSocket)
return;
if (session.connect) {
/* Don't bother with status if already playing/watching. */
if (statsock) {
statsock.close();
statsock = null;
}
return;
}
if ("lcred" in sessionStorage) {
/* When starting the socket, the choices list might not be initialized. */
getchoices();
}
if (statsock)
return;
var wsproto = "ws://";
if (window.location.protocol == "https:")
wsproto = "wss://";
statsock = new WebSocket(wsproto + window.location.host + "/status");
statsock.onmessage = function (ev) {
var msg;
try {
msg = JSON.parse(ev.data);
} catch (e) {
if (e instanceof SyntaxError)
return;
}
if (msg.t == "t") {
currentList = msg.g;
currentTS = new Date();
tableCurrent(currentList);
}
else if (msg.t == "p") {
var now = new Date();
var idletimes = {};
for (var i = 0; i < msg.g.length; i++) {
var tag = msg.g[i].g + "/" + msg.g[i].p;
idletimes[tag] = msg.g[i].i;
}
for (var i = 0; i < currentList.length; i++) {
var tag = currentList[i].g + "/" + currentList[i].p;
if (tag in idletimes) {
currentList[i].i = idletimes[tag];
}
else {
currentList[i].i += now - currentTS;
}
}
currentTS = now;
tableCurrent(currentList);
}
else if (msg.t == "b") {
var justbegun = {};
justbegun.g = msg.g;
justbegun.p = msg.p;
justbegun.c = msg.c;
justbegun.i = 0;
currentList.push(justbegun);
tableCurrent(currentList);
if (msg.p == sessionStorage.getItem("lname")) {
getchoices();
}
}
else if (msg.t == "e") {
var i = 0;
while (i < currentList.length) {
if (currentList[i].g == msg.g && currentList[i].p == msg.p)
break;
i++;
}
if (i < currentList.length) {
currentList.splice(i, 1);
}
tableCurrent(currentList);
if (msg.p == sessionStorage.getItem("lname")) {
getchoices();
}
}
};
statsock.onclose = function (ev) {
statsock = null;
}
}
/* FIXME gamelist API has changed */
function getcurrent(clear) {
if (window.WebSocket) {
return;
}
if (session.connect || clear) {
if (statInterval) {
window.clearInterval(statInterval);
statInterval = null;
}
return;
}
if (!statInterval) {
statInterval = window.setInterval(getcurrent, statDelta);
}
if ("lcred" in sessionStorage)
getchoices();
var req = new XMLHttpRequest();
req.onerror = errHandler;
req.onreadystatechange = function () {
if (req.readyState != 4 || req.status != 200)
return;
var reply;
try {
reply = JSON.parse(req.responseText);
} catch (e) {
if (e instanceof SyntaxError)
return;
}
if (!reply.s) {
return;
}
tableCurrent(reply.g);
};
req.open('GET', '/status', true);
req.send();
return;
}
function getchoices() {
if (session.connect || !("lcred" in sessionStorage))
return;
var req = new XMLHttpRequest();
req.onerror = errHandler;
req.onreadystatechange = function () {
if (req.readyState != 4 || req.status != 200)
return;
var reply;
try {
reply = JSON.parse(req.responseText);
} catch (e) {
if (e instanceof SyntaxError)
return;
}
if (!("name" in reply) || reply["name"] != sessionStorage.getItem("lname")
|| !("stat" in reply))
return;
var optdiv = document.getElementById("opttable");
/* Don't remove the first child, it's the header. */
while (optdiv.childNodes.length > 1)
optdiv.removeChild(optdiv.childNodes[1]);
for (var gname in games) {
if (!(gname in reply.stat))
continue;
var acttext;
if (reply.stat[gname] == "s")
acttext = "Resume your game";
else if (reply.stat[gname] == "0")
acttext = "Start a game";
else if (reply.stat[gname] == "p" || reply.stat[gname] == "d")
acttext = "Force save";
else
continue;
var button = document.createElement("span");
button.appendChild(document.createTextNode(acttext));
if ("s0".indexOf(reply.stat[gname]) >= 0) {
button.onclick = makeStarter(gname);
button.className = "ibutton";
}
else {
button.onclick = makeStopper(gname);
button.className = "ibutton";
}
var actdiv = document.createElement("div");
actdiv.appendChild(button);
var gamediv = document.createElement("div");
gamediv.appendChild(document.createTextNode(games[gname].name));
var rowdiv = document.createElement("div");
rowdiv.appendChild(gamediv);
rowdiv.appendChild(actdiv);
optdiv.appendChild(rowdiv);
}
};
req.open('GET', '/pstatus/' + sessionStorage.getItem("lname"), true);
req.send();
return;
}
/* This can't be in the loop in getchoices(), or the closure's scope will
* get overwritten on the next iteration, and then all the games end up
* being Super-Rogue.
*/
function makeStarter(gname) {
if (!(gname in games))
return null;
var game = games[gname];
function starter(ev) {
startgame(game);
}
return starter;
}
function makeStopper(gname) {
if (!(gname in games))
return null;
var game = games[gname];
function stopper(ev) {
stopgame(game);
}
return stopper;
}
function stopgame(game) {
if (!("lcred" in sessionStorage))
return;
var stopmsg = {"key": sessionStorage.getItem("lcred"), "g": game.uname};
var req = new XMLHttpRequest();
req.onerror = errHandler;
req.onreadystatechange = function () {
if (req.readyState != 4 || req.status != 200)
return;
var reply = JSON.parse(req.responseText);
if (reply.t == 'E') {
if (reply.c == 7)
message("That game has already stopped.");
else if (reply.c == 1) {
logout();
message("The server forgot about you, please log in again.", "warn");
}
else {
message("That game could not be stopped because: " + reply.s +
"This might be a bug.", "warn");
}
}
}
req.open('POST', '/quit', true);
req.send(JSON.stringify(stopmsg));
return;
}
function startgame(game) {
if (!("lcred" in sessionStorage) || session.connect)
return;
if (!window.WebSocket) {
return;
}
var wsproto = "ws://";
if (window.location.protocol == "https:")
wsproto = "wss://";
var sockurl = wsproto + window.location.host + "/play/" + game.uname;
sockurl += "?key=" + sessionStorage.getItem("lcred") + "&w=80&h=24";
ws = new WebSocket(sockurl);
ws.onopen = function (event) {
session.connect = true;
session.playing = true;
session.sock = ws;
setmode("play");
};
ws.onmessage = function (event) {
var msgObject = JSON.parse(event.data);
if (msgObject.t == 's') {
termemu.resize(msgObject.h, msgObject.w);
message("You are now playing " + games[msgObject.g].name + ".");
}
else if (msgObject.t == 'd') {
writeData(msgObject.d);
}
};
ws.onclose = function (event) {
session.sock = null;
gameover();
};
}
function makeWatcher(t) {
function watcher(ev) {
startwatching(t);
}
return watcher;
}
function startwatching(tag) {
if (session.connect)
return;
var wsproto = "ws://";
if (window.location.protocol == "https:")
wsproto = "wss://";
var sockurl = wsproto + window.location.host + "/watch/" + tag;
var ws = new WebSocket(sockurl);
ws.onopen = function (event) {
session.connect = true;
session.sock = ws;
setmode("watch");
};
ws.onmessage = function (event) {
var msgObject = JSON.parse(event.data);
if (msgObject.t == 'w') {
termemu.resize(msgObject.h, msgObject.w);
termemu.reset();
termemu.toAltBuf();
var pname = msgObject.p;
var gname = games[msgObject.g].name;
session.playername = pname;
message("You are now watching " + pname + " play " + gname + ".");
}
else if (msgObject.t == 'd') {
writeData(msgObject.d);
}
};
ws.onclose = function (event) {
session.sock = null;
gameover();
};
}
function formreg(ev) {
ev.preventDefault();
/* This ought to check for being logged in instead. */
if (session.connect)
return;
var regmsg = {};
regmsg["name"] = document.getElementById("regin_name").value;
regmsg["pw"] = document.getElementById("regin_pw").value;
regmsg["email"] = document.getElementById("regin_email").value;
var req = new XMLHttpRequest();
req.onerror = errHandler;
req.onreadystatechange = function () {
if (req.readyState != 4 || req.status != 200)
return;
var reply = JSON.parse(req.responseText);
if (reply.t == 'r') {
/* Success */
message("Welcome " + reply.u + ", you are now registered.");
sessionStorage.setItem("lcred", reply.k);
sessionStorage.setItem("lname", reply.u);
message("You are now logged in as " + reply.u + ".");
setmode("choose");
}
else if (reply.t == 'E') {
var failmsg = "Registration failed.";
if (reply.c == 2) {
var errdesc = reply.s.match(/Invalid data: (.*)/)[1];
if (errdesc.match(/No name/))
failmsg += " You need to choose a name.";
else if (errdesc.match(/No password/))
failmsg += " You need to choose a password.";
else if (errdesc.match(/Invalid/)) {
failmsg += " Names must be letters and numbers. E-mail addresses " +
"can also contain these characters: @.-_";
}
else if (errdesc.match(/Username/))
failmsg += " Someone else is already using that name.";
else
failmsg += " This is probably a bug.";
}
else
failmsg += " This is probably a bug.";
message(failmsg, "warn");
document.getElementById("regin_name").value = "";
document.getElementById("regin_pw").value = "";
document.getElementById("regin_email").value = "";
}
};
req.open('POST', '/addacct', true);
req.send(JSON.stringify(regmsg));
return;
}
function gameover() {
if (!session.connect)
return;
/* TODO IFACE2 If the end was unexpected, tell player the game was saved. */
if (session.playing) {
message("Finished playing.");
}
else {
if (session.leaving) {
/* Client-initiated: the user stopped watching. */
message("Finished watching " + session.playername + ".");
}
else {
/* Server-initiated: end of game. */
message(session.playername + " has finished playing.");
}
session.playername = null;
}
session.connect = false;
session.playing = false;
session.leaving = false;
termemu.toNormBuf();
if ("lcred" in sessionStorage)
setmode("choose");
else
setmode("login");
return;
}
function logout() {
sessionStorage.removeItem("lcred");
sessionStorage.removeItem("lname");
setmode("login");
}
function stop() {
if (!session.connect)
return;
if (session.sock) {
session.leaving = true;
session.sock.close();
return;
}
}
function setmode(mode, ev) {
if (ev)
ev.preventDefault();
if (mode == "play") {
document.getElementById("keyboard").style.display = "block";
document.getElementById("playctl").style.display = "block";
document.getElementById("startgame").style.display = "none";
document.getElementById("login").style.display = "none";
document.getElementById("register").style.display = "none";
document.getElementById("current").style.display = "none";
}
else if (mode == "watch") {
document.getElementById("keyboard").style.display = "none";
document.getElementById("playctl").style.display = "block";
document.getElementById("startgame").style.display = "none";
document.getElementById("login").style.display = "none";
document.getElementById("register").style.display = "none";
document.getElementById("current").style.display = "none";
}
else if (mode == "choose") {
document.getElementById("keyboard").style.display = "none";
document.getElementById("playctl").style.display = "none";
document.getElementById("startgame").style.display = "block";
document.getElementById("login").style.display = "none";
document.getElementById("register").style.display = "none";
document.getElementById("current").style.display = "block";
getcurrent();
}
else if (mode == "login") {
document.getElementById("keyboard").style.display = "none";
document.getElementById("playctl").style.display = "none";
document.getElementById("startgame").style.display = "none";
document.getElementById("login").style.display = "block";
document.getElementById("register").style.display = "none";
document.getElementById("current").style.display = "block";
getcurrent();
}
else if (mode == "register") {
document.getElementById("keyboard").style.display = "none";
document.getElementById("playctl").style.display = "none";
document.getElementById("startgame").style.display = "none";
document.getElementById("login").style.display = "none";
document.getElementById("register").style.display = "block";
document.getElementById("current").style.display = "none";
}
wsCurrent();
}
function toggleBlock(id) {
var element = document.getElementById(id);
if (!element)
return;
if (element.style.display != "block")
element.style.display = "block";
else
element.style.display = "none";
}
function message(msg, mtype) {
var msgdiv = document.createElement("div");
var msgtext = document.createTextNode(msg);
msgdiv.appendChild(msgtext);
if (mtype) {
msgdiv.className = mtype;
}
var msgcontainer = document.getElementById("messages");
msgcontainer.insertBefore(msgdiv, msgcontainer.firstChild);
}
function debug(level, msg) {
if (level < debugSuppress)
return;
var msgdiv = document.createElement("div");
var msgtext = document.createTextNode(msg);
msgdiv.appendChild(msgtext);
document.getElementById("debug").appendChild(msgdiv);
return;
}
function setTitle(tstr) {
message(tstr);
}
function textsize(larger) {
var cssSize = termemu.view.style.fontSize;
if (!cssSize) {
return null;
}
var match = cssSize.match(/\d*/);
if (!match) {
return null;
}
var csize = Number(match[0]);
var nsize;
if (larger) {
if (csize >= 48)
nsize = 48;
else if (csize >= 20)
nsize = csize + 4;
else if (csize >= 12)
nsize = csize + 2;
else if (csize >= 8)
nsize = csize + 1;
else
nsize = 8;
}
else {
if (csize <= 8)
nsize = 8;
else if (csize <= 12)
nsize = csize - 1;
else if (csize <= 20)
nsize = csize - 2;
else if (csize <= 48)
nsize = csize - 4;
else
nsize = 48;
}
var cssvalstr = nsize.toString() + "px";
document.getElementById("term").style.fontSize = cssvalstr;
document.getElementById("keys").style.fontSize = cssvalstr;
termemu.fixsize();
debug(1, "Changing font size to " + nsize.toString());
return nsize;
}
function idlestr(ms) {
/* Minute accuracy is good enough. */
if (typeof(ms) != "number")
return "?";
var minutes = Math.round(ms / 60000);
if (minutes < 60)
return String(minutes) + " min";
var hours = Math.floor(minutes / 60);
if (hours < 24)
return String(hours) + " hr " + String(minutes % 60) + " min";
var days = Math.floor(hours / 24);
if (days == 1)
return "1 day " + String(hours % 24) + " hr";
else
return String(days) + " days " + String(hours % 24) + " hr";
}
function bell(on) {
var imgnode = document.getElementById("bell");
if (on) {
imgnode.style.visibility = "visible";
window.setTimeout(bell, 1500, false);
}
else
imgnode.style.visibility = "hidden";
return;
}