websockify/include/wstelnet.js

338 lines
9.4 KiB
JavaScript

/*
* WebSockets telnet client
* Copyright (C) 2011 Joel Martin
* Licensed under LGPL-3 (see LICENSE.txt)
*
* Includes VT100.js from:
* http://code.google.com/p/sshconsole
* Which was modified from:
* http://fzort.org/bi/o.php#vt100_js
*
* Telnet protocol:
* http://www.networksorcery.com/enp/protocol/telnet.htm
* http://www.networksorcery.com/enp/rfc/rfc1091.txt
*
* ANSI escape sequeneces:
* http://en.wikipedia.org/wiki/ANSI_escape_code
* http://ascii-table.com/ansi-escape-sequences-vt-100.php
* http://www.termsys.demon.co.uk/vtansi.htm
* http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
*
* ASCII codes:
* http://en.wikipedia.org/wiki/ASCII
* http://www.hobbyprojects.com/ascii-table/ascii-table.html
*
* Other web consoles:
* http://stackoverflow.com/questions/244750/ajax-console-window-with-ansi-vt100-support
*/
function Telnet(target, connect_callback, disconnect_callback) {
var that = {}, // Public API interface
vt100, ws, sQ = [];
termType = "VT100";
Array.prototype.pushStr = function (str) {
var n = str.length;
for (var i=0; i < n; i++) {
this.push(str.charCodeAt(i));
}
}
function do_send() {
if (sQ.length > 0) {
Util.Debug("Sending " + sQ);
ws.send(sQ);
sQ = [];
}
}
function do_recv() {
//console.log(">> do_recv");
var arr = ws.rQshiftBytes(ws.rQlen()), str = "",
chr, cmd, code, value;
Util.Debug("Received array '" + arr + "'");
while (arr.length > 0) {
chr = arr.shift();
switch (chr) {
case 255: // IAC
cmd = chr;
code = arr.shift();
value = arr.shift();
switch (code) {
case 254: // DONT
Util.Debug("Got Cmd DONT '" + value + "', ignoring");
break;
case 253: // DO
Util.Debug("Got Cmd DO '" + value + "'");
if (value === 24) {
// Terminal type
Util.Info("Send WILL '" + value + "' (TERM-TYPE)");
sQ.push(255, 251, value);
} else {
// Refuse other DO requests with a WONT
Util.Debug("Send WONT '" + value + "'");
sQ.push(255, 252, value);
}
break;
case 252: // WONT
Util.Debug("Got Cmd WONT '" + value + "', ignoring");
break;
case 251: // WILL
Util.Debug("Got Cmd WILL '" + value + "'");
if (value === 1) {
// Server will echo, turn off local echo
vt100.noecho();
// Affirm echo with DO
Util.Info("Send Cmd DO '" + value + "' (echo)");
sQ.push(255, 253, value);
} else {
// Reject other WILL offers with a DONT
Util.Debug("Send Cmd DONT '" + value + "'");
sQ.push(255, 254, value);
}
break;
case 250: // SB (subnegotiation)
if (value === 24) {
Util.Info("Got IAC SB TERM-TYPE SEND(1) IAC SE");
// TERM-TYPE subnegotiation
if (arr[0] === 1 &&
arr[1] === 255 &&
arr[2] === 240) {
arr.shift(); arr.shift(); arr.shift();
Util.Info("Send IAC SB TERM-TYPE IS(0) '" +
termType + "' IAC SE");
sQ.push(255, 250, 24, 0);
sQ.pushStr(termType);
sQ.push(255, 240);
} else {
Util.Info("Invalid subnegotiation received" + arr);
}
} else {
Util.Info("Ignoring SB " + value);
}
break;
default:
Util.Info("Got Cmd " + cmd + " " + value + ", ignoring"); }
continue;
case 242: // Data Mark (Synch)
cmd = chr;
code = arr.shift();
value = arr.shift();
Util.Info("Ignoring Data Mark (Synch)");
break;
default: // everything else
str += String.fromCharCode(chr);
}
}
if (sQ) {
do_send();
}
if (str) {
vt100.write(str);
}
//console.log("<< do_recv");
}
that.connect = function(host, port, encrypt) {
var host = host,
port = port,
scheme = "ws://", uri;
Util.Debug(">> connect");
if ((!host) || (!port)) {
console.log("must set host and port");
return;
}
if (ws) {
ws.close();
}
if (encrypt) {
scheme = "wss://";
}
uri = scheme + host + ":" + port;
Util.Info("connecting to " + uri);
ws.open(uri);
Util.Debug("<< connect");
}
that.disconnect = function() {
Util.Debug(">> disconnect");
if (ws) {
ws.close();
}
vt100.curs_set(true, false);
disconnect_callback();
Util.Debug("<< disconnect");
}
function constructor() {
/* Initialize Websock object */
ws = new Websock();
ws.on('message', do_recv);
ws.on('open', function(e) {
Util.Info(">> WebSockets.onopen");
vt100.curs_set(true, true);
connect_callback();
Util.Info("<< WebSockets.onopen");
});
ws.on('close', function(e) {
Util.Info(">> WebSockets.onclose");
that.disconnect();
Util.Info("<< WebSockets.onclose");
});
ws.on('error', function(e) {
Util.Info(">> WebSockets.onerror");
that.disconnect();
Util.Info("<< WebSockets.onerror");
});
/* Initialize the terminal emulator/renderer */
vt100 = new VT100(80, 24, target);
/*
* Override VT100 I/O routines
*/
// Set handler for sending characters
vt100.getch(
function send_chr(chr, vt) {
var i;
Util.Debug(">> send_chr: " + chr);
for (i = 0; i < chr.length; i++) {
sQ.push(chr.charCodeAt(i));
}
do_send();
vt100.getch(send_chr);
}
);
vt100.debug = function(message) {
Util.Debug(message + "\n");
}
vt100.warn = function(message) {
Util.Warn(message + "\n");
}
vt100.curs_set = function(vis, grab, eventist)
{
this.debug("curs_set:: vis: " + vis + ", grab: " + grab);
if (vis !== undefined)
this.cursor_vis_ = (vis > 0);
if (eventist === undefined)
eventist = window;
if (grab === true || grab === false) {
if (grab === this.grab_events_)
return;
if (grab) {
this.grab_events_ = true;
VT100.the_vt_ = this;
Util.addEvent(eventist, 'keydown', vt100.key_down);
Util.addEvent(eventist, 'keyup', vt100.key_up);
} else {
Util.removeEvent(eventist, 'keydown', vt100.key_down);
Util.removeEvent(eventist, 'keyup', vt100.key_up);
this.grab_events_ = false;
VT100.the_vt_ = undefined;
}
}
}
vt100.key_down = function(e) {
var vt = VT100.the_vt_, keysym, ch, str = "";
if (vt === undefined)
return true;
keysym = getKeysym(e);
if (keysym < 128) {
if (e.ctrlKey) {
if (keysym == 64) {
// control 0
ch = 0;
} else if ((keysym >= 97) && (keysym <= 122)) {
// control codes 1-26
ch = keysym - 96;
} else if ((keysym >= 91) && (keysym <= 95)) {
// control codes 27-31
ch = keysym - 64;
} else {
Util.Info("Debug unknown control keysym: " + keysym);
}
} else {
ch = keysym;
}
str = String.fromCharCode(ch);
} else {
switch (keysym) {
case 65505: // Shift, do not send directly
break;
case 65507: // Ctrl, do not send directly
break;
case 65293: // Carriage return, line feed
str = '\n'; break;
case 65288: // Backspace
str = '\b'; break;
case 65289: // Tab
str = '\t'; break;
case 65307: // Escape
str = '\x1b'; break;
case 65361: // Left arrow
str = '\x1b[D'; break;
case 65362: // Up arrow
str = '\x1b[A'; break;
case 65363: // Right arrow
str = '\x1b[C'; break;
case 65364: // Down arrow
str = '\x1b[B'; break;
default:
Util.Info("Unrecoginized keysym " + keysym);
}
}
if (str) {
vt.key_buf_.push(str);
setTimeout(VT100.go_getch_, 0);
}
Util.stopEvent(e);
return false;
}
vt100.key_up = function(e) {
var vt = VT100.the_vt_;
if (vt === undefined)
return true;
Util.stopEvent(e);
return false;
}
return that;
}
return constructor(); // Return the public API interface
} // End of Telnet()