Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

TelnetSocket

4.47/5 (20 votes)
3 Apr 2010CPOL6 min read 1   6K  
A wrapper for a System.Net.Sockets.TcpClient that performs simple Telnet negotiation and is scriptable.

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:

C#
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:

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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
            {
                /* Expired time limit will trigger an abort */
                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.

C#
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
C#
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 ] )
                {
                    /* If two IACs are together they represent one data byte 255 */
                    case TelnetByte.IAC :
                    {
                        Buffer [ resplen++ ] = Buffer [ index ] ;
 
                        index += 2 ;
 
                        break ;
                    }
 
                    /* Ignore the Go-Ahead command */
                    case TelnetByte.GA :
                    {
                        index += 2 ;
 
                        break ;
                    }
 
                    /* Respond WONT to all DOs and DONTs */
                    case TelnetByte.DO   :
                    case TelnetByte.DONT :
                    {
                        Buffer [ index + 1 ] = TelnetByte.WONT ;
 
                        lock ( this.stream )
                        {
                            this.stream.Write ( Buffer , index , 3 ) ;
                        }
 
                        index += 3 ;
 
                        break ;
                    }
 
                    /* Respond DONT to all WONTs */
                    case TelnetByte.WONT :
                    {
                        Buffer [ index + 1 ] = TelnetByte.DONT ;
 
                        lock ( this.stream )
                        {
                            this.stream.Write ( Buffer , index , 3 ) ;
                        }
 
                        index += 3 ;
 
                        break ;
                    }
 
                    /* Respond DO to WILL ECHO and WILL SUPPRESS GO-AHEAD */
                    /* Respond DONT to all other WILLs                    */
                    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 )
            {
                /* If there aren't enough bytes to form a command, terminate the loop */
                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.

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)