10 Jan, 2013, Telgar wrote in the 61st comment:
Votes: 0
Awesome. I just updated my code to at least attempt to finish negotiation with Windows Telnet and the result I got is that I now can crash Windows Telnet….

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:
10 Jan, 2013, Telgar wrote in the 62nd comment:
Votes: 0
Kaz said:
If I recall correctly, you can set the terminal mode within Windows Telnet manually before you log into the server, and this will change the order of the TTYPE responses. Of course, as the saying goes: you can protect against Murphy, but protecting against Machiavelli is harder.

Strangely, I remember that the safest thing to do seemed to be to set the mode explicitly to VTNT. That way, it wouldn't "land" on it and be horrible when the TTYPE cycle ended.


Playing with option negotiation, I find that I can force Windows telnet into VT100 mode instead of VTNT mode. All you have to do is resend the IAC DO TTYPE command, which causes it to restart negotiation, which I choose to terminate on VT100.

[ :j: Handling command: 250 ]
[ :j: SB command: 7 bytes ]
[ :j: TTYPE: VTNT ]
[ :j: Sent: IAC_DO_TTYPE ]
[ :j: Sent: IAC_TTYPE_SEND ]

[ :j: Handling command: 251 ]
[ :j: state: SETTTYPE ]
[ :j: Handling command: 250 ]
[ :j: SB command: 7 bytes ]
[ :j: SETTTYPE: ANSI ]
[ :j: Sent: IAC_TTYPE_SEND ]
10 Jan, 2013, Tyche wrote in the 63rd comment:
Votes: 0
Telgar said:
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…

It's common and correct for telnet clients to attempt to negotiate options independent of the server.
10 Jan, 2013, Telgar wrote in the 64th comment:
Votes: 0
Tyche said:
Telgar said:
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…

It's common and correct for telnet clients to attempt to negotiate options independent of the server.


My original reading^H^H^H^H^H^H^Hglancing over the telnet RFCs led me to believe clients were not supposed to initiate any negotiations. Right now I'm depending on receiving WILL / WONT responses in the order I send them, which isn't entirely compatible with client sent negotiations, but in the end it all works anyway, as I simply ignore negotiations I haven't requested and renegotiate them if and when I want to.

Even more interesting is that Windows actually allows resizing the screen (not width, but you can change length) and reports this correctly. This is cool because I use that to automatically figure out where to place page breaks when paginating text. I was under the impression this was the only spontaneously reported client property (though I haven't read all the option RFCs).

After working around the Windows telnet crash, I've successfully got an option negotiation going with Win Telnet!!! The only thing required is that the user set the local echo option if they want to see text. I could actually add echoing code to the server, but I think I've reached the limit of the amount of complexity I want to add for Windows diehards.
10 Jan, 2013, Scandum wrote in the 65th comment:
Votes: 0
Windows Telnet should respond to a request for SYSTEMTYPE with WIN32 when using NEW ENVIRON. As far as I know that response is unique to Windows Telnet.
10 Jan, 2013, Tyche wrote in the 66th comment:
Votes: 0
Telgar said:
Right now I'm depending on receiving WILL / WONT responses in the order I send them, which isn't entirely compatible with client sent negotiations, but in the end it all works anyway, as I simply ignore negotiations I haven't requested and renegotiate them if and when I want to.

That won't work. You have to keep track of both sides of the negotiation, what you wanted and what the client wanted. The responses won't necessarily be in sequence either.
For example, if you ignore Windows Telnet sending IAC WILL NAWS, later when you attempt to do IAC DO NAWS, Windows Telnet sees it as a response to its initial negotiation
and you won't receive a IAC WILL NAWS as a response. Anyway there was a discussion on TMC on the pitfalls of ignoring clients. There are far more broken mud servers than broken clients. ;-)

Telgar said:
Even more interesting is that Windows actually allows resizing the screen (not width, but you can change length) and reports this correctly. This is cool because I use that to automatically figure out where to place page breaks when paginating text. I was under the impression this was the only spontaneously reported client property (though I haven't read all the option RFCs).


Certainly you can resize both the width and height.
Telnet runs in the Console and the console buffer sizes/display sizes are whatever you've set the defaults or your current window to.
Try right clicking on the title bar.
11 Jan, 2013, Twisol wrote in the 67th comment:
Votes: 0
Telgar said:
My original reading^H^H^H^H^H^H^Hglancing over the telnet RFCs led me to believe clients were not supposed to initiate any negotiations. Right now I'm depending on receiving WILL / WONT responses in the order I send them, which isn't entirely compatible with client sent negotiations, but in the end it all works anyway, as I simply ignore negotiations I haven't requested and renegotiate them if and when I want to.

Telnet was designed so that the server and the client are equals at the protocol level. Either can initiate a negotiation. This is what prompted the development of the Q method of negotiation, eliminating issues with negotiation loops. The technically correct thing to do with unwanted WILLs is to respond with DONT, but if you don't support the option (and the remote end is well-behaved) it won't be enabled anyway. The worst that can happen (and this is for very particular options and combinations therein) is that the remote end will buffer user input while waiting for your response. If you're not going to invest in a good Telnet stack (I recommend Elanthis' libtelnet), it works well enough.
11 Jan, 2013, Telgar wrote in the 68th comment:
Votes: 0
Twisol said:
If you're not going to invest in a good Telnet stack (I recommend Elanthis' libtelnet), it works well enough.


Weird, that C code totally did not do anything when I loaded it into my Javascript telnet stack … :devil:

But yeah, I'll do what Tyche said. It's totally a breeze to save negotiated options… I already parse them out anyway. It's like 2 lines of code to cache all the options I received and consult that at any point in my stack state machine. Another 5 lines of code to make sure someone doesn't try to option bomb me… thinking of how painful this would be in C makes my head hurt.

(Edit: considering there are only 256 possible telnet options, looks like I dont need to worry about bombing, lol.)
11 Jan, 2013, Tyche wrote in the 69th comment:
Votes: 0
I used the Q Method as well. I've done in both C and Ruby, around 2000 and 700 lines respectively.
11 Jan, 2013, Telgar wrote in the 70th comment:
Votes: 0
Tyche said:
I used the Q Method as well. I've done in both C and Ruby, around 2000 and 700 lines respectively.


The Q method, plus with robust debugging and the ability to toggle line / character mode, toggle echoing, filter out control characters, chunk input into lines, strip out Unicode characters, strip or add ANSI color sequences is 781 lines in Javascript .. :tongue:

I actually cheat slightly, I'm not responding to client sent IAC DONT yet, not sure I really need to…
11 Jan, 2013, Twisol wrote in the 71st comment:
Votes: 0
Telgar said:
Twisol said:
If you're not going to invest in a good Telnet stack (I recommend Elanthis' libtelnet), it works well enough.


Weird, that C code totally did not do anything when I loaded it into my Javascript telnet stack … :devil:

Oh. In that case, I might have something to offer in a few months. :) I'm doing a client in Javascript.

Tyche said:
I used the Q Method as well. I've done in both C and Ruby, around 2000 and 700 lines respectively.

My JS implementation of Q is about 300 lines. Actually… my C implementation of Telnet as a whole is under a thousand, though that's not counting individual telopt details. 2000 seems huge!
:surprised:
11 Jan, 2013, Telgar wrote in the 72nd comment:
Votes: 0
Well here's a reference implementation in Javascript. It negotiates to completion with Windows telnet, tintin++, Mac OS terminal, iterm2, as well as a few Linux clients. Feel free to use it as you see fit.

//////////////////////////////////////////////////////////////////
// //
// 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; }
};

})());
11 Jan, 2013, Telgar wrote in the 73rd comment:
Votes: 0
Also, special thanks to Scandum, Tyche, KaVir, quixadhal, Kaz, and whoever else I forgot to mention for pointing me in the right direction here… being new to this site I didn't know what to expect, and clearly it is not amateur hour over here. It's good to have people egging you on to do things properly.

:biggrin:
11 Jan, 2013, quixadhal wrote in the 74th comment:
Votes: 0
Telgar said:
The Q method, plus with robust debugging and the ability to toggle line / character mode, toggle echoing, filter out control characters, chunk input into lines, strip out Unicode characters, strip or add ANSI color sequences is 781 lines in Javascript .. :tongue:

I actually cheat slightly, I'm not responding to client sent IAC DONT yet, not sure I really need to…


Stripping ANSI sequences seems useful. Adding? If you mean you implemented a color code translation system, I think it's a mistake to place it at that low a level. It makes it more difficult to NOT do the translation in cases where you want to see the embedded markup (an OLC description editor, debugging statements, etc). Depending on how you handle user-defined colors and terminal types, that might also add some bloat to an otherwise tight system.

For example, the LP mud code I have translates the token codes (pinkfish) into different things based on the intended use. If sending to the user for normal output, it translates to their terminal type's codes (ANSI, xterm-256, a greyscale subset of xterm-256, etc). If sending to the I3 intermud network, it translates from my extended pinkfish to "standard" pinkfish. If sending to the IMC2 intermud network, it translates to the IMC color code syntax. The built-in web server hackishly tries to translate to HTML.

If you stuff that into the output layer, you either have to duplicate code for things NOT headed for the socket, or go mess with the main socket code when all you want to do is tweak the web server or something else.

But then, LP muds are designed to allow you to mess with subsystems without having to reboot the whole server. If your system will reboot on a code change anyways, it probably doesn't matter.
11 Jan, 2013, Telgar wrote in the 75th comment:
Votes: 0
quixadhal said:
Telgar said:
The Q method, plus with robust debugging and the ability to toggle line / character mode, toggle echoing, filter out control characters, chunk input into lines, strip out Unicode characters, strip or add ANSI color sequences is 781 lines in Javascript .. :tongue:

I actually cheat slightly, I'm not responding to client sent IAC DONT yet, not sure I really need to…


Stripping ANSI sequences seems useful. Adding? If you mean you implemented a color code translation system, I think it's a mistake to place it at that low a level. It makes it more difficult to NOT do the translation in cases where you want to see the embedded markup (an OLC description editor, debugging statements, etc). Depending on how you handle user-defined colors and terminal types, that might also add some bloat to an otherwise tight system.

For example, the LP mud code I have translates the token codes (pinkfish) into different things based on the intended use. If sending to the user for normal output, it translates to their terminal type's codes (ANSI, xterm-256, a greyscale subset of xterm-256, etc). If sending to the I3 intermud network, it translates from my extended pinkfish to "standard" pinkfish. If sending to the IMC2 intermud network, it translates to the IMC color code syntax. The built-in web server hackishly tries to translate to HTML.

If you stuff that into the output layer, you either have to duplicate code for things NOT headed for the socket, or go mess with the main socket code when all you want to do is tweak the web server or something else.

But then, LP muds are designed to allow you to mess with subsystems without having to reboot the whole server. If your system will reboot on a code change anyways, it probably doesn't matter.


I handle viewing embedded markup by escaping the markup character at a higher level (such as OLC, but also in some player viewable fields, such as the title). I think the descriptor is exactly the right place to handle converting the color codes; they are dependent on two properties of the descriptor, first the terminal type, and second, the chromaticity of the display on which data from the descriptor is displayed. I don't want to unpickle color codes above this layer as that requires exporting knowledge of the various terminal types to higher layer code, and I think that makes more of a mess than making all the descriptor classes aware of my internal pickling code.

For exporting to other types of endpoints, this is also the proper place to make those decisions. I imagine the descriptor class for an I3 intermud connection would be a bit different, but the interface would be the same, and I would rather do the conversion to pinkfish at the layer that knows about pinkfish, using an I3 descriptor class.

More problematic is converting to HTML based markup. All the regular pinkfish / &y / @C code schemes I've come across use the "set color" / "reset" color paradigm, not a <begin> <end> marker, so things like %^YELLOW%^Hey there%^RED%^ YOU MAKIN ME RED.%^NORMAL%^ are possible which don't translate directly to html. This gets worse when you consider that you can combine in reverse video, underlines, and other special effects, so your current color state needs to be tracked, etc..

I added a push / pop color state pickling code to my markup to help deal with that, but I haven't begun messing with HTML formatted output yet. More than likely, I would simply punt that problem to the browser, and since, hey, my display and formatting code is in Javascript anyways, I can simply export that module as a script, skip any formatting at all, send the raw pickled data and do the processing in the browser.
60.0/75