/* * Date: 12/15/2005 * Time: 10:54 PM * * Copyright 2005, Static Boy Productions */ using System; using System.Collections; using System.IO; using System.Net.Sockets; using Jessie.Utils; namespace Jessie.Comm { public delegate void jtbvPongReceivedHandler(jtbvTelnetComm sender); /// /// jtbvTelnetComm handles telnet communications for connecting to dGameLaunch or devnull telnet Nethack sessions /// public class jtbvTelnetComm : jtbvITTYComm { public jtbvTelnetComm() { } private const int WIDTH = 120; private const int HEIGHT = 24; #region Control constants // Telnet control characters private const char SE = (char) 240; // End of subnegotiation private const char NOP = (char) 241; // No operation private const char DM = (char) 242; // Data mark private const char BRK = (char) 243; // Break; private const char IP = (char) 244; // Suspend private const char AO = (char) 245; // Abort output private const char AYT = (char) 246; // Are you there? private const char EC = (char) 247; // Erase character private const char EL = (char) 248; // Erase line private const char GA = (char) 249; // Go ahead private const char SB = (char) 250; // Subnegotiation private const char WILL = (char) 251; // Will private const char WONT = (char) 252; // Won't private const char DO = (char) 253; // Do private const char DONT = (char) 254; // Don't private const char IAC = (char) 255; // Interpret as command private const char IS = (char) 0; // Is private const char SEND = (char) 1; // Send private const char VAR = (char) 0; // Var private const char VALUE =(char) 1; // Value // ESC 2 // USERVAR 3 // Telnet option codes private const char ECHO = (char) 1; // Echo, RFC 857 private const char SGA = (char) 3; // Suppress go ahead, RFC 858 private const char STATUS = (char) 5; // Status, RFC 859 private const char TIME_MARK = (char) 6; // Timing mark, RFC 860 private const char TELOPT_TTYPE = (char) 24; // Terminal type, RFC 1091 private const char TELOPT_NAWS = (char) 31; // Window size, RFC 1073 private const char TERM_SPD = (char) 32; // Terminal speed, RFC 1079 private const char RFC = (char) 33; // Remote flow control, RFC 1372 private const char LINEMODE = (char) 34; // Kludge line mode, RFC 1184 private const char ENV_VAR = (char) 36; // Environment variables, RFC 1408 private const char X_DISPLAY_LOC = (char) 35; // X Display Location, RFC 1096 private const char NEW_ENV_OPT = (char) 39; // New Environment Option, RFC 1572 // Misc characters private const char BACKSPACE = (char) 127; // Backspace private const char DELETE = (char) 8; // Delete private const char CARRIAGE_RETURN = (char) 10; // Carriage return private const char NEW_LINE = (char) 13; // Newline private const char ESC = (char) 0x1B; // The ESC character used for the VT100 escape codes #endregion Control constants private System.Net.Sockets.TcpClient client; private System.Net.Sockets.NetworkStream stream; private System.IO.StreamReader streamReader; private System.Text.Encoding encoding; private DateTime lastReceivedTime; /// /// Connects to a specified address /// /// A string in the format of "nethack.alt.org:23" where "nethack.alt.org" is the hostname and "23" is the host port to connect to. public void Connect(string address) { encoding = System.Text.Encoding.GetEncoding("iso-8859-1"); string[] hostport = address.Split(':'); client = new TcpClient(hostport[0], System.Convert.ToInt32(hostport[1])); stream = client.GetStream(); streamReader = new StreamReader(stream, encoding); lastReceivedTime = DateTime.Now; this.Send(IAC.ToString() + DO.ToString() + STATUS.ToString()); this.Send(IAC.ToString() + WILL.ToString() + TELOPT_NAWS.ToString()); string naws_str = IAC.ToString() + SB.ToString() + TELOPT_NAWS.ToString() + ((char) 0).ToString() + ((char) (WIDTH)).ToString() + ((char) 0).ToString() + ((char) (HEIGHT)).ToString() + IAC.ToString() + SE.ToString(); // jtbvUtils.HexPrint(naws_str); this.Send(naws_str); } /// /// GetInput() is meant to be called on a regular basis, and is responsible /// for checking the state of the socket, reading characters from the /// socket and adding them to the buffer, and executing commands when /// a carriage return is received. /// public string GetInput() { string retVal = ""; string recBuf = ""; while (stream.DataAvailable) { recBuf += ((char)stream.ReadByte()); // recBuf += streamReader.ReadToEnd(); } if (recBuf.Length > 0) { jtbvLog.LogToFile("Received: " + recBuf); for (int cnt = 0; cnt < recBuf.Length; cnt++) { switch (recBuf[cnt]) { #region Telnet Negotiation case IAC: // Interpret as command cnt++; if (cnt < recBuf.Length) { switch (recBuf[cnt]) { case SE: case NOP: case DM: case BRK: case IP: case AO: case AYT: case EC: case EL: case GA: case SB: // Telopt subnegotiation cnt++; if (cnt < recBuf.Length) { switch (recBuf[cnt]) { case STATUS: if (recBuf[cnt+1] == IS) { this.pongReceived = true; } else if (recBuf[cnt+1] == SEND) // The server is requesting us to send our STATUS { } // Console.WriteLine("Status received, cnt = {0}, recBuf.Length = {1}", cnt, recBuf.Length); // Console.WriteLine("Counter moved, cnt = {0}, recBuf.Length = {1}", cnt, recBuf.Length); break; case TELOPT_TTYPE: if ((recBuf[cnt+1] == SEND) && (recBuf[cnt+2] == IAC) && (recBuf[cnt+3] == SE)) { // Server wishes the client to send the next ttype // Console.WriteLine("Sending terminal type to the server..."); this.Send(IAC.ToString() + SB.ToString() + TELOPT_TTYPE.ToString() + IS.ToString() + "XTERM" + IAC.ToString() + SE.ToString()); } break; case TERM_SPD: if ((recBuf[cnt+1] == SEND) && (recBuf[cnt+2] == IAC) && (recBuf[cnt+3] == SE)) { // Server wishes the client to send the next ttype // Console.WriteLine("Sending terminal speed to the server..."); this.Send(IAC.ToString() + SB.ToString() + TERM_SPD.ToString() + IS.ToString() + "38400,38400" + IAC.ToString() + SE.ToString()); } break; case TELOPT_NAWS: break; case NEW_ENV_OPT: this.Send(IAC.ToString() + SB.ToString() + NEW_ENV_OPT.ToString() + IS.ToString() + IAC.ToString() + SE.ToString()); break; default: Console.WriteLine("Unhandled telnet option negotiation! Option = {0}, cnt = {1}", ((int) recBuf[cnt]), cnt); break; } // Next, we advance the cursor to the end of the option subnegotiation section. // Console.WriteLine("Advancing cursor..."); // jtbvUtils.HexPrint(recBuf, recBuf.Length); while (cnt < recBuf.Length) { // Console.WriteLine("Looping... cnt = {0}", cnt); if (cnt >= 2) { if ((recBuf[cnt-1] == IAC) && (recBuf[cnt] == SE)) { // Console.WriteLine("Saw end of subnegotiation phrase. Exiting while..."); break; } } cnt++; } // Console.WriteLine("Leaving telnet option negotiation, cnt = {0}, recBuf.Length = {1}", cnt, recBuf.Length); //cnt++; } else { Console.WriteLine("Error in telnet option subnegotiation -- ran out of bytes. cnt = {0}, recBuf.Length = {1}", cnt, recBuf.Length); jtbvUtils.HexPrint(recBuf); } break; case WILL: // Server wants to do this... cnt++; if (cnt < recBuf.Length) { // Console.WriteLine("Server will do option " + recBuf[cnt].ToString()); switch (recBuf[cnt]) { case ECHO: case SGA: case STATUS: case TIME_MARK: case TELOPT_TTYPE: case TELOPT_NAWS: case TERM_SPD: case RFC: case LINEMODE: this.Send(IAC.ToString() + DO.ToString() + ((char) recBuf[cnt]).ToString()); break; } } else { Console.WriteLine("Error in telnet 'will' block -- ran out of bytes"); } break; case WONT: // Server won't do... cnt++; if (cnt < recBuf.Length) { switch (recBuf[cnt]) { case ECHO: case SGA: case STATUS: case TIME_MARK: case TELOPT_TTYPE: case TELOPT_NAWS: case TERM_SPD: case RFC: case LINEMODE: this.Send(IAC.ToString() + WONT.ToString() + ((char) recBuf[cnt]).ToString()); break; } } else { Console.WriteLine("Error in telnet 'wont' block -- ran out of bytes"); } break; case DO: // Client asked to... cnt++; if (cnt < recBuf.Length) { // Console.WriteLine("Client asked to do option " + recBuf[cnt].ToString()); switch (recBuf[cnt]) { // Enabled options case RFC: case ECHO: case TELOPT_TTYPE: case NEW_ENV_OPT: case TELOPT_NAWS: this.Send(IAC.ToString() + WILL.ToString() + ((char) recBuf[cnt]).ToString()); break; // Disabled options... case SGA: case STATUS: case TIME_MARK: case LINEMODE: case TERM_SPD: default: this.Send(IAC.ToString() + WONT.ToString() + ((char) recBuf[cnt]).ToString()); // Console.WriteLine("Refusing..."); break; } } else { Console.WriteLine("Error in telnet 'do' block -- ran out of bytes"); } break; case DONT: // Client is forbidden from... cnt++; if (cnt < recBuf.Length) { // Console.WriteLine("Client asked to NOT do option " + recBuf[cnt].ToString()); switch (recBuf[cnt]) { case ECHO: case SGA: case STATUS: case TIME_MARK: case TELOPT_TTYPE: case TELOPT_NAWS: case TERM_SPD: case RFC: case LINEMODE: default: this.Send(IAC.ToString() + WONT.ToString() + ((char) recBuf[cnt]).ToString()); break; } } else { Console.WriteLine("Error in telnet 'dont' block -- ran out of bytes"); } break; } } break; #endregion Telnet Negotiation default: // If it's not a Telnet command... // Then we just send this information on up to the TTY reader retVal += recBuf[cnt]; break; } } lastReceivedTime = DateTime.Now; } if (DateTime.Now > (lastReceivedTime + TimeSpan.FromSeconds(10))) { // If we haven't receieved any data for a long time, then we set our timeout flag so that we can bail out. this.timeout = true; Console.WriteLine("Timeout detected..."); } return (retVal); } public void Send(string sendStr) { jtbvLog.LogToFile("Sent: " + sendStr); byte[] outBuffer = encoding.GetBytes(sendStr); try { if (outBuffer.Length > 0) stream.Write(outBuffer, 0, outBuffer.Length); } catch (IOException e) { this.timeout = true; Console.WriteLine(e.Message); } } /// /// Request that the server send me his status. When his status is received and processed, the OnPong event is fired. /// public void Ping() { // Reset our pong status. pongReceived = false; // Request that the server send me his status. string pingString = IAC.ToString() + SB.ToString() + STATUS.ToString() + SEND.ToString() + IAC.ToString() + SE.ToString(); this.Send(pingString); } private bool pongReceived; public bool PongReceived { get { // Make sure we wait for receiving data for 250 milliseconds before confirming that a pong was received. if (DateTime.Now > (lastReceivedTime + TimeSpan.FromMilliseconds(250))) return pongReceived; else return false; } } private bool timeout; public bool Timeout { get { return timeout; } } } }