Loggender Proxy (Linux-Magazin, April 2000)

Was treibt der Browser, während wir browsen? Er sendet Cookies und versteckte Formularfelder und springt nach Redirect-Anweisungen unsichtbar herum. Ein in Perl geschriebener Proxy-Server kommt ihm auf die Schliche.

Ein Proxy-Server für HTTP-Requests steht zwischen Browser und Webserver und vermittelt die Kommunikation zwischen den beiden: So fordert der Browser nicht mehr Seiten von einem Webserver selbst an, sondern beauftragt den Proxy, eine bestimmte Seite von einer angegebenen Website zu holen. Der Proxy spricht dann kurz mit dem entsprechenden Webserver, holt die Information dort ab und gibt sie anschließend an den Browser weiter. Browser und Website treten also nie direkt in Kontakt: Der Browser spricht mit dem Proxy und auf der anderen Seite merkt der Webserver nicht, was hinter dem Proxy steckt -- er hält diesen für einen ganz normalen Browser. Derartige Systeme finden in Firewall-Lösungen und/oder Caching-Systemen Einsatz.

Doch heute wollen wir erforschen, was unser Browser treibt, während wir zu Webseiten springen, Formulare ausfüllen und Submit-Knöpfe drücken. Welche Parameter wandern über's Netz? Wie sehen die Cookies aus? Diese Fragen beantwortet ein selbstgeschriebener Proxy-Server, der die Browser-Requests einfach weitergibt und nebenbei mitloggt, was so passiert.

Ist der Browser einmal auf den Proxy eingestellt (siehe Abschnitt Installation), muss zunächst der Proxy laufen:

    proxy.pl

startet ihn, lässt ihn den eingestellten Port schnappen und auf Requests lauschen, was er folgendermaßen bekanntgibt:

    Server listening at port 8017

Tippen wir nun beispielsweise

    http://perlmeister.com/cgi/dump.cgi?a=b&c=d

in das Adressfeld des Browsers ein und fordern den URL an, zeigt der Browser ohne Umschweife das Ergebnis des entsprechenden CGI-Skripts an, während der im Hintergrund laufende Proxy folgendes bekanntgibt:

    URL ..... http://perlmeister.com/cgi/dump.cgi
    Method .. GET
    Cookies:
        id .. VE100 IP205.134.229.185 CT946152787 ID94615278717305
    Parameters
        a ... b
        c ... d

Es handelt sich also um einen GET-Request (klar, da er im Adressfeld und nicht über ein POST-Formular eingegeben wurde) und die Parameter a und c wurden auf b bzw. d gesetzt. Auch ein Cookie schmuggelte der Browser heimlich unter: Dasjenige des in [1] vorgestellten Cookie-Trackers nämlich, welches der Browser permanent parat hält und an alle Seiten in der Domain perlmeister.com schickt.

Der loggende Proxy ist außerdem nützlich beim Aufspüren von versteckten Parametern in Web-Applikationen, welche findige Perl-Hacker mit automatischen Skripts bedienen -- ganz wie in den letzten beiden Folgen besprochen.

Als Einschränkung ist zu beachten, dass das Ganze mit https-Requests nicht funktioniert: In diesen Fällen stellt der Proxy lediglich einen "Tunnel" bereit, der den verschlüsselten Verkehr zwischen Browser und Endserver lediglich durchschleust, aber nicht analysieren kann.

Fragen wir den Server der deutschen Bundesbahn auf http://www.bahn.de spaßeshalber nach der nächsten Verbindung von Bullerbü nach Babylon, loggt der Proxy-Server den POST-Request folgendermaßen:

    URL .......... http://bahn.hafas.de/bin/query.exe/dn
    Method ....... POST
    Referer ...... http://www.bahn.de/
    Parameters
        protocol . http:
        filter ... ALL#0#0#0
        datesel .. custom
        from ..... bullerbü
        to ....... babylon
        via.1 .... 
        date ..... 12.01.2000
        time ..... 18:57
        timesel .. depart
        start .... Suchen

Der Referer-Header wurde richtig auf die Eingangsseite des Bahnservers gesetzt und die POST-Parameter korrekt dargestellt.

Abb.1: Der loggende Proxy-Server loggt fleißig mit

Ein Proxy-Server in 21 Zeilen

Wie aufwendig ist es nun, einen Proxy-Server in Perl zu schreiben? Ganze 21 Zeilen lang ist die Version, die in simple.pl zu sehen ist. Sie ist zwar relativ langsam, da sie vom Browser kommende Requests seriell abarbeitet -- aber vom Design her für unseren Zweck voll ausreichend. Für ausgereiftere Versionen sei auf [2] und [3] verwiesen. Das Modul HTTP::Daemon aus der großen LWP-Bibliothek von Gisle Aas, die über das CPAN zu beziehen ist, macht alles so einfach. Zeile 3 setzt den Port, auf dem der Proxy-Server lauschen soll, und die Zeilen 5 und 6 ziehen als Zusatzmodule das besagte HTTP::Daemon und das hinlänglich bekannte LWP::UserAgent für Web-Requests hinzu. Zeile 8 erzeugt ein neues Objekt, das einen laufenden HTTP-Server repräsentiert und Zeile 9 bricht ab, falls etwas schief geht, was meistens daran liegen dürfte, dass der eingestellte Port noch von einem anderen Prozess benutzt wird. Dies passiert manchmal, nachdem simple.pl unterbrochen wurde, und so kann es sein, dass man vor einem erfolgreichen Neustart des Proxy-Servers ein wenig warten muss, bis der Port vom Betriebssystem freigegeben wird -- eine unschöner Nebeneffekt des verwendeten TCP-Protokolls. Zeile 10 gibt dem Benutzer an, auf welchen Port der Server hört, damit dieser seinen Browser danach ausrichten kann.

Zeile 12 erzeugt ein neues Objekt der Klasse LWP::UserAgent. Dieses wird die Requests, die der Browser bei uns abgibt, später vom Netz holen. Die agent-Methode setzt den Header, mit dem sich der Proxy-Server beim Webserver zu erkennen gibt, auf hotlogger/1.0, damit die Webmaster tüfteln können, welches brandheiße Proxyprodukt da wohl wieder zugange ist.

Zeile 15 akzeptiert andockende Browser und Zeile 16 arbeitet alle Requests eines Browsers ab. Ein professioneller Proxy würde hier natürlich entweder parallelle Prozesse abfeuern oder die Last auf bereits agierende Child-Prozesse abladen, damit sofort der nächste Browser andocken kann, aber unser Testproxylein arbeitet ja lokal und wird nicht von Millionen Benutzern gleichzeitig traktiert. Außerdem ist's einfacher, die Logging-Ausgaben zu synchronisieren, wenn immer nur ein Request nach dem anderen abgearbeitet wird.

Zeile 17 holt mit der simple_request-Methode die angeforderten Daten vom Netz, folgt aber im Gegensatz zur sonst üblichen request-Methode keinen Redirect-Anweisungen, sondern liefert das Ergebnis in jedem Fall zum Browser zurück, der dann selbst, falls erforderlich, einen Redirect einleitet -- so woll'n wir's haben, denn schließlich wollen wir ja sehen, was abläuft.

Zeile 18 sendet dann das Ergebnis zurück zum Browser -- fertig ist der Lack.

Der loggende Proxy

Genauso funktioniert der loggende Proxy aus Listing proxy.pl -- nur dass dieser in Zeile 22 für jeden erfolgreichen Request die details-Funktion aufruft, die nützliche Informationen schön formatiert auf der Standardausgabe ausgibt.

Die send_response-Methode in Zeile 23 bemüht sich, die Daten an den Browser zurückzuschicken, lässt sich aber nur ungern dabei unterbrechen, denn dann wirft sie sofort mit einem SIGPIPE-Signal um sich, das, falls es nicht abgefangen wird, den jeweiligen Unix-Prozess beendet. Damit's nicht jedesmal schnackelt, wenn der Benutzer auf einen Link einer erst halb dargestellten Seite klickt oder genervt den Stop-Button drückt, falls das Laden zu lange dauert, definiert Zeile 10 einen Pseudo-Signal-Handler, der auftretende SIGPIPE-Signale schlichtweg ignoriert.

Die Hilfsfunktion pkv (kurz für print_key_value) gibt Wertepaare im Format

    Erster Schlüssel ..... Wert
    Zweiter Schlüssel .... Wert

aus, wobei sie die Ausgaben so formatiert, dass die Werte auch bei unterschiedlicher Schlüssellänge schön untereinander stehen, falls die eingestellte Kolumnenbreite $COLS gross genug ist. Auch nimmt sie als optionalen dritten Parameter einen $indent-Parameter entgegen, der angibt, wieviele Spalten die Ausgabe nach rechts eingerückt werden soll.

Die details-Funktion ab Zeile 41 erwartet ein Objekt vom Typ HTTP::Request und stellt dessen Parameter im anfangs gezeigten Format dar. Erst wird der URL angezeigt, der sich aus den Werten zusammensetzt, die die netloc- und die path-Methode des entsprechenden URI-Objekts liefern. Die Methode (meist GET oder POST) weist die method-Methode aus. Ist ein Referer-Header vorhanden, zeigt ihn Zeile 57.

Bei der GET-Methode hängen alle Parameter bekanntlich dem URL nach einem Fragezeichen (?) an und die query_form-Methode des zum HTTP::Request gehörenden URI-Objekts gibt in Zeile 62 eine Liste von Key-Value-Paaren zurück, die die weiter unten definierte dump_form-Routine grafisch aufbereitet.

Im Falle eines POST-Requests stehen die Parameter statt an den URL angehängt im gleichen Format direkt im Body des Requests und Zeile 63 springt den entsprechenden Programmzweig an. Die content-Methode des HTTP::Request-Objekts holt den Parameterblock hervor und die split-Anweisung in Zeile 66 zerlegt die Key-Value-Paare quick & dirty in eine Liste, in der sich Keys und Values abwechseln. Jeder Eintrag muss freilich noch URL-dekodiert werden, da Spezialzeichen im Parameterblock im %xx-Format vorliegen. Die uri_unescape-Funktion aus dem URI::Escape-Modul nimmt die Umwandlung vor. Vorher beim GET-Request erschlug dies schon die query_form-Funktion.

Zeile 69 druckt nur noch 50 Gleichheitszeichen aus, damit eine optische Trennung zwischen den Requestdaten sichtbar wird. Abbildung 1 zeigt den Proxy beim fleißigen Mitloggen, während der Browser die vielen kleinen Icons auf perlmeister.com eins nach dem anderen reinzieht.

dump_form ab Zeile 73 druckt die Wertepaare einer übergebenen Liste aus, indem sie die weiter oben definierte pkv-Funktion zurate zieht und diese die Ausgaben um vier Spalten nach rechts einrücken lässt.

Installation

Damit z. B. ein Netscape-Browser während des Browsens nicht direkt zu Webservern Kontakt aufnimmt, sondern unseren Proxy zwischenschaltet, müssen im Menü Edit->Preferences->Advanced->Proxies der Punkt Manual Proxy Configuration selektiert und nach dem Drücken des View-Knopfes folgende Einträge gesetzt werden:

    HTTP Proxy: localhost 
    Port: 8017

Der Secure Proxy-Eintrag bleibt aus den schon oben erwähnten Gründen für das Nicht-Funktionieren von SSL-Requests frei. Ähnlich geht's beim Internet Explorer, dort ist's die Seite View->Internet Options->Connection, wo die Checkbox Access the Internet using a proxy server anzukreuzen und, wie oben beim Netscape-Browser, localhost und Port 8017 einzutragen ist. Wird proxy.pl anschließend proxy.pl von der Kommandozeile aus gestartet, kann die Browserei sofort losgehen!

Die verwendeten Module HTTP::Daemon, HTTP::Response und LWP::UserAgent sind allesamt in der LWP-Bibliothek von Gisle Aas enthalten, die sich am Einfachsten mit dem CPAN-Modul installieren lässt. URI::Escape kommt mit dem URI-Modul, alles zusammen installiert sich folgendermaßen:

    perl -MCPAN -eshell
    cpan> install Bundle::LWP
    cpan> install URI

Loggt fleißig! Schaut den Webservern auf die Finger! Bis zum nächsten Mal!

Listing simple.pl

    01 #!/usr/bin/perl -w
    02 
    03 my $PORT = 8017;
    04 
    05 use HTTP::Daemon;
    06 use LWP::UserAgent;
    07 
    08 my $proxy = HTTP::Daemon->new( LocalPort => $PORT );
    09 die "@_" unless defined $proxy;
    10 print "Listening on port $PORT\n";
    11 
    12 my $ua = LWP::UserAgent->new;
    13 $ua->agent("miniproxy/1.0");
    14 
    15 while (my $conn = $proxy->accept) {
    16     while (my $request = $conn->get_request) {
    17         my $response = $ua->simple_request($request);
    18         $conn->send_response($response);
    19     }
    20     $conn->close;
    21 }

Listing proxy.pl

    01 #!/usr/bin/perl -w
    02 
    03 my $PORT = 8017;
    04 
    05 use HTTP::Daemon;
    06 use LWP::UserAgent;
    07 use URI::Escape;
    08 
    09 # If Browser disconnects suddenly
    10 $SIG{PIPE} = 'IGNORE';
    11 
    12 my $SRV = HTTP::Daemon->new( LocalPort => $PORT );
    13 die "Can't start server ($@)" unless defined $SRV;
    14 print "Server listening at port $PORT\n";
    15 
    16 my $UA = LWP::UserAgent->new;
    17 $UA->agent("hotlogger/1.0");
    18 
    19 while (my $conn = $SRV->accept) {
    20     while (my $request = $conn->get_request) {
    21         my $resp = $UA->simple_request($request);
    22         details($request) if $resp->is_success;
    23         $conn->send_response($resp);
    24     }
    25     $conn->close;
    26 }
    27 
    28 ##################################################
    29 sub pkv {
    30 ##################################################
    31     my ($key, $value, $indent) = @_;
    32     $indent ||= 0;
    33     my $COLS = 20;
    34   
    35     $dots = $COLS - length($key) - $indent - 2;
    36     print " " x $indent, 
    37           "$key ", "." x $dots, " ", "$value\n";
    38 }
    39 
    40 ##################################################
    41 sub details {
    42 ##################################################
    43     my $req     = shift;
    44     my $cookies = $req->header("Cookie");
    45 
    46     pkv "URL", "http://" . $req->uri->host_port .
    47                            $req->uri->path;
    48     pkv "Method", $req->method;
    49 
    50     if($cookies) {
    51         print "Cookies:\n";
    52         dump_form(map { uri_unescape $_ } 
    53                       split /;\s*|=/, $cookies);
    54     }
    55 
    56     if($req->header("Referer")) {
    57         pkv "Referer", $req->header("Referer");
    58     }
    59 
    60     if($req->method eq "GET") {
    61         print "Parameters\n" if $req->uri =~ /\?/;
    62         dump_form($req->uri->query_form);
    63     } elsif ($req->method eq "POST") {
    64         print "Parameters\n";
    65         dump_form(map { uri_unescape $_ } 
    66                       split /&|=/, $req->content);
    67     }
    68 
    69     print "=" x 50, "\n";
    70 }
    71 
    72 ##################################################
    73 sub dump_form {
    74 ##################################################
    75     my @form = @_;
    76 
    77     while(my ($key, $val) = splice(@form, 0, 2)) {
    78         pkv($key, $val, 4);
    79     }
    80 }

Referenzen

[1]

Michael Schilli, Linux-Magazin, "Ja wo laufen sie denn?", Juli 1999, http://www.linux-magazin.de/ausgabe/1999/07/Cookie/cookie.html

[2]

Randal Schwartz, WebTechniques, Februar 1997, http://web.stonehenge.com/merlyn/WebTechniques/col11.html

[3]

Randal Schwartz, WebTechniques, Februar 1999, http://web.stonehenge.com/merlyn/WebTechniques/col34.html

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.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 4:

Non-ASCII character seen before =encoding in 'während'. Assuming ISO8859-1