Introduction
This article deals with my TelnetSocket
class, which is a wrapper around System.Net.Sockets.TcpClient
and adds some simple Telnet option negotiation. Although an instance of TelnetSocket
may be used on its own (see the demo), many of the features of TelnetSocket
are in response to the needs of my communication scripting engine (which will have its own article soon).
What is presented here (and when I write the other article) is the result of several years evolution; originally, the socket and the script engine were combined in one class, but I have since decided that the engine could be used to execute scripts on other types of communication links, so I split them apart.
Background
One of the more irksome, yet challenging and ultimately rewarding things I had to accomplish on my last job was the automation of Telnetting into a Unix system, running a third-party application, navigating a character-based menu/form system, entering data, and responding to feedback -- and still exiting neatly. It quickly became apparent that I needed to have my code write a script and then have something execute the script. For various reasons, I decided to write my own script language and interpreter. To do this, I needed a class that would handle the communication across the network, and TcpClient
wasn't enough.
An online search provided me with just such a class, written in C# (yay!). I have since lost the original code I downloaded, I don't remember who wrote it, and I don't remember where I got it (it wasn't CodeProject). I'd just like to at least acknowledge here that even though I believe that all the code presented here is mine, I had help getting started.
Recently in the C# forum, someone asked about a class to Telnet into another system, so I felt it was time to finally publish this. I hope someone finds it useful (at least as the basis for his own class).
IScriptableCommunicator
As mentioned above, the primary concern in my design and implementation of this class was the ability to have a script engine use it to automate interaction with a third-party application on a Unix system. The scripts were executed within a Windows Service -- there was no need for a user interface, so no concern was given to being usable by interactive processes.
The result is the following interface:
public delegate void DataReceived ( string Data ) ;
public delegate void ExceptionCaught ( System.Exception Exception ) ;
public interface IScriptableCommunicator : System.IDisposable
{
void Connect ( string Host ) ;
void WriteLine ( string Data , params object[] Parameters ) ;
void Write ( string Data , params object[] Parameters ) ;
void Close () ;
System.TimeSpan ResponseTimeout { get ; set ; }
System.Text.Encoding Encoding { get ; set ; }
event DataReceived OnDataReceived ;
event ExceptionCaught OnExceptionCaught ;
}
With an instance of a class that implements this interface, your code can:
- Set a timeout (the connection will abort if the timeout expires, the default is one minute)
- Specify an encoding to use to convert between
byte[]
and string
(the default is ASCII) - Connect to a Telnet server
- Send data to the server
- Receive data back
- Receive Exceptions that occur when sending data
ScriptableCommunicator
... and abstract class:
public abstract class ScriptableCommunicator : IScriptableCommunicator
{
private System.Text.Encoding encoding ;
private string lineterminator ;
protected ScriptableCommunicator
(
System.TimeSpan ResponseTimeout
,
System.Text.Encoding Encoding
,
string LineTerminator
)
{
this.ResponseTimeout = ResponseTimeout ;
this.Encoding = Encoding ;
this.LineTerminator = LineTerminator ;
this.Timer = null ;
return ;
}
public abstract void Connect ( string Host ) ;
public abstract void Write ( string Data , params object[] Parameters ) ;
public abstract void Close () ;
public virtual void
WriteLine
(
string Format
,
params object[] Parameters
)
{
this.Write ( Format + this.LineTerminator , Parameters ) ;
return ;
}
public virtual System.TimeSpan ResponseTimeout { get ; set ; }
public virtual System.Text.Encoding Encoding
{
get
{
return ( this.encoding ) ;
}
set
{
if ( value == null )
{
throw ( new System.InvalidOperationException
( "The value of Encoding must not be null" ) ) ;
}
this.encoding = value ;
return ;
}
}
public virtual string
LineTerminator
{
get
{
return ( this.lineterminator ) ;
}
set
{
if ( value == null )
{
throw ( new System.InvalidOperationException
( "The value of LineTerminator must not be null" ) ) ;
}
this.lineterminator = value ;
return ;
}
}
protected virtual System.Timers.Timer Timer { get ; set ; }
public event DataReceived OnDataReceived ;
protected virtual void
RaiseDataReceived
(
string Data
)
{
if ( this.Timer != null )
{
this.Timer.Stop() ;
}
if ( this.OnDataReceived != null )
{
this.OnDataReceived ( Data ) ;
}
if ( this.Timer != null )
{
this.Timer.Start() ;
}
return ;
}
public event ExceptionCaught OnExceptionCaught ;
protected virtual void
RaiseExceptionCaught
(
System.Exception Exception
)
{
if ( OnExceptionCaught != null )
{
OnExceptionCaught ( Exception ) ;
}
return ;
}
public virtual void
Dispose
(
)
{
this.Close() ;
return ;
}
}
TelnetSocket
Once it was stripped of its scripting duties, TelnetSocket
became fairly simple.
Fields and Constructors
There are fields for the underlying TcpClient
, a thread to read from the socket asynchronously, and a boolean to let the thread know that it should stop.
The constructor simply sets the default timeout and encoding.
public sealed class TelnetSocket : PIEBALD.Types.ScriptableCommunicator
{
private System.Net.Sockets.TcpClient socket = null ;
private System.Net.Sockets.NetworkStream stream = null ;
private System.Threading.Thread reader = null ;
private bool abort = false ;
public TelnetSocket
(
)
{
this.ResponseTimeout = new System.TimeSpan ( 0 , 1 , 0 ) ;
this.Encoding = System.Text.Encoding.ASCII ;
return ;
}
}
Connect
The two overloads of Connect
perform input validation.
The real work is done by DoConnect
; it instantiates the TcpClient
and spawns off the thread for the reader.
public override void
Connect
(
string Host
)
{
if ( Host == null )
{
throw ( new System.ArgumentNullException ( "Host" , "Host must not be null" ) ) ;
}
string[] parts = Host.Split ( new char[] { ':' } , 2 ) ;
int port = 23 ;
if ( ( parts.Length > 1 ) && !int.TryParse ( parts [ 1 ] , out port ) )
{
throw ( new System.ArgumentException ( "Unable to parse the port" , "Host" ) ) ;
}
this.DoConnect ( parts [ 0 ] , port ) ;
return ;
}
public override void
Connect
(
string Host
,
int Port
)
{
if ( Host == null )
{
throw ( new System.ArgumentNullException ( "Host" , "Host must not be null" ) ) ;
}
this.DoConnect ( Host , Port ) ;
return ;
}
private void
DoConnect
(
string Host
,
int Port
)
{
this.socket = new System.Net.Sockets.TcpClient ( Host , Port ) ;
this.socket.NoDelay = true ;
this.stream = this.socket.GetStream() ;
this.stream.ReadTimeout = 1000 ;
this.reader = new System.Threading.Thread ( this.Reader ) ;
this.reader.Priority = System.Threading.ThreadPriority.BelowNormal ;
this.reader.IsBackground = true ;
this.reader.Start() ;
return ;
}
WriteLine / Write
WriteLine
simply adds a carriage return to the data and passes it to Write
.
Write
formats the parameters into the data, and uses the selected encoding to convert the string to an array of bytes, then handles the writing of the data and the catching and reporting of Exceptions.
public override void
WriteLine
(
string Format
,
params object[] Parameters
)
{
this.Write ( Format + "\r" , Parameters ) ;
return ;
}
public override void
Write
(
string Format
,
params object[] Parameters
)
{
if ( this.socket == null )
{
throw ( new System.InvalidOperationException (
"The socket appears to be closed" ) ) ;
}
try
{
if ( ( Parameters != null ) && ( Parameters.Length > 0 ) )
{
Format = System.String.Format
(
Format
,
Parameters
) ;
}
byte[] data = this.Encoding.GetBytes ( Format ) ;
lock ( this.stream )
{
this.stream.Write ( data , 0 , data.Length ) ;
}
}
catch ( System.Exception err )
{
this.RaiseExceptionCaught ( err ) ;
throw ;
}
return ;
}
Close
Close
initiates a shutdown of the asynchronous reader thread and disconnects the socket.
public override void
Close
(
)
{
this.Abort() ;
if ( this.socket != null )
{
this.stream = null ;
this.socket.Close() ;
this.socket = null ;
}
return ;
}
Abort
Attempts an orderly shutdown of the asynchronous reader thread, but will resort to aborting it if it doesn't shut itself down within fifteen seconds.
private void
Abort
(
)
{
this.abort = true ;
if ( this.reader != null )
{
if ( this.reader.IsAlive )
{
if ( !this.reader.Join ( 15000 ) )
{
this.reader.Abort() ;
this.reader.Join ( 15000 ) ;
}
}
this.reader = null ;
}
return ;
}
Reader
Reader
is the method that executes on the reader thread and performs the reading of the data from the socket. It uses an instance of the Negotiator
class to handle the Telnet option negotiation. Any non-command data is passed to the OnDataReceived
event. The method will exit if signaled to by the Abort
method or if there has been no data received within the timeout period.
private void
Reader
(
)
{
using
(
Negotiator neg
=
new Negotiator ( this.stream )
)
{
byte[] buffer = new byte [ this.socket.ReceiveBufferSize ] ;
int bytes ;
System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch() ;
timer.Start() ;
while ( !this.abort )
{
bytes = 0 ;
lock ( this.stream )
{
if ( this.socket.Available > 0 )
{
bytes = this.stream.Read ( buffer , 0 , buffer.Length ) ;
}
}
if ( bytes > 0 )
{
timer.Reset() ;
bytes = neg.Negotiate ( buffer , bytes ) ;
if ( bytes > 0 )
{
this.RaiseDataReceived (
this.Encoding.GetString ( buffer , 0 , bytes ) ) ;
}
timer.Start() ;
}
else
{
if ( timer.Elapsed > this.ResponseTimeout )
{
this.abort = true ;
}
else
{
System.Threading.Thread.Sleep ( 100 ) ;
}
}
}
timer.Stop() ;
}
return ;
}
Negotiator
The Negotiator
class encapsulates the Telnet command negotiation process.
private sealed class Negotiator : System.IDisposable
{
private System.Net.Sockets.NetworkStream stream ;
public Negotiator
(
System.Net.Sockets.NetworkStream Stream
)
{
this.stream = Stream ;
return ;
}
public void
Dispose
(
)
{
if ( this.stream != null )
{
this.stream = null ;
}
return ;
}
}
Negotiate
The Negotiate
method detects and extracts Telnet commands from the received data (and shifts any following data). The implementation of this process is based on the work of someone else, but I streamlined it significantly. I'm no expert on this subject; most of what I know, I learned from the original code I downloaded.
Basically, there are many options that can be used during a Telnet session, and the host and client must negotiate what options will be used during a particular session. The simple form is <IAC><action><option>. E.g.: <IAC><WILL><ECHO>. There is also an extended form, which I won't discuss here.
The option requirements for this class are simple: don't offer any options, don't request any options, refuse to perform any options if requested, refuse any options other than ECHO and SUPPRESS-GO-AHEAD. You can find some information on Telnet commands at:
The basic algorithm used here is:
- Iterate across the incoming bytes
- Assume that an IAC (byte 255) is the first of a two- or three-byte Telnet command and handle it:
- If two IACs are together, they represent one data byte 255
- Ignore the Go-Ahead command
- Respond WONT to all DOs and DONTs
- Respond DONT to all WONTs
- Respond DO to WILL ECHO and WILL SUPPRESS GO-AHEAD
- Respond DONT to all other WILLs
- Any other bytes are data; ignore nulls, and shift the rest as necessary
- Return the number of bytes that remain after removing the Telnet command and ignoring nulls
public int
Negotiate
(
byte[] Buffer
,
int Count
)
{
int resplen = 0 ;
int index = 0 ;
while ( index < Count )
{
if ( Buffer [ index ] == TelnetByte.IAC )
{
try
{
switch ( Buffer [ index + 1 ] )
{
case TelnetByte.IAC :
{
Buffer [ resplen++ ] = Buffer [ index ] ;
index += 2 ;
break ;
}
case TelnetByte.GA :
{
index += 2 ;
break ;
}
case TelnetByte.DO :
case TelnetByte.DONT :
{
Buffer [ index + 1 ] = TelnetByte.WONT ;
lock ( this.stream )
{
this.stream.Write ( Buffer , index , 3 ) ;
}
index += 3 ;
break ;
}
case TelnetByte.WONT :
{
Buffer [ index + 1 ] = TelnetByte.DONT ;
lock ( this.stream )
{
this.stream.Write ( Buffer , index , 3 ) ;
}
index += 3 ;
break ;
}
case TelnetByte.WILL :
{
byte action = TelnetByte.DONT ;
if ( Buffer [ index + 2 ] == TelnetByte.ECHO )
{
action = TelnetByte.DO ;
}
else if ( Buffer [ index + 2 ] == TelnetByte.SUPP )
{
action = TelnetByte.DO ;
}
Buffer [ index + 1 ] = action ;
lock ( this.stream )
{
this.stream.Write ( Buffer , index , 3 ) ;
}
index += 3 ;
break ;
}
}
}
catch ( System.IndexOutOfRangeException )
{
index = Count ;
}
}
else
{
if ( Buffer [ index ] != 0 )
{
Buffer [ resplen++ ] = Buffer [ index ] ;
}
index++ ;
}
}
return ( resplen ) ;
}
Using the Code
The zip file contains ScriptableCommunicator.cs, TelnetSocket.cs, and TelnetSocketDemo.cs. The demo can be compiled with the command:
csc TelnetSocketDemo.cs TelnetSocket.cs ScriptableCommunicator.cs
The prompts contained in the enclosed version are set up for communicating with one of my AlphaServers. If you have a Telnet server handy, you can alter the demo program to match it, compile it, and try it out.
The important things to notice about how the TelnetSocket
is used in the demo is how tedious it is to write a program that will interact with a Telnet host given just a simple class like this and that the code begins to look almost like a script. You may then see how making the leap from hard-coded commands to a full-blown script can save you a lot of trouble and offer you a lot of flexibility.
TelnetSocketDemo
Here's the demo program; it is very simplistic, it is intended for testing and demonstration purposes only.
It has handlers for the events of the TelnetSocket
and rudimentary ability to wait for certain text to arrive in the host's responses before sending the next command. A TelnetSocket
is instantiated, has the handlers attached, connects, interacts, and disconnects.
The interaction takes the form: wait for a prompt, send some data, repeat.
namespace TelnetSockectDemo
{
public static class TelnetSocketDemo
{
private static readonly System.Text.StringBuilder response =
new System.Text.StringBuilder() ;
private static void
DataReceived
(
string Data
)
{
lock ( response )
{
response.Append ( Data ) ;
}
return ;
}
private static void
ExceptionCaught
(
System.Exception Exception
)
{
throw ( Exception ) ;
}
private static void
WaitFor
(
string Prompt
)
{
while ( response.ToString().IndexOf ( Prompt ) == -1 )
{
System.Threading.Thread.Sleep ( 100 ) ;
}
lock ( response )
{
System.Console.Write ( response ) ;
response.Length = 0 ;
}
return ;
}
[System.STAThreadAttribute()]
public static int
Main
(
string[] args
)
{
int result = 0 ;
try
{
if ( args.Length > 3 )
{
using
(
PIEBALD.Types.TelnetSocket socket
=
new PIEBALD.Types.TelnetSocket()
)
{
socket.OnDataReceived += DataReceived ;
socket.OnExceptionCaught += ExceptionCaught ;
socket.Connect ( args [ 0 ] ) ;
WaitFor ( "Username:" ) ;
socket.WriteLine ( args [ 1 ] ) ;
WaitFor ( "Password:" ) ;
socket.WriteLine ( args [ 2 ] ) ;
WaitFor ( "mJB>" ) ;
for ( int i = 3 ; i < args.Length ; i++ )
{
socket.WriteLine ( args [ i ] ) ;
WaitFor ( "mJB>" ) ;
}
socket.WriteLine ( "lo" ) ;
WaitFor ( "logged out" ) ;
socket.Close() ;
}
}
else
{
System.Console.WriteLine ( "Syntax: TelnetSocketDemo" +
" host username password command..." ) ;
}
}
catch ( System.Exception err )
{
System.Console.WriteLine ( err ) ;
}
return ( result ) ;
}
}
}
The hard-coded prompts are a limiting factor to the usefulness of such a utility. Another limiting factor is the lack of ability to branch or otherwise react to variable situations. For these and other reasons, I saw the need to develop a scripting language; I should have an article on that written shortly.
History
- 2010-03-03: First submitted
- 2010-04-22: Updated with changes to the code made while developing
ProcessCommunicator