PREVIOUS  TABLE OF CONTENTS  NEXT 

Telnetting with Perl

Jay Rogers

This article introduces Net::Telnet, a module that allows your Perl program to communicate with networked hosts or devices such as workstations, terminal servers, routers, and the like. The dialog is established via a client connection to a TCP port - typically to a port using the telnet protocol.

Simple I/O methods such as print(), get(), and getline() are provided, of course, but more sophisticated interactive features are available as well - features tailored for communicating with programs intended solely for human interaction. This includes the ability to specify a timeout and to wait for patterns to appear in the input stream, such as a shell prompt.

Net::Telnet is written entirely in Perl; it doesn't require a locally installed telnet program. This makes Net::Telnet especially easy to install and use on those ubiquitous Windows 95 and NT machines.

The Problem

Sounded easy when you first thought about it: Your Perl program needs to monitor disk space on a remote machine named sparky, but it can access sparky only by connecting to it via telnet. So you try something like this:

open  TELNET,  "|telnet sparky"; 
print TELNET  "joebob\n"; 
print TELNET  "passwd-for-joebob\n"; 
print TELNET  "df -k\n";

You find that piping commands to telnet's standard input doesn't work. The telnet program connects just fine, but sparky doesn't process any of the commands you send it. You discover that your telnet only reads from the terminal (also known as a tty) and not from its standard input.

Well, you could use a package like Comm.pl to create a pseudo-terminal (a pty) and make telnet read from that instead. But then you think, "Hey, why don't I just connect a socket directly to TCP port 23 on sparky and read and write directly to the telnet port? That way I don't even need a telnet program!"

So now you write some socket code - only to find out you still have the same problem. You can connect a socket to the TCP telnet port on sparky but the remote side doesn't seem to respond to I/O.

After digging through the RFC documentation for the telnet protocol, you learn that telnet sends control information along with data in the same socket stream. The reason the remote side wasn't sending data was because you weren't responding properly to its control queries. In other words, you weren't speaking the telnet protocol.

The Solution

Net::Telnet solves this problem. It recognizes the telnet control commands, removes them from the data stream, and sends back an appropriate response. This allows you to read from and write to a port without having to worry about the vagaries of the telnet protocol.

Net::Telnet also provides sophisticated features to help you establish dialogs with programs designed for human interaction. "Human interaction" means an online conversation between you and some remote service (such as a shell, or a MUD, or IRC). What this boils down to is the ability to perform I/O with a specified timeout while waiting for patterns to appear in the input stream.

The Example (the hard way)

Here's some code that prints out a summary of disk usage on sparky. Net::Telnet is used to log in as user "joebob" and to issue the df command to get the disk information. We assume that joebob's shell prompt is '$ '.

use Net::Telnet ();
$remote = new Net::Telnet (Timeout => 10,
                           Errmode => 'return'); 

$remote->open("sparky") or die $remote->errmsg;
$remote->waitfor('/login: $/i') or die $remote->errmsg;

$remote->print("joebob") or die $remote->errmsg;
$remote->waitfor('/password: $/i') or die $remote->errmsg;

$remote->print("passwd-for-joebob") or die $remote->errmsg; 
$remote->waitfor('/\$ $/') or die $remote->errmsg;

$remote->print("df -k") or die $remote->errmsg;

($output) = $remote->waitfor('/\$ $/') 
                          or die $remote->errmsg;

print $output;

As with many of the Perl I/O modules, the first step is to create an object (new Net::Telnet); afterwards, all actions are invoked via that object's methods. Here's what the output looks like:

df 
Filesystem  1024-blocks  Used  Available  Capacity  Mounted on
/dev/hda2       11709    6184      4921      56%    / 
/dev/hda5      298573  237172     45981      84%    /usr 
/dev/hda6      435180  298877    113827      72%    /home

Net::Telnet has a flexible way of handling errors: the Errmode parameter. When Errmode is set to return as in our example above, any errors encountered in Net::Telnet methods are saved in the Net::Telnet object, and the method returns false. That error message can be obtained with $obj->errmsg.

The Example (the easier way)

If you always want to die() on error and print out the error message, as the code above does, you can set Errmode to die. Net::Telnet will kill your program for you, showing you the line where the error occurred. Let's clean up the above code a bit using an Errmode of die. That's the default, but we'll specify it anyway.

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

$remote->open("sparky"); 
$remote->waitfor('/login: $/i');

$remote->print("joebob") 
$remote->waitfor('/password: $/i');

$remote->print("passwd-for-joebob") 
$remote->waitfor('/\$ $/'); 

$remote->print("df -k"); 
($output) = $remote->waitfor('/\$ $/');

print $output;

Now let's look a bit closer at what this code does. The new() creates a new object which has a base class of (that is, it inherits from) FileHandle or IO::Socket::INET, depending on whether or not you have the IO:: libraries installed. (The IO:: libraries now come standard with Perl 5.004.)

What this means is that the $remote object can be used as a filehandle anywhere a Perl filehandle would be used. If you have IO::Socket::INET installed, you have access to its routines via method inheritance. For example, you can call its peeraddr() routine as $remote->peeraddr(). (That returns the packed IP address of the remote host to which you're connected.)

In our code we configured the object to use a ten second timeout when connecting, reading, and writing. We connect to the remote telnet port using open() and then use a series of waitfor() and print() routines to log in to the host. Three prompts must be waited for: the login prompt, the password prompt, and the shell prompt '$ '.

The example above makes prominent use of waitfor(). Let's examine it more closely. waitfor() takes a string containing a Perl regular expression and waits for the pattern to appear in the data stream within Timeout seconds. Assuming the pattern appears, it reads and removes everything in the data stream up to and including the matched string.

If the pattern doesn't occur before the timeout, Net::Telnet signals an error. For example, suppose the following command is issued, but the remote side doesn't print 'login: ' within ten seconds:

    $remote->waitfor('/login: $/i');

That's an error, so waitfor() will either print an error message and die(), or just return with a false value depending on whether Errmode is set to die or return.

Once we're logged in and our shell prompt has appeared, we issue the df command. We pause until df finishes, simply by waiting for the next shell prompt. Notice how we use the return value of waitfor() to collect the output from the df command:

$remote->print("df -k"); 
($output) = $remote->waitfor('/\$ $/');

In a list context waitfor() returns two values akin to $' and $& (also known as $PREMATCH and $MATCH if you use English). The first value is all the characters before the matched string, and the second value is the matched string itself. In our example we only use one of the returned values - whatever matches up to the dollar sign and space at the end of the string. $output is thus exactly what we want: everything between the shell prompts, which, if all goes according to plan, will be the output from the df command.

The Example (the easiest way)

Because they're such common tasks, Net::Telnet provides a routine to log in (login()) and a routine to send a command and retrieve the output (cmd()). Let's rewrite our example one last time to use these routines.

Both login() and cmd() need to be able to recognize the remote shell's prompt, so we set the object's Prompt attribute when it's created.

use Net::Telnet (); 
$remote = new Net::Telnet (Timeout => 10, 
                           Prompt => '/\$ $/');

$remote->open("sparky"); 
$remote->login("joebob", "passwd-for-joebob"); 
print $remote->cmd("df -k");

The login() method expects the login prompt to look like either /login[: ]*$/i or /username[: ]*$/i. The password prompt must look like /password[: ]*/i.

If the thing you're connecting to doesn't use one of those prompts, you'll have to customize your own login using a series of waitfor() and print() commands.

Special considerations

You may have noticed in our example that none of the strings being sent via print() or cmd() ends with a newline. As a convenience, the output_record_separator Net::Telnet attribute is set to a newline because that's what you normally see at the end of each line. If for some reason you need to write characters without an ending newline, just change output_record_separator to the null string, "".

At some point you might need to debug programs that use Net::Telnet. The typical symptom will be a timeout error because you've made incorrect assumptions about what the remote side is sending. The easiest way to reconcile the remote side with your expectations is to use the input_log() or dump_log() methods. dump_log() allows you to see the data being sent from the remote side before any translation is done, while input_log() shows you the results after translation.

The translation includes converting end of line characters. Typically, when you're communicating with a TCP port such as telnet or SMTP, newlines are designated using a two character ASCII sequence: a carriage return followed by a line feed. As a convenience, Net::Telnet converts sequences of carriage returns and line feeds to mere line feeds on input, and vice versa for output. You can control this translation with binmode().

The translation also includes stripping and responding to telnet commands embedded in the data stream. You can control this translation with telnetmode(). For example, you might want to turn off this mode ($remote->telnetmode(0)) if you're connecting to a non-telnet port such SMTP. For the curious, telnet commands are preceded by hexadecimal 255.

But wait...there's more

Like the telnet program itself, Net::Telnet can be used to communicate with any TCP port, not just ports using the telnet protocol. Net::Telnet is frequently used to create the client side of client-server applications, because of its ability to time out when reading and writing.

You might also want to use Net::Telnet's interactive features with filehandles other than sockets. The fhopen() method can be used to associate an already open filehandle with a telnet object:

    $obj->fhopen(\*STDIN);

Now, whenever you read from the object $obj, you're actually reading from standard input - with all the features of Net::Telnet available.

If you use Net::Telnet, be sure to read the embedded documentation, which includes several examples. Enjoy!

_ _END_ _


Jay Rogers wrote Net::Telnet. He works as a software consultant specializing in Unix systems programming. He lives in the Boston area with his wife and 16 month old daughter, and can be reached at jay@rgrs.com.
PREVIOUS  TABLE OF CONTENTS  NEXT