using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MUDClientEssentials { enum Telnet : byte { //escape InterpretAsCommand = 255, //commands SubnegotiationEnd = 240, NoOperation = 241, DataMark = 242, Break = 243, InterruptProcess = 244, AbortOutput = 245, AreYouThere = 246, EraseCharacter = 247, EraseLine = 248, GoAhead = 249, SubnegotiationBegin = 250, //negotiation WILL = 251, WONT = 252, DO = 253, DONT = 254, //options (common) SuppressGoAhead = 3, Status = 5, Echo = 1, TimingMark = 6, TerminalType = 24, TerminalSpeed = 32, RemoteFlowControl = 33, LineMode = 34, EnvironmentVariables = 36, NAWS = 31, //options (MUD-specific) MSDP = 69, MXP = 91, MCCP1 = 85, MCCP2 = 86, MSP = 90 }; class TelnetParser { private System.Net.Sockets.TcpClient tcpClient; public TelnetParser(System.Net.Sockets.TcpClient tcpClient) { this.tcpClient = tcpClient; } public List<byte> HandleAndRemoveTelnetBytes(byte[] buffer, int receivedCount, out List<string> telnetMessages) { //list to hold a report of any telnet control sequences received or sent telnetMessages = new List<string>(); //list to hold any bytes which aren't telnet bytes (which will be most of the bytes) List<byte> contentBytes = new List<byte>(); //we'll scan for telnet control sequences. anything NOT a telnet control sequence will be added to the contentBytes list for later processing. int currentIndex = 0; while (currentIndex < receivedCount) { //search for an IAC, which may signal the beginning of a telnet message while (currentIndex < receivedCount && buffer[currentIndex] != (byte)Telnet.InterpretAsCommand) { contentBytes.Add(buffer[currentIndex]); currentIndex++; } //if at the end of the data, stop. otherwise we've encountered an IAC and there should be at least one more byte here if (++currentIndex == receivedCount) break; //read the next byte byte secondByte = buffer[currentIndex]; //if another IAC, then this was just sequence IAC IAC, which is the escape sequence to represent byte value 255 (=IAC) in the content stream if (secondByte == (byte)Telnet.InterpretAsCommand) { //write byte value 255 to the content stream and move on contentBytes.Add(secondByte); } //otherwise we have a "real" telnet sequence, where the second byte is a command or negotiation else { //start building a string representation of this message, to be reported to the caller //caller might want to show this info to the user always, or optionally for debugging purposes StringBuilder stringVersionOfMessage = new StringBuilder(); //also build a string version of the response (if any) StringBuilder stringVersionOfResponse = new StringBuilder(); //DO if (secondByte == (byte)Telnet.DO) { stringVersionOfMessage.Append("DO "); //what are we being told to do? currentIndex++; if (currentIndex == receivedCount) break; byte thirdByte = buffer[currentIndex]; stringVersionOfMessage.Append(interpretByteAsTelnet(thirdByte)); //if NAWS (negotiate about window size) if (thirdByte == (byte)Telnet.NAWS) { //on connection, we offered to negotiate about window size. so this is a "go ahead and negotiate" response. //so then, send information about client window size per the NAWS protocol //we're lieing to server by telling it a ridiculously large size, so that it won't do line breaking or paging for us (annoying!) stringVersionOfResponse.Append(this.sendTelnetBytes( (byte)Telnet.SubnegotiationBegin, (byte)31, 254, 254, 254, 254, (byte)Telnet.InterpretAsCommand, (byte)Telnet.SubnegotiationEnd)); } //everything else the server might ask us to do is unsupported by us else { stringVersionOfMessage.Append(interpretByteAsTelnet(thirdByte)); //sorry, i won't do whatever "that thing you said to do" was stringVersionOfResponse.Append(this.sendTelnetBytes((byte)Telnet.InterpretAsCommand, (byte)Telnet.WONT, thirdByte)); } } //DONT else if (secondByte == (byte)Telnet.DONT) { stringVersionOfMessage.Append("DONT "); currentIndex++; if (currentIndex == receivedCount) break; byte thirdByte = buffer[currentIndex]; stringVersionOfMessage.Append(interpretByteAsTelnet(thirdByte)); //whatever you want me to stop doing, that's no problem because i wasn't going to do it anyway stringVersionOfResponse.Append(this.sendTelnetBytes((byte)Telnet.WONT, thirdByte)); } //WILL else if (secondByte == (byte)Telnet.WILL) { stringVersionOfMessage.Append("WILL "); //find out what the server is willing to do currentIndex++; if (currentIndex == receivedCount) break; byte thirdByte = buffer[currentIndex]; stringVersionOfMessage.Append(interpretByteAsTelnet(thirdByte)); //anything the server offers to do for us, we'll tell it not to because we don't know what it is stringVersionOfResponse.Append((this.sendTelnetBytes((byte)Telnet.DONT, thirdByte))); } //WONT else if (secondByte == (byte)Telnet.WONT) { stringVersionOfMessage.Append("WONT "); //find out what the server is NOT willing to do currentIndex++; if (currentIndex == receivedCount) break; byte thirdByte = buffer[currentIndex]; stringVersionOfMessage.Append(interpretByteAsTelnet(thirdByte)); //because we haven't asked the server to DO anything, should not expect to receive any WONT //however if we do receive a WONT, respond with a DONT to confirm that the server can go ahead and NOT do that thing it doesn't want to do stringVersionOfResponse.Append(this.sendTelnetBytes((byte)Telnet.DONT, thirdByte)); } //subnegotiations else if (secondByte == (byte)Telnet.SubnegotiationBegin) { stringVersionOfMessage.Append("SB "); List<byte> subnegotiationBytes = new List<byte>(); //read until an IAC followed by an SE while (currentIndex < receivedCount - 1 && !(buffer[currentIndex] == (byte)Telnet.InterpretAsCommand && buffer[currentIndex] == (byte)Telnet.SubnegotiationEnd)) { subnegotiationBytes.Add(buffer[currentIndex]); currentIndex++; } byte[] subnegotiationBytesArray = subnegotiationBytes.ToArray(); //append the content of the subnegotiation to the incoming message report string stringVersionOfMessage.Append(AsciiDecoder.AsciiToUnicode(subnegotiationBytesArray, subnegotiationBytes.Count)); //append the subnegotiation end stringVersionOfMessage.Append(" SE"); } //any other telnet message else { //try to convert it to a known message via the enum defined above stringVersionOfMessage.Append(interpretByteAsTelnet(secondByte)); } //report the control sequence we found, if any string messageToReport = stringVersionOfMessage.ToString(); if (!string.IsNullOrEmpty(messageToReport)) { telnetMessages.Add("RECV: " + messageToReport.ToString()); } //report the response message sent, if any string responseToReport = stringVersionOfResponse.ToString(); if (!string.IsNullOrEmpty(responseToReport)) { telnetMessages.Add("SEND: " + stringVersionOfResponse.ToString()); } } //move up to the next byte in the data currentIndex++; } return contentBytes; } #region "friendly" text for telnet sequences private string interpretByteAsTelnet(byte thisByte) { //try to convert the byte value to a string representation based on the Telnet enumeration string friendlyName = Enum.GetName(typeof(Telnet), thisByte); //if failed, just show the byte's numerical value in brackets, like [254] if (string.IsNullOrEmpty(friendlyName)) { friendlyName = '[' + thisByte.ToString() + ']'; } return friendlyName; } #endregion public string sendTelnetBytes(params byte[] bytes) { //if not connected, do nothing if (!this.tcpClient.Connected) return ""; //send IAC this.tcpClient.Client.Send(new byte[] { (byte)Telnet.InterpretAsCommand }); //send the specified bytes this.tcpClient.Client.Send(bytes); //start building a string to report to the caller StringBuilder reportMessage = new StringBuilder(); for (int i = 0; i < bytes.Length; i++) { //convert the byte value to something readable, and append it to the report string reportMessage.Append(this.interpretByteAsTelnet(bytes[i]) + " "); } return reportMessage.ToString(); } } }