338 lines
9.4 KiB
JavaScript
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()
|