Compare commits

..

10 commits

Author SHA1 Message Date
John "Elwin" Edwards
29f17aa874 Fix race condition related to watching DGL games.
It's possible for a dgamelaunch game to end and cause rlgwebd to stop watching
it before rlgwebd has started watching it.
2020-08-16 20:56:18 -04:00
John "Elwin" Edwards
c38fb5d107 rlgwebd: use some newer Javascript features. 2020-04-03 15:15:02 -04:00
John "Elwin" Edwards
9004afebdc Terminal emulation: implement the CSI b sequence.
CSI b repeats the previous character, if it was printable and not an
escape sequence.
2019-09-05 15:19:27 -04:00
John "Elwin" Edwards
f2256500e1 Changes for compatibility with recent versions of NodeJS.
The pty.js module is replaced with node-pty, now-mandatory callbacks
are added to various fs functions, and deprecated Buffer() calls are
replaced with Buffer.from() or Buffer.alloc().
2019-08-25 21:27:31 -04:00
John "Elwin" Edwards
4940bf86ae Move the NODE_PATH location.
Modules are now expected to be in /var/local/lib/node_modules.  This is
intended to make it easier to avoid running npm as root.
2017-12-26 13:23:55 -05:00
John "Elwin" Edwards
4059bf2983 Fix possibly insecure permissions on the control socket.
The server's control socket is now in a private directory.
2017-01-28 09:57:31 -05:00
John "Elwin" Edwards
c4d10ba33d rlgwebd-stop: avoid the deprecated domain module.
Instead of catching connection errors with domains, install an error
listener on the socket before connecting.
2017-01-27 19:18:31 -05:00
John "Elwin" Edwards
2f40fc5387 RLGWebD: replace deprecated fs.exists() with fs.access(). 2017-01-27 15:43:10 -05:00
John "Elwin" Edwards
c824ea924c Fix syntax error in Makefile. 2017-01-12 19:02:20 -05:00
John "Elwin" Edwards
5b790718d8 Use either HTTP or HTTPS.
If HTTPS is enabled, RLGWebD will not use insecure HTTP.
2017-01-08 16:17:11 -05:00
8 changed files with 247 additions and 163 deletions

View file

@ -23,7 +23,7 @@ install: all
mkdir -p ${CHROOT}/var/www mkdir -p ${CHROOT}/var/www
cp ${WEBASSETS} ${CHROOT}/var/www cp ${WEBASSETS} ${CHROOT}/var/www
cp rlgwebd.service /usr/lib/systemd/system cp rlgwebd.service /usr/lib/systemd/system
if test ! -f /etc/rlgwebd.conf; cp rlgwebd.conf /etc; fi if test ! -f /etc/rlgwebd.conf; then cp rlgwebd.conf /etc; fi
# Libraries are not removed. Something else might be using them. # Libraries are not removed. Something else might be using them.
uninstall: uninstall:

View file

@ -5,12 +5,11 @@ browser. It is intended to be compatible with dgamelaunch.
Node Node
--- ---
RLGWebD currently works with Node v0.10. RLGWebD is currently being updated to work with Node v10.x.
It requires the 'posix', 'pty.js', and 'websocket' modules. Currently, It requires the 'posix', 'node-pty', and 'websocket' modules. Currently,
it expects them to be installed in the global location, which is it expects them to be installed in "/var/local/lib/node_modules". It
"/usr/lib/node_modules". It is planned to eventually use a different is not recommended to run npm as root when installing the modules.
location so that npm will not need to run as root.
init init
--- ---
@ -22,14 +21,11 @@ a proper initscript, but it could form the basis of one.
Configuration Configuration
--- ---
You can set some options by changing some variables in the first few A configuration file is installed at /etc/rlgwebd.conf. It contains a
lines of the rlgwebd script: list of options.
Option Variable Default If the domain_name option and the SSL-related options are set, rlgwebd
will use HTTPS instead of insecure HTTP.
Chroot path chrootDir /var/dgl
Username dropToUser rodney
Server port httpPort 8080
If you change the chroot location, change it in the first line of the If you change the chroot location, change it in the first line of the
Makefile too. Makefile too.
@ -59,6 +55,7 @@ Running "make install" will:
Copy the C programs and the libraries they need into the chroot Copy the C programs and the libraries they need into the chroot
Install the main RLGWebD script in /usr/local/bin Install the main RLGWebD script in /usr/local/bin
Place the systemd unit file in the proper directory Place the systemd unit file in the proper directory
Copy a configuration file into /etc
If you don't use systemd, or want to change the installation locations, If you don't use systemd, or want to change the installation locations,
you will have to edit the Makefile. you will have to edit the Makefile.

View file

@ -1,8 +1,8 @@
#!/bin/sh #!/bin/sh
NODE_PATH=/usr/lib/node_modules NODE_PATH=/var/local/lib/node_modules
LOGFILE=/var/log/rlgwebd.log LOGFILE=/var/log/rlgwebd.log
CTLSOCKET=/var/run/rlgwebd.sock CTLSOCKET=/var/run/rlgwebd/rlgwebd.sock
RLGWEBDJS=/usr/local/bin/rlgwebd RLGWEBDJS=/usr/local/bin/rlgwebd
export NODE_PATH export NODE_PATH

154
rlgwebd
View file

@ -1,35 +1,38 @@
#!/usr/bin/env node #!/usr/bin/env node
var http = require('http'); const http = require('http');
var https = require('https'); const https = require('https');
var net = require('net'); const net = require('net');
var url = require('url'); const url = require('url');
var path = require('path'); const path = require('path');
var fs = require('fs'); const fs = require('fs');
var events = require('events'); const events = require('events');
var child_process = require('child_process'); const child_process = require('child_process');
// Dependencies // Dependencies
var posix = require("posix"); const posix = require("posix");
var pty = require("pty.js"); const pty = require("node-pty");
var WebSocketServer = require("websocket").server; const WebSocketServer = require("websocket").server;
const errorcodes = [ "Generic Error", "Not logged in", "Invalid data",
"Login failed", "Already playing", "Game launch failed",
"Server shutting down", "Game not in progress" ];
/* Default options */ /* Default options */
var rlgwebd_options = { var rlgwebd_options = {
control_socket: "/var/run/rlgwebd.sock", control_socket: "/var/run/rlgwebd/rlgwebd.sock",
http_port: 8080, port: 8080,
https_port: 8081,
chrootDir: "/var/dgl/", chrootDir: "/var/dgl/",
username: "rodney", username: "rodney",
static_root: "/var/www/" static_root: "/var/www/"
}; };
/* Read configuration from a file */ /* Read configuration from a file */
var config_file = "/etc/rlgwebd.conf"; const config_file = "/etc/rlgwebd.conf";
var config_lines = read_or_die(config_file, "Configuration file").toString().split('\n'); var config_lines = read_or_die(config_file, "Configuration file").toString().split('\n');
for (var i = 0; i < config_lines.length; i++) { for (let conf_line of config_lines) {
if (config_lines[i].length > 0 && config_lines[i][0] != '#') { if (conf_line.length > 0 && conf_line[0] != '#') {
var config_fields = config_lines[i].split('='); var config_fields = conf_line.split('=');
if (config_fields.length < 2) if (config_fields.length < 2)
continue; continue;
var option_name = config_fields[0].trim(); var option_name = config_fields[0].trim();
@ -44,13 +47,13 @@ if ("domain_name" in rlgwebd_options && "keyfile" in rlgwebd_options &&
"certfile" in rlgwebd_options) "certfile" in rlgwebd_options)
rlgwebd_options["use_https"] = true; rlgwebd_options["use_https"] = true;
var clearbufs = [ const clearbufs = [
new Buffer([27, 91, 72, 27, 91, 50, 74]), // xterm: CSI H CSI 2J Buffer.from([27, 91, 72, 27, 91, 50, 74]), // xterm: CSI H CSI 2J
new Buffer([27, 91, 72, 27, 91, 74]) // screen: CSI H CSI J Buffer.from([27, 91, 72, 27, 91, 74]) // screen: CSI H CSI J
]; ];
/* Data on the games available. */ /* Data on the games available. */
var games = { const games = {
"rogue3": { "rogue3": {
"name": "Rogue V3", "name": "Rogue V3",
"uname": "rogue3", "uname": "rogue3",
@ -110,7 +113,7 @@ function BaseGame() {
this.watchers = []; this.watchers = [];
/* replaybuf holds the output since the last screen clear, so watchers can /* replaybuf holds the output since the last screen clear, so watchers can
* begin with a complete screen. replaylen is the number of bytes stored. */ * begin with a complete screen. replaylen is the number of bytes stored. */
this.replaybuf = new Buffer(1024); this.replaybuf = Buffer.alloc(1024);
this.replaylen = 0; this.replaylen = 0;
/* Time of last activity. */ /* Time of last activity. */
this.lasttime = new Date(); this.lasttime = new Date();
@ -126,12 +129,12 @@ BaseGame.prototype.tag = function () {
BaseGame.prototype.framepush = function(chunk) { BaseGame.prototype.framepush = function(chunk) {
/* If this chunk resets the screen, discard what preceded it. */ /* If this chunk resets the screen, discard what preceded it. */
if (isclear(chunk)) { if (isclear(chunk)) {
this.replaybuf = new Buffer(1024); this.replaybuf = Buffer.alloc(1024);
this.replaylen = 0; this.replaylen = 0;
} }
/* Make sure there's space. */ /* Make sure there's space. */
while (this.replaybuf.length < chunk.length + this.replaylen) { while (this.replaybuf.length < chunk.length + this.replaylen) {
var nbuf = new Buffer(this.replaybuf.length * 2); var nbuf = Buffer.alloc(this.replaybuf.length * 2);
this.replaybuf.copy(nbuf, 0, 0, this.replaylen); this.replaybuf.copy(nbuf, 0, 0, this.replaylen);
this.replaybuf = nbuf; this.replaybuf = nbuf;
if (this.replaybuf.length > 65536) { if (this.replaybuf.length > 65536) {
@ -211,7 +214,9 @@ function TermSession(gname, pname, wsReq) {
var progressdir = path.join("/dgldir/inprogress", this.gname); var progressdir = path.join("/dgldir/inprogress", this.gname);
this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec"); this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec");
var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n'; var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n';
fs.writeFile(this.lock, lmsg, "utf8"); fs.writeFile(this.lock, lmsg, "utf8", function (err) {
if (err) tslog("Locking failed: %s", err);
});
var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname,
ts + ".ttyrec"); ts + ".ttyrec");
this.record = fs.createWriteStream(ttyrec, { mode: 0664 }); this.record = fs.createWriteStream(ttyrec, { mode: 0664 });
@ -239,8 +244,8 @@ TermSession.prototype = new BaseGame();
/* Currently this also sends to the player and any watchers. */ /* Currently this also sends to the player and any watchers. */
TermSession.prototype.write_ttyrec = function (datastr) { TermSession.prototype.write_ttyrec = function (datastr) {
this.lasttime = new Date(); this.lasttime = new Date();
var buf = new Buffer(datastr); var buf = Buffer.from(datastr);
var chunk = new Buffer(buf.length + 12); var chunk = Buffer.alloc(buf.length + 12);
/* TTYREC headers */ /* TTYREC headers */
chunk.writeUInt32LE(Math.floor(this.lasttime.getTime() / 1000), 0); chunk.writeUInt32LE(Math.floor(this.lasttime.getTime() / 1000), 0);
chunk.writeUInt32LE(1000 * (this.lasttime.getTime() % 1000), 4); chunk.writeUInt32LE(1000 * (this.lasttime.getTime() % 1000), 4);
@ -274,7 +279,7 @@ TermSession.prototype.input_msg = function (message) {
if (hexstr.length % 2 != 0) { if (hexstr.length % 2 != 0) {
hexstr = hexstr.slice(0, -1); hexstr = hexstr.slice(0, -1);
} }
var keybuf = new Buffer(hexstr, "hex"); var keybuf = Buffer.from(hexstr, "hex");
this.write(keybuf); this.write(keybuf);
} }
}; };
@ -287,7 +292,9 @@ TermSession.prototype.close = function () {
TermSession.prototype.destroy = function () { TermSession.prototype.destroy = function () {
var tag = this.tag(); var tag = this.tag();
fs.unlink(this.lock); fs.unlink(this.lock, function (err) {
if (err) tslog("Lock removal failed: %s", err);
});
this.record.end(); this.record.end();
var watchsocks = this.watchers; var watchsocks = this.watchers;
this.watchers = []; this.watchers = [];
@ -356,7 +363,7 @@ DglSession.prototype.startchunk = function () {
if (this.reading) if (this.reading)
return; return;
this.reading = true; this.reading = true;
var header = new Buffer(12); var header = Buffer.alloc(12);
fs.read(this.fd, header, 0, 12, this.rpos, this.datachunk.bind(this)); fs.read(this.fd, header, 0, 12, this.rpos, this.datachunk.bind(this));
}; };
@ -377,7 +384,7 @@ DglSession.prototype.datachunk = function (err, n, buf) {
// Something is probably wrong... // Something is probably wrong...
tslog("DGL %s: looking for %d bytes", this.tag(), datalen); tslog("DGL %s: looking for %d bytes", this.tag(), datalen);
} }
var databuf = new Buffer(datalen); var databuf = Buffer.alloc(datalen);
fs.read(this.fd, databuf, 0, datalen, this.rpos, this.handledata.bind(this)); fs.read(this.fd, databuf, 0, datalen, this.rpos, this.handledata.bind(this));
}; };
@ -394,9 +401,9 @@ DglSession.prototype.handledata = function (err, n, buf) {
/* Process the data */ /* Process the data */
this.framepush(buf); this.framepush(buf);
var wmsg = JSON.stringify({"t": "d", "d": buf.toString("hex")}); var wmsg = JSON.stringify({"t": "d", "d": buf.toString("hex")});
for (var i = 0; i < this.watchers.length; i++) { for (let watcher of this.watchers) {
if (this.watchers[i].connected) if (watcher.connected)
this.watchers[i].sendUTF(wmsg); watcher.sendUTF(wmsg);
} }
this.emit("data", buf); this.emit("data", buf);
/* Recurse. */ /* Recurse. */
@ -411,6 +418,8 @@ DglSession.prototype.notifier = function (ev, finame) {
}; };
DglSession.prototype.close = function () { DglSession.prototype.close = function () {
/* The watcher might not be open yet. */
if ("recwatcher" in this)
this.recwatcher.close(); this.recwatcher.close();
/* Ensure all data is handled before quitting. */ /* Ensure all data is handled before quitting. */
this.startchunk(); this.startchunk();
@ -420,7 +429,9 @@ DglSession.prototype.close = function () {
if (connlist[i].connected) if (connlist[i].connected)
connlist[i].close(); connlist[i].close();
} }
fs.close(this.fd); fs.close(this.fd, function (err) {
if (err) tslog("PTY close failed: %s", err);
});
this.emit("close"); this.emit("close");
gamemux.emit('end', this.gname, this.pname); gamemux.emit('end', this.gname, this.pname);
tslog("DGL %s: closed", this.tag()); tslog("DGL %s: closed", this.tag());
@ -487,8 +498,11 @@ function checksaved(user, game, callback, args) {
var savedirc = game.uname + "save"; var savedirc = game.uname + "save";
var basename = String(pwent.uid) + "-" + user + game.suffix; var basename = String(pwent.uid) + "-" + user + game.suffix;
var savefile = path.join("/var/games/roguelike", savedirc, basename); var savefile = path.join("/var/games/roguelike", savedirc, basename);
fs.exists(savefile, function (exist) { fs.access(savefile, function (err) {
args.unshift(exist); if (err)
args.unshift(false);
else
args.unshift(true);
callback.apply(null, args); callback.apply(null, args);
}); });
} }
@ -567,8 +581,8 @@ function bufncmp(buf1, buf2, n) {
} }
function isclear(buf) { function isclear(buf) {
for (var i = 0; i < clearbufs.length; i++) { for (let clearer of clearbufs) {
if (bufncmp(buf, clearbufs[i], clearbufs[i].length)) if (bufncmp(buf, clearer, clearer.length))
return true; return true;
} }
return false; return false;
@ -688,10 +702,15 @@ function login(req, res, formdata) {
function regsetup(username) { function regsetup(username) {
function regsetup_l2(err) { function regsetup_l2(err) {
for (var g in games) { for (var g in games) {
fs.mkdir(path.join("/dgldir/ttyrec", username, games[g].uname), 0755); fs.mkdir(path.join("/dgldir/ttyrec", username, games[g].uname), 0755,
function (err) {
if (err) tslog("ttyrec mkdir failed: %s", err);
});
} }
} }
fs.mkdir(path.join("/dgldir/userdata", username), 0755); fs.mkdir(path.join("/dgldir/userdata", username), 0755, function (err) {
if (err) tslog("Userdata mkdir failed: %s", err);
});
fs.mkdir(path.join("/dgldir/ttyrec/", username), 0755, regsetup_l2); fs.mkdir(path.join("/dgldir/ttyrec/", username), 0755, regsetup_l2);
} }
@ -779,7 +798,9 @@ function stopgame(res, formdata) {
if (err.code == "ESRCH") { if (err.code == "ESRCH") {
var nodere = RegExp("^" + pname + ":node:"); var nodere = RegExp("^" + pname + ":node:");
if (fname.match(nodere)) { if (fname.match(nodere)) {
fs.unlink(fullfile); fs.unlink(fullfile, function (err) {
if (err) tslog("Stale lock removal failed: %s", err);
});
} }
} }
} }
@ -832,6 +853,8 @@ function startProgressWatcher() {
} }
function serveStatic(req, res, fname) { function serveStatic(req, res, fname) {
if (fname[0] !== "/")
fname = "/" + fname;
var nname = path.normalize(fname); var nname = path.normalize(fname);
if (nname == "" || nname == "/") if (nname == "" || nname == "/")
nname = "index.html"; nname = "index.html";
@ -839,9 +862,9 @@ function serveStatic(req, res, fname) {
path.join(nname, "index.html"); /* it was a directory */ path.join(nname, "index.html"); /* it was a directory */
var realname = path.join(rlgwebd_options.static_root, nname); var realname = path.join(rlgwebd_options.static_root, nname);
var extension = path.extname(realname); var extension = path.extname(realname);
fs.exists(realname, function (exists) { fs.access(realname, function (access_err) {
var resheaders = {}; var resheaders = {};
if (!exists || !extension || extension == ".html") if (access_err || !extension || extension == ".html")
resheaders["Content-Type"] = "text/html; charset=utf-8"; resheaders["Content-Type"] = "text/html; charset=utf-8";
else if (extension == ".png") else if (extension == ".png")
resheaders["Content-Type"] = "image/png"; resheaders["Content-Type"] = "image/png";
@ -853,7 +876,7 @@ function serveStatic(req, res, fname) {
resheaders["Content-Type"] = "image/svg+xml"; resheaders["Content-Type"] = "image/svg+xml";
else else
resheaders["Content-Type"] = "application/octet-stream"; resheaders["Content-Type"] = "application/octet-stream";
if (exists) { if (!access_err) {
fs.readFile(realname, function (error, data) { fs.readFile(realname, function (error, data) {
if (error) { if (error) {
res.writeHead(500, {}); res.writeHead(500, {});
@ -1024,10 +1047,6 @@ function setuinfo(req, res, postdata) {
} }
} }
var errorcodes = [ "Generic Error", "Not logged in", "Invalid data",
"Login failed", "Already playing", "Game launch failed",
"Server shutting down", "Game not in progress" ];
function sendError(res, ecode, msg, box) { function sendError(res, ecode, msg, box) {
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
var edict = {"t": "E"}; var edict = {"t": "E"};
@ -1264,6 +1283,21 @@ if (rlgwebd_options.use_https) {
tls_options.ca = read_or_die(rlgwebd_options.cafile, "CA file"); tls_options.ca = read_or_die(rlgwebd_options.cafile, "CA file");
}; };
/* Make sure the socket directory is secure. */
var socket_dir = path.dirname(rlgwebd_options.control_socket);
try {
fs.mkdirSync(socket_dir, 0o700);
}
catch (err) {
if (err.code == "EEXIST") {
fs.chownSync(socket_dir, 0, 0);
fs.chmodSync(socket_dir, 0o700);
}
else {
throw err;
}
}
/* Open the control socket before chrooting where it can't be found */ /* Open the control socket before chrooting where it can't be found */
var ctlServer = net.createServer(function (sock) { var ctlServer = net.createServer(function (sock) {
sock.on('data', consoleHandler); sock.on('data', consoleHandler);
@ -1288,19 +1322,21 @@ ctlServer.listen(rlgwebd_options.control_socket, function () {
tslog("Could not drop permissions: %s", err); tslog("Could not drop permissions: %s", err);
process.exit(1); process.exit(1);
} }
if (rlgwebd_options.use_https) {
httpServer = https.createServer(tls_options, webHandler);
httpServer.listen(rlgwebd_options.port);
tslog('rlgwebd running on port %d (TLS)', rlgwebd_options.port);
wsServer = new WebSocketServer({"httpServer": httpServer});
wsServer.on("request", wsHandler);
tslog('Secure WebSockets are online');
}
else {
httpServer = http.createServer(webHandler); httpServer = http.createServer(webHandler);
httpServer.listen(rlgwebd_options.http_port); httpServer.listen(rlgwebd_options.port);
tslog('rlgwebd running on port %d', rlgwebd_options.http_port); tslog('rlgwebd running on port %d', rlgwebd_options.port);
wsServer = new WebSocketServer({"httpServer": httpServer}); wsServer = new WebSocketServer({"httpServer": httpServer});
wsServer.on("request", wsHandler); wsServer.on("request", wsHandler);
tslog('WebSockets are online'); tslog('WebSockets are online');
if (rlgwebd_options.use_https) {
var httpsServer = https.createServer(tls_options, webHandler);
httpsServer.listen(rlgwebd_options.https_port);
tslog('TLS running on port %d', rlgwebd_options.https_port);
var wssServer = new WebSocketServer({"httpServer": httpsServer});
wssServer.on("request", wsHandler);
tslog('Secure WebSockets are online');
} }
progressWatcher = startProgressWatcher(); progressWatcher = startProgressWatcher();
setInterval(pushStatus, 40000); setInterval(pushStatus, 40000);

View file

@ -1,22 +1,19 @@
#!/usr/bin/env node #!/usr/bin/env node
var net = require('net'); var net = require('net');
var domain = require('domain'); var sockpath = "/var/run/rlgwebd/rlgwebd.sock";
var sockpath = "/var/run/rlgwebd.sock";
var dom = domain.create(); var sock = new net.Socket();
dom.on('error', function (err) { sock.on('error', function (err) {
console.log("Cannot connect to " + sockpath + ", rlgwebd already stopped."); console.log("Cannot connect to " + sockpath + ", rlgwebd already stopped.");
process.exit(0); process.exit(0);
}); });
dom.run(function () { sock.connect(sockpath, function () {
var sock = net.connect(sockpath, function () { sock.on('close', function (had_error) {
sock.on('close', function () {
if (process.argv[2] == "debug") if (process.argv[2] == "debug")
console.log("Control socket closed"); console.log("Control socket closed");
}); });
sock.write("quit\n"); sock.write("quit\n");
}); });
});

View file

@ -3,11 +3,9 @@
# These values are set by default: # These values are set by default:
# Location of the socket for start/stop commands # Location of the socket for start/stop commands
#control_socket = /var/run/rlgwebd.sock #control_socket = /var/run/rlgwebd/rlgwebd.sock
# Port number to bind # Port number to bind
#http_port = 8080 #port = 8080
# Port number for HTTPS
#https_port = 8081
# Path to the dgamelaunch installation to chroot into # Path to the dgamelaunch installation to chroot into
# If you change this, change the Makefile too # If you change this, change the Makefile too
#chrootDir = /var/dgl/ #chrootDir = /var/dgl/

View file

@ -4,7 +4,7 @@ After=network.target syslog.target
[Service] [Service]
Type=simple Type=simple
Environment=NODE_PATH=/usr/lib/node_modules Environment=NODE_PATH=/var/local/lib/node_modules
ExecStart=/usr/local/bin/rlgwebd ExecStart=/usr/local/bin/rlgwebd
ExecStop=/usr/local/bin/rlgwebd-stop ExecStop=/usr/local/bin/rlgwebd-stop
Restart=on-failure Restart=on-failure

View file

@ -71,6 +71,7 @@ var termemu = {
scrB: 0, // init() will set this properly scrB: 0, // init() will set this properly
c: null, // Contains cursor position and text attributes c: null, // Contains cursor position and text attributes
offedge: false, // Going off the edge doesn't mean adding a new line offedge: false, // Going off the edge doesn't mean adding a new line
lastcode: 0, // Last printed character
clearAttrs: function () { clearAttrs: function () {
/* Make sure to reset ALL attribute properties and NOTHING else. */ /* Make sure to reset ALL attribute properties and NOTHING else. */
this.c.bold = false; this.c.bold = false;
@ -460,6 +461,7 @@ var termemu = {
this.screen.replaceChild(this.makeRow(), this.screen.childNodes[i]); this.screen.replaceChild(this.makeRow(), this.screen.childNodes[i]);
} }
this.flipCursor(); // make it appear in the new row this.flipCursor(); // make it appear in the new row
this.lastcode = 0;
return; return;
}, },
write: function (codes) { write: function (codes) {
@ -531,6 +533,10 @@ var termemu = {
debug(1, "Unrecognized sequence ESC " + codes[i].toString(16)); debug(1, "Unrecognized sequence ESC " + codes[i].toString(16));
this.comseq = []; this.comseq = [];
} }
if (this.comseq.length == 0) {
// A complete sequence was processed, clear lastcode.
this.lastcode = 0;
}
} }
else if (this.comseq.length == 2 && this.comseq[0] == 27) { else if (this.comseq.length == 2 && this.comseq[0] == 27) {
/* An ESC C N sequence. Not implemented. Doesn't check validity /* An ESC C N sequence. Not implemented. Doesn't check validity
@ -555,6 +561,7 @@ var termemu = {
String.fromCharCode(this.comseq[1]) + " 0x" + String.fromCharCode(this.comseq[1]) + " 0x" +
codes[i].toString(16)); codes[i].toString(16));
this.comseq = []; this.comseq = [];
this.lastcode = 0;
} }
else if (this.comseq[0] == 157) { else if (this.comseq[0] == 157) {
/* Commands beginning with OSC */ /* Commands beginning with OSC */
@ -566,6 +573,7 @@ var termemu = {
debug(0, "Got " + (this.comseq.length - 1) + "-byte OSC sequence"); debug(0, "Got " + (this.comseq.length - 1) + "-byte OSC sequence");
this.oscProcess(); this.oscProcess();
this.comseq = []; this.comseq = [];
this.lastcode = 0;
} }
else else
this.comseq.push(codes[i]); this.comseq.push(codes[i]);
@ -582,15 +590,17 @@ var termemu = {
/* Chars in csiPre can only occur right after the CSI */ /* Chars in csiPre can only occur right after the CSI */
debug(1, "Invalid CSI sequence: misplaced prefix"); debug(1, "Invalid CSI sequence: misplaced prefix");
this.comseq = []; this.comseq = [];
this.lastcode = 0;
} }
else else
this.comseq.push(codes[i]); this.comseq.push(codes[i]);
} }
else if (csiPost.indexOf(this.comseq[this.comseq.length - 1]) >= 0 && else if (csiPost.indexOf(this.comseq[this.comseq.length - 1]) >= 0 &&
!csiFinal(codes[i])) { !csiFinal(codes[i])) {
/* Chars is csiPost must come right before the final char */ /* Chars in csiPost must come right before the final char */
debug(1, "Invalid CSI sequence: misplaced postfix"); debug(1, "Invalid CSI sequence: misplaced postfix");
this.comseq = []; this.comseq = [];
this.lastcode = 0;
} }
else if ((codes[i] >= 48 && codes[i] <= 57) || codes[i] == 59 || else if ((codes[i] >= 48 && codes[i] <= 57) || codes[i] == 59 ||
csiPost.indexOf(codes[i]) >= 0) { csiPost.indexOf(codes[i]) >= 0) {
@ -605,30 +615,49 @@ var termemu = {
else { else {
debug(1, "Invalid CSI sequence: unknown code " + codes[i].toString(16)); debug(1, "Invalid CSI sequence: unknown code " + codes[i].toString(16));
this.comseq = []; this.comseq = [];
this.lastcode = 0;
} }
} }
else { else {
debug(1, "Unknown sequence with " + this.comseq[0].toString(16)); debug(1, "Unknown sequence with " + this.comseq[0].toString(16));
this.comseq = []; this.comseq = [];
this.lastcode = 0;
} }
continue;
} }
/* Treat it as a single character. */ else if ((codes[i] >= 32 && codes[i] < 127) || codes[i] >= 160) {
if (codes[i] == 5) { /* If it's ASCII, it's printable; take a risk on anything higher */
if ((this.c.cset == "0") && (codes[i] in decChars)) {
// DEC special character set
this.lastcode = decChars[codes[i]];
}
else {
this.lastcode = codes[i];
}
this.placechar(String.fromCharCode(this.lastcode));
}
else {
/* Treat it as a single control character. */
this.singleCtl(codes[i]);
}
}
return;
},
singleCtl: function (ctlcode) {
if (ctlcode == 5) {
sendback("06"); sendback("06");
} }
else if (codes[i] == 7) { else if (ctlcode == 7) {
/* bell */ /* bell */
bell(true); bell(true);
} }
else if (codes[i] == 8) { else if (ctlcode == 8) {
/* backspace */ /* backspace */
if (this.offedge) if (this.offedge)
this.offedge = false; this.offedge = false;
else if (this.c.x > 0) else if (this.c.x > 0)
this.cmove(null, this.c.x - 1); this.cmove(null, this.c.x - 1);
} }
else if (codes[i] == 9) { else if (ctlcode == 9) {
/* tab */ /* tab */
var xnew; var xnew;
if (this.c.x < this.w - 1) { if (this.c.x < this.w - 1) {
@ -641,55 +670,47 @@ var termemu = {
this.offedge = true; this.offedge = true;
} }
} }
else if (codes[i] >= 10 && codes[i] <= 12) { else if (ctlcode >= 10 && ctlcode <= 12) {
/* newline, vertical tab, form feed */ /* newline, vertical tab, form feed */
if (this.offedge) if (this.offedge)
this.newline(true); this.newline(true);
else else
this.newline(false); this.newline(false);
} }
else if (codes[i] == 13) { else if (ctlcode == 13) {
/* carriage return \r */ /* carriage return \r */
this.cmove(null, 0); this.cmove(null, 0);
} }
else if (codes[i] == 14) { else if (ctlcode == 14) {
/* shift out */ /* shift out */
// Currently assuming that G1 is DEC Special & Line Drawing // Currently assuming that G1 is DEC Special & Line Drawing
this.c.cset = "0"; this.c.cset = "0";
debug(0, "Using DEC graphics charset."); debug(0, "Using DEC graphics charset.");
} }
else if (codes[i] == 15) { else if (ctlcode == 15) {
/* shift in */ /* shift in */
// Currently assuming that G0 is ASCII // Currently assuming that G0 is ASCII
this.c.cset = "B"; this.c.cset = "B";
debug(0, "Using ASCII charset."); debug(0, "Using ASCII charset.");
} }
else if (codes[i] == 27) { else if (ctlcode == 27) {
/* escape */ /* escape */
this.comseq.push(codes[i]); this.comseq.push(27);
}
else if (codes[i] < 32 || (codes[i] >= 127 && codes[i] < 160)) {
/* Some kind of control character. */
debug(1, "Unprintable character 0x" + codes[i].toString(16));
} }
else { else {
/* If it's ASCII, it's printable; take a risk on anything higher */ debug(1, "Unprintable character 0x" + ctlcode.toString(16));
if ((this.c.cset == "0") && (codes[i] in decChars)) {
// DEC special character set
this.placechar(String.fromCharCode(decChars[codes[i]]));
} }
else { if (ctlcode != 27) {
this.placechar(String.fromCharCode(codes[i])); // Sequences should preserve lastcode until they are completed
this.lastcode = 0;
} }
}
}
return;
}, },
csiProcess: function () { csiProcess: function () {
/* Processes the CSI sequence in this.comseq */ /* Processes the CSI sequence in this.comseq */
var c = this.comseq[this.comseq.length - 1]; var c = this.comseq[this.comseq.length - 1];
if (this.comseq[0] != 155 || !csiFinal(c)) if (this.comseq[0] != 155 || !csiFinal(c))
return; return;
var printed = false;
var comstr = ""; var comstr = "";
for (var i = 1; i < this.comseq.length; i++) for (var i = 1; i < this.comseq.length; i++)
comstr += String.fromCharCode(this.comseq[i]); comstr += String.fromCharCode(this.comseq[i]);
@ -698,6 +719,7 @@ var termemu = {
var matchCSI = comstr.match(reCSI); var matchCSI = comstr.match(reCSI);
if (!matchCSI) { if (!matchCSI) {
debug(1, "Unrecognized CSI sequence: " + comstr); debug(1, "Unrecognized CSI sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
var prefix = null; var prefix = null;
@ -725,6 +747,7 @@ var termemu = {
/* @ - insert spaces at cursor */ /* @ - insert spaces at cursor */
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI @ sequence: " + comstr); debug(1, "Invalid CSI @ sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
/* The cursor stays still, but characters move out from under it. */ /* The cursor stays still, but characters move out from under it. */
@ -745,6 +768,7 @@ var termemu = {
/* E - next line, F - previous line, G - to column */ /* E - next line, F - previous line, G - to column */
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI sequence: " + comstr); debug(1, "Invalid CSI sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
/* These may be out of range, but cmove will take care of that. */ /* These may be out of range, but cmove will take care of that. */
@ -769,6 +793,7 @@ var termemu = {
var y = 0; var y = 0;
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI H sequence: " + comstr); debug(1, "Invalid CSI H sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
if (params[0]) if (params[0])
@ -787,6 +812,7 @@ var termemu = {
var x = this.c.x; var x = this.c.x;
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI I sequence: " + comstr); debug(1, "Invalid CSI I sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
while (count > 0) { while (count > 0) {
@ -808,6 +834,7 @@ var termemu = {
debug(1, "Warning: CSI ?J not implemented"); debug(1, "Warning: CSI ?J not implemented");
else if (prefix || postfix) { else if (prefix || postfix) {
debug(1, "Invalid CSI J sequence: " + comstr); debug(1, "Invalid CSI J sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
if (!params[0]) { if (!params[0]) {
@ -828,6 +855,7 @@ var termemu = {
} }
else { else {
debug(1, "Unimplemented parameter in CSI J sequence: " + comstr); debug(1, "Unimplemented parameter in CSI J sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
for (var nrow = start; nrow <= end; nrow++) { for (var nrow = start; nrow <= end; nrow++) {
@ -855,6 +883,7 @@ var termemu = {
debug(1, "Warning: CSI ?K not implemented"); debug(1, "Warning: CSI ?K not implemented");
else if (prefix || postfix) { else if (prefix || postfix) {
debug(1, "Invalid CSI K sequence: " + comstr); debug(1, "Invalid CSI K sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
/* 0 (default): right, 1: left, 2: all. Include cursor position. */ /* 0 (default): right, 1: left, 2: all. Include cursor position. */
@ -885,11 +914,14 @@ var termemu = {
* M - delete current lines */ * M - delete current lines */
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI sequence: " + comstr); debug(1, "Invalid CSI sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
/* CSI LM have no effect outside of the scrolling region */ /* CSI LM have no effect outside of the scrolling region */
if (this.c.y < this.scrT || this.c.y > this.scrB) if (this.c.y < this.scrT || this.c.y > this.scrB) {
this.lastcode = 0;
return; return;
}
this.flipCursor(); this.flipCursor();
while (count > 0) { while (count > 0) {
var blankrow = this.makeRow(); var blankrow = this.makeRow();
@ -915,6 +947,7 @@ var termemu = {
/* P - delete at active position, causing cells on the right to shift. */ /* P - delete at active position, causing cells on the right to shift. */
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI P sequence: " + comstr); debug(1, "Invalid CSI P sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
var cursrow = this.screen.childNodes[this.c.y]; var cursrow = this.screen.childNodes[this.c.y];
@ -930,6 +963,7 @@ var termemu = {
/* S - scroll up, T - scroll down */ /* S - scroll up, T - scroll down */
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI sequence: " + comstr); debug(1, "Invalid CSI sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
if (c == 83) if (c == 83)
@ -941,6 +975,7 @@ var termemu = {
/* X - erase characters */ /* X - erase characters */
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI sequence: " + comstr); debug(1, "Invalid CSI sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
var row = this.screen.childNodes[this.c.y]; var row = this.screen.childNodes[this.c.y];
@ -954,6 +989,7 @@ var termemu = {
var x = this.c.x; var x = this.c.x;
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI Z sequence: " + comstr); debug(1, "Invalid CSI Z sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
while (count > 0) { while (count > 0) {
@ -970,14 +1006,26 @@ var termemu = {
/* ` - go to col */ /* ` - go to col */
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI ` sequence: " + comstr); debug(1, "Invalid CSI ` sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
this.cmove(null, count - 1); this.cmove(null, count - 1);
} }
else if (c == 98) {
/* b - repeat previous character */
if (this.lastcode !== 0) {
while (count > 0) {
this.placechar(String.fromCharCode(this.lastcode));
count--;
}
printed = true;
}
}
else if (c == 99) { else if (c == 99) {
/* c - query terminal attributes */ /* c - query terminal attributes */
if (prefix !== null) { if (prefix !== null) {
debug(1, "Unimplemented CSI sequence: " + comstr); debug(1, "Unimplemented CSI sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
/* "CSI ? 1 ; 2 c" - VT100 */ /* "CSI ? 1 ; 2 c" - VT100 */
@ -987,6 +1035,7 @@ var termemu = {
/* d - go to row */ /* d - go to row */
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI d sequence: " + comstr); debug(1, "Invalid CSI d sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
this.cmove(count - 1, null); this.cmove(count - 1, null);
@ -997,6 +1046,7 @@ var termemu = {
var y = 0; var y = 0;
if (prefix || postfix) { if (prefix || postfix) {
debug(1, "Invalid CSI f sequence: " + comstr); debug(1, "Invalid CSI f sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
if (params[0]) if (params[0])
@ -1009,6 +1059,7 @@ var termemu = {
/* h - set modes */ /* h - set modes */
if (prefix != '?') { if (prefix != '?') {
debug(1, "Unimplemented CSI sequence: " + comstr); debug(1, "Unimplemented CSI sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
for (var i = 0; i < params.length; i++) { for (var i = 0; i < params.length; i++) {
@ -1047,6 +1098,7 @@ var termemu = {
} }
else { else {
debug(1, "Unimplemented CSI sequence: " + comstr); debug(1, "Unimplemented CSI sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
} }
@ -1080,6 +1132,7 @@ var termemu = {
/* m - character attributes */ /* m - character attributes */
if (prefix !== null) { if (prefix !== null) {
debug(1, "Unimplemented CSI sequence: " + comstr); debug(1, "Unimplemented CSI sequence: " + comstr);
this.lastcode = 0;
return; return;
} }
if (params.length == 0) if (params.length == 0)
@ -1133,15 +1186,18 @@ var termemu = {
t = params[0] - 1; t = params[0] - 1;
if (params[1] && params[1] <= this.h) if (params[1] && params[1] <= this.h)
b = params[1] - 1; b = params[1] - 1;
if (b <= t) if (b > t) {
return;
this.scrT = t; this.scrT = t;
this.scrB = b; this.scrB = b;
this.cmove(0, 0); this.cmove(0, 0);
} }
}
else { else {
debug(1, "Unimplemented CSI sequence: " + comstr); debug(1, "Unimplemented CSI sequence: " + comstr);
} }
if (!printed) {
this.lastcode = 0;
}
return; return;
}, },
oscProcess: function () { oscProcess: function () {