f7e1a711db
This makes the client usable for other plain text services that telnet clients can normally communicate with (IRC, SMTP, etc).
336 lines
9.3 KiB
JavaScript
336 lines
9.3 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 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()
|