diff --git a/Makefile b/Makefile index a98c561..466012e 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ install: all mkdir -p ${CHROOT}/var/www cp ${WEBASSETS} ${CHROOT}/var/www 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. uninstall: diff --git a/README.txt b/README.txt index 391ef53..4098765 100644 --- a/README.txt +++ b/README.txt @@ -5,12 +5,11 @@ browser. It is intended to be compatible with dgamelaunch. 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 expects them to be installed in the global location, which is -"/usr/lib/node_modules". It is planned to eventually use a different -location so that npm will not need to run as root. +It requires the 'posix', 'node-pty', and 'websocket' modules. Currently, +it expects them to be installed in "/var/local/lib/node_modules". It +is not recommended to run npm as root when installing the modules. init --- @@ -22,14 +21,11 @@ a proper initscript, but it could form the basis of one. Configuration --- -You can set some options by changing some variables in the first few -lines of the rlgwebd script: +A configuration file is installed at /etc/rlgwebd.conf. It contains a +list of options. -Option Variable Default - -Chroot path chrootDir /var/dgl -Username dropToUser rodney -Server port httpPort 8080 +If the domain_name option and the SSL-related options are set, rlgwebd +will use HTTPS instead of insecure HTTP. If you change the chroot location, change it in the first line of the Makefile too. @@ -59,6 +55,7 @@ Running "make install" will: Copy the C programs and the libraries they need into the chroot Install the main RLGWebD script in /usr/local/bin 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, you will have to edit the Makefile. diff --git a/initscript b/initscript index 5b311b9..9df71c7 100644 --- a/initscript +++ b/initscript @@ -1,8 +1,8 @@ #!/bin/sh -NODE_PATH=/usr/lib/node_modules +NODE_PATH=/var/local/lib/node_modules LOGFILE=/var/log/rlgwebd.log -CTLSOCKET=/var/run/rlgwebd.sock +CTLSOCKET=/var/run/rlgwebd/rlgwebd.sock RLGWEBDJS=/usr/local/bin/rlgwebd export NODE_PATH diff --git a/rlgwebd b/rlgwebd index 9239d17..855caa8 100755 --- a/rlgwebd +++ b/rlgwebd @@ -1,35 +1,38 @@ #!/usr/bin/env node -var http = require('http'); -var https = require('https'); -var net = require('net'); -var url = require('url'); -var path = require('path'); -var fs = require('fs'); -var events = require('events'); -var child_process = require('child_process'); +const http = require('http'); +const https = require('https'); +const net = require('net'); +const url = require('url'); +const path = require('path'); +const fs = require('fs'); +const events = require('events'); +const child_process = require('child_process'); // Dependencies -var posix = require("posix"); -var pty = require("pty.js"); -var WebSocketServer = require("websocket").server; +const posix = require("posix"); +const pty = require("node-pty"); +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 */ var rlgwebd_options = { - control_socket: "/var/run/rlgwebd.sock", - http_port: 8080, - https_port: 8081, + control_socket: "/var/run/rlgwebd/rlgwebd.sock", + port: 8080, chrootDir: "/var/dgl/", username: "rodney", static_root: "/var/www/" }; /* 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'); -for (var i = 0; i < config_lines.length; i++) { - if (config_lines[i].length > 0 && config_lines[i][0] != '#') { - var config_fields = config_lines[i].split('='); +for (let conf_line of config_lines) { + if (conf_line.length > 0 && conf_line[0] != '#') { + var config_fields = conf_line.split('='); if (config_fields.length < 2) continue; 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) rlgwebd_options["use_https"] = true; -var clearbufs = [ - new Buffer([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 +const clearbufs = [ + Buffer.from([27, 91, 72, 27, 91, 50, 74]), // xterm: CSI H CSI 2J + Buffer.from([27, 91, 72, 27, 91, 74]) // screen: CSI H CSI J ]; /* Data on the games available. */ -var games = { +const games = { "rogue3": { "name": "Rogue V3", "uname": "rogue3", @@ -110,7 +113,7 @@ function BaseGame() { this.watchers = []; /* replaybuf holds the output since the last screen clear, so watchers can * 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; /* Time of last activity. */ this.lasttime = new Date(); @@ -126,12 +129,12 @@ BaseGame.prototype.tag = function () { BaseGame.prototype.framepush = function(chunk) { /* If this chunk resets the screen, discard what preceded it. */ if (isclear(chunk)) { - this.replaybuf = new Buffer(1024); + this.replaybuf = Buffer.alloc(1024); this.replaylen = 0; } /* Make sure there's space. */ 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 = nbuf; if (this.replaybuf.length > 65536) { @@ -211,7 +214,9 @@ function TermSession(gname, pname, wsReq) { var progressdir = path.join("/dgldir/inprogress", this.gname); this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec"); 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, ts + ".ttyrec"); 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. */ TermSession.prototype.write_ttyrec = function (datastr) { this.lasttime = new Date(); - var buf = new Buffer(datastr); - var chunk = new Buffer(buf.length + 12); + var buf = Buffer.from(datastr); + var chunk = Buffer.alloc(buf.length + 12); /* TTYREC headers */ chunk.writeUInt32LE(Math.floor(this.lasttime.getTime() / 1000), 0); chunk.writeUInt32LE(1000 * (this.lasttime.getTime() % 1000), 4); @@ -274,7 +279,7 @@ TermSession.prototype.input_msg = function (message) { if (hexstr.length % 2 != 0) { hexstr = hexstr.slice(0, -1); } - var keybuf = new Buffer(hexstr, "hex"); + var keybuf = Buffer.from(hexstr, "hex"); this.write(keybuf); } }; @@ -287,7 +292,9 @@ TermSession.prototype.close = function () { TermSession.prototype.destroy = function () { 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(); var watchsocks = this.watchers; this.watchers = []; @@ -356,7 +363,7 @@ DglSession.prototype.startchunk = function () { if (this.reading) return; 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)); }; @@ -377,7 +384,7 @@ DglSession.prototype.datachunk = function (err, n, buf) { // Something is probably wrong... 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)); }; @@ -394,9 +401,9 @@ DglSession.prototype.handledata = function (err, n, buf) { /* Process the data */ this.framepush(buf); var wmsg = JSON.stringify({"t": "d", "d": buf.toString("hex")}); - for (var i = 0; i < this.watchers.length; i++) { - if (this.watchers[i].connected) - this.watchers[i].sendUTF(wmsg); + for (let watcher of this.watchers) { + if (watcher.connected) + watcher.sendUTF(wmsg); } this.emit("data", buf); /* Recurse. */ @@ -411,7 +418,9 @@ DglSession.prototype.notifier = function (ev, finame) { }; DglSession.prototype.close = function () { - this.recwatcher.close(); + /* The watcher might not be open yet. */ + if ("recwatcher" in this) + this.recwatcher.close(); /* Ensure all data is handled before quitting. */ this.startchunk(); var connlist = this.watchers; @@ -420,7 +429,9 @@ DglSession.prototype.close = function () { if (connlist[i].connected) connlist[i].close(); } - fs.close(this.fd); + fs.close(this.fd, function (err) { + if (err) tslog("PTY close failed: %s", err); + }); this.emit("close"); gamemux.emit('end', this.gname, this.pname); tslog("DGL %s: closed", this.tag()); @@ -487,8 +498,11 @@ function checksaved(user, game, callback, args) { var savedirc = game.uname + "save"; var basename = String(pwent.uid) + "-" + user + game.suffix; var savefile = path.join("/var/games/roguelike", savedirc, basename); - fs.exists(savefile, function (exist) { - args.unshift(exist); + fs.access(savefile, function (err) { + if (err) + args.unshift(false); + else + args.unshift(true); callback.apply(null, args); }); } @@ -567,8 +581,8 @@ function bufncmp(buf1, buf2, n) { } function isclear(buf) { - for (var i = 0; i < clearbufs.length; i++) { - if (bufncmp(buf, clearbufs[i], clearbufs[i].length)) + for (let clearer of clearbufs) { + if (bufncmp(buf, clearer, clearer.length)) return true; } return false; @@ -688,10 +702,15 @@ function login(req, res, formdata) { 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/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); } @@ -779,7 +798,9 @@ function stopgame(res, formdata) { if (err.code == "ESRCH") { var nodere = RegExp("^" + pname + ":node:"); 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) { + if (fname[0] !== "/") + fname = "/" + fname; var nname = path.normalize(fname); if (nname == "" || nname == "/") nname = "index.html"; @@ -839,9 +862,9 @@ function serveStatic(req, res, fname) { path.join(nname, "index.html"); /* it was a directory */ var realname = path.join(rlgwebd_options.static_root, nname); var extension = path.extname(realname); - fs.exists(realname, function (exists) { + fs.access(realname, function (access_err) { var resheaders = {}; - if (!exists || !extension || extension == ".html") + if (access_err || !extension || extension == ".html") resheaders["Content-Type"] = "text/html; charset=utf-8"; else if (extension == ".png") resheaders["Content-Type"] = "image/png"; @@ -853,7 +876,7 @@ function serveStatic(req, res, fname) { resheaders["Content-Type"] = "image/svg+xml"; else resheaders["Content-Type"] = "application/octet-stream"; - if (exists) { + if (!access_err) { fs.readFile(realname, function (error, data) { if (error) { 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) { res.writeHead(200, { "Content-Type": "application/json" }); var edict = {"t": "E"}; @@ -1264,6 +1283,21 @@ if (rlgwebd_options.use_https) { 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 */ var ctlServer = net.createServer(function (sock) { sock.on('data', consoleHandler); @@ -1288,20 +1322,22 @@ ctlServer.listen(rlgwebd_options.control_socket, function () { tslog("Could not drop permissions: %s", err); process.exit(1); } - httpServer = http.createServer(webHandler); - httpServer.listen(rlgwebd_options.http_port); - tslog('rlgwebd running on port %d', rlgwebd_options.http_port); - wsServer = new WebSocketServer({"httpServer": httpServer}); - wsServer.on("request", wsHandler); - 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); + 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.listen(rlgwebd_options.port); + tslog('rlgwebd running on port %d', rlgwebd_options.port); + wsServer = new WebSocketServer({"httpServer": httpServer}); + wsServer.on("request", wsHandler); + tslog('WebSockets are online'); + } progressWatcher = startProgressWatcher(); setInterval(pushStatus, 40000); }); diff --git a/rlgwebd-stop b/rlgwebd-stop index d81b5f5..5e81ea1 100755 --- a/rlgwebd-stop +++ b/rlgwebd-stop @@ -1,22 +1,19 @@ #!/usr/bin/env node var net = require('net'); -var domain = require('domain'); -var sockpath = "/var/run/rlgwebd.sock"; +var sockpath = "/var/run/rlgwebd/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."); process.exit(0); }); -dom.run(function () { - var sock = net.connect(sockpath, function () { - sock.on('close', function () { - if (process.argv[2] == "debug") - console.log("Control socket closed"); - }); - sock.write("quit\n"); +sock.connect(sockpath, function () { + sock.on('close', function (had_error) { + if (process.argv[2] == "debug") + console.log("Control socket closed"); }); + sock.write("quit\n"); }); diff --git a/rlgwebd.conf b/rlgwebd.conf index 5d67cd5..a2ccd44 100644 --- a/rlgwebd.conf +++ b/rlgwebd.conf @@ -3,11 +3,9 @@ # These values are set by default: # 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 -#http_port = 8080 -# Port number for HTTPS -#https_port = 8081 +#port = 8080 # Path to the dgamelaunch installation to chroot into # If you change this, change the Makefile too #chrootDir = /var/dgl/ diff --git a/rlgwebd.service b/rlgwebd.service index ec0ab3a..9014502 100644 --- a/rlgwebd.service +++ b/rlgwebd.service @@ -4,7 +4,7 @@ After=network.target syslog.target [Service] Type=simple -Environment=NODE_PATH=/usr/lib/node_modules +Environment=NODE_PATH=/var/local/lib/node_modules ExecStart=/usr/local/bin/rlgwebd ExecStop=/usr/local/bin/rlgwebd-stop Restart=on-failure diff --git a/termemu.js b/termemu.js index 5e4ad1a..0a4f74d 100644 --- a/termemu.js +++ b/termemu.js @@ -71,6 +71,7 @@ var termemu = { scrB: 0, // init() will set this properly c: null, // Contains cursor position and text attributes offedge: false, // Going off the edge doesn't mean adding a new line + lastcode: 0, // Last printed character clearAttrs: function () { /* Make sure to reset ALL attribute properties and NOTHING else. */ this.c.bold = false; @@ -460,6 +461,7 @@ var termemu = { this.screen.replaceChild(this.makeRow(), this.screen.childNodes[i]); } this.flipCursor(); // make it appear in the new row + this.lastcode = 0; return; }, write: function (codes) { @@ -531,6 +533,10 @@ var termemu = { debug(1, "Unrecognized sequence ESC " + codes[i].toString(16)); 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) { /* An ESC C N sequence. Not implemented. Doesn't check validity @@ -555,6 +561,7 @@ var termemu = { String.fromCharCode(this.comseq[1]) + " 0x" + codes[i].toString(16)); this.comseq = []; + this.lastcode = 0; } else if (this.comseq[0] == 157) { /* Commands beginning with OSC */ @@ -566,6 +573,7 @@ var termemu = { debug(0, "Got " + (this.comseq.length - 1) + "-byte OSC sequence"); this.oscProcess(); this.comseq = []; + this.lastcode = 0; } else this.comseq.push(codes[i]); @@ -582,15 +590,17 @@ var termemu = { /* Chars in csiPre can only occur right after the CSI */ debug(1, "Invalid CSI sequence: misplaced prefix"); this.comseq = []; + this.lastcode = 0; } else this.comseq.push(codes[i]); } else if (csiPost.indexOf(this.comseq[this.comseq.length - 1]) >= 0 && !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"); this.comseq = []; + this.lastcode = 0; } else if ((codes[i] >= 48 && codes[i] <= 57) || codes[i] == 59 || csiPost.indexOf(codes[i]) >= 0) { @@ -605,91 +615,102 @@ var termemu = { else { debug(1, "Invalid CSI sequence: unknown code " + codes[i].toString(16)); this.comseq = []; + this.lastcode = 0; } } else { debug(1, "Unknown sequence with " + this.comseq[0].toString(16)); this.comseq = []; - } - continue; - } - /* Treat it as a single character. */ - if (codes[i] == 5) { - sendback("06"); - } - else if (codes[i] == 7) { - /* bell */ - bell(true); - } - else if (codes[i] == 8) { - /* backspace */ - if (this.offedge) - this.offedge = false; - else if (this.c.x > 0) - this.cmove(null, this.c.x - 1); - } - else if (codes[i] == 9) { - /* tab */ - var xnew; - if (this.c.x < this.w - 1) { - xnew = 8 * (Math.floor(this.c.x / 8) + 1); - if (xnew >= this.w) - xnew = this.w - 1; - this.cmove(null, xnew); - } - else { - this.offedge = true; + this.lastcode = 0; } } - else if (codes[i] >= 10 && codes[i] <= 12) { - /* newline, vertical tab, form feed */ - if (this.offedge) - this.newline(true); - else - this.newline(false); - } - else if (codes[i] == 13) { - /* carriage return \r */ - this.cmove(null, 0); - } - else if (codes[i] == 14) { - /* shift out */ - // Currently assuming that G1 is DEC Special & Line Drawing - this.c.cset = "0"; - debug(0, "Using DEC graphics charset."); - } - else if (codes[i] == 15) { - /* shift in */ - // Currently assuming that G0 is ASCII - this.c.cset = "B"; - debug(0, "Using ASCII charset."); - } - else if (codes[i] == 27) { - /* escape */ - this.comseq.push(codes[i]); - } - 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 ((codes[i] >= 32 && codes[i] < 127) || codes[i] >= 160) { /* 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.placechar(String.fromCharCode(decChars[codes[i]])); + this.lastcode = decChars[codes[i]]; } else { - this.placechar(String.fromCharCode(codes[i])); + 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"); + } + else if (ctlcode == 7) { + /* bell */ + bell(true); + } + else if (ctlcode == 8) { + /* backspace */ + if (this.offedge) + this.offedge = false; + else if (this.c.x > 0) + this.cmove(null, this.c.x - 1); + } + else if (ctlcode == 9) { + /* tab */ + var xnew; + if (this.c.x < this.w - 1) { + xnew = 8 * (Math.floor(this.c.x / 8) + 1); + if (xnew >= this.w) + xnew = this.w - 1; + this.cmove(null, xnew); + } + else { + this.offedge = true; + } + } + else if (ctlcode >= 10 && ctlcode <= 12) { + /* newline, vertical tab, form feed */ + if (this.offedge) + this.newline(true); + else + this.newline(false); + } + else if (ctlcode == 13) { + /* carriage return \r */ + this.cmove(null, 0); + } + else if (ctlcode == 14) { + /* shift out */ + // Currently assuming that G1 is DEC Special & Line Drawing + this.c.cset = "0"; + debug(0, "Using DEC graphics charset."); + } + else if (ctlcode == 15) { + /* shift in */ + // Currently assuming that G0 is ASCII + this.c.cset = "B"; + debug(0, "Using ASCII charset."); + } + else if (ctlcode == 27) { + /* escape */ + this.comseq.push(27); + } + else { + debug(1, "Unprintable character 0x" + ctlcode.toString(16)); + } + if (ctlcode != 27) { + // Sequences should preserve lastcode until they are completed + this.lastcode = 0; + } + }, csiProcess: function () { /* Processes the CSI sequence in this.comseq */ var c = this.comseq[this.comseq.length - 1]; if (this.comseq[0] != 155 || !csiFinal(c)) return; + var printed = false; var comstr = ""; for (var i = 1; i < this.comseq.length; i++) comstr += String.fromCharCode(this.comseq[i]); @@ -698,6 +719,7 @@ var termemu = { var matchCSI = comstr.match(reCSI); if (!matchCSI) { debug(1, "Unrecognized CSI sequence: " + comstr); + this.lastcode = 0; return; } var prefix = null; @@ -725,6 +747,7 @@ var termemu = { /* @ - insert spaces at cursor */ if (prefix || postfix) { debug(1, "Invalid CSI @ sequence: " + comstr); + this.lastcode = 0; return; } /* 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 */ if (prefix || postfix) { debug(1, "Invalid CSI sequence: " + comstr); + this.lastcode = 0; return; } /* These may be out of range, but cmove will take care of that. */ @@ -769,6 +793,7 @@ var termemu = { var y = 0; if (prefix || postfix) { debug(1, "Invalid CSI H sequence: " + comstr); + this.lastcode = 0; return; } if (params[0]) @@ -787,6 +812,7 @@ var termemu = { var x = this.c.x; if (prefix || postfix) { debug(1, "Invalid CSI I sequence: " + comstr); + this.lastcode = 0; return; } while (count > 0) { @@ -808,6 +834,7 @@ var termemu = { debug(1, "Warning: CSI ?J not implemented"); else if (prefix || postfix) { debug(1, "Invalid CSI J sequence: " + comstr); + this.lastcode = 0; return; } if (!params[0]) { @@ -828,6 +855,7 @@ var termemu = { } else { debug(1, "Unimplemented parameter in CSI J sequence: " + comstr); + this.lastcode = 0; return; } for (var nrow = start; nrow <= end; nrow++) { @@ -855,6 +883,7 @@ var termemu = { debug(1, "Warning: CSI ?K not implemented"); else if (prefix || postfix) { debug(1, "Invalid CSI K sequence: " + comstr); + this.lastcode = 0; return; } /* 0 (default): right, 1: left, 2: all. Include cursor position. */ @@ -885,11 +914,14 @@ var termemu = { * M - delete current lines */ if (prefix || postfix) { debug(1, "Invalid CSI sequence: " + comstr); + this.lastcode = 0; return; } /* 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; + } this.flipCursor(); while (count > 0) { var blankrow = this.makeRow(); @@ -915,6 +947,7 @@ var termemu = { /* P - delete at active position, causing cells on the right to shift. */ if (prefix || postfix) { debug(1, "Invalid CSI P sequence: " + comstr); + this.lastcode = 0; return; } var cursrow = this.screen.childNodes[this.c.y]; @@ -930,6 +963,7 @@ var termemu = { /* S - scroll up, T - scroll down */ if (prefix || postfix) { debug(1, "Invalid CSI sequence: " + comstr); + this.lastcode = 0; return; } if (c == 83) @@ -941,6 +975,7 @@ var termemu = { /* X - erase characters */ if (prefix || postfix) { debug(1, "Invalid CSI sequence: " + comstr); + this.lastcode = 0; return; } var row = this.screen.childNodes[this.c.y]; @@ -954,6 +989,7 @@ var termemu = { var x = this.c.x; if (prefix || postfix) { debug(1, "Invalid CSI Z sequence: " + comstr); + this.lastcode = 0; return; } while (count > 0) { @@ -970,14 +1006,26 @@ var termemu = { /* ` - go to col */ if (prefix || postfix) { debug(1, "Invalid CSI ` sequence: " + comstr); + this.lastcode = 0; return; } 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) { /* c - query terminal attributes */ if (prefix !== null) { debug(1, "Unimplemented CSI sequence: " + comstr); + this.lastcode = 0; return; } /* "CSI ? 1 ; 2 c" - VT100 */ @@ -987,6 +1035,7 @@ var termemu = { /* d - go to row */ if (prefix || postfix) { debug(1, "Invalid CSI d sequence: " + comstr); + this.lastcode = 0; return; } this.cmove(count - 1, null); @@ -997,6 +1046,7 @@ var termemu = { var y = 0; if (prefix || postfix) { debug(1, "Invalid CSI f sequence: " + comstr); + this.lastcode = 0; return; } if (params[0]) @@ -1009,6 +1059,7 @@ var termemu = { /* h - set modes */ if (prefix != '?') { debug(1, "Unimplemented CSI sequence: " + comstr); + this.lastcode = 0; return; } for (var i = 0; i < params.length; i++) { @@ -1047,6 +1098,7 @@ var termemu = { } else { debug(1, "Unimplemented CSI sequence: " + comstr); + this.lastcode = 0; return; } } @@ -1080,6 +1132,7 @@ var termemu = { /* m - character attributes */ if (prefix !== null) { debug(1, "Unimplemented CSI sequence: " + comstr); + this.lastcode = 0; return; } if (params.length == 0) @@ -1133,15 +1186,18 @@ var termemu = { t = params[0] - 1; if (params[1] && params[1] <= this.h) b = params[1] - 1; - if (b <= t) - return; - this.scrT = t; - this.scrB = b; - this.cmove(0, 0); + if (b > t) { + this.scrT = t; + this.scrB = b; + this.cmove(0, 0); + } } else { debug(1, "Unimplemented CSI sequence: " + comstr); } + if (!printed) { + this.lastcode = 0; + } return; }, oscProcess: function () {