Net::Telnet


 
Network Programming with Perl
By Lincoln  D.  Stein
Slots : 1
Table of Contents
Chapter  6.   FTP and Telnet

    Content

FTP is the quintessential line-oriented server application. Every command issued by the client takes the form of a single, easily parsed line, and each response from the server to the client follows a predictable format. Many of the server applications that we discuss in later chapters, including POP, SMTP, and HTTP, are similarly simple. This is because the applications were designed to interact primarily with software, not with people.

Telnet is almost exactly the opposite . It was designed to interact directly with people, not software. The output from a Telnet session is completely unpredictable, depending on the remote host's configuration, the shell the user has installed, and the setup of the user 's environment.

Telnet does some things that make it easy for human beings to use: It puts its output stream into a mode that echoes back all commands that are sent to it, allowing people to see what they type, and it puts its input stream into a mode that allows it to read and respond to one character at a time. This allows command-line editing and full-screen text applications to work.

While these features make it easy for humans to use Telnet-based applications, it makes scripting such applications a challenge. Because the Telnet protocol is more complex than sending commands and receiving responses, you can't simply connect a socket to port 23 (Telnet's default) on a remote machine and start exchanging messages. Before the Telnet client and server can talk, they must engage in a handshake procedure to negotiate communications session parameters. Nor is it possible for a Perl script to open a pipe to the Telnet client program because the Telnet, like many interactive programs, expects to be opened on a terminal device and tries to change the characteristics of the device using various ioctl() calls.

Given these factors, it is best not to write clients for interactive applications. Sometimes, though, it's unavoidable. You may need to automate a legacy application that is available only as an interactive terminal application. Or you may need to remotely drive a system utility that is only accessible in interactive form. A classic example of the latter is the UNIX passwd program for changing users' login passwords. Like Telnet, passwd expects to talk directly to a terminal device, and you must do special work to drive it from a Perl script.

The Net::Telnet module provides access to Telnet-based services. With its facilities, you can log into a remote host via the Telnet protocol, run commands, and act on the results using a straightforward pattern-matching idiom. When combined with the IO::Pty module, you can also use Net::Telnet to control local interactive programs.

Net::Telnet was written by Jay Rogers and is available on CPAN. It is a pure Perl module, and will run unmodified on Windows and Macintosh systems. Although it was designed to interoperate with UNIX Telnet daemons, it is known to work with the Windows NT Telnet daemon available on the Windows NT Network Resource Kit CD and several of the freeware daemons.

A Simple Net::Telnet Example

Figure 6.3 shows a simple script that uses Net::Telnet. It logs into a host, runs the command ps -ef to list all running processes, and then echoes the information to standard output.

Figure 6.3. remoteps.pl logs into a remote host and runs the "ps" command

graphics/06fig03.gif

Lines 1 “3: Load modules We load the Net::Telnet module. Because it is entirely object-oriented, there are no symbols to import.

Lines 4 “6: Define constants We hard-code constants for the host to connect to, and the user and password to log in as (no, this isn't my real password!). You'll need to change these as appropriate for your system.

Line 7: Create a new Net::Telnet object We call Net::Telnet->new() with the name of the host. Net::Telnet attempts to connect to the host, returning a new Net::Telnet object if successful or, if a connection could not be established, undef .

Line 8: Log in to remote host We call the Telnet object's login() method with the username and password. login() will attempt to log in to the remote system, and will return true if successful.

Lines 9 “10: Run the "ps" command We invoke the cmd() method with the command to run, in this case ps -ef . If successful, this method returns an array of lines containing the output of the command (including the newlines). We print the result to standard output.

When we run the remoteps.pl script, there is a brief pause while the script logs into the remote host, and then the output of the ps command appears, as follows:

 %  remoteps1.pl  UID    PID PPID C STIME TTY      TIME CMD root     1   0  0 Jun26 ?    00:00:04 init root     2   1  0 Jun26 ?    00:00:15 [kswapd] root     3   1  0 Jun26 ?    00:00:00 [kflushd] root     4   1  0 Jun26 ?    00:00:01 [kupdate] root    34   1  0 Jun26 ?    00:00:01 /sbin/cardmgr root   114   1 30 Jun26 ?    19:18:46 [kapmd] root   117   1  0 Jun26 ?    00:00:00 [khubd] bin    130   1  0 Jun26 ?    00:00:00 /usr/sbin/rpc.portmap root   134   1  0 Jun26 ?    00:00:25 /usr/sbin/syslogd ... 

Net::Telnet API

To accommodate the many differences between Telnet implementations and shells among operating systems, the Net::Telnet module has a large array of options. We only consider the most frequently used of them here. See the Net::Telnet documentation for the full details.

Net::Telnet methods generally have both a named-argument form and a "shortcut" form that takes a single argument only. For example, new() can be called either this way:

 my $telnet = Net::Telnet->new('phage.cshl.org'); 

or like this:

 my $telnet = Net::Telnet->new(Host=>'phage.cshl.org', Timeout=>5); 

We show both forms when appropriate.

The new() method is the constructor for Net::Telnet objects:

$telnet = Net::Telnet->new($host)

$telnet = Net::Telnet->new(Option1=>$value1,Option2=>$value2 ..)

The new() method creates a new Net::Telnet object. It may be called with a single argument containing the name of the host to connect to, or with a series of option/ value pairs that provide finer control over the object. new() recognizes many options, the most common of which are shown in Table 6.2.

Table 6.2. Net::Telnet->new() Arguments
Option Description Default Value
Host Host to connect to "localhost"
Port Port to connect to 23
Timeout Timeout for pattern matches, in seconds 10
Binmode Suppress CRLF translation false
Cmd_remove_mode Remove echoed command from input "auto"
Errmode Set the error mode "die"
Input_log Log file to write input to none
Fhopen Filehandle to communicate over none
Prompt Command-line prompt to match "/[\$%#>]$/"

The Host and Port options are the host and port to connect to, and Timeout is the period in seconds that Net::Telnet will wait for an expected pattern before declaring a timeout.

Binmode controls whether Net::Telnet will perform CRLF translation. By default ( Binmode=>0 ), every newline sent from the script to the remote host is translated into a CRLF pair, just as the Telnet client does it. Likewise, every CRLF received from the remote host is translated into a newline. With Binmode set to a true value, this translation is suppressed and data is transmitted verbatim.

Cmd_remove_mode controls the removal of echoed commands. Most implementations of the Telnet server echo back all user input. As a result, text you send to the server reappears in the data read back from the remote host. If CMD_REMOVE_MODE is set to true, the first line of all data received from the server will be stripped. A false value prevents stripping, and a value of "auto" allows Net::Telnet to decide for itself whether to strip based on the "echo" setting during the initial Telnet handshake.

Errmode determines what happens when an error occurs, typically an expected pattern not being seen before the timeout. The value of Errmode can be one of the strings "die" (the default) or "return". When set to "die", Net::Telnet dies on anerror, aborting your program. A value of "return" modifies this behavior, so that instead of dying the failed method returns undef . You can then recover the specific error message using errmsg() . In addition to these two strings, Errmode accepts either a code reference or an array reference. Both of these forms are used to install custom handlers that are invoked when an error occurs. The Net::Telnet documentation provides further information.

The value for Input_log should be a filename or a filehandle. All data received from the server is echoed to this file or filehandle. Since the received data usually contains the echoed command, this is a way to capture a transcript of the Net::Telnet session and is invaluable for debugging. If the argument is a previously opened filehandle, then the log is written to that filehandle. Otherwise , the argument is treated as the name of a file to open or create.

The Fhopen argument can be used to pass a previously opened filehandle to Net::Telnet for it to use in communication. Net::Telnet will use this filehandle instead oftrying to open its own connection. We use this later to coerce Net::Telnet into working across a Secure Shell link.

Prompt sets the regular expression that Net::Telnet uses to identify the shell command-line prompt. This is used by the login() and cmd() methods to determine that the command ran to completion. By default, Prompt is set to a pattern that matches the default sh, csh, ksh, and tcsh prompts.

Once a Net::Telnet object is opened you control it with several object modules:

$result = $telnet->login($username,$password)

$result = $telnet->login(Name => $username,

Password => $password,

[Prompt => $prompt,]

[Timeout=> $timeout])

The login() method attempts to log into the remote host using the provided username and password. In the named-parameter form of the method call, you may override the values of Prompt and Timeout provided to new().

If the Errmode is "die" and the login method encounters an error, the call aborts your script with an error message. Otherwise, login() returns false.

$result = $telnet->print(@values)

Print a value or list of values to the remote host. A newline is automatically added for you unless you explicitly disable this feature (see the Net::Telnet documentation for details). The method returns true if all of the data was successfully written.

It is also possible to bypass Net::Telnet's character translation routines and write directly to the remote host by using the Net::Telnet object as a filehandle:

 print $telnet "ls -lF52"; 

$result = $telnet->waitfor($pattern)

($before,$match) = $telnet->waitfor($pattern)

($before,$match) = $telnet->waitfor([Match=>$pattern,]

[String=>$string,]

[Timeout=>$timeout])

The waitfor() method is the workhorse of Net::Telnet. It waits up to Timeout seconds for the specified string or pattern to appear on the data stream coming from the remote host. In a scalar context, waitfor() returns a true value if the desired pattern was seen. In a list context, the method returns a two-element list consisting of the data seen before the match and the matched string itself.

You can give waitfor() a regular expression to pattern match or a simple string, in which case Net::Telnet uses index() to scan for it in incoming data. In the method's named-argument form, use the Match argument for a pattern match, and String for a simple string match. You can specify multiple alternative patterns or strings to match simply by providing more than one Match and/or String arguments.

The strings used for MATCH must be correctly delimited Perl pattern match operators. For example, "/bash> $/" and "m(bash> $)" will both work, but "bash> $" won't because of the absence of pattern match delimiters.

In the single-argument form of waitfor() , the argument is a pattern match. The Timeout argument may be used to override the default timeout value.

This code fragment will issue an ls -lF command, wait for the command line prompt to appear, and print out what came before the prompt, which ought to be the output of the ls command:

 $telnet->print('ls -lF'); ($before,$match) = $telnet->waitfor('/[$%#>] $/'); print $before; 

To issue a command to the remote server and wait for a response, you can use one of several versions of cmd() :

$result = $telnet->cmd($command)

@lines = $telnet->cmd($command)

@lines = $telnet->cmd(String=>$command,

[Output=>$ref,] [Prompt=>$pattern,]

[Timeout=>$timeout,] [Cmd_remove_mode=>$mode]

The cmd() method is used to send a command to the remote host and return its output, if any. It is equivalent to a print() of the command, followed by a waitfor() using the default shell prompt pattern.

In a scalar context, cmd() returns true if the command executed successfully, false if the method timed out before the shell prompt was seen. In a list context, this method returns all the lines received prior to matching the prompt.

In the named-argument form of the call, the Output argument designates either a scalar reference or an array reference to receive the lines that preceded the match. The Prompt , Timeout , and Cmd_remove_mode arguments allow you to override the corresponding settings.

Note that a true result from cmd() does not mean that the command executed successfully. It only means that the command completed in the time allotted for it.

To receive data from the server without scanning for patterns, use get() , getline() , or getlines() :

$data = $telnet->get([Timeout=>$timeout])

The get() method performs a timed read on the Telnet session, returning any data that is available. If no data is received within the allotted time, the method dies if Errmode is set to "die" or returns undef otherwise. The get() method also returns undef on end-of-file (indicating that the remote host has closed the Telnet session). You can use eof() and timed_out() to distinguish these two possibilities.

$line = $telnet->getline([Timeout=>$timeout])

The getline() method reads the next line of text from the Telnet session. Like get() , it returns undef on either a timeout or an end-of-file. You may change the module's notion of the input record separator using the input_record_separator() method, described below.

@lines = $telnet->getlines([Timeout=>$timeout])

Return all available lines of text, or an empty list on timeout or end-of-file.

Finally, several methods are useful for debugging and for tweaking the communications session:

$msg = $telnet->errmsg

This method returns the error message associated with a failed method call. For example, after a timeout on a waitfor() , errmsg() returns "pattern match timed-out."

$line = $telnet->lastline

This method returns the last line read from the object. It's useful to examine this value after the remote host has unexpectedly terminated the connection because it might contain clues to the cause of this event.

$value = $telnet->input_record_separator([$newvalue])

$value = $telnet->output_record_separator([$newvalue])

These two methods get and/or set the input and output record separators. The input record separator is used to split input into lines, and is used by the getline() , getlines() , and cmd() methods. The output record separator is printed at the end of each line output by the print() method. Both values default to \n .

$value = $telnet->prompt([$newvalue])

$value = $telnet->timeout([$newvalue])

$value = $telnet->binmode([$newvalue])

$value = $telnet->errmode([$newvalue])

These methods get and/or set the corresponding settings, and can be used to examine or change the defaults after the Telnet object is created.

$telnet->close

The close() method severs the connection to the remote host.

A Remote Password-Changing Program

As a practical example of Net::Telnet, we'll develop a remote password-changing script named change_passwd.pl . This script will contact each of the hosts named on the command line in turn and change the user's login password. This might be useful for someone who has accounts on several machines that don't share the same authentication database. The script is used like this:

 %  change_passwd.pl --old=mothergOOse --new=bopEEp chiron masdorf sceptre  

This command line requests the script to change the current user's password on the three machines chiron , masdorf , and sceptre. The script reports success or failure to change the password on each of the indicated machines.

The script uses the UNIX passwd program to do its work. In order to drive passwd, we need to anticipate its various prompts and errors. Here's a sample of a successful interaction:

 %  passwd  Changing password for lstein Old password:   xyzzy   Enter the new password (minimum of 5, maximum of 8 characters) Please use a combination of upper and lower case letters and numbers. New password:   plugn   Re-enter new password:   plugn   Password changed. 

At the three password: prompts I typed my current and new passwords. However, the passwd program turns off terminal echo so that the passwords don't actually display on the screen.

A number of errors may occur during execution of passwd . In order to be robust, the password-changing script must detect them. One error occurs when the original password is typed incorrectly:

 %  passwd  Changing password for lstein Old password:   xyzyy   Incorrect password for lstein. The password for lstein is unchanged. 

Another error occurs when the new password doesn't satisfy the passwd program's criteria for a secure, hard-to-guess password:

 %  passwd  Changing password for lstein Old password:   xyzzy   Enter the new password (minimum of 5, maximum of 8 characters) Please use a combination of upper and lower case letters and numbers. New password:   hi   Bad password: too short. Try again. New password:   aaaaaaaaaa   Bad password: a palindrome. Try again. New password:   12345   Bad password: too simple. Try again. 

This example shows several attempts to set the password, each one rejected for a different reason. The common part of the error message is "Bad password." We don't have to worry about a third common error in running passwd , which is failing to retype the password correctly at the confirmation prompt.

The change_passwd.pl script is listed in Figure 6.4.

Figure 6.4. Remote password-changing script

graphics/06fig04.gif

Lines 1 “4: Load modules We load Net::Telnet and the Getopt::Long module for command-line option parsing.

Lines 5 “12: Define constants We create a DEBUG flag. If this is true, then we instruct the Net::Telnet module to log all its input to a file named passwd.log . This file contains password information, so be sure to delete it promptly. The USAGE constant contains the usage statement printed when the user fails to provide the correct command-line options.

Lines 13 “19: Parse command line options We call GetOptions() to parse the command-line options. We default to the current user's login name if none is provided explicitly using the LOGNAME environment variable. The old and new password options are mandatory.

Line 20: Invoke change_passwd() subroutine For each of the machines named on the command line, we invoke an internal subroutine named change_passwd() , passing it the name of the machine, the user login name, and the old and new passwords.

Lines 21 “41: change_passwd() subroutine Most of the work happens in change_ passwd() . We begin by opening up a new Net::Telnet object on the indicated host, and then store the object in a variable named $shell . If DEBUG is set, we turn on logging to a hard-coded file. We also set errmode() to "return" so that Net::Telnet calls will return false rather than dying on an error.

We now call login() to attempt to log in with the user's account name and password. If this fails, we return with a warning constructed from the Telnet object's errmsg() routine.

Otherwise we are at the login prompt of the user's shell. We invoke the passwd command and wait for the expected "Old password:" prompt. If the prompt appears within the timeout limit, we send the old password to the server. Otherwise, we return with an error message.

Two outcomes are possible at this point. The passwd program may accept the password and prompt us for the new password, or it may reject the password for some reason. We wait for either of the prompts to appear, and then examine the match string returned by waitfor() to determine which of the two patterns we matched. In the former case, we proceed to provide the new password. In the latter, we return with an error message.

After the new desired password is printed (line 33), there are again two possibilities: passwd may reject the proposed password because it is too simple, or it may accept it and prompt us to confirm the new password. We handle this in the same way as before.

The last step is to print the new password again, confirming the change. We do not expect any errors at this point, but we do wait for the "Password changed" confirmation before reporting success.

Because there is little standardization among passwd programs, this script is likely to work only with those variants of UNIX that use a passwd program closely derived from the BSD version. To handle other passwd variants, you will need to modify the pattern matches appropriately by including other Match patterns in the calls to waitfor() .

Running change_passwd.pl on a network of Linux systems gives output like this:

 %  change_passwd.pl --user=george --old=m00nd0g --new=swampH0und  \   localhost pesto prego romano  Password changed for george on localhost. Password changed for george on pesto. Password changed for george on prego. Password changed for george on romano. 

While change_passwd.pl is running, the old and new passwords are visible to anyone who runs a ps command to view the command lines of running programs. If you wish to use this script in production, you will probably want to modify it so as to accept this sensitive information from standard input. Another consideration is that the password information is passed in the clear, and therefore vulnerable to network sniffers. The SSH-enabled password-changing script in the next section overcomes this difficulty.

Using Net::Telnet for Non-Telnet Protocols

Net::Telnet can be used to automate interactions with other network servers. Often it is as simple as providing the appropriate Port argument to the new() call. The Net::Telnet manual page provides an example of this with the POP3 protocol, which we discuss in Chapter 8.

With help from the IO::Pty module, Net::Telnet can be used to automate more complicated network services or to script local interactive programs. Like the standard Telnet client, the problem with local interactive programs is that they expect access to a terminal device (a TTY) in order to change screen characteristics, control the cursor, and so forth. What the IO::Pty module does is to create a "pseudoterminal device" for these programs to use. The pseudoterminal is basically a bidirectional pipe. One end of the pipe is attached to the interactive program; from the program's point of view, it looks and acts like a TTY. The other end of the pipe is attached to your script, and can be used to send data to the program and read its output.

Because the use of pseudoterminals is a powerful technique that is not well documented, we will show a practical example. Many security-conscious sites have replaced Telnet and FTP with the Secure Shell (SSH), a remote login protocol that authenticates and encrypts login sessions using a combination of public key and symmetric cryptography. The change_passwd.pl script does not work with sites that have disabled Telnet in favor of SSH, and we would like to use the ssh client to establish the connection to the remote host in order to run the passwd command.

The ssh client emits a slightly different login prompt than Telnet. A typical session looks like this:

 %  ssh -l george prego  george@prego's password:  *******  Last login: Mon Jul 3 08:20:28 2000 from localhost Linux 2.4.01. % 

The ssh client takes an optional -l command-line switch to set the name of the user to log in as, and the name of the remote host (we use the short name rather than the fully qualified DNS name in this case). ssh prompts for the password on the remote host, and then attempts to log in.

To work with ssh , we have to make two changes to change_passwd.pl : (1) we open a pseudoterminal on the ssh client and pass the controlling filehandle to Net::Telnet->new() as the Fhopen argument and (2) we replace the call to login() with our own pattern matching routine so as to handle ssh 's login prompt.

The IO::Pty module, available on CPAN, has a simple API:

$pty = IO::Pty->new

The new() method takes no arguments and returns a new IO::Pty pseudoterminal object. The returned object is a filehandle corresponding to the controlling end of the pipe. Your script will ordinarily use this filehandle to send commands and read results from the program you're driving.

$tty = $pty->slave

Given a pseudoterminal created with a call to IO::Pty->new() , the slave() , method returns the TTY half of the pipe. You will ordinarily pass this filehandle to the program you want to control.

Figure 6.5 shows the idiom for launching a program under the control of a pseudoterminal. The do_cmd() subroutine accepts the name of a local command to run and a list of arguments to pass it. We begin by creating a pseudoterminal filehandle with IO::Pty->new() (line 3). If successful, we fork() , and the parent process returns the pseudoterminal to the caller. The child process, however, has a little more work to do. We first detach from the current controlling TTY by calling POSIX::setsid() (see Chapter 10 for details). The next step is to recover the TTY half of the pipe by calling the IO::Pty object's slave() , method, and then close the pseudoterminal half (lines 7 “8).

Figure 6.5. Launching a program in a pseudo-tty

graphics/06fig05.gif

We now reopen STDIN , STDOUT , and STDERR on the new TTY object using fdopen() , and close the now-unneeded copy of the filehandle (lines 9 “12). We make STDOUT unbuffered and invoke exec () to run the desired command and arguments. When the command runs, its standard input and output will be attached to thenew TTY, which in turn will be attached to the pseudo-tty controlled by the parent process.

With do_cmd() written, the other changes to change_passwd.pl are relatively minor. Figure 6.6 shows the revised script written to use the ssh client, change_passwd_ssh.pl.

Figure 6.6. Changing passwords over a Secure Shell connection

graphics/06fig06.gif

Lines 1 “6: Load modules We load IO::Pty and the setsid() routine from the POSIX module.

Lines 7 “23: Process command-line arguments and call change_passwd() The only change here is a new constant, PROMPT , that contains the pattern match that we will expect from the user's shell command prompt.

Lines 24 “27: Launch ssh subprocess We invoke do_cmd() to run the ssh program using the requested username and host. If do_cmd() is successful, it returns a filehandle connected to the pseudoterminal driving the ssh subprocess.

Lines 28 “31: Create and initialize Net:: Telnet object In the change_passwd() routine, we create a new Net::Telnet object, but now instead of allowing Net::Telnet to open a connection to the remote host directly, we pass it the ssh filehandle using the Fhopen argument. After creating the Net::Telnet object, we configure it by putting it into binary mode with binmode() , setting the input log for debugging, and setting the error mode to "return". The use of binary mode is a small but important modification of the original script. Since the SSH protocol terminates its lines with a single LF character rather than CRLF pairs, the default Net::Telnet CRLF translation is inappropriate.

Lines 32 “34: Log in Instead of calling Net::Telnet's built-in login() method, which expects Telnet-specific prompts, we roll our own by waiting for the ssh "password:" prompt and then providing the appropriate response. We then wait for the user's command prompt. If, for some reason, this fails, we return with an error message.

Lines 35 “49: Change password The remainder of the change_passwd() subroutine is identical to the earlier version.

Lines 50 “65: do_cmd() subroutine This is the same subroutine that we examined earlier.

The change_passwd_ssh.pl program now uses the Secure Shell to establish connections to the indicated machines and change the user's password. This is a big advantage over the earlier version, which was prone to network eavesdroppers who could intercept the new password as it passed over the wire in unencrypted form. On multiuser systems you will still probably want to modify the script to read the passwords from standard input rather than from the command line.

For completeness, Figure 6.7 lists a routine, prompt_for_passwd(i), that uses the UNIX stty program to disable command-line echo temporarily while the user is typing the password. You can use it like this:

 $old = get_password('old password'); $new = get_password('new password'); 
Figure 6.7. Disabling echo while prompting for a password

graphics/06fig07.gif

A slightly more sophisticated version of this subroutine, which takes advantage of the Term ::ReadKey module, if available, appears in Chapter 20.

The Expect Module

An alternative to Net::Telnet is the Expect module, which provides similar services for talking to local and remote processes that expect human interaction. Expect implements a rich command language, which among other things can pause the script and prompt the user for information, such as passwords. Expect can be found on CPAN.


   
Top


Network Programming with Perl
Network Programming with Perl
ISBN: 0201615711
EAN: 2147483647
Year: 2000
Pages: 173

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net