Aus Linux-Magazin 05/2001

HTTP-Proxy simuliert Modem-Leitungen

Abb. 1: Der Proxy zwischen Browser und Server.

Ein kleiner Proxyserver erlaubt es, auch auf superschnellen InternetVerbindungen die Welt durch die Brille armer Modem-Benutzer zu sehen.

Neulich war ich mal für eine Woche in Deutschland und schaute durch Zufall meine Website durch eine traditionelle Modemverbindung an. Zu meinem nicht geringen Erstaunen dauerte es ungefähr 30 Sekunden, bis der Browser die Amerika-Rundbriefe auf Perlmeister.com anzeigte. Grund dafür war das HTML-Design der Seiten, das aus einer riesigen zweispaltigen Tabelle bestand, die der Browser erst dann anfing darzustellen, als die ganze 50 KByte große Seite durch die enge Leitung gepumpt war. Unter DSL war mir das nie aufgefallen, da dort 50 KByte in einem Bruchteil der Zeit durchrauschen.

Wieder daheim in den USA angekommen, nahm ich mir deshalb vor, meine Seiten vor der Veröffentlichung künftig auch auf meinem flinken DSL-Anschluss mittels eines kleinen Tricks unter Schneckenmodem-Geschwindigkeit zu testen. Hierzu wird einfach der heute vorgestellte Proxyserver gestartet, dann ein handelsüblicher Browser darauf konfiguriert – und schon drosselt der Proxy die verfügbare Bandbreite auf beliebig einstellbare Werte herunter.

Proxyserver in 85 Zeilen

Wie in [2] schon einmal vorgestellt, ist es ein Leichtes, unter Perl einen Proxyserver zu schreiben, der zwischen dem Browser und dem kontaktierten Webserver steht und allerlei lustige Streiche treibt. Das Modul HTTP::Daemon von Gisle Aas erledigt die Feinheiten, wir müssen nur die Logik hinzufügen, die den Durchsatz verlangsamt.

Listing 1 zeigt die Implementierung. Zeile 5 definiert den Port, auf dem der Proxyserver lauscht, und Zeile 6 den maximalen Durchsatz in Bytes pro Sekunde. Die Zeilen 8 und 9 ziehen die benötigten Zusatzmodule herein, die wir im Kasten “Installation” vom CPAN holen werden.

Zeile 12 setzt einen Signal-Handler auf, der das SIGPIPE-Signal ignoriert. Es kann auftreten, wenn ein Browser unvermittelt die Verbindung abbricht. Der zweite Signal-Handler in Zeile 14 erlöst beendete Prozesskinder aus ihrem Zombiestatus – weiter unten werden wir Parallelprozesse abfeuern.

Zeile 17 erzeugt den neuen HTTP-Dämon, den eigentlichen Proxy. Er lauscht auf dem eingestellten Port auf Anfragen, holt die angeforderten Seiten anschließend vom Web und liefert sie schließlich wieder an den anfragenden Rechner zurück. Der Reuse-Parameter lässt den Server auch dann starten, wenn der Socket einer kurz zuvor rüde unterbrochenen Instanz von slowie.pl noch etwas unschlüssig auf dem Port herumhängt.

Zeile 21 beendet das Programm sofort, falls der Dämon nicht starten kann. Andernfalls schreibt Zeile 24 eine Meldung auf die Standardausgabe, die angibt, unter welchem Port der Proxy zu erreichen ist.

Abb. 1: Der Proxy zwischen Browser und Server.

Abb. 1: Der Proxy zwischen Browser und Server.

Zeile 26 erzeugt ein Objekt vom Typ LWP::UserAgent, das später beim Einholen von Webpages behilflich sein wird. Die im Anschluss aufgerufene agent()-Methode bestimmt, wie der Proxy den UserAgent-Header bei Anfragen an den Webserver setzt – slowie/1.0 wird sicher zur Erheiterung des einen oder anderen Webmasters beitragen.

Die accept()-Methode in Zeile 29 blockt so lange, bis ein Request vom Browser ankommt, und besetzt dann $conn mit einer Referenz auf das Verbindungsobjekt. Geht dabei etwas schief, bricht das Programm ab.

Installation

Die beiden verwendeten Module HTTP::Daemon und LWP::UserAgent sind Bestandteil der LWP-Bibliothek von Gisle Aas. Das CPAN-Modul installiert alles zügig mit:

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

Dann konfiguriert man den vom Proxy zu verwendenden Port in Zeile 5 von slowie.pl und den gewünschten Durchsatz in Bytes pro Sekunde in Zeile 6. Jetzt wird slowie.pl von der Kommandozeile gestartet, was etwa Folgendes anzeigen sollte:

Server listening at port 8018

Anschließend muss der Web-Browser auf den Proxy zeigen, bei Netscape wird hierzu im Menü Edit->Preferences->Advanced->Proxies der Punkt Manual Proxy Configuration selektiert. Nach dem Drücken des View-Knopfes werden folgende Einträge gesetzt:

HTTP Proxy: localhost 
Port: 8018

Der Eintrag Secure Proxy bleibt frei. Im Falle des Internet Explorers ist es die Seite View->Internet Options->Connection, in der die Checkbox Access the Internet using a proxy server anzukreuzen und ebenfalls localhost und Port 8018 einzutragen sind. Dann einfach die Konfigurationsfenster schließen, eine URL in den Browser tippen, zurücklehnen und langsam genießen!

Da der Browser Requests unter Umständen schnell hintereinander abfeuert und der Proxy mehrere Anfragen quasi gleichzeitig bearbeiten soll, ist es wichtig, dass er – noch während der Request bearbeitet und die Daten vom Web geholt werden – schnellstens wieder zur accept()-Methode in Zeile 29 zurückkehrt, um gleich die nächste Anfrage des Browsers entgegenzunehmen. Das löst slowie.pl durch parallele Prozesse, die der fork()-Befehl in Zeile 32 kreiert.

Zeile 34 schickt den Vaterprozess sofort wieder zur accept()-Methode am Anfang des Blocks zurück, während der neue Kindprozess in Zeile 37 damit fortfährt, die Request-Daten vom Browser entgegenzunehmen. Zeile 39 nutzt das Objekt vom Typ LWP::UserAgent, um die gewünschten Daten vom Web zu holen. Dabei verwendet es bewusst simple_ request() und nicht request(), da wir dem Browser keinerlei Arbeit abnehmen wollen und er demgemäß den Redirects auch gefälligst selber folgen muss.

Zeile 47 ruft daraufhin die Methode send_response() der Browser-Verbindung auf, um die Antwort zurückzuschicken; send_response() versteht zwei verschiedene Parameterarten: Einen String sendet sie sofort zurück zum Browser, und eine Referenz auf ein Objekt vom Typ HTTP::Response nutzt sie, um dessen content()-Methode immer und immer wieder aufzurufen und das jeweils zurückgelieferte Ergebnis als String stückweise weiter an den Browser weiterzuleiten.

Verkehrsberuhigende Maßnahmen

Genau diesen Mechanismus nutzt slowie.pl dazu, den Datendurchsatz zum Browser künstlich zu begrenzen. Für den Fall, dass der Webserver auf die gestellte Anfrage hin tatsächlich Daten liefert, ruft Zeile 43 die Funktion get_slowsub() auf, die eine Referenz auf eine Funktion zurückliefert, die jene mit $resp->content() ursprünglich übergebenen Webserver-Daten speichert und bei jedem anschließenden Aufruf einen kleinen Bissen davon so lange zurückgibt, bis schließlich alle Daten geliefert wurden. Die in $subref gespeicherte Funktionsreferenz schmuggelt Zeile 44 dem Response-Objekt $resp als Inhalt unter und ersetzt damit die ursprünglich dort gespeicherten Antwortdaten des Webservers.

Listing 1: slowie.pl

01 #!/usr/bin/perl -w
02 
03 use strict;
04 
05 my $PORT      = 8018;
06 my $BYTE_RATE = 1000;
07 
08 use HTTP::Daemon;
09 use LWP::UserAgent;
10 
11     # Falls der Browser plötzlich abbricht
12 $SIG{PIPE} = `IGNORE';
13     # Reaper für terminierte Kindprozesse
14 $SIG{CHLD} = sub { wait(); };
15 
16     # Neuen Dämon erzeugen
17 my $srv = HTTP::Daemon->new( LocalPort => $PORT, 
18                              Reuse     => 1 );
19 
20     # Fehler aufgetreten?
21 die "Can't start server ($@)" unless defined $srv;
22 
23     # Erfolgsmeldung
24 print "Server listening at port $PORTn";
25 
26 my $ua = LWP::UserAgent->new();
27 $ua->agent("slowie/1.0");
28 
29 while(my $conn = $srv->accept()) {
30 
31         # Parallelprozess abfeuern
32     defined(my $pid = fork()) or die "Can't fork!";
33         # Vater kehrt zurück zum accept()
34     next if $pid;
35 
36         # Kind bearbeitet Requests der Verbindung
37     while (my $request = $conn->get_request) {
38 
39         my $resp = $ua->simple_request($request);
40 
41         if($resp->is_success()) {
42             my $subref = 
43                 get_slowsub($resp->content());
44             $resp->content($subref);
45         } 
46 
47         $conn->send_response($resp);
48     }
49     $conn->close;
50         # Kind beendet sich
51     exit(0);
52 }
53 
54 ##################################################
55 sub get_slowsub {
56 ##################################################
57     my ($content) = @_;
58 
59     my $start      = time() - 1;
60     my $followup   = 0;
61 
62         # Closure erzeugen
63     my $subref = sub {
64 
65             # Ende der Übertragung?
66         if(0 == length($content)) {
67             return undef;
68         }
69 
70         sleep(1) if $followup++;
71 
72             # Maximal verfügbare Bytes
73         my $max = (time() - $start) * $BYTE_RATE;
74 
75             # Timer zurücksetzen
76         $start = time();
77                       
78             # Bereich aus $content ausschneiden
79             # und zurückgeben
80         my $chunk = substr($content, 0, $max, "");
81         return($chunk);
82     };
83 
84     return $subref;
85 }

Wenn alles gut ging, steckt also in Zeile 47 in dem an send_response() übergebenen HTTP::Response-Objekt der Wolf im Schafspelz: send_ response() wird feststellen, dass $resp->content() keine Daten, sondern eine Funktionsreferenz liefert, und deswegen die dahinter steckende Funktion wieder und wieder aufrufen, bis sie einen leeren String oder einen undefinierten Wert liefert. Alle bis dato von der Funktion gelieferten Daten schickt sie stückweise an den Browser. Danach hat der Kindprozess seine Aufgabe erledigt.

Zeigt die while-Schleife in Zeile 37 an, dass der Browser keinen neuen Request mehr in dieser Session hat, wird die Verbindung zu ihm in Zeile 49 gekappt und der Kindprozess mit exit(0) beendet. Der schon erwähnte Signalhandler in Zeile 14 sorgt dafür, dass aus ihm kein Zombie wird.

Nun zur trickreichen Funktion get_ slowsub(), die eine Referenz auf eine Funktion liefert, die die von der Webseite schon vollständig empfangenen Daten nur sehr zögerlich, der eingestellten Bandbreitenbegrenzung entsprechend herausgibt. get_slowsub() nimmt einen String entgegen, der dem Inhalt der angeforderten Website entspricht, und definiert eine so genannte Closure, um eine Funktion mit Gedächtnis zu verwirklichen, deren Referenz sie anschließend zurückgibt.

Geheimnisvolle Closure-Welt

Die Closure ist eine Funktion, die nicht nur ihren Programmcode kennt, sondern auch noch die Zustände der sie umgebenden lexikalischen Variablen behält. Ein Beispiel:

{ my $count = 1;

  sub zaehle {
    print($count++, "n");
  }
}

Nach diesem Block gibt es zwar die Funktion zaehle(), aber $count ist wegen seines lexikalischen Scopes verschwunden. Doch halt – nicht ganz: zaehle() hat während seiner Entstehung von der Variablen $count Kenntnis genommen und deswegen führt das in zaehle() referenzierte $count beim nächsten Aufruf von zaehle() außerhalb des Blocks den Wert 1! Weitere Aufrufe von zaehle() geben 2, 3, … aus:

zaehle();  # => 1
zaehle();  # => 2
zaehle();  # => 3

So erzeugt die Definition in Zeile 63 nicht nur eine Referenz auf eine Funktion, sondern schließt in diese auch gleich die Variablen $content, $start und $followup ein, die sich innerhalb der definierten Funktion ähnlich wie globale Variablen verhalten, aber außerhalb der Funktion nicht sichtbar sind, sobald get_slowsub() verlassen wurde.

Die Funktion führt die sie umgebenden lexikalischen Variablen wie in einer Einkaufstasche mit sich. Sie enthalten beim ersten Aufruf der Funktion die Werte, die ihnen bereits vor der Erzeugung der Funktion zugewiesen wurden.

Wozu das Ganze? get_slowsub() soll eine Referenz auf eine Funktion erzeugen, die persistente Zustandsvariablen mit sich führt. Diese müssen zudem für jede neue Funktionsinstanz eindeutig sein, da unter Umständen viele dieser Verzögerer gleichzeitig aktiv sind. get_slowsub() erzeugt also mit der Closure eine Art Objekt mit Instanzvariablen.

In der lexikalischen Variablen $content steht die Textantwort der befragten Website als String, in $start die Uhrzeit des letzten Datentransfers (am Anfang die aktuelle Uhrzeit minus eine Sekunde) und $followup zeigt an, ob die Funktion zum ersten Mal oder schon mehrmals aufgerufen wurde.

Zeile 66 prüft, ob die Länge der verbleibenden Nachricht gleich 0 ist, und lässt die Closure undef zurückgeben, falls dem so ist, weil so send_response() weiter oben den Transfer zum Browser beendet. Zeile 70 schläft eine Sekunde, falls es sich nicht um den ersten Aufruf der Funktion handelt, um send_response() zu bremsen, das die Closure Schlag auf Schlag aufruft.

Zeile 73 ermittelt aufgrund der zulässigen Bandbreite und der seit dem letzten Datentransfer (oder dem ersten Aufruf) verstrichenen Zeit die Anzahl der Zeichen, die die Funktion freigeben darf. Zeile 76 setzt den Timer wieder zurück, damit beim nächsten Aufruf die Berechnung der freigegebenen Bytes wieder stimmt. Zeile 80 schlägt zwei Fliegen mit einer Klappe: Sie extrahiert die ermittelte Anzahl von Zeichen aus der Closure-Variablen $content und nutzt die Vier-Parameter-Version von substr() dazu, den extrahierten String auch gleich im Original zu löschen. Der Rückgabewert von substr() ist dann ein String mit den ausgeschnittenen Zeichen, die zum Browser geschickt werden dürfen.

Noch einmal: get_slowsub() definiert nur eine Funktion und gibt eine Referenz darauf zurück. Die innen definierte Funktion tut nichts anderes, als bei jedem Aufruf etwas mehr von einem vorgegebenen String herauszurücken, der ihr bei ihrer Definition als Closure-Variable überreicht wurde und die sie stets mit sich führt. Eine hervorragende Erklärung des komplexen Themas Closures ist übrigens in [4] zu finden.

Macht eure Webseiten auch für arme Modem-Benutzer erträglich! ( tfr)

Infos

[1] Die Amerika-Rundbriefe auf Perlmeister.com: http://perlmeister.com/private

[2] “Loggender Proxy”, Michael Schilli, Linux-Magazin 4/2000: https://www.linux-magazin.de/ausgabe/2000/04/Proxy/proxy.html

[3] “Slow down the download!”, Randal Schwartz, 1998, ein ähnliches Skript, allerdings als CGI: http://web.stonehenge.com/merlyn/WebTechniques/col21.html

[4] Zum Thema Closures: “Object Oriented Perl”, Damian Conway, Manning, 1999, Seite 56ff.

Der Autor

Michael Schilli arbeitet als Web-Engineer für AOL/Netscape in Mountain View, Kalifornien. Er hat “Goto Perl 5” (deutsch) und “Perl Power” (englisch) für Addison-Wesley geschrieben, arbeitet gerade an dem neuen Buch “Perl Lernen” für Anfänger und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage:

http://perlmeister.com.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben