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. |