Open Source im professionellen Einsatz

HTTP-Proxy simuliert Modem-Leitungen

Urlaub in der Steinzeit

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.

Diesen Artikel als PDF kaufen

Als digitales Abo

Als PDF im Abo bestellen

comments powered by Disqus

Ausgabe 07/2013

Preis € 6,40

Insecurity Bulletin

Insecurity Bulletin

Im Insecurity Bulletin widmet sich Mark Vogelsberger aktuellen Sicherheitslücken sowie Hintergründen und Security-Grundlagen. mehr...

Linux-Magazin auf Facebook