Registrieren Websurfer ihre Email-Adressen, müssen Web-Applikationen oft deren Richtigkeit überprüfen. Ein CGI-Skript lädt heute zum Registrieren ein, schickt Emails an die eingetragene Adresse und stellt so sicher, dass das Konto auch tatsächlich dem Web-Nutzer gehört.
Email-Adressen folgen gewissen syntaktischen Anforderungen. Zum Beispiel ist immer ein @-Zeichen drin. Das zu Überprüfen, genügt aber bei weitem nicht, um sicher zu stellen, dass es sich um ein real existierendes Email-Konto handelt, geschweige denn dass die angegebene Adresse auch tatsächlich dem gerade auf der Seite herumbrowsenden Web-Nutzer gehört.
Um sicherzustellen, dass der Websurfer auch tatsächlich seine Email richtig in das zur Verfügung gestellte Formular eingetragen hat, und nicht etwa inkognito reist oder gar einen üblen Spass mit der Email-Adresse seines Lieblingsfeindes treibt, hilft nur eines: Die Web-Applikation muss einen schwer zu erratenden Code an die angegebene Email-Adresse schicken, und deren Besitzer dazu bewegen, dieses Geheimnis wieder zurück zur Web-Applikation zu schicken -- zum Beispiel per Formularfeld auf einer weiteren Webseite.
Das heute vorgestellte CGI-Skript emailreg
zeigt zunächst ein Formular
an, in das der Benutzer seine Email einträgt
(Abbildung 1). Durch einen Mausklick auf den Submit-Button erhält der
Webserver die Eingabe und führt rudimentäre Tests durch. Er prüft,
ob tatsächlich irgendetwas eingetragen wurde und ob die Eingabe ein '@'
enthält. Im Fehlerfall verzweigt er wieder zurück zum Eingabeformular,
das einen entsprechenden Fehlertext anzeigt (Abbildung 2).
Abbildung 1: Email-Adresse im Formular registrieren |
Abbildung 2: Ungültig! |
Genügt die eingetragene Email den etwas schludrig gestalteten Anforderungen, generiert der Server einen alphanumerischen Zufallscode und schickt ihn an die angegebene Email-Adresse. Der Browser stellt indes eine weitere Seite dar, die ein Formular mit vorausgefüllter Email-Addresse enthält und außerdem in einem weiteren Feld den geheimen Code erwartet (Abbildung 3). Den weiss freilich nur der rechtmäßige Besitzer des Email-Kontos, auf dem die Post mit der geheimen Nachricht ankommt (Abbildung 4).
Abbildung 3: Warten auf Bestätigung |
Abbildung 4: You've got mail! |
Bei falsch eingetragenem Bestätigungs-Code verzweigt die Registrierungs-Seite wieder zurück zur Bestätigungsseite, die dann eine passende Fehlermeldung anzeigt. Stimmt die Eingabe aber mit dem übermittelten Code überein, übernimmt der Webserver die Email als gültig in seine Datenbank und zeigt eine ``Danke!''-Seite an (Abbildung 5).
Abbildung 5: Registrierung erfolgreich. Email-Adresse bestätigt. |
Derartig simple Web-Transaktionen lassen sich schnell zusammenschustern. Traditionell gibt es hierzu zwei Möglichkeiten:
[a] ist nur für 08/15-Projekte geeignet -- für alle anderen stellt sich heraus, dass Perl-Hacker besser mit Code umgehen als mit anspruchsvollem Seiten-Layout. [b] umgeht dieses Problem geschickt dadurch, dass HTML-Editoren den eingebetteten Perl-Code einfach unterdrücken, bzw. große Teile einfach in Bibliotheksdateien liegen. Im rauhen Projektalltag und mit konstant eintrudelnden Verbesserungs- und Änderungswünschen seitens des Kunden landet jedoch, wenn man nicht genau aufpasst, manchmal mehr Perl-Code in den HTML-Seiten als für ein sauber entworfenes und leicht wartbares System zuträglich wäre.
Behandeln wir das Problem mal systematisch: Wenn man sich's genau überlegt, ist eigentlich eine Webapplikation nichts anderes als ein finiter Automat aus der Informatikvorlesung: Es gibt eine endliche Anzahl von Zuständen (Email eingeben, Bestätigungs-Code eingeben, Danke-Seite anzeigen) und eine Reihe von Übergangsbedingungen (z.B. zurück zum Formular mit Fehlermeldung, falls die Email falsch ist), um zwischen den Zuständen hin- und herzuwandern. Abbildung 6 zeigt das Ablaufdiagramm der Email-Überprüfung.
Abbildung 6: Ablaufdiagramm |
Diesen Automaten implementiert das vom CPAN erhältliche Modul
CGI::Application
von Jesse Erlbaum. Der CGI-Programmierer
teilt nur mit, welche Zustände seine Applikation anspringt und
welche Parameter die Übergänge einleiten. Im Zusammenspiel mit
dem Modul HTML::Template
von Sam Tregar entsteht daraus eine
flexible Applikationsplattform.
Listing emailreg
zeigt das ganze CGI-Skript:
Zeile 9 lässt den Browser detaillierte Fehlermeldungen anzeigen, falls
das Skript auf irgendwelche Probleme läuft --
immer eine gute Idee bei der CGI-Entwicklung. Unter Produktionsbedingungen
sollte die Zeile freilich verschwinden.
01 #!/usr/bin/perl 02 ########################################### 03 # emailreg - CGI to register/confirm emails 04 # Mike Schilli, 2002 (m@perlmeister.com) 05 ########################################### 06 use warnings; 07 use strict; 08 09 use CGI::Carp qw(fatalsToBrowser); 10 use EmailReg; 11 my $emailreg = EmailReg->new( 12 TMPL_PATH => "/data/templates/reg"); 13 $emailreg->run();
Zeile 10 zieht das nachfolgend besprochene Modul EmailReg
herein,
das die ganze Ablauflogik enthält. Zeile 11 definiert eine
Instanz des finiten Automaten und teilt ihm mit, in welchem
Verzeichnis die HTML-Templates
liegen, die das graphische Layout der Zustände definieren.
Und Zeile 13 wirft schließlich den Automaten an, der seinen
aktuellen Zustand über CGI-Variablen steuert. Das war's schon!
Die Templates enthalten ganz normales HTML, angereichert durch eine Handvoll simpler Macros, die der Template-Motor durch entsprechenden Text ersetzt. Die Betonung liegt auf simpel: Außer trivialer Variableninterpolation im Format
<tmpl_var variable_name>
bietet HTML::Template
noch if-else-Logik und Schleifen,
aber keines der gezeigten HTML-Snippets nutzt diese ``fortgeschrittenen''
Funktionen -- lediglich einfache Variablen wie die Email-Addresse des Benutzers
oder der Text einer Fehlermeldung werden ersetzt.
Die Beschränkung auf triviale Textersetzung ist bewußt gewählt: Die Intelligenz des Skripts liegt im finiten Automaten, nicht im HTML-Code der Templates. So ist gewährleistet, dass niemand komplizierte Logik in die Seiten einbaut, die losgelöst von der zentralen Steuerung im Automaten nur schwer verständlich ist, sobald sie eine gewisse Komplexitätsgrenze überschreitet.
signup.tmpl
enthält das HTML-Formular für die Eingabe der Emailadresse
samt Submit-Knopf wie in Abbildung 1 dargestellt.
Liegt in der Variablen err_text
vom Automaten
eine Fehlermeldung vor, wird <tmpl_var err_text>
gegen diese
ausgetauscht und wegen des <FONT>
-Tags fett in Rot dargestellt
(Abbildung 2).
Auch wird das Email-Eingabefeld vorbesetzt, falls die Variable email
gesetzt war.
01 <HTML><HEAD><TITLE>Sign In</TITLE></HEAD> 02 <BODY><P> 03 <FONT color=red><B> 04 <tmpl_var err_text></B></FONT> 05 06 <FORM method=POST> 07 <INPUT TYPE=text NAME=email 08 VALUE="<tmpl_var email>"> 09 <INPUT TYPE=hidden NAME=mode VALUE=verify> 10 <INPUT TYPE=submit VALUE="Sign Up"> 11 <FORM> 12 13 </BODY>
confirm.tmpl
zeigt ein Eingabeformular für den Bestätigungs-Code.
Auch hier stehen die Platzhalter <tmpl_var email>
und
<tmpl_var err_text>
für die vorbelegte Email-Adresse und
eine eventuell gezeigte Fehlermeldung. Als letztes HTML-Snippet
zeigt schließlich confirm.tmpl
nur eine Bestätigung, falls der Code
richtig eingegeben wurde und die Registrierung klappte.
01 <HTML><HEAD><TITLE>Confirm</TITLE></HEAD> 02 <BODY> 03 <P><FONT color=red><B><tmpl_var err_text> 04 </B></FONT> 05 06 <FORM method=POST><TABLE><TR><TD>Email: 07 </TD><TD> 08 <INPUT TYPE=text NAME=email 09 VALUE="<tmpl_var email>"> 10 </TD></TR><TR><TD>Confirmation Code: 11 </TD><TD> 12 <INPUT TYPE=text NAME=code> 13 </TD></TR></TABLE> 14 <INPUT TYPE=hidden NAME=mode 15 VALUE=chk_confirm> 16 <INPUT TYPE=submit VALUE="Confirm"> 17 <FORM> 18 </BODY>
1 <HTML><HEAD><TITLE>Welcome</TITLE></HEAD> 2 <BODY> 3 Welcome <tmpl_var email>! 4 <P> 5 <tmpl_var email>, you are now subscribed. 6 </BODY>
Ans Eingemachte geht's in Listing EmailReg.pm
, einem Modul, das,
wie in der CGI::Application
-Welt üblich, eine von CGI::Application
abgeleitete Klasse EmailReg
enthält.
Die im vorher gezeigten Skript emailreg
aufgerufene run()
-Methode
startet den in EmailReg.pm
definierten Automaten, der, falls er nicht
weiss, in welchem Zustand er steht, einfach die setup()
-Methode aufruft.
setup()
definiert zunächst mittels der mode_param()
-Methode den
Namen des CGI-Parameters, der den Zustand des Automaten zwischen Browser
und Server hin- und herschleift: mode
. Der Methodenaufruf
start_mode("signup")
in Zeile 33
bestimmt, dass der Automat mit der ``signup''-Methode zu starten ist. Diese
ist weiter unten definiert und wird später das Template zur Eingabe
der Email-Addresse in den Browser zaubern.
Die run_modes()
-Methode in Zeile 34 bestimmt die Namen der Zustände, die der
Automat annehmen kann (siehe auch Abbildung 6)
und deren zugehörige Methoden in EmailReg
:
verify
.
signup
. Bei Erfolg wird die
Email abgeschickt und es geht mit confirm
weiter.
thanks
weiter. Ist er falsch, geht's mit einer Fehlermeldung
zurück zu confirm
.
Anschließend bindet der tie
-Befehl in Zeile 42 den globalen Hash
%EMAILS an eine DB_File
-Datei, die wegen der O_RDWR|O_CREAT
-Kombination
aus dem Fcntl
-Modul zum Lesen und Schreiben geöffnet und neu angelegt
wird, falls sie noch nicht existiert. Die Zugriffsrechte der Datei
werden mit 0644 (rw-r--r--) festgelegt, die aus DB_File
exportierte
Variable $DB_HASH
bestimmt,
dass die DBM-Datei im DB_File
-Format einfach den angegebenen Hash
%EMAILS
persistent macht.
DB_File::Lock
ist eine von DB_File
abgeleitete Klasse, die außer den
regulären tie
-Funktionen auch noch die Zugriffe über Locks synchronisiert,
denn schließlich können auf einem Webserver unter hoher Last leicht mal
zwei Instanzen des Skripts gleichzeitig auf die Datenbank einhämmern.
Der 'write'
-Parameter weist DB_File::Lock
an, einen Schreib-Lock
zu holen, also die Datenbank exklusiv zu sperren, während der Hash
gebunden ist.
Zeile 16 legt den Namen der DBM-Datei fest, in denen der Hash %EMAILS
über DB_File
seine Daten ablegt.
Nach setup
kommt wegen Zeile 33
der Startzustand signup
dran, falls der Browser
keinen mode
-Parameter sandte, um einen anderen Zustand anzufordern,
was beim ersten Aufruf des Skripts emailreg
der Fall ist.
signup
(ab Zeile 56) holt lediglich mittels
$self->query()->param('error');
den eventuell auf eine Nummer gesetzten CGI-Parameter error
ab
und ruft die ab Zeile 66 definierte Methode _signup
(Unterstrich, da kein Zustand)
mit dem Wertepaar
error => Fehlernummer
auf und gibt ihr Ergebnis zurück.
_signup
erwartet als Parameter (außer der sowieso mitgelieferten
Referenz auf das EmailReg
-Objekt) optionale
Attributwerte unter den Schlüsseln error
und email
.
Mit load_tmpl("signup.tmpl")
wird dann das HTML-Template geladen und
die param()
-Methoden legen die Werte für die Macro-Ersetzung fest.
Für error
muss (falls ein Wert ungleich 0 vorliegt) erst die entsprechende
Fehlermeldung aus dem ab Zeile 18 definierten Hash %ERRORS
extrahiert
werden -- schließlich soll der CGI-Parameter error
nur Nummern hin- und
herschleifen und nicht vollständige Fehlertexte, die irgendwelche Schlingel
dann auch noch für ihre Zwecke modifizieren könnten.
Die output
-Methode (Zeile 79)
des Template-Objekts gibt den HTML-Text des Templates
einschließlich der mittels param()
ersetzten Variablen zurück. Es ist
wichtig, darauf zu achten, dass der Automat niemals Text über printf
auf
STDOUT ausgibt -- die Ausgabe erfolgt dadurch, dass Zustandsmethoden
Textwerte zurückgeben, die der Automat dann unter Hinzufügung der notwendigen
HTTP-Header an den Browser schickt.
Der ab Zeile 83 definierte verify
-Zustand führt elementare
Syntaxprüfungen mit der eingegebenen Email-Adresse durch und verzweigt im
Fehlerfall zurück zu _signup
und setzt die Fehlernummern entsprechend
auf 1 (keine Email da) oder 2 (kein @ drin). Im zweiten Fall kommt
auch noch der email
-Parameter mit, der _signup()
veranlasst,
die Email gleich wieder mit dem falschen Wert vorzubesetzen, damit der
Benutzer sie gleich verbessern kann, ohne alles wieder von vorne
einzutippen.
Genügt die Email den minimalen Anforderungen, holt Zeile 98 das
MD5
-Modul herein, dessen hexhash
-Methode in Zeile 99 einen String
aus einer Zufallszahl und der gerade laufenden Prozessnummer in einen
MD5-Hash umwandelt und diesen auf 5 Zeichen kürzt -- ein einigermaßen schwer
zu erratender alphanumerischer Zufallsstring.
Zeile 101 setzt ein ``U'' (für Unconfirmed) davor und legt das ganze
im persistenten Hash %EMAILS
unter der eingegebenen Email-Adresse ab.
Zwischen 103 und 109 sendet das Mail::Mailer
-Modul eine Email mit
dem geheimen Code an die Email-Adresse.
Zeile 111 verzweigt daraufhin zur Methode _confirm
(wieder Unterstrich, da Zwischenzustand)
und damit zur Anzeige des Bestätigungsformulars
(Abbildung 3).
Dessen HTML (confirm.html
) setzt mit
<INPUT TYPE=hidden NAME=mode VALUE=chk_confirm>
den Zustand des Automaten auf chk_confirm
, sodass der Server
chk_confirm()
anspringt, falls der Benutzer den Submit-Knopf drückt.
Die Logik ab Zeile 137 prüft dort, ob der Benutzer in der Datenbank
steht, sein Code mit 'U'
beginnt (Unconfirmed) und der Rest mit
dem im HTML-Formular eingegebenen Code (verfügbar unter
$self->query()->param('code')
) übereinstimmt.
Falls ja, setzt Zeile 141 den Hasheintrag unter der Email-Adresse
auf 'C'
(für Confirmed) und Zeile 142
springt in den thanks
-Zustand,
der die Dankesmeldung im Browser anzeigt.
Falls nein, geht's mit einer Fehlermeldung
zurück zu _confirm
.
Am Ende einer Runde, bevor die Daten zurück an den Browser gehen,
springt CGI::Application
jedes Mal zuverlässig die teardown()
-Methode
an.
Ab Zeile 48 wird dort
der Hash %EMAILS
wieder ordnungsgemäß von
der Datenbankdatei abgekoppelt.
001 ########################################### 002 package EmailReg; 003 ########################################### 004 # Register and confirm Emails on the Web 005 # Mike Schilli, 2002 (m@perlmeister.com) 006 ########################################### 007 008 use strict; 009 use warnings; 010 011 use CGI::Application; 012 use DB_File::Lock; 013 use Fcntl qw(:flock O_RDWR O_CREAT); 014 use Mail::Mailer; 015 016 our $DB_FILE = "/tmp/emails.dat"; 017 018 our %ERRORS = ( 019 1 => 'No email address given', 020 2 => 'Not a valid email address', 021 3 => 'Confirmation failed', 022 ); 023 024 our @ISA = qw(CGI::Application); 025 our %EMAILS = (); 026 027 ########################################### 028 sub setup { 029 ########################################### 030 my($self) = @_; 031 032 $self->mode_param("mode"); 033 $self->start_mode("signup"); 034 $self->run_modes( 035 signup => "signup", 036 verify => "verify", 037 confirm => "confirm", 038 chk_confirm => "chk_confirm", 039 thanks => "thanks", 040 ); 041 042 tie %EMAILS, 'DB_File::Lock', $DB_FILE, 043 O_RDWR|O_CREAT, 0644, $DB_HASH, 044 'write' or die $@; 045 } 046 047 ########################################### 048 sub teardown { 049 ########################################### 050 my($self) = @_; 051 052 untie %EMAILS; 053 } 054 055 ########################################### 056 sub signup { 057 ########################################### 058 my($self) = @_; 059 060 my $e = $self->query()->param('error'); 061 062 return $self->_signup(error => $e || 0); 063 } 064 065 ########################################### 066 sub _signup { 067 ########################################### 068 my($self, %opt) = @_; 069 070 my $tmpl = 071 $self->load_tmpl("signup.tmpl"); 072 073 $tmpl->param(err_text => 074 $ERRORS{$opt{error}}) if $opt{error}; 075 076 $tmpl->param(email => $opt{email}) if 077 exists $opt{email}; 078 079 return $tmpl->output(); 080 } 081 082 ########################################### 083 sub verify { 084 ########################################### 085 my($self) = @_; 086 087 my $email = 088 $self->query()->param('email'); 089 090 return $self->_signup(error => 1) 091 unless $email; 092 093 if($email !~ /@/) { 094 return $self->_signup(email => $email, 095 error => 2); 096 } 097 098 require MD5; 099 my $code = substr(MD5->hexhash( 100 rand().$$), 0, 5); 101 $EMAILS{$email} = "U$code"; 102 103 my $mail = Mail::Mailer->new("sendmail"); 104 $mail->open( 105 {From => 'email@service.org', 106 To => $email, 107 Subject => 'Confirm'}); 108 print $mail "Confirmation code: $code\n"; 109 $mail->close; 110 111 return $self->_confirm(email => $email); 112 } 113 114 ########################################### 115 sub _confirm { 116 ########################################### 117 my($self, %opt) = @_; 118 119 my $tmpl = 120 $self->load_tmpl("confirm.tmpl"); 121 $tmpl->param(err_text => 122 $ERRORS{$opt{error}}) if $opt{error}; 123 $tmpl->param(email => $opt{email}) 124 if exists $opt{email}; 125 126 return $tmpl->output(); 127 } 128 129 ########################################### 130 sub chk_confirm { 131 ########################################### 132 my($self) = shift; 133 134 my $email=$self->query()->param('email'); 135 my $code = $self->query()->param('code'); 136 137 if(exists $EMAILS{$email} and 138 $EMAILS{$email} =~ /(.)(.*)/ and 139 $1 eq "U" and 140 $2 eq $code) { 141 $EMAILS{$email} = "C"; 142 return $self->thanks(email => $email); 143 } else { 144 return $self->_confirm(error => 3, 145 email => $email); 146 } 147 } 148 149 ########################################### 150 sub thanks { 151 ########################################### 152 my($self, %opt) = @_; 153 154 my $template = 155 $self->load_tmpl("thanks.tmpl"); 156 $template->param(email => $opt{email}); 157 return $template->output(); 158 } 159 160 1;
Um die bestätigten Emails aus der Datenbank zu ernten, genügt ein einfaches
Skript wie dumphash
, das den Hash bindet (auch wieder mit Lock,
aber diesmal nur lesend mit 'read'
, durch die Einträge iteriert
und nach Emails sucht, deren Code aus 'C'
besteht. Fertig!
01 #!/usr/bin/perl 02 ########################################### 03 # dumphash -- Print confirmed emails 04 # Mike Schilli, 2002 (m@perlmeister.com) 05 ########################################### 06 use warnings; 07 use strict; 08 09 use DB_File::Lock; 10 use Fcntl qw(:flock O_RDONLY); 11 use EmailReg; 12 13 tie my %DATA, 'DB_File::Lock', 14 $EmailReg::DB_FILE, 15 O_RDONLY, 0644, $DB_HASH, 16 'read' or die "Cannot tie"; 17 18 for (keys %DATA) { 19 print "$_\n" if $DATA{$_} eq "C"; 20 } 21 22 untie %DATA;
Weitere Informationen zu CGI::Application
finden sich in den Manualseiten
(perldoc CGI::Application
) sowie in [2], das nicht nur schön erklärt,
wie man Module für's CPAN schreibt, sondern beschreibt, wie man das beste
aus CGI::Application
mit und ohne Templatesystem herausholt.
Neben CGI::Application
benötigt das Registrierungssystem
die folgenden CPAN-Module: Mail::Mailer
, MD5
, DB_File
,
DB_File::Lock
. Alle installieren sich wie üblich mit der
CPAN-Shell.
Das Skript emailreg
muss ins cgi-bin
-Verzeichnis des Webservers
und das Modul EmailReg.pm
irgendwohin, wo emailreg
es findet --
am einfachsten ins selbe Verzeichnis. Die HTML-Templates kommen das
Verzeichnis, das für sie in Zeile 16 in EmailReg.pm
gesetzt wurde.
Wer einen Webdesigner an der Hand hat, kann die Templates ohne weiteres
verschönern lassen.
Die vom Automaten angelegte
DB_File
-Datei legt Zeile 16 in EmailReg.pm
mit
/tmp/emails.dat
fest. Ein Skript nach Listing dumphash
liest sie aus
und gibt alle bestätigten Emailadressen aus.
Michael Schilliarbeitet 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. |