-= Network Protocol Tutorial using Perl =-
-= Network Protocol Tutorial using Perl =-

Telnet Protocol Client

Chris Huyler     Josh Birkins

  1. Statement of Purpose
  2. Introduction to Telnet
  3. Using the Telnet Protocol with Perl - Intro to Net-Telnet
  4. A Telnet Client Example - Get a file through telnet
  5. Telnet Interactions - Textual screen shots
  6. Appendix - Code


Statement of Purpose

This tutorial is designed to give you an overview of the telnet protocol and using the net-telnet Perl module to write a client script. Included is a brief introduction to telnet, a sample conversation between client and server, and a short programming exercise using Perl.


Introduction to Telnet

Telnet is the main Internet protocol for creating a connection with a remote machine. It gives the user the opportunity to be on one computer system and do work on another, which may be across the street or thousands of miles away. Where modems are limited, in the majority, by the quality of telephone lines and a single connection, telnet provides a connection that's error-free and nearly always faster than the latest conventional modems. Many telnet clients also include a third option, the port on which the connection should take place. Normally, port 23 is the default telnet port; the user never has to think about it. But sometimes it's desirable to telnet to a different port on a system, where there may be a service available, or to aid in debugging a problem.

Using Telnet:

As with FTP (file transfer protocol), the actual command for negotiating a telnet connection varies from system to system. The most common is telnet itself, though. It takes the form of: telnet somewhere.domain. To be safe, we'll use your local system as a working example. By now, you hopefully know your site's domain name. If not, ask or try to figure it out. You'll not get by without it.

To open the connection, type: telnet your.system.name
If the system were www.bright-blade.net, for example, the command would look like:
telnet www.bright-blade.net
The system will respond with something similar to:
Trying 24.24.92.198…
Connected to www.bright-blade.net.
Escape character is '^]'

The escape character, in this example ^] (Control-]), is the character that will let you go back to the local system to close the connection, suspend it, etc. To close this connection, the user would type ^], and respond to the telnet> prompt with the command close. Local documentation should be checked for information on specific commands, functions, and escape character that can be used.

Once a connection is established, a welcome message is usually printed by the server and then the user must log in. The server will request that the user enter his/her username and password.
login: bilbo
password: *******

After the user has entered a valid username and password they are given the command prompt. The command prompt usually contains user name and the name of the server followed by a symbol such as a $, #, or %. However, the prompt may contain more information like the current directory, or no information at all and just a symbol. Here are some examples:
www% (Ithaca College's web server)
[bilbo@sturm bilbo]$ (my server: username@servername, current_directory)

From the command prompt the user can execute shell commands which alter the state of the machine or run programs. Each system is different and can use any number of shells that have different command structures. One that is universal is the exit command. simply type exit or logout to disconnect from the remove server.
[bilbo@sturm bilbo]$ exit

Here is a complete telnet conversation. Lines starting with a C: are written by the client and lines starting with a S: are written by the server.

S: Welcome to sturm.bright-blade.net
S: Linux Mandrake release 7.0 (Air)
S: Kernel 2.2.14-15mdk on an i686
S: login: 
C: bilbo
S: Password:
C: baggins           <- this is sometimes all *s or not echoed to the screen at all
S: Last login: Thu Apr 18 09:23:06 from 147.129.100.172
S: [bilbo@sturm bilbo]$
C: exit 

See Example 1 in the Interactions section for a full interaction.


Coding a Telnet Client with Perl

In order to implement a telnet client using Perl, we must first install the Net-Telnet module. Open a DOS command window and enter the following:

C:\> cd \Perl\bin
C:\> ppm
PPM> install net-telnet
PPM> exit

Once you have the net-telnet module installed, create a new text file titled telnet1.pl. Start out by typing the following lines to create a new telnet data structure.

use Net::Telnet;
$telnet = new Net::Telnet ( Timeout=>20,
                            Errmode=>'die');

Now you need to open the connection by using the following function:

$telnet->open('www.bright-blade.net');

Following the telnet conversation described above, we need to give the remote host our username and password in order to login. Then before issuing any commands we need to wait for the command prompt. Since different servers use different prompts we can use a regular expression to recognize any prompt. The phrase "[\$%#>] " means try to match one of the following characters (the $ is a special character in Perl so it must be preceded by a \) followed by a space--do not forget the space!

$telnet->waitfor('/login: $/i');
telnet->print('bilbo');
$telnet->waitfor('/password: $/i');
$telnet->print('baggins');

$telnet->waitfor('/[\$%#>] $/i');

Now that we are connected we can send a command to the receiving end and print the results to the screen:

$telnet->print('who');
$output = $telnet->waitfor('/[\$%#>] $/i');

We do not have to close the connection because once the script ends the connection is terminated. However, if you want, you can print "exit" or "logout" to the socket. Save your file and execute it by typing perl telnet1.pl at the command line. If all is well, it should print the users currently connected to that server including yourself.

See Example 2 in the Interactions section to see the output of this script.

A simpler way of doing this is outlined in the following code. Using functions built into the net-telnet module, we can make a connection and execute commands very simply. Create a new file called telnet2.pl and type this script. Note that the prompt is defined when creating the telnet structure so it is possible to just call commands without waiting for the prompt each time.

use Net::Telnet;
$telnet = new Net::Telnet ( Timeout=>20,
                            Errmode=>'die',
                            Prompt => '/[\$%#>] $/i');

$telnet->open('www.bright-blade.net');

$telnet->login('bilbo', 'baggins');
print $telnet->cmd('who');

A Telnet Example

Now that you understand the basics of using the net-telnet module we can make a more complex script. Follow the following steps to create a script that uses a telnet connection to retrieve the contents of a file.

First we need to create and initialize some variables that we are going to use. You can substitute any of these for an existing account you know of. (Note: Kevin, I have created this account on my server so you can test the script)

## define variables
my ($block, $filename, $host, $hostname, $k_per_sec, $line,
        $num_read, $passwd, $prevblock, $prompt, $size, $size_bsd,
        $size_sysv, $start_time, $total_time, $username);

## initialize variables
    $hostname = "www.bright-blade.net";
    $username = "bilbo";
    $passwd = "baggins";
    $filename = ".bashrc";

Next we need to establish a connection. I have chosen to use STDERR to print status messages to the screen so that the contents of the file can be redirected using a pipe to an output file without including the messages. These messages are not necessary, but they are excelent for debugging.

## Connect and login.
    use Net::Telnet ();
    $host = new Net::Telnet (Timeout =>60,
			     Errmode=>'die',
                             Prompt =>'/[\$%#>_] $/i');
    $host->open($hostname);
      print STDERR "connected...\n";
      print STDERR "logging in: $username...";
    $host->login($username,$passwd);
      print STDERR "\tdone\n";

When sending commands to the remote host you may have noticed that the command prompt is always the last piece of text returned to the client. In order to remove this from our file we must change it to something we know will not be included in the file. That way we can cut it off after the file is retrieved. Here is the code to change the command prompt in a bash shell. If you are using a different shell, the command may be different.

## Make sure prompt won't match anything in send data.
      print STDERR "switching prompt...";
    $prompt = 'funkyPrompt% ';
    $host->prompt("/$prompt\$/");
    $host->cmd("export PS1='$prompt'");
      print STDERR "\tdone\n";

Now we need to obtain the file size so that when we finish reading it we can check to see that we have received the entire file. This is done by using the ls -l command which will return all the information about the file. We then parse that information for a variable containing only digits which should be the file size.

## Get size of file.
      print STDERR "getting file size...";
    ($line) = $host->cmd("/bin/ls -l $filename");
    ($size_bsd, $size_sysv) = (split ' ', $line)[3,4];
    if ($size_sysv =~ /^\d+$/) {
        $size = $size_sysv;
    }
    elsif ($size_bsd =~ /^\d+$/) {
        $size = $size_bsd;
    }
    else {
        die "$filename: no such file on $hostname";
    }
     print STDERR "\tdone\n";

Now is the tricky part: getting the file. Basicly we have the server echo the file to the client using the cat function then read it one line at a time until the prompt is read. We also want to time how long this takes by storing the current time value in $start_time. Once the file is retrieved we can close the connection.

## Start sending the file.
      print STDERR "getting file...";
    binmode STDOUT;
    $host->binmode(1);
    $host->print("/bin/sh -c 'stty raw; cat $filename'");
    $host->getline;    # discard echoed back line

## Read file a block at a time.
    $num_read = 0;
    $prevblock = '';
    $start_time = time;
    while (($block = $host->get) and ($block !~ /$prompt$/o)) {
        if (length $block >= length $prompt) {
            print $prevblock;
            $num_read += length $prevblock;
            $prevblock = $block;
        }
        else {
            $prevblock .= $block;
        }    }
    $host->close;
      print STDERR "\t\tdone\n";

Next, we need to remove the prompt from the last line by swapping it with an empty string before we print it. Then we can check the overall length of the file to see if it matches the size we determined above.

## Print last block without trailing prompt.
    $prevblock .= $block;
    $prevblock =~ s/$prompt$//;
    print $prevblock;
    $num_read += length $prevblock;
    die "error: expected size $size, received size $num_read\n"
        unless $num_read == $size;    

Finally, we can print the elapsed time and the size of the file.

## Print totals.
    $total_time = (time - $start_time) || 1;
    $k_per_sec = ($size / 1024) / $total_time;
    $k_per_sec = sprintf "%3.1f", $k_per_sec;
    warn("$num_read bytes received in $total_time seconds ",
         "($k_per_sec Kbytes/s)\n");
    exit;  

Once you have entered in all the code above save it as getfile.pl. To run it you can do two things: print it to the screen or print it to a file. See Examples 3 and 4 in the Interactions section.


Telnet Interactions

Example 1 - a telnet login

[chuyler1@sturm chuyler1]$ telnet www.bright-blade.net
Trying 24.24.22.198...
Connected to www.bright-blade.net.
Escape character is '^]'.
Welcome to sturm.bright-blade.net
Linux Mandrake release 7.0 (Air)
Kernel 2.2.14-15mdk on an i686
login: bilbo
Password:
Last login: Wed Apr 17 21:19:09 from 192.168.1.1
[bilbo@sturm bilbo]$ who
chuyler1 tty1     Apr 17 21:12
chuyler1 pts/0    Apr 17 21:17
bilbo    pts/1    Apr 17 21:19
[bilbo@sturm bilbo]$ exit
Connection closed by foreign host.

Example 2 - telnet1.pl output

C:\Perl\bin>perl telnet1.pl
connected...
entered username...
entered password...
logged in!
chuyler1 tty1     Apr 17 21:12
bilbo    pts/0    Apr 18 08:27
[bilbo@sturm bilbo]

Example 3 - telnet2.pl output

C:\Perl\bin>perl telnet2.pl
connected...
logged in!
chuyler1 tty1     Apr 17 21:12
bilbo    pts/0    Apr 18 08:27
[bilbo@sturm bilbo]

Example 4 - getfile.pl output 1

C:\Perl\bin>perl getfile.pl
connected...
logging in: bilbo...    done
switching prompt...     done
getting file size...    done
getting file...         done
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/bin
BASH_ENV=$HOME/.bashrc
USERNAME=""

export USERNAME BASH_ENV PATH

230 bytes received in 1 seconds (0.2 Kbytes/s)

Example 5 - getfile.pl output 2 (contents of file stored in myfile.txt)

C:\Perl\bin>perl getfile.pl > myfile.txt
connected...
logging in: bilbo...    done
switching prompt...     done
getting file size...    done
getting file...         done
230 bytes received in 1 seconds (0.2 Kbytes/s)

Appendix

telnet1.pl

use Net::Telnet;

$telnet = new Net::Telnet ( Timeout=>20,
                             Errmode=>'die');

$telnet->open('www.bright-blade.net');
  print "connected...\n";

$telnet->waitfor('/login: $/i');
$telnet->print('bilbo');
  print "entered username...\n";

$telnet->waitfor('/password: $/i');
$telnet->print('baggins');
  print "entered password...\n";

$telnet->waitfor('/[\$%#>_] $/i');
  print "logged in!\n";

$telnet->print('who');
$output = $telnet->waitfor('/\$ $/i');
print $output;

telnet2.pl

use Net::Telnet;

$telnet = new Net::Telnet ( Timeout=>20,
                            Errmode=>'die',
                            Prompt => '/\$ $/i');

$telnet->open('www.bright-blade.net');
  print "connected...\n";

$telnet->login('bilbo', 'baggins');
  print "logged in!\n";

print $telnet->cmd('who');

getfile.pl

## define variables
my ($block, $filename, $host, $hostname, $k_per_sec, $line,
        $num_read, $passwd, $prevblock, $prompt, $size, $size_bsd,
        $size_sysv, $start_time, $total_time, $username);

## initialize variables
    $hostname = "www.bright-blade.net";
    $username = "bilbo";
    $passwd = "baggins";
    $filename = ".bashrc";

## Connect and login.
    use Net::Telnet ();
    $host = new Net::Telnet (Timeout =>60,
                             Errmode=>'die',
                             Prompt =>'/[\$%#>_] $/i');
    $host->open($hostname);
      print STDERR "connected...\n";
      print STDERR "logging in: $username...";
    $host->login($username,$passwd);
      print STDERR "\tdone\n";

## Make sure prompt won't match anything in send data.
      print STDERR "switching prompt...";
    $prompt = 'funkyPrompt% ';
    $host->prompt("/$prompt\$/");
    $host->cmd("export PS1='$prompt'");
      print STDERR "\tdone\n";

## Get size of file.
      print STDERR "getting file size...";
    ($line) = $host->cmd("/bin/ls -l $filename");
    ($size_bsd, $size_sysv) = (split ' ', $line)[3,4];
    if ($size_sysv =~ /^\d+$/) {
        $size = $size_sysv;
    }
    elsif ($size_bsd =~ /^\d+$/) {
        $size = $size_bsd;
    }
    else {
        die "$filename: no such file on $hostname";
    }
      print STDERR "\tdone\n";

## Start sending the file.
      print STDERR "getting file...";
    binmode STDOUT;
    $host->binmode(1);
    $host->print("/bin/sh -c 'stty raw; cat $filename'");
    $host->getline;    # discard echoed back line

## Read file a block at a time.
    $num_read = 0;
    $prevblock = '';
    $start_time = time;
    while (($block = $host->get) and ($block !~ /$prompt$/o)) {
        if (length $block >= length $prompt) {
            print $prevblock;
            $num_read += length $prevblock;
            $prevblock = $block;
        }
        else {
            $prevblock .= $block;
        }    }
    $host->close;
      print STDERR "\t\tdone\n";

## Print last block without trailing prompt.
    $prevblock .= $block;
    $prevblock =~ s/$prompt$//;
    print $prevblock;
    $num_read += length $prevblock;
    die "error: expected size $size, received size $num_read\n"
        unless $num_read == $size;

## Print totals.
    $total_time = (time - $start_time) || 1;
    $k_per_sec = ($size / 1024) / $total_time;
    $k_per_sec = sprintf "%3.1f", $k_per_sec;
    warn("$num_read bytes received in $total_time seconds ",
         "($k_per_sec Kbytes/s)\n");
    exit;  
www.bright-blade.net
[ Email: | chuyler1@ic3.ithaca.edu | jbirkin1@ic3.ithaca.edu ]