Hurra, Expect ist da! (Linux-Magazin, Oktober 1998)

In der Februar-Kolumne stellte ich Net::Telnet vor, jubelnd, daß endlich Ersatz für das praktische Tcl-expect zumindest im Telnet-Bereich eingetroffen sei. Wie wenig wußte ich um die Dinge, die da kommen würden, mir den Alltag zu erleichtern und das Leben zu versüßen! Austin Schutz stellte jüngst sein Expect-Modul vor - und nun kann jedes Perl-Skript alle Applikationen ansprechen, die auf die immer gleichen Ausgaben die immer gleichen Antworten erwarten: Ob man sich auf einem Cisco-Router einloggt, oder ein Paßwort-Programm aufruft, das sich gemeinerweise gegen Eingaben aus der Standardeingabe sträubt - die Send-Expect-Logik des Expect-Moduls erschlägt einfach alles.

Automatisches Einloggen

Das Skript aus Listing telnet.pl loggt sich per telnet auf einem fremden Rechner ein und bietet anschließend interaktiven Zugriff auf die geöffnete Shell an. Da mir Andreas König einmal bescheinigt hat, daß telnet nur etwas für ``Ewiggestrige'' wäre (der Rest der Welt nimmt angeblich ssh): Das Beispiel dient nur dazu, die prinzipielle Funktionsweise aufzuzeigen, schließlich beschränkt sich die Anwendung von Expect nicht nur auf telnet, vielmehr läßt sich jede beliebige Applikation so heinzelmännchengleich ansteuern.

Der Konstruktor spawn aus dem Expect-Paket nimmt den Pfad auf ein externes Programm sowie eine Reihe von (optionalen) Parametern dafür entgegen, startet es und liefert eine Referenz auf ein Expect-Objekt zurück. In Listing telnet.pl startet das Programm /bin/telnet mit dem anzusteuernden Rechner remotehost als Parameter.

Das Telnet-Programm wird erfahrungsgemäß etwas folgendes liefern:

    Trying 205.44.189.17...
    Connected to remotehost.
    Escape character is '^]'.
    Linux 2.0.30 (ruebe.remote.de) (ttyp3)
    ruebe login:

und gleich anschließend auf die Eingabe des Login-Namens zu warten. Der Aufruf der expect-Methode in Zeile 12 verfolgt die Ausgabe aufmerksam und kehrt zurück, falls das eingestellte Suchmuster daherkommt. Taucht der eingestellte Ausdruck auch nach Ablauf der im Parameter $timeout eingestellten Zeit nicht auf, bricht expect ab und liefert undef zurück. telnet.pl bricht in diesem Fall mit der die-Funktion aus Zeile 13 ab und meldet den aufgetretenen Fehler. Der Aufruf der send_slow-Methode in Zeile 14 antwortet dem Login-Programm, indem es ihm eine Sequenz von Zeichen schickt, die jenes genauso interpretiert, als würde ein Anwender den Namen eintippen und die Return-Taste drücken. Wichtig ist dabei, send_slow das \n-Zeichen mitzugeben, sonst tut sich nichts.

Langsam senden

Der erste Parameter von send_slow ist die Anzahl der Sekunden, die send_slow zwischen jedem gesendeten Zeichen warten soll - so kann man auch mit dem langsamsten Modem kommunizieren, ohne daß dieses den Faden verliert, für die Kommunikation mit dem Login-Programm ist jedoch keine Zeitverzögerung notwendig.

Wie man statt auf feste Strings auf Muster wartet, die regulären Ausdrücken genügen, zeigt in Zeile 20 die Wartefunktion auf die Paßworteingabe, die mit [Pp]assword entweder auf Password oder password wartet. Geht dem Wartemuster der Parameter -re voran, interpretiert expect den nachfolgenden String als regulären Ausdruck.

Enthält der Regex einen Match aufs Zeilenende, ist darauf zu achten, daß das angesprochene Terminal \r\n als Zeilenende-Sequenz sendet und so muß statt dem Perl-üblichen $ der Ausdruck \r?$ herhalten, der ein optionales \r vor dem eigentlich Umbruch erlaubt. Ein Zeilenanfang notiert wie gehabt mit ^. Ausdrücke, die mehrere Zeilen abdecken, sind erlaubt.

Listing telnet.pl wartet in Zeile 25 lediglich auf das $-Zeichen, den Prompt der Bourne- bzw. Bash-Shell, für C-Shells wäre dies etwa durch % zu ersetzen.

Die Krönung: Interaktivität

Der Knüller freilich ist die interact-Methode aus Zeile 28, die die Standardeingabe des Skripts mit dem Eingabekanal der geöffneten Telnet-Session kurzschließt und somit freie Interaktion erlaubt. Verläßt der Benutzer die Shell wieder, bekommt interact() dies mit, kehrt zurück, und telnet.pl beendet sich.

Freilich sollte man keine Paßwörter in öffentlich zugänglichen Skripts herumliegen lassen - chmod 700 telnet.pl stellt wenigstens sicher, daß niemand außer dem Eigentümer und dem Super-User den Inhalt lesen kann. Für mehr Sicherheit bot der Februar-Artikel eine Sandkasten-Shell an.

Die automatischen Kontaktaufnahme beschränkt sich wie gesagt nicht auf telnet-Programm - auch ftp bietet sich an (auch wenn Net::FTP schöner ist) und auch alle möglichen Prompt-gesteuerten Applikationen.

htpasswd austricksen

Eine weitere Anwendung zeigt Listing htp.pl: Um Programme im Batch-Betrieb anzusteuern, die eine Paßworteingabe verlangen, reicht ein Here-Dokument nicht aus:

    htpasswd htpasswd.dat user <<EOT
    pass
    pass
    EOT

ruft zwar das der Apache-Distribution beiliegende User-Paßwort-Programm auf, dieses nimmt aber die zwei per Standardeingabe hereingereichten Paßwörter gemeinerweise nicht entgegen, sondern wartet beharrlich mit

    Adding user user
    New password:

auf eine Eingabe, die tatsächlich vom Terminal (tty) kommt. Mit dem Expect-Modul hingegen, das mit Pseudo-ttys herumhantiert, ist das alles kein Problem: Listing htp.pl öffnet in Zeile 16 einen Kanal zum htpasswd-Programm, und wartet in Zeile 20 auf einen von zwei Strings: "Changing" oder "New password:". Erhält die expect-Methode nämlich mehr als einen String als zu erwartetes Muster mitgeliefert, kehrt expect schon dann zurück, falls nur einer der Strings in der Ausgabe des zu überwachenden Programms erscheint. Der Rückgabewert der Methode (in skalarem Kontext) zeigt dann an, welches der angegebenen Muster gefunden wurde: 1 indiziert, daß das erste Suchmuster zutraf, 2 das zweite usw. htpasswd zeigt für den Fall, daß der angegebene User in der Paßwortdatei schon existiert, "Changing password for user X" an, bevor der Paßwort-Prompt kommt. Während htpasswd in einem solchen Fall einfach eine Paßwortänderung eines bestehenden Benutzers erwartet, hat sich htp.pl in den Kopf gesetzt, das Skript abbrechen, falls der Benutzer schon existiert. Hierzu fängt es die Meldung ab und terminiert mit einer die-Meldung. Ist der Rückgabewert der expect-Methode undef, wurde bis zum Timeout keines der Muster gefunden, ist er 1, war's die Changing-Meldung, ist er 2, wartet htpasswd bereits auf das Paßwort eines neuen Benutzers.

Als Alternative ginge auch

                               # Schon vorhanden?
    $pattern = $robot->expect($timeout, "New password:");
    die "New password: not found" unless defined $pattern;
  
    die "User already there" if 
        $robot->exp_before() =~ /Changing password/;

Die Methoden exp_before, exp_match und exp_after auf ein Expect-Objekt zeigen nach einem Treffer jeweils an, was vor dem Match kam, welche Zeichenkette als Muster erkannt wurde, und was danach noch folgte. Findet Expect also die Zeichenkette "New Password:", sieht es nach, ob vor dem Match im Expect-Akkumulator Changing password steht - falls ja, gab es den Benutzer schon und das Skript bricht ab.

Zeile 38 wartet noch, bis sich das htpasswd-Programm verabschiedet, fehlt diese Zeile, würgt das sofort terminierende htp.pl das laufende htpasswd-Programm rücksichtslos ab und das Ergebnis ist eventuell ein nicht oder nur unvollständig geschriebener Record in der Paßwortdatei.

Debugging

Funktioniert etwas nicht, und es wäre hilfreich, wenn Expect erzählen würde, welche Muster es gefunden hat und was darauf als Antwort folgte, läßt sich ein Expect-Objekt einfach in den Debug-Modus schalten:

    $exp->debug(1);         # Debug an
    $exp->debug(2);         # Gesprächiger Debug
    $exp->debug(0);         # Debug wieder ausschalten

Dokumentation

Die vollständige Dokumentation zu Expect kommt -- wie gehabt -- nach der Installation des Moduls mit perldoc Expect zum Vorschein. Fröhliches Automatisieren!

telnet.pl

    01 #!/usr/bin/perl -w
    02 ##################################################
    03 # Michael Schilli, 1998 (mschilli@perlmeister.com)
    04 ##################################################
    05  
    06 use Expect;
    07 
    08 my $timeout = 10;          # Grundsätzlich 10 sec
    09                            # auf Antwort warten
    10 my $host    = "remotehost";
    11 my $host    = "localhost";
    12 my $passwd  = "super-geheim";
    13  
    14                            # Telnet starten
    15 $telnet = Expect->spawn("/bin/telnet", $host);
    16  
    17                            # Login
    18 $r = $telnet->expect($timeout, 'login');
    19 die "No 'login' prompt" unless defined $r;
    20 $telnet->send_slow(0, "$user\n");
    21  
    22                            # Paßwort
    23 $r = $telnet->expect($timeout, -re => '[Pp]assword');
    24 die "No 'password' prompt" unless defined $r;
    25 $telnet->send_slow(0, "$passwd\n");
    26  
    27                            # Unix-Prompt
    28 $r = $telnet->expect($timeout, '\$');
    29 die "No unix prompt (\$)" unless defined $r;
    30 
    31 $telnet->interact();       # In interaktiven 
    32                            # Modus schalten

htp.pl

    01 #!/usr/bin/perl -w
    02 ##################################################
    03 # Michael Schilli, 1998 (mschilli@perlmeister.com)
    04 ##################################################
    05  
    06 use Expect;
    07 
    08 $username      = "user";   # Neuer user
    09 $passwd        = "pass";   # ... und sein passwd
    10 
    11 $passwdfile    = "htpasswd.dat";
    12 $htpasswd_prog = "./htpasswd";
    13 
    14 my $timeout = 10;          # Grundsätzlich 10 sec
    15                            # auf Antwort warten
    16 
    17                            # htpasswd-Programm 
    18                            # starten
    19 $robot = Expect->spawn($htpasswd_prog, 
    20                        $passwdfile, $username);
    21 
    22                            # Schon vorhanden?
    23 $pattern = $robot->expect($timeout, 
    24                       "Changing", "New password:");
    25 
    26 if(!defined $pattern) {
    27     die "New password: not found";
    28 } elsif($pattern == 1) {
    29     die "User already there";
    30 }
    31                            # Erstes Paßwort
    32 $robot->send_slow(0, "$passwd\n");
    33 
    34                            # Paßwort-Bestätigung
    35 $robot->expect($timeout, 
    36                "Re-type new password:") || 
    37     die "Pattern Re-type new password: not found";
    38 $robot->send_slow(0, "$passwd\n");
    39 
    40                      # Bis zum Programmende warten
    41 $robot->expect(undef);

Installation

Das Expect-Modul benötigt noch zwei TTY-Module, sodaß insgesamt drei Distributionen her müssen (in dieser Reihenfolge):

    AUSCHUTZ/IO-Stty-.02.tar.gz
    GBARR/IO-Tty-0.02.tar.gz
    AUSCHUTZ/Expect.pm-1.07.tar.gz

Das IO::Tty-Modul führt bei der Installation durch ein längliches Konfigurationsskript, das an eine Perl-Installation erinnert - blindes Hämmern auf die Return-Taste übernimmt die Default-Werte, die unter Linux ``passen''. Der Rest geht nahtlos.

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.