Wer die Apache-Request-Phasen verstanden hat und Perl beherrscht, dem stehen mächtige Webserver-Module offen. Dieser Artikel verdeutlicht das Prinzip an einer echten Anwendung: Eigene Translation-Handler und Filter implementieren ein Usertracking-Modul, das keine Cookies braucht.
Nachdem der erste Teil des Workshops[1] die Bearbeitungsphasen im Apache-Webserver erklärt hat, soll dies Wissen nun in einem Beispiel aus der realen Welt Anwendung finden. Als Ausgangspunkt dient ein historisch gewachsenes Webangebot mit statischen Seiten, Bildern, Server Side Includes und CGI-Programmen, die sowohl Bilder als auch HTML ausgeben. Das Angebot soll um Session-Handling erweitert werden, um später analysieren zu können, welchen Weg ein Benutzer durch die Site nahm. Apaches Mod_usertrack löst das Problem mit einem Cookie, die Lösung dieses Workshops kommt ohne aus.
Session-Tracking mit URLs
Als Alternative zu Cookies bietet es sich an, das Session-Handle über die URL zu übertragen. Dabei kodiert man das Handle entweder im Hostnamen oder im folgenden Pfadteil. Die erste Möglichkeit lässt sich mit einigen Rewrite-Rules auch ohne Mod_perl[2] implementieren, ist aber leider von der Firma Sevenval patentiert[3].
Zuerst soll für das Session-Handle in der URL ein Format festgelegt werden: Beginnt der Pfad mit »/-…/«, so enthält der hier durch drei Punkte angedeutete Teil das Session-Handle. Beginnt sie anders, leitet die Anfrage eine neue Session ein. Zum Beispiel führen also »/cgi-bin/viewtable.pl« oder »/super/toll.shtml« zu neuen Sessions, »/-ABCScad23/cgi-bin/viewtable.pl« und »/-ABCScad23 /super/toll.shtml« setzen dagegen die existierende Session »ABCScad23« fort. Das Minuszeichen weist darauf hin, dass bis zum nächsten Slash ein Session-Handle folgt. Natürlich setzt das voraus, dass es auf der bestehenden Site keine Seite gibt, deren Name mit diesem Zeichen beginnt.
Das Usertracking-Modul soll die URL einer ankommenden Anfrage analysieren und eine eventuell vorhandene Session abschneiden, damit der Server die Dateien im Document-Root findet. Dann soll es eine neue Session erzeugen, falls noch keine existiert.
Translation und Filter
Beim Ausliefern einer »text/html«-Antwort an den Browser soll das Modul eingebaute Links so verändern, dass sie die Session weiterreichen. Das Usertracking-Modul muss also in die URL-Translation-Phase eingreifen und einen anfrageorientierten Output-Filter implementieren (siehe[1]). Listing 1 zeigt die wesentlichen Teile des Translation-Handler. Zum Testen fehlen noch einige Einträge in der »httpd.conf«, Listing 2 zeigt dafür ein Beispiel.
Das Modul ist in einer Datei namens »Apache/ClickPath.pm« im Perl-Suchpfad zu speichern, den die Anweisung »PerlSwitches« (Zeile 1) erweitert. Am einfachsten lässt sich der Translation-Handler mit »curl« testen:
> curl http://localhost/Test SESSION=hk-LfH8AAAIAAFgBAQcAAAAE > curl http://localhost/Test SESSION=gV2z0n8AAAIAAFf@AHEAAAAB > curl http://localhost/-gV2z0n8AAAIAAFf@AHEAAAAB/Test SESSION=gV2z0n8AAAIAAFf@AHEAAAAB
Die Anweisung »package Test« in der »httpd.conf« definiert einen Handler für die Response-Phase, der den Content-Typ des auszuliefernden Dokuments auf »text/plain« setzt und dann die Umgebungsvariable »SESSION« ausgibt. Der Handler erzeugt die Ausgabe des Curl-Befehls. Der Location-Block verbindet diesen Response-Handler mit dem URL-Pfad »/Test«. Die Ausgaben der beiden ersten Curl-Aufrufe unterscheiden sich, während die letzten beiden gleich sind. Die ersten beiden Aufrufe beginnen jeweils eine neue Session. Der dritte Aufruf führt dann die Session des zweiten Aufrufs fort.
Nach dem nötigen Vorspann definiert »Apache/ClickPath.pm« eine Funktion »handler« (Listing 1, Zeile 14), die das »PerlTransHandler«-Statement als Translation-Handler deklariert. Sie wird wie jeder Handler des HTTP-Anfragezyklus mit dem Anfrageobjekt als Parameter aufgerufen. Die Variable »$uri« erhält den URI, so wie ihn der Client übermittelt hat. Der If-Zweig prüft, ob der URI mit »/-Session-ID<$>/« beginnt (Zeile 19). Wenn ja, setzt das Modul eine laufende Session fort. Es schneidet den Session-Teil von »$uri« ab und weist den resultierenden String dem Anfrage-URI zu. Die Funktion »subprocess_env« initialisiert die »SESSION«-Umgebungsvariable mit der fortgeführten Session.
Der erfahrene Perl-Programmierer wird sich hier vielleicht fragen, wozu »subprocess_env« dient, greift er doch normalerweise in Perl über »%ENV« auf Umgebungsvariablen zu. Das Usertracking-Modul läuft eingebettet im Webserver, muss also mit den Apache-Strukturen arbeiten. Der Apache benutzt aber aus verschiedenen Gründen nicht den üblichen Mechanismus zur Manipulation der Umgebungsvariablen, sondern eigene Tabellen.
|
Neues API mit RC5 |
|---|
|
Als dieser Artikel entstand, war »mod_perl-2.0RC3« aktuell. Die im Text vorgestellten Beispiele funktionieren mit allen Release-Kandidaten bis einschließlich RC4. Am 11.4. wurde jedoch RC5 veröffentlicht, das einige Interface-Änderungen bringt. Obwohl sich das Apache-API der Version 2.x wesentlich von Version 1.x unterscheidet, entschieden sich die Mod_perl-Entwickler dafür, keinen neuen Namensraum »Apache2::« einzuführen, sondern auch Mod_perl-2-Module im Namespace »Apache::« anzusiedeln. Der Grund ist, dass im »Apache::«-Namensraum schon sehr viele CPAN-Module existieren, die sich mit nur wenigen Handgriffen an das Mod_perl-2-Interface anpassen lassen. Mittlerweile hat sich diese Entscheidung aber doch als Fehler erwiesen. Deshalb führt RC5 für die zu Mod_perl 2 gehörenden Module nun den Namensraum »Apache2::« ein. Glücklicherweise sind es nur Namensänderungen, die Beispiele sollten also mit kleineren Änderungen lauffähig sein. Nähere Informationen zu den Änderungen in RC5 finden sich unter dieser Adresse: [http://perl.apache.org/docs/2.0/rename.html] |
Links filtern
Den Else-Zweig des Translation-Handler (Listing 1, Zeile 24) durchläuft das Modul, wenn es eine neue Session erzeugen muss. Die Anforderungen an die Session-ID sind knapp: Sie muss eindeutig sein. Am einfachsten ist es also, einen existierenden eindeutigen String einzusetzen. Der erste Handler benutzt die von Mod_unique_id in der »PostReadRequest«-Phase erzeugte Umgebungsvariable »UNIQUE_ID«. Wenn der Translation-Handler nun arbeitet, fehlt noch ein passender Filter, der die Links in bestehenden Seiten anpasst. Für HTTP-Links gibt es mehrere Ausdrucksmöglichkeiten: »<a href=…>«, dann »<script src=…>« oder »<img src=…>«. Die letzten beiden Fälle bleiben unberücksichtigt, da sie keine eigenständigen Seiten darstellen.
Zusätzlich zu den erwähnten Link-Möglichkeiten, existieren noch »<area href=…>«, »<frame src=…>« und »<iframe src=…>«. Diese Elemente muss der Filter bearbeiten. Weiterhin gibt es noch zwei HTTP-Header, die zu Folgeseiten führen: »Location« und »Refresh«. Der Refresh-Header lässt sich auch in der Form »<meta http-equiv= “refresh” …>« im Kopfbereich der HTML-Seite übergeben.
Der Einfachheit halber unterstützt die erste Handler-Variante nur den Location-Header sowie die Tags »<area …>« und »<a …>« . Wenn das klappt, ist der Rest nur Fleißarbeit. Da das Modul jede Antwort getrennt bearbeitet, ist dieser Output-Filter anfrageorientiert und nicht etwa verbindungsorientiert. Listing 4 zeigt den relevanten Ausschnitt aus der »httpd.conf«, Listing 3 die Filterimplementation, etwas gekürzt. Das vollständige Listing ist auf dem Server des Linux-Magazins zu finden[4].
Der Code ist bereits reichlich kompliziert. So hat sich der Translation-Handler gegenüber der ersten Version aus Listing 1 etwas geändert. Die zusätzliche Zeile 6 in Listing 3 mit »unless()« benötigt er, um mit internen Redirects und Subrequests umzugehen.
|
Listing 1: Erster |
|---|
01 package Apache::ClickPath;
02
03 use 5.008;
04 use strict;
05
06 use Apache::RequestRec ();
07 use Apache::RequestUtil ();
08 use Apache::RequestIO ();
09 use Apache::Const -compile => qw(DECLINED);
10
11 our $VERSION='0.01';
12 our $tag='-';
13
14 sub handler {
15 my $r=shift; # das Anfrage-Objekt
16
17 my $uri=$r->uri;
18
19 if( $uri=~s!^/+ Q$tagE ( [^/]+ ) /!/!x ) {
20 my $session=$1;
21
22 $r->uri( $uri );
23 $r->subprocess_env( SESSION=>$session );
24 } else {
25 my $session=$r->subprocess_env('UNIQUE_ID');
26 $r->subprocess_env( SESSION=>$session );
27 }
28
29 return Apache::DECLINED
30 }
31
32 1;
|
|
Listing 2: |
|---|
01 PerlSwitches -I Perl-Library-Pfad
02
03 <Perl>
04 package Test;
05
06 use strict;
07 use Apache::Const -compile => qw(OK);
08
09 sub handler {
10 my $r=shift; # das Anfrage-Objekt
11
12 $r->content_type( 'text/plain' );
13 $r->print( "SESSION=".$r->subprocess_env( 'SESSION' )."n" );
14
15 return Apache::OK;
16 }
17
18 </Perl>
19
20 PerlModule Apache::ClickPath
21 PerlTransHandler Apache::ClickPath
22
23 <Location /Test>
24 SetHandler modperl
25 PerlResponseHandler Test
26 </Location>
|
|
Listing 3: |
|---|
001 sub handler {
002 my $r=shift; # das Anfrage-Objekt
003
004 my $uri=$r->uri;
005
006 unless( $r->is_initial_req ) {
007 # sub-request oder internal redirect
008 my $pr = $r->main # sub-request
009 || $r->prev; # internal redirect
010
011 $r->subprocess_env( SESSION=>$pr->subprocess_env('SESSION') );
012 $r->pnotes( newsession=>$pr->pnotes( 'newsession' ) );
013 return Apache::DECLINED
014 }
015 ...
016 }
017
018 sub OutputFilter {
019 my $f=shift; # das Filter-Objekt
020 my $sess;
021 my $host;
022 my $context;
023 my ($re, $re1, $the_request);
024
025 unless ($f->ctx) { # Initialisierung
026 my $r=$f->r; # Das Anfrage-Objekt
027
028 $sess='/'.$tag.$r->subprocess_env('SESSION');
029 $host=$r->headers_in->{Host};
030
031 if( $r->pnotes( 'newsession' ) ) {
032 $the_request=$r->the_request;
033 $the_request=~s/^s*w+s+//;
034 $the_request=~s![^/]*[s?].*$!!;
035
036 my $re=qr,^(https?://Q$hostE)?(?!w+:)(.),i;
037 $r->headers_out->{Location}=~s!$re!$2 eq '/'
038 ? $1.$sess.$2
039 : $1.$sess.$the_request.$2
040 !e
041 if( exists $r->headers_out->{Location} );
042 } else {
043 $the_request="";
044
045 my $re=qr!^(https?://Q$hostE)?/!i;
046 $r->headers_out->{Location}=~s!$re!$1$sess/!
047 if( exists $r->headers_out->{Location} );
048 }
049
050 # Nur Dokumente des Typs text/html dürfen gepatcht werden
051 #
052 unless( $r->content_type =~ m!text/html!i ) {
053 $f->remove;
054 return Apache::DECLINED;
055 }
056
057 if( $r->pnotes( 'newsession' ) ) {
058 ...
059 }
060
061 # store the configuration
062 $f->ctx( +{
063 extra => '',
064 sess => $sess,
065 req => $the_request,
066 re => qr/(<[^>]*)$/,
067 re1 => $re1,
068 } );
069
070 # Der Output-Filter aendert die Laenge der Antwort. Daher muss der
071 # vorher berechnete Content-Length Header geloescht werden. Apache
072 # benutzt dann Transfer-Encoding: chunked
073 $r->headers_out->unset('Content-Length');
074 }
075
076 # Nach der Initialisierung beginnt der eigentliche Filter
077 # hole den Filter Kontext
078 $context=$f->ctx;
079
080 $sess=$context->{sess};
081 $re1=$context->{re1};
082 $re=$context->{re};
083 $the_request=$context->{req};
084
085 # jetzt wird der Datenstrom bearbeitet
086 while( $f->read(my $buffer, 1024) ) {
087
088 # Beim letzten Aufruf koennte ein halbes HTML-Tag im Buffer
089 # uebrig geblieben sein. Das wird hier vor den neuen Datenblock
090 # gehaengt.
091 $buffer=$context->{extra}.$buffer if( length $context->{extra} );
092
093 # Wenn der aktuelle Datenblock mit einem halben HTML Tag endet,
094 # wird es abgeschnitten und im Filterkontext zwischengespeichert,
095 # bis der naechste Datenblock kommt.
096 if (($context->{extra}) = $buffer =~ m/$re/) {
097 $buffer=substr( $buffer, 0, -length($context->{extra}) );
098 }
099
100 if( length $the_request ) {
101 $buffer=~s!$re1!(substr($3, 0, 1) eq '/')
102 ? $1.$sess.$3
103 : $1.$sess.$the_request.$3
104 !ge;
105 } else {
106 $buffer=~s!$re1!$1$sess$3!g;
107 }
108
109 $f->print( $buffer );
110 }
111
112 if( $f->seen_eos ) {
113 # das war's. Wir haben keine Daten mehr zu verarbeiten.
114
115 # Hier muss keine Ersetzung durchgeführt werden, da
116 # $context->{extra} für richtige HTML Dokumente leer sein muss.
117 #
118 $f->print( $context->{extra} ) if( length $context->{extra} );
119 }
120
121 return Apache::OK;
122 }
123
124 1;
|
Mit »$r->pnotes()« übergibt der Perl-Programmierer Daten zwischen einzelnen Phasen der Bearbeitung. Hier wird die Funktion eingesetzt, um dem Filter mitzuteilen, ob es sich um eine neue Session handelt. Ist das nicht der Fall, muss das Modul relative Links nämlich nicht anpassen.
Die »OutputFilter«-Funktion implementiert den anfrageorientierten Output-Filter. Filter werden im Gegensatz zu Handlern des Anfragezyklus mit dem Filterobjekt als Parameter aufgerufen. Das Anfrageobjekt ist jedoch über das Filterobjekt abfragbar.
Daten im Filterkontext
Filter werden pro Anfrage im allgemeinen mehrmals aufgerufen. Apache liefert ein Dokument in der Response-Phase blockweise aus und ruft den Filter für jeden Block auf. Das Filterobjekt besitzt allerdings einen Kontext, der Daten zwischen den einzelnen Aufrufen erhält.
Beim ersten Aufruf des Filters ist der Kontext »$f->ctx« leer. Zu diesem Zeitpunkt hat der Client noch keine Daten empfangen. Insbesondere lassen sich die HTTP-Headerfelder der Antwort jetzt noch ändern.
Weil der hier implementierte Filter die Länge der Antwort vergrößert, ist es nötig, den Content-Length-Header zu löschen, da »HTTP/1.1«-Clients mit einer Keep-alive-Verbindung sonst verwirrt wären. Es wäre auch möglich, den Header einfach auf den richtigen Wert zu setzen. Doch der ist erst bekannt, wenn alle Blocks bearbeitet sind. Ohne Content-Length-Header sendet Apache den Header »Transfer-Encoding: chunked« und überträgt das Dokument blockweise, wobei jeder Block mit seiner Größe eingeleitet wird. E
|
Listing 4: Filter in der |
|---|
01 PerlSwitches -I Perl-Library-Pfad
02 PerlModule Apache::ClickPath
03 PerlTransHandler Apache::ClickPath
04 PerlOutputFilterHandler Apache::ClickPath::OutputFilter
05 LogFormat "%h %l %u %t "%m %U%q %H" %>s %b "%{Referer}i" "%{User-agent}i" "%{SESSION}e""
|

Abbildung 1: Die Indexseite von Self-HTML mit angepassten Links, die nun Session-Informationen enthalten.
Der erste Filteraufruf dient dazu, einen möglicherweise vorhandenen Location-Header zu ändern. Die HTTP-Header der Antwort sind über die Funktion »$r->headers_out« zugänglich (Zeilen 37 und 46). Anhand des Content-Typs der Antwort entscheidet das Modul während der Filter-Initialisierung, ob es den Inhalt der Antwort überhaupt bearbeiten darf. Ist das Dokument nicht vom Typ »text/html«, entkoppelt »$f->remove« in Zeile 53 den Filter von der aktuellen Anfrage, damit er für sie nicht noch einmal aufgerufen wird.
Ab Zeile 86 beginnt der eigentliche Filter. In der »while«-Schleife liest er Datenblöcke, verändert sie und schreibt sie wieder neu. Da es möglich ist, dass ein Block mitten in einem HTML-Tag endet, dient ein Zwischenspeicher im Filter-Kontext dazu, das angefangene Tag zu speichern. Es wird dem nächsten Block am Anfang vorangesetzt.
Die Funktion »$f->seen_eos« (Zeile 112) prüft, ob es sich um den letzten Filteraufruf der aktuellen Anfrage handelt. Sie dient hier dazu, übrig gebliebenen Inhalt des Zwischenspeichers zu schreiben.
Test mit Self-HTML
Um das Usertracking-Modul auszuprobieren, kopiert man einen existierenden HTML-Seitenbaum ins Document-Root des Servers, etwa Self-HTML[5]. Nach dem Aufruf der ersten Seite »http:// localhost/selfhtml/index.htm« erscheinen die Links in der Form »http://localhost/-XJubW8CoAAQAACzBAGEAAAAA /selfhtml/grafik/index.htm«. Für das Auswerten des Usertracking muss das Session-Handle im Logfile landen. Das erledigt der »%{SESSION}e«-Teil in der »LogFormat«-Anweisung der »httpd .conf«. Abbildung 2 zeigt den Ausschnitt eines Logfile.
Das Usertracking-Modul ist damit in den Grundzügen fertig, besitzt aber durchaus noch Erweiterungspotenzial. Führt eine Website Dokumente, deren Name mit einem Minus beginnt, lässt es sich jedoch überhaupt nicht einsetzen. Indiziert eine Suchmaschine die Website, landet womöglich ein Link mit einem Session-Handle in den Suchergebnissen. Damit wäre das Usertracking dahin: Jeder, der über diesen Link zur Website findet, setzt die Session fort.
Weitere Einstellungen
Zusätzlich könnte man einige Session-Informationen CGI-Programmen verfügbar machen. Das Format des Session-Handler ließe sich ändern, damit Lastverteiler alle Zugriffe zu einer Session auf demselben Rechner halten können. In der bisherigen Version funktioniert das Modul nicht besonders gut mit Server Side Includes und ähnlichen Filtern. Als kleines Schmankerl wäre es möglich, für die Session einen eigenen Buchstaben in der »LogFormat«-Anweisung zu reservieren. Viele dieser Features sind im fertigen CPAN-Modul[6] implementiert, das zum Produktionseinsatz taugt, aber auch als Fundgrube für eigene Programmierversuche. (ofr)
|
Infos |
|---|
|
[1] Torsten Förtsch, “Eingriff ins Getriebe, Apache-Interna mit Mod_perl programmieren”: Linux-Magazin 05/05, S. 100 [2] Mod_perl: [http://perl.apache.org] [3] Sevenval-Patent: [http://depatisnet.dpma.de], EP1081612 und DE69901832 [4] Listings zum Artikel: [https://www.linux-magazin.de/Service/Listings/2005/06/Apache/] [5] Self-HTML: [http://aktuell.de.selfhtml.org/extras/download.shtml] [6] Apache::ClickPath: [http://search.cpan.org/~opi/] |
|
Der Autor |
|---|
|
Dipl.-Inf. Torsten Förtsch arbeitet als Freiberufler für verschiedene Websites. Seine ersten Programmiererfahrungen sammelte er vor vielen Jahren in Nowosibirsk und Dubna. Hin und wieder liefert er Patches zu Open-Source-Projekten wie Perl, Mod_perl, Curl, SSH und zum Linux Kernel. Seine Website ist: [http://foertsch.name] |






