Hastig abgenickt (Linux-Magazin, November 2010)

Ein Perlskript erspart dem eiligen Nutzer das Herumklicken auf Splash-Pages von WiFi-Providern, nickt Geschäftsbedingungen automatisch ab und schaltet den Zugriff auch ohne Browser schnellstens frei.

Bevor Hotel-, Internet-Cafe- oder Flughafen-WiFis neue User ins Netz lassen, leiten sie Browser-Requests um (Abbildung 1) und der Surfdurstige muss sich erst durch eine Splash-Page (Abbildung 2) quälen. Dort erwarten ihn entweder ellenlange Nutzungsbedingungen, die eh niemand liest, Checkboxen, die zu aktivieren sind, oder Werbung, oder alle drei genannten Ärgernisse zugleich. WiFi-Provider stellen Splash-Pages als kleine Hürden auf, um die MAC-Addresse des Clients im WiFi-Netz zentral zu erfassen und den User an seine Pflichten als vorbildlichen Netzbürger zu erinnern. Absolviert dieser den Pflichttanz ordnungsgemäß, lässt der Access Point den Schutzschild fallen und nicht nur der Web-Verkehr fließt, sondern auch beliebige TCP-Verbindungen ins Internet stehen auf einmal offen.

Abbildung 1: Obwohl der User google.com wünscht, leitet das WiFi ihn zunächst zur Splash-Page des Providers um.

Ohne Web kein Zugang

Manchmal möchte der User aber gar keinen Web-Zugang, sondern nur einen SSH-Tunnel öffnen, und wundert sich bei offener Shell und geschlossenem Browser, warum er zwar eine IP-Addresse zugewiesen bekommt, aber offensichtlich eine Firewall den Zugriff aufs Internet blockiert.

Abbildung 2: Das Hotel-Wifi leitet den ersten Browser-Request zu einer Splash-Page um.

Abbildung 3: Mit einem Klick akzeptiert der Benutzer die Bedingungen und darf fortan 24 Stunden unbehelligt surfen.

Das Perl-Skript splash hilft uns dabei heute aus der Patsche. Es wühlt sich blind durch die zwischen den erwartungsfrohen User und den ungetrübten Internetgenuss gestellten Web-Formulare, klickt auf alle Links, checkt die Checkboxen, sammelt die Cookies ein und schickt sie brav an den WiFi-Provider zurück, dem es so vorgaukelt, mit einem Browser samt lebensechtem Bediener zu kommunizieren.

Flexible Plugin-Taktik

Doch wie kann ein einfaches Skript mit potentiell tausenden verschiedener Splash-Seiten zurechtkommen? WiFi-Provider könnten nicht nur beliebig komplexe Formulare erzeugen, sondern auch schwere Geschütze auffahren, wie mit JavaScript zu tricksen oder gar Flash-Animationen einzusetzen, um automatische Skripts abzuwehren.

Deshalb fährt das Skript eine Plugin-Strategie, mit der User oder auch freundliche Dritte es an neue Verfahren anpassen können. Jeder Plugin probiert nach seiner eigene Methode, die Splash-Seite zu überlisten: Der Plugin ClickAllLinks.pm klickt alle Links auf der Seite durch, während CheckBoxFill.pm alle dargestellten Checkboxen des ersten Webformulars aktiviert und anschließend einen eventuell vorhandenen Submit-Button drückt. Neu entwickelte Plugins fügt der User ins Plugin-Verzeichnis ein, von wo sie das Skript ohne weitere Konfiguration automatisch aufschnappt und als neue Taktik ausprobiert.

Nach jedem Plugin-Lauf prüft splash, ob ein Request für http://google.com auch wirklich die Webseite des Suchriesen einholt oder ob schon wieder die verhasste Splash-Page erscheint. Falls sich noch kein Erfolg eingestellt hat, kommt der nächste Plugin zum Zug, andernfalls bricht das Skript mit einer Erfolgsmeldung ab.

Abbildung 4: Falls keine Netzwerkverbindung besteht, probiert das Skript in Abständen von 5 Sekunden, den Google-Server zu erreichen.

Bis die Mauer fällt

Abbildung 4 zeigt, wie das Skript reagiert, falls einfach noch keine Netzwerkverbindung existiert, wohl weil der User noch keines der verfügbaren WiFi-Netzwerke selektiert oder noch nicht das erforderliche WPA-Passwort eingegeben hat. Solange die Google-Anfrage nur Fehler liefert oder länger als fünf Sekunden blockiert, probiert es das Skript nach kurzer Verschnaufpause einfach nochmal. Sobald der Client dann eine IP zugewiesen bekommt und sich bei Web-Anfragen wenigstens die Splash-Page einstellt, wirft es einen Plugin nach dem anderen ins Getümmel (Abbildung 5), bis einer schließlich die künstliche Mauer überwindet.

Abbildung 5: Trifft das Skript auf eine Splash-Page, versucht es zunächst nach dem Verfahren click-all-links alle Links durchzuklicken.

Listing 1 zeigt die Basisklasse SplashJumper aller Plugins, die lediglich einen Konstruktor definiert. Sie zieht das CPAN-Modul Module::Pluggable hinzu und übergibt ihm die Parameter require => 1. Das Modul durchforstet daraufhin das Unterverzeichnis SplashJumper/Plugin nach .pm-Dateien und lädt sie wegen der require-Anweisung alle in das gerade laufende Skript.

Listing 1: SplashJumper.pm

    01 ###########################################
    02 package SplashJumper;
    03 ###########################################
    04 # 2010, Mike Schilli <m@perlmeister.com>
    05 use strict;
    06 use warnings;
    07 use Module::Pluggable require => 1;
    08 
    09 sub new {
    10     bless {}, shift;
    11 }
    12 
    13 1;

Die Besten zuerst

Einen typischen Plugin zeigt Listing 2 mit dem Modul ClickAllLinks. Wenn das Hauptskript den Plugin lädt, ruft es dessen register()-Methode auf, die den Namen der implementierten Taktik und eine Prioritätszahl zurückliefert. Das Hauptskript sortiert die Plugins numerisch nach den von ihnen mitgeteilten Prioritätszahlen, ruft also Plugins mit niedrigen Werten zuerst auf. So können die Plugins untereinander auskarteln, wer wann an die Reihe kommt. Üblicherweise sollte der mit den besten Erfolgsaussichten den Anfang machen, das verkürzt den Skriptlauf. Die beide heute vorgestellten Plugins definieren die Prioritäten 10 beziehungsweise 50, so dass das Skript immer zuerst ClickAllLinks versucht, bevor das weiter unten gezeigte CheckBoxFill-Verfahren zum Zug kommt.

Listing 2: ClickAllLinks.pm

    01 ###########################################
    02 package 
    03   SplashJumper::Plugin::ClickAllLinks;
    04 ###########################################
    05 # Mike Schilli, 2010 (m@perlmeister.com)
    06 ###########################################
    07 use Log::Log4perl qw(:easy);
    08 
    09 ###########################################
    10 sub register {
    11 ###########################################
    12     return "click-all-links", 10;
    13 }
    14 
    15 ###########################################
    16 sub process {
    17 ###########################################
    18   my($self, $mech) = @_;
    19 
    20   for my $link ( $mech->links() ) {
    21 
    22       INFO "Clicking on ", $link->url();
    23       my $resp = $mech->get( $link );
    24 
    25       INFO "Got ", 
    26            length( $resp->content() ),
    27            " bytes back";
    28 
    29       $mech->back();
    30   }
    31 }
    32 
    33 1;

Kommt ein Plugin zum Überwinden der Splash-Page zum Einsatz, ruft das Skript dessen process()-Methode auf und übergibt ihr den Browser-Simulator $mech, ein Objekt der Klasse WWW::Mechanize. Dieses CPAN-Modul eignet sich hervorragend dazu, Webseiten einzuholen, deren Inhalt zu analysieren und zu tieferliegenden Links vorzudringen. Es kommt häufig bei der Implementierung von Screen-Scrapern zum Einsatz, da beinahe alle Browser-Funktionen beherrscht (abgesehen von JavaScript-Code oder Flash-Plugins) und Cookies automatisch empfängt und korrekt wieder an den Server zurückschickt. Im Plugin ClickAllLinks findet zunächst die Methode links() alle auf der eingeholten Splash-Seite vorhandenen Links in Form von WWW::Mechanize::Link-Objekten und iteriert über die Liste mittels einer for-Schleife. Die Funktion INFO aus dem Log4perl-Fundus zeigt dann dem neugierigen User an, welchen Link der Plugin gerade anklickt. Die Methode get() des Browser-Simulators führt den Web-Request anschließend durch.

Mechanize mit Autocheck

Eine explizite Fehlerprüfung findet nicht statt, da WWW::Mechanize sich standardmäßig im autocheck-Modus befindet, in dem es bei jedem auftretenden Fehler eine Exception wirft, die ein im Hauptskript um den Plugin-Aufruf gewickelter eval {}-Block abfängt. Anschließend behandelt das Hauptprogramm den Fehler.

Listing 3: CheckBoxFill.pm

    01 ###########################################
    02 package SplashJumper::Plugin::CheckBoxFill;
    03 ###########################################
    04 # Mike Schilli, 2010 (m@perlmeister.com)
    05 ###########################################
    06 use Log::Log4perl qw(:easy);
    07 
    08 ###########################################
    09 sub register {
    10 ###########################################
    11     return "checkbox-fill", 50;
    12 }
    13 
    14 ###########################################
    15 sub process {
    16 ###########################################
    17   my($self, $mech) = @_;
    18 
    19   $mech->form_number(1);
    20 
    21   my @inputs = $mech->current_form->
    22            find_input( undef, "checkbox" );
    23 
    24   for my $input ( @inputs ) {
    25       $input->check();
    26   }
    27 
    28   INFO "Submitting form 1";
    29   $mech->submit_form( form_number => 1 );
    30   $mech->back();
    31 }
    32 
    33 1;

In Zeile 25 zeigt der Plugin dann mit einer weiteren INFO-Anweisung an, wieviele Bytes über den geklickten Link zurückkamen und die Methode back() in Zeile 29 drückt den "Back"-Button des virtuellen Browsers, damit dieser wieder zur Splash-Page zurückrudert.

Einen weitere Taktik zeigt der Plugin CheckBoxFill in Listing 3. Es sucht im HTML der Splash-Seite das erste Webformular und setzt dieses mit der Anweisung form_number(1) als current_form. Die Methode find_input() extrahiert daraus alle Eingabefelder vom Typ checkbox und die for-Schleife ab Zeile 24 klickt sie alle an, indem sie deren check()-Methode aufruft. Zeile 29 schickt das Webformular dann mit submit_form() an den Server zurück, bevor Zeile 30 wieder auf die Splash-Seite zurückkehrt, damit der eventuell gleich folgende Plugin wieder normale Ausgangsbedingungen vorfindet. Mit dieser Methode lassen sich Splash-Pages wie in den Abbildungen 6 und 7 knacken, die beide dem User abverlangen, eine Checkbox zu aktivieren und dann den "Submit"-Button anzuklicken.

Abbildung 6: Das kostenlose WiFi am Flughafen von San Diego verlangt, eine Checkbox zu anzuklicken und das Web-Form abzuschicken.

Abbildung 7: Das kostenlose WiFi am Flughafen von San Francisco, ebenfalls mit einer Checkbox.

Listing 3 zeigt schließlich das Hauptprogramm splash. Es definiert zunächst in Zeile 7 den Test-URL, den das Skript einholt, um festzustellen, ob der Internetzugang bereits offen steht. Die Google-Seite eignet sich dazu hervorragend, da sie leichtgewichtig und mit hoher Wahrscheinlichkeit in einem funktionierenden Internet verfügbar ist. Zeile 9 initialisiert das Log4perl-Framework mit dem Level DEBUG, damit der User detaillierte Informationen darüber erhält, welcher Plugin gerade läuft und auf welche Links dieser klickt.

Ein neu angelegtes Objekt der Basisklasse SplashJumper verfügt dank des dort eingebundenen Moduls Module::Pluggable über eine Methode plugins(), die eine Liste aller im Plugin-Unterverzeichnis eingestellten Plugins zurückliefert. Zeile 17 prüft, ob jeder Plugin tatsächlich vorschriftsgemäß eine Methode namens register() anbietet und überspringt mangelhaft implementierte Plugins mit einer Fehlermeldung.

Listing 4: splash

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use SplashJumper;
    04 use WWW::Mechanize;
    05 use Log::Log4perl qw(:easy);
    06 
    07 my $url = "http://www.google.com";
    08 
    09 Log::Log4perl->easy_init($DEBUG);
    10 
    11 my $sj = SplashJumper->new();
    12 
    13 my @ways = ();
    14 
    15 for my $plugin ( $sj->plugins() ) {
    16 
    17   if( ! $plugin->can("register") ) {
    18     ERROR "$plugin can't do register()";
    19     next;
    20   }
    21 
    22   my($algo, $order) = $plugin->register();
    23 
    24   push @ways, [$algo, $plugin, $order];
    25 }
    26 
    27   # sort by plugin priority
    28 @ways = sort { $a->[2] <=> $b->[2] } @ways;
    29 
    30 my $mech = WWW::Mechanize->new();
    31 $mech->timeout(5);
    32 
    33   # wait until network is up
    34 { 
    35   INFO "Trying $url";
    36   eval { $mech->get( $url ); };
    37   if($@) {
    38     INFO "Connection down, retrying";
    39     sleep 5;
    40     redo;
    41   }
    42 }
    43 
    44   # try to get past splash page
    45 for my $ways ( @ways ) {
    46   eval { $mech->get( $url ); };
    47 
    48   my $current_url = 
    49     $mech->response->request->uri;
    50 
    51   if( $current_url eq $url ) {
    52       INFO "Link is up.";
    53       last;
    54   } else {
    55       INFO "Link still down.";
    56   }
    57 
    58   my($algo, $plugin, $order) = @$ways;
    59 
    60   eval {
    61     INFO "Processing splash page ",
    62          "$current_url with algo $algo";
    63     $plugin->process( $mech );
    64   };
    65 
    66   if($@) {
    67     ERROR "Algo $algo failed ($@)";
    68   } else {
    69     INFO "Plugin $algo succeeded";
    70   }
    71 }

Ordnungsgemäße Plugins geben in Zeile 22 ihre Taktik in $algo und die gewünschte numerische Priorität in der Variablen $order zurück. Das Skript verpackt die gefundenen Daten in einen Array und schiebt eine Referenz darauf ans Ende des Arrays @ways. Zeile 28 sortiert dessen Elemente dann numerisch nach dem order-Feld, so dass ein Plugin mit Priorität 10 vor einem Plugin mit Priorität 50 läuft.

Der Browser-Simulator vom Typ WWW::Mechanize setzt seinen Timeout in Zeile 31 auf 5 Sekunden, so dass die Schleife ab Zeile 33 jeweils nur 5 Sekunden in der get()-Methode hängt, bevor sie aufgibt und 5 Sekunden schläft, bevor sie erneut probiert, den Google-Server zu kontaktieren. Am Ende des Blocks, den Perl mit redo wiederholt, funktioniert zumindest das lokale WiFi-Netz und der Client bekam eine gültige IP-Adresse zugewiesen, doch der WiFi-Provider leitet Requests an www.google.com eventuell noch zu einem internen Server weiter, der die Splash-Page produziert.

Diese versucht die for-Schleife ab Zeile 45 mit den verschiedenen Plugins zu überwinden und sobald die URL des letzten Requests gleich der Test-URL ist (also kein Redirect zur Splash-Page mehr stattfand) befindet Zeile 51, dass die Splash-Page überwunden ist und die Internetverbindung offen steht.

Ist dies noch nicht der Fall, ruft Zeile 62 die Methode process() des nächsten Plugins auf, gemäß der vorher definerierten Priortiäten. Der eval{}-Block ab Zeile 59 fängt im Plugin auftretende Fehler ab und Zeile 65 prüft die Variable $@, um festzustellen, ob etwas vorgefallen ist.

Installation

Damit das Skript das Modul SplashJumper findet, muss letzteres im Suchpfad %INC des Skripts installiert sein, am einfachsten geht das, indem beide im gleichen Verzeichnis landen. Die Plugins wandern ins neu angelegte Unterverzeichnis SplashJumper/Plugin, so dass das Datei-Layout wie folgt aussieht:

    splash
    SplashJumper.pm
    SplashJumper/Plugin/ClickAllLinks.pm
    SplashJumper/Plugin/CheckBoxFill.pm

Weitere Plugins, die eventuell mehrere Webformulare verarbeiten oder gar Javascript-Tricks überlisten legt der User ebenfalls ins Verzeichnis Plugin ab und weist ihnen in der Methode register() einen Taktiknamen und eine Priorität zu, um die Reihenfolge ihres Einsatzes festzulegen. Nach dem Aufklappen des Laptops am Flughafen ist dann einfach splash zu starten. Die Ausgabe des Skripts zeigt anschließend, wie sich der kleine Krieger in der neuen Umgebung schlägt und ob er dort ebenfalls die in den Weg gestellten Hürden umschubsen kann.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2010/11/Perl

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.