//////////////////////////////////////////////////////////////////
// //
// Telnet Protocol Handler //
// //
//////////////////////////////////////////////////////////////////
Namespace.register("Telnet", "withTelnet", (function() {
var Comm;
var Graphics;
var VT100;
const debug = true;
const BS = "\x08";
const TAB = "\x09";
const DEL = "\x7f";
const LF = "\x0a";
const CR = "\x0d";
const IAC = "\xff"; // 255
const DONT = "\xfe"; // 254
const DO = "\xfd"; // 253
const WONT = "\xfc"; // 252
const WILL = "\xfb"; // 251
const SB = "\xfa"; // 250
const GA = "\xf9";
const EL = "\xf8";
const EC = "\xf7";
const AYT = "\xf6";
const AO = "\xf5";
const IP = "\xf4";
const BREAK = "\xf3";
const DM = "\xf2";
const NOP = "\xf1";
const SE = "\xf0";
const BINARY = "\x00"; // 00
const ECHO = "\x01"; // 01
const SGA = "\x03"; // 03
const TTYPE = "\x18"; // 24
const NAWS = "\x1f"; // 31
const ENVIRON= "\x27"; // 39
const MSDP = "\x45"; // 69
const MSSP = "\x46"; // 70
const MCCP = "\x56"; // 86
const IAC_DO_BINARY = IAC+DO+BINARY;
const IAC_DONT_BINARY = IAC+DONT+BINARY;
const IAC_WILL_BINARY = IAC+WILL+BINARY;
const IAC_WONT_BINARY = IAC+WONT+BINARY;
const IAC_WILL_ECHO = IAC+WILL+ECHO;
const IAC_WONT_ECHO = IAC+WONT+ECHO;
const IAC_WILL_SGA = IAC+WILL+SGA;
const IAC_WONT_SGA = IAC+WONT+SGA;
const IAC_WILL_TTYPE = IAC+WILL+TTYPE;
const IAC_WONT_TTYPE = IAC+WONT+TTYPE;
const IAC_DO_TTYPE = IAC+DO+TTYPE;
const IAC_DONT_TTYPE = IAC+DONT+TTYPE;
const IAC_WILL_NAWS = IAC+WILL+NAWS;
const IAC_WONT_NAWS = IAC+WONT+NAWS;
const IAC_DO_NAWS = IAC+DO+NAWS;
const IAC_DONT_NAWS = IAC+DONT+NAWS;
const IAC_WILL_ENVIRON = IAC+WILL+ENVIRON;
const IAC_WONT_ENVIRON = IAC+WONT+ENVIRON;
const IAC_DO_ENVIRON = IAC+DO+ENVIRON;
const IAC_DONT_ENVIRON = IAC+DONT+ENVIRON;
const IAC_TTYPE_SEND = IAC+SB+TTYPE+"\x01"+IAC+SE;
const IAC_ENVIRON_SEND_VAR = IAC+SB+ENVIRON+"\x01"+"\x00"+IAC+SE;
const IAC_ENVIRON_SEND_USERVAR = IAC+SB+ENVIRON+"\x01"+"\x03"+IAC+SE;
const IAC_ENVIRON_SEND_USER = IAC+SB+ENVIRON+"\x01"+"\x00"+"USER"+IAC+SE;
const IAC_ENVIRON_SEND_IPADDR = IAC+SB+ENVIRON+"\x01"+"\x00"+
"IPADDRESS"+IAC+SE;
const MTTS_ANSI = 1;
const MTTS_VT100 = 2;
const MTTS_UTF8 = 4;
const MTTS_256_COLOR = 8;
const MTTS_MOUSE_TRACKING = 16;
const MTTS_PROXY = 128;
const TTYPES = [
{ name : "TINTIN++", implies : 0 },
{ name : "xterm-256color", implies : MTTS_ANSI | MTTS_256_COLOR | MTTS_VT100 },
{ name : "xterm-color", implies : MTTS_ANSI | MTTS_VT100 },
{ name : "xterm", implies : MTTS_VT100 },
{ name : "vt100", implies : MTTS_VT100 },
{ name : "ansi", implies : 0 }
];
function select_ttype(list) {
for (var i = 0, len = TTYPES.length; i < len; i++) {
for (var j = 0, jlen = list.length; j < jlen; j++) {
if (!strcasecmp(TTYPES[i].name, list[j])) {
return list[j];
}
}
}
return list[0];
};
function dump(buf, str) {
for (var i = 0, len = buf.length; i < len; i++) {
mudlog (str + buf.charCodeAt(i));
}
};
Descriptor.prototype.init = function() {
this.state = "New";
this.badPws = 0;
this.badAlts = 0;
this.playerName = undefined;
this.primaryPlayerName = undefined;
this.player = undefined;
this.columns = 80;
this.rows = 25;
this.lineMode = true;
this.varCount = 0;
this.buffer = "";
this.paging = undefined;
this.page = 0;
this.color = {};
this.negotiating = "BINARY";
this.options = {};
this.termType = undefined;
this.termTypes = [];
this.nTermTypes = 0;
this.lastTtype = undefined;
this.hasUnicode = false;
this.hasColor = false;
this.has256Color = false;
this.hasVT100 = false;
this.identUser = undefined;
this.telnetUser = undefined;
this.dnsName = undefined;
this.seenMotD = false;
this.seeniMotD = false;
this.newPwd = null;
// Start telnet protocol negotiations
this._send(IAC_DO_BINARY);
if (debug) {
mudlog("Sent: IAC_DO_BINARY");
}
};
Descriptor.prototype.close = function() {
if (this.player) {
this.player.clearDesc();
}
this._send("Closing.\r\n");
this._close();
};
Descriptor.prototype.ident = function(ident) {
if (debug) {
mudlog("Received ident result " + ident);
}
this.identUser = ident;
return 0;
};
Descriptor.prototype.resolve = function(name) {
if (debug) {
mudlog("Received reverse DNS lookup: " + name + " (" + this.ipAddress + ")");
}
this.dnsName = name;
return 0;
};
Descriptor.prototype.resetPlayer = function() {
this.playerName = undefined;
if (this.player) {
this.player.clearDesc();
}
this.player = undefined;
this.menu = null;
this.state = "Greeting";
};
Descriptor.prototype.reconnect = function(player) {
if (this.player.menu) {
this.setState("Menu");
this.player.view = undefined;
}
if (this.player.view) {
this.setState("View");
this.player.menu = undefined;
}
};
Descriptor.prototype.input = function(input) {
var newbuf;
function kill_iac(desc) {
desc._send("\r\nPROTOCOL ERROR!!!\r\n");
desc.buffer = "";
if (desc.negotiating) {
desc.negotiating = undefined;
if (debug) {
mudlog("PROTCOL ERROR");
}
Comm.stateMachine(desc, "");
}
};
function strip_iac(buffer, start, end) {
var buf = buffer.substring(0, start) +
buffer.substring(end+1);
return newbuf = buf;
};
function set_ttype_opts(desc, opts) {
if (typeof opts === "string") {
for (var i = 0, len = TTYPES.length; i < len; i++) {
if (!strcasecmp(opts, TTYPES[i].name)) {
opts = TTYPES[i].implies;
break;
}
}
if (i === TTYPES.length) {
return;
}
}
if (opts & MTTS_ANSI) {
desc.hasColor = true;
}
if (opts & MTTS_VT100) {
desc.hasVT100 = true;
}
if (opts & MTTS_UTF8) {
desc.hasUnicode = true;
}
if (opts & MTTS_256_COLOR) {
desc.has256Color = true;
}
};
if (debug) {
dump(input, "Input: ");
}
newbuf = this.buffer = this.buffer + input;
// Scan for Telnet protocol commands
var iac = newbuf.indexOf(IAC);
while (iac >= 0) {
var cmd = newbuf.substring(iac+1);
if (cmd.length == 0) {
return 0;
}
if (debug) {
mudlog("Handling command: " + cmd.charCodeAt(0));
}
switch (cmd.charAt(0)) {
// Are you there ?
case AYT:
this._send("\r\n");
newbuf = strip_iac(newbuf, iac, iac+1);
break;
// Desperate measures.. convert to a panic signal
case BREAK: // Break
case AO: // Abort output
case IP: // Interrupt process
case DM: // Data mark
if (this.lineMode) {
this.buffer = "PANIC\r\n";
break;
} else {
newbuf = strip_iac(newbuf, iac, iac+1);
break;
}
// We treat these as nops..
case EL: // Erase line
case GA: // Go ahead
case NOP: // NOP
newbuf = strip_iac(newbuf, iac, iac+1);
break;
// Erase character - convert to \b
case EC:
newbuf = newbuf.substring(0, iac) + "\b" +
newbuf.substring(iac+2);
this.buffer = newbuf;
break;
// Protocol negotiations (should be server initiated)
case DO:
case DONT:
case WILL:
case WONT:
// Need more data
if (cmd.length < 2) {
return 0;
}
cmd = newbuf.substring(iac, iac+3);
var will_do = cmd[1];
var opt = cmd[2];
if (will_do === WILL || will_do === WONT) {
this.options[opt] = will_do;
}
newbuf = strip_iac(newbuf, iac, iac+2);
break;
case SE:
case IAC:
// Uhh… WTF. We are hosed.
// Throw it all away
mudlog("Bogus " +
(cmd.charAt(0) == SE ? "SE" : "IAC") + " command encountered");
kill_iac(this);
return 0;
case SB:
var close = cmd.indexOf(IAC);
if (close < 0 || cmd.length === close + 1) {
if (cmd.length > 80) {
mudlog("SB command sent too much data");
kill_iac(this);
}
return 0;
}
while (cmd.charAt(close+1) === IAC) {
close = cmd.indexOf(IAC, close+2);
if (close < 0 || cmd.length === close + 1) {
if (cmd.length > 80) {
mudlog("SB command sent too much data");
kill_iac(this);
}
return 0;
}
}
if (cmd.charAt(close + 1) !== SE) {
mudlog("SB command not closed with SE");
kill_iac(this);
return 0;
}
if (debug) {
mudlog("SB command: " + close + " bytes");
}
newbuf = strip_iac(newbuf, iac, close + 2);
this.buffer = newbuf;
// Handle window size negotiations
if (cmd.charAt(1) === NAWS) {
var pos = 2;
var byte = [];
if (close < 6) {
mudlog("Not enough data in NAWS negotiation");
kill_iac(this);
return 0;
}
for (i = 0; i < 4; i++) {
if (cmd.charAt(pos) === IAC) {
byte.push(255);
pos += 2;
} else {
byte.push(cmd.charCodeAt(pos));
pos += 1;
}
}
this.columns = (byte[0] << 8) | byte[1];
this.rows = (byte[2] << 8) | byte[3];
if (this.negotiating) {
this._send("Detected " + this.columns + "x" +
this.rows + " terminal.\r\n");
}
if (this.negotiating === "NAWS") {
this.negotiating = "ENVIRON";
if (!this.options.hasOwnProperty(ENVIRON)) {
this._send(IAC_DO_ENVIRON);
if (debug) {
mudlog("Sent: IAC_DO_ENVIRON");
}
return 0;
}
}
break;
}
// Ignore spurious data
if (!this.negotiating) {
break;
}
switch (this.negotiating) {
case "TTYPE" :
var ttype = cmd.substring(3, close);
if (debug) {
mudlog("TTYPE: " + ttype);
}
if (ttype !== this.lastTtype) {
this.lastTtype = ttype;
if (/MTTS [0-9]+/.test(ttype)) {
var comps = ttype.split(' ');
var opts = parseInt(comps[1]);
set_ttype_opts(this, opts);
} else {
this.termTypes.push(ttype);
if (this.termTypes.length > 20) {
this._send("Term type overload!!!");
this.buffer = "";
kill_iac(this);
return 0;
}
}
this._send(IAC_TTYPE_SEND);
if (debug) {
mudlog("Sent: IAC_TTYPE_SEND");
}
return 0;
} else {
if (this.termTypes.length > 1 &&
!/MTTS [0-9]+/.test(ttype)) {
this.termType = select_ttype(this.termTypes);
this.negotiating = "SETTTYPE";
this._send(IAC_DO_TTYPE);
this._send(IAC_TTYPE_SEND);
if (debug) {
mudlog("Sent: IAC_DO_TTYPE");
mudlog("Sent: IAC_TTYPE_SEND");
}
} else {
this.termType = ttype;
set_ttype_opts(this, ttype);
this.negotiating = "NAWS";
if (!this.options.hasOwnProperty(NAWS)) {
this._send(IAC_DO_NAWS);
if (debug) {
mudlog("Sent: IAC_DO_NAWS");
}
break;
}
}
}
break;
case "SETTTYPE" :
var ttype = cmd.substring(3, close);
if (debug) {
mudlog("SETTTYPE: " + ttype);
}
// Older client. No more negotiating
if (ttype === this.lastTtype) {
this.termType = ttype;
if (debug) {
mudlog("Older terminal type stuck negotiation");
}
}
if (++this.nTermTypes > this.termTypes.length) {
// WTF are you doing ?
this.termType = ttype;
if (debug) {
mudlog("Bizarre terminal type stuck negotiation");
}
}
if (ttype === this.termType) {
set_ttype_opts(this, ttype);
this.negotiating = "NAWS";
if (!this.options.hasOwnProperty(NAWS)) {
this._send(IAC_DO_NAWS);
if (debug) {
mudlog("Sent: IAC_DO_NAWS");
}
break;
}
} else {
this._send(IAC_TTYPE_SEND);
if (debug) {
mudlog("Sent: IAC_TTYPE_SEND");
}
}
break;
case "ENVIRON" :
if (debug) {
mudlog("GOT ENVIRON STRING");
}
if (cmd.charAt(2) == '\x00' &&
(cmd.charAt(3) == '\x00' || cmd.charAt(3) == '\x03')) {
var value = cmd.indexOf('\x01');
if (value < 0) {
value = close;
}
var key = cmd.substring(4, value);
value = cmd.substring(value+1, close);
if (debug) {
mudlog("GOT ENVIRON KEY: \"" + key +
"\", VALUE: \"" + value + "\"");
}
if (key === "USER") {
this.telnetUser = value;
}
if (–this.varCount === 0) {
this.negotiating = null;
if (debug) {
mudlog("Finished negotiation");
}
}
}
break;
default:
mudlog("Error - unhandled SB negotiation "+this.negotiating);
break;
}
break;
}
iac = newbuf.indexOf(IAC);
}
this.buffer = newbuf;
if (this.negotiating) {
if (debug) {
mudlog("state: " + this.negotiating);
}
switch (this.negotiating) {
case "BINARY" :
this.negotiating = "TTYPE";
if (!this.options.hasOwnProperty(TTYPE)) {
this._send(IAC_DO_TTYPE);
if (debug) {
mudlog("Sent: IAC_DO_TTYPE");
}
break;
}
case "TTYPE" :
if (!this.options.hasOwnProperty(TTYPE)) {
break;
}
if (this.options[TTYPE] === WILL) {
this.buffer = newbuf;
this._send(IAC_TTYPE_SEND);
if (debug) {
mudlog("Sent: IAC_TTYPE_SEND");
}
break;
} else {
this.negotiating = "NAWS";
if (!this.options.hasOwnProperty(NAWS)) {
this._send(IAC_DO_NAWS);
if (debug) {
mudlog("Sent: IAC_DO_NAWS");
}
break;
}
}
case "NAWS" :
if (!this.options.hasOwnProperty(NAWS)) {
break;
}
this.negotiating = "ENVIRON";
if (!this.options.hasOwnProperty(ENVIRON)) {
this._send(IAC_DO_ENVIRON);
if (debug) {
mudlog("Sent: IAC_DO_ENVIRON");
}
break;
}
case "ENVIRON" :
if (!this.options.hasOwnProperty(ENVIRON)) {
break;
}
if (this.options[ENVIRON] === WILL) {
if (this.termTypes.indexOf("VTNT") >= 0) {
// Hello, Windows…
this.echoOn();
this.negotiating = null;
if (debug) {
mudlog("Finished negotiation - Windows client");
}
} else {
this._send(IAC_ENVIRON_SEND_USER);
this._send(IAC_ENVIRON_SEND_IPADDR);
if (debug) {
mudlog("Sent: IAC_ENVIRON_SEND_USER");
mudlog("Sent: IAC_ENVIRON_SEND_IPADDR");
}
}
this.varCount = 2;
Comm.stateMachine(this, "");
if (debug) {
mudlog("Awaiting user variables");
}
return 0;
}
this.negotiating = null;
Comm.stateMachine(this, "");
break;
case "SETTTYPE" :
break;
default :
mudlog("Error - unhandled negotiation");
break;
}
}
// Phew!! All that work and we finally get to user input
// Now we just need to deal with line buffering…
this.buffer = newbuf;
while (newbuf.length) {
var token = "";
if (this.lineMode) {
// Handle backspaces
newbuf = newbuf.replace(/[^\r\n]?\u0008/g, "");
// Convert CR/LF to NL
var nl = newbuf.replace(/\r\n/g, "\n");
// Find newlines
var nl = newbuf.search(/[\r\n]/);
if (nl < 0) {
break;
}
token = newbuf.substring(0, nl);
// Remove all control characters and non-UTF8 input
token = token.replace(/[\x00-\x1f\xf8-\xff]/g, "");
newbuf = newbuf.substring(nl+1, newbuf.length);
if (newbuf.charAt(0) == '\n') {
newbuf = newbuf.substring(1, newbuf.length);
}
} else {
var translations = { "key_up" : "UP",
"key_down" : "DOWN",
"key_left" : "LEFT",
"key_right" : "RIGHT",
"key_enter" : "ENTER"};
token = null;
for (var key in translations) {
if (VT100.hasOwnProperty(key)) {
var key_code = VT100[key];
var key_len = key_code.length;
var match = newbuf.substring(0, key_len);
if (match === key_code) {
token = translations[key];
newbuf = newbuf.substring(key_len, newbuf.length);
break;
}
}
}
if (!token) {
token = newbuf.substring(0, 1);
newbuf = newbuf.substring(1, newbuf.length);
}
if (token === TAB) {
token = "TAB";
} else if (token === DEL) {
token = "DELETE";
} else if (token === CR) {
token = "ENTER";
}
}
if (debug) {
mudlog("Sending token: " + token);
}
if (!Comm.stateMachine(this, token)) {
return -1;
}
}
// Should not be having lines this long…
this.buffer = newbuf;
if (newbuf.length >= IO_.MAX_RAW_INPUT_LENGTH) {
this._send("Input overflow!!!");
this.buffer = "";
return -1;
}
return 0;
};
Descriptor.prototype.echoOn = function() {
this._send(IAC_WONT_ECHO+"\r\n");
};
Descriptor.prototype.echoOff = function() {
this._send(IAC_WILL_ECHO);
};
Descriptor.prototype.setCharMode = function() {
this._send(IAC_WILL_SGA);
this.echoOff();
this.send(VT100.keypad_xmit);
this.lineMode = false;
if (debug) {
mudlog("Entered char mode");
}
};
Descriptor.prototype.setLineMode = function() {
this._send(IAC_WONT_SGA);
this.echoOn();
this.send(VT100.keypad_local);
this.lineMode = true;
if (debug) {
mudlog("Entered line mode");
}
};
Descriptor.prototype.setState = function(state) {
if (state === "View" && this.state !== "View") {
this.setCharMode();
} else if (state !== "View" && this.state === "View") {
this.setLineMode();
}
this.state = state;
return this;
};
Descriptor.prototype.setColor = function(isOn) {
this.hasColor = isOn;
return this;
};
Descriptor.prototype.setUnicode = function(isOn) {
this.hasUnicode = isOn;
return this;
};
Descriptor.prototype.bell = function() {
this._send("\x07");
};
Descriptor.prototype.pulse = function() {
mudlog("pulsing");
Comm.stateMachine(this, null);
};
Descriptor.prototype.send = function(text) {
if (!this.hasUnicode) {
text = text.replace(/[\x80-\xbf]/g, "");
text = text.replace(/[\xc0-\xff]/g, "?");
}
if (!this.hasColor) {
text = Graphics.stripColor(text);
} else {
text = Graphics.addColor(text, this.color);
}
this._send(text);
};
return {
withComm : function(module) { Comm = module; },
withVT100 : function(module) { VT100 = module; },
withGraphics : function(module) { Graphics = module; }
};
})());
Again, my thoughts on this have already I think been adequately expressed.
There's an interesting signature for Windows telnet though.
[ :j: Sent: IAC_DO_BINARY ]
[ :j: Sent: IAC_DO_TTYPE ]
[]>
[ :j: Received reverse DNS lookup: null (172.255.255.103) ]
[]>
[ :j: Input: 255 ]
[ :j: Input: 251 ]
[ :j: Input: 0 ]
[ :j: Input: 255 ]
[ :j: Input: 251 ]
[ :j: Input: 24 ]
[ :j: Input: 255 ]
[ :j: Input: 251 ]
[ :j: Input: 31 ]
So I sent IAC DO BINARY, IAC DO TTYPE and Window responds with IAC WILL BINARY, IAC WILL TTYPE, IAC WILL NAWS.
Well that's interesting, I didn't ask for NAWS yet but I guess its good that you support that…
[ :j: state: ENVIRON ]
[ :j: Sent: IAC_ENVIRON_SEND_USER ]
[ :j: Sent: IAC_ENVIRON_SEND_IPADDR ]
[ :j: updating state: old New new: AnsiDetect ]
[ :j: Awaiting user variables ]
And sending a request for environment variables crashes Windows Telnet…. :ghostface: