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