Spammer senden nicht nur Emails, sondern nisten sich auch in Diskussionsforen und Blogs ein, um mit linkreichen Pseudopostings die großen Suchmaschinen hinters Licht zu führen. Ein Perlskript macht sauber.
Mein kleines Diskussionsforum auf usarundbrief.com wird in letzter Zeit immer mehr von Link-Spammern heimgesucht. Diese Parasiten des Internets setzen ihre Bots gezielt auf bekannte Forumsoftware wie phpBB oder Blogapplikationen wie Wordpress an, um sie mit mit Beiträgen zuzumüllen, die fast nichts als Links zu Poker- und Sexseiten enthalten. So versuchen sie, Diskussionsteilnehmer zum Klicken auf die Webseiten ihrer Auftraggeber zu animieren und die großen Suchmaschinen hinters Licht führen, die die Relevanz einer Seite anhand der auf sie zeigenden Links berechnen.
Abbildung 1: Ein Spammer hat sich ins Forum eingenistet |
Der sogenannte ``Comment Spam'' ([2]) lässt sich eindämmen, wenn nur registrierte User posten dürfen. Doch diese Hürde schreckt auch legitime Nutzer wegen Datenschutzbedenken ab. Wird jedes Posting zuerst von einem Moderator geprüft, bevor es auf der Website erscheint, bleiben zwar die Spammer draußen, aber manuelles Prüfen kostet Arbeitszeit und verursacht diskussionslähmende Verzögerungen.
So genannte ``Captchas'' (``Tippen Sie diese Nummer ab'') sollen sicherstellen, dass am anderen Ende der Leitung tatsächlich ein Mensch und kein Computer sitzt. Sie müssen gar nicht einmal so schwer zu knacken sein wie die der großen Registrierungssites. Ein verblüffend einfaches Beispiel zeigt Jeremy Zawodnys Blog, auf dem der User einfach ``Jeremy'' in ein zusätzliches Feld eintragen muss. Die meisten Bots scheitern daran, da sie sich auf den Massenmarkt konzentrieren und nicht auf individuelle Anpassungen eingehen können, die eh nur Prozentbruchteile des Marktes ausmachen. Auch verstehen manche Bots die Interaktionen von JavaScript mit dem DOM des Browsers nur unvollständig, sodass manchmal schon eine triviale lokale Anpassung der Forumsoftware mit etwas verschleierndem JavaScript die Bots zum Aufgeben zwingt.
Die Abbildungen 2 und 3 zeigt eine triviale Erweiterung von phpBB, die einen Schalter einfügt, der den Poster zunächst als Spammer klassifiziert. Ein Spam-Bot wird den Schalter einfach ignorieren oder den eingestellten Defaultwert übernehmen, was ihn anhand des beim Posten übermittelten Wertes des Radiobuttons als Spammer entlarvt. Falls ein menschlicher User den zusätzlichen Button übersieht, teilt ihm eine Fehlermeldung (Abbildung 3) mit, dass auf der vorhergehenden Seite noch ein Knopf umzustellen ist, damit das Posting durchgeht.
Abbildung 2: Ein neuer Schalter legt die Spammer aufs Kreuz. |
Abbildung 3: Die Fehlermeldung, falls der User vergessen hat, von "Spammer" auf "User" umzustellen. |
Eine weitere Möglichkeit ist das Erkennen von Comment-Spam anhand des Nachrichtentextes. Steht dort nur wenig Text, aber 15 Links, handelt es sich mit hoher Wahrscheinlichkeit um einen Spammer. Ein Problem stellen freilich die ``Fehler zweiter Art'' (falsche Positive) dar, also der Fall, dass eine als Spam eingestufte Nachricht tatsächlich von einem legitimen Benutzer stammt. Wandern solche Nachrichten unprüft in die Mülltonne, verärgert dies die User, die dann zu anderen Foren abwandern.
Hält sich der Forumsverkehr in Grenzen, eignet sich die heute vorgestellte
Moderation per Email zur Spam-Eindämmung. Das per Cronjob gestartete
Skript posting-watcher
fragt auf dem Forumsrechner regelmässig die Mysql-Datenbank des
Forumssystems phpBB ab, entdeckt noch nicht gesichtete Einträge in der
phpBB-Tabelle phpbb_posts
und speichert deren IDs mitsamt einem generierten
Zufallsschlüssel in einem lokalen Cache auf der Platte ab. Es schickt
anschließend eine Email nach Abbildung 4 zum Moderator, die
die wichtigsten Daten des Postings enthält.
Abbildung 4: Anhand dieser Email kann der Forumverwalter entscheiden, ob das Posting behalten oder gelöscht wird. |
Abbildung 5: Der Inhalt des File-Cache, der die Zufallsschlüssel und die zugehörigen Post-IDs speichert. |
Mails mit legitimen Postings ignoriert der Moderator einfach. Falls der Moderator ein Posting hingegen als Spam klassifiziert, betätigt er lediglich die ``Reply''-Funktion des Mail-Clients, worauf die Mail zurück zum ursprünglichen Skript geschickt wird. Dieses greift sich den in der Mail gespeicherten Zufallsschlüssel, ermittelt über den File-Cache das zugehörige Posting in der Datenbank, und setzt einen Scraper auf die Website an, um das Posting mittels der Web-Administrations-Schnittstelle der phpBB-Forumssoftware zu löschen.
Anschließend schickt es eine Bestätigung zurück an den Moderator. Abbildung 6 zeigt den gesamten Ablauf. Nach diesem Verfahren werden alle Postings (einschließich Spam) zunächst einmal von phpBB dargestellt, aber später ohne großen Aufwand gelöscht, falls sich ein Posting als Spam entpuppt. Da die Moderation über eine Schnittstelle angesprochen wird, die viele sowieso dauernd nutzen (Email), hält sich der Zusatzaufwand bei geringer Forumsaktivität in Grenzen.
Abbildung 6: Der Content-Spam-Killer kommuniziert per Email mit dem Moderator. |
Sowohl der vom Cronjob regelmäßig aufgerufene Datenbank-Überwacher
als auch der als Auftragskiller agierende Scraper
werden vom gleichen Skript posting-watcher
implementiert. Mit dem Parameter -c
(check) aufgerufen, durchsucht
es die Datenbank nach neuen Postings und schickt für jedes
eine Email an den Moderator. Mit dem Kommandozeilenparameter
-k
(kill) hingegen erwartet das Skript eine Email auf STDIN, die
einen Kill-Schlüssel enthält.
Warum enthält die Email nicht einfach die ID des Postings?
Damit posting-watcher
weiss, dass die Auftragskiller-Email
vom Moderator (und nicht etwa von einem Scherzbold) kommt, legt
es beim Absenden einen schwer zu erratenden Zufallsschlüssel an und
speichert diesen sowohl in einer lokalen Tabelle (Abbildung 5)
zusammen mit der ID
des zu untersuchenden Postings. Kommt die Email vom Moderator zurück,
extrahiert das Skript den Zufallsschlüssel und prüft anhand der Tabelle,
ob dieser tatsächlich einem gerade moderierten Posting zugeordnet ist
und leitet nur in diesem Fall die Löschung ein.
Die Forumssoftware phpBB legt die Forumsdaten verteilt in 30 Mysql-
Tabellen ab. Die Postings finden sich in der
Tabelle phpbb_posts
. Dort
steht allerdings nicht direkt der Wortlaut des
Nachrichtentextes. Vielmehr nutzt phpBB die Spalte post_id
in der
Tabelle phpbb_posts
, um über diese einen Zusammenhang zwischen
phpbb_posts
und einer weiteren Tabelle phpbb_posts_text
herzustellen und so dem Posting seinen Volltext zuzuordnen (Abb. 8).
Abbildung 7: Die Tabellen in der Datenbank der Forumssoftware phpBB |
Abfragen, die zwei Tabellen auf diese Art verknüpfen, lassen sich mit etwas SQL leicht in den Griff bekommen. Allerdings sieht mit Perl gemixtes SQL nicht gerade schön aus und deswegen hat es sich in letzter Zeit immer mehr eingebürgert, objektorientierte Perl-Wrapper auf relationalen Datenbanken aufzusetzen.
Bei diesem Verfahren fragen Objektmethoden die Datenbankdaten
ab führen auch Manipulationen durch. SQL bleibt draußen.
In [5] kam im
Snapshot schon einmal Class::DBI
zum Einsatz, aber kürzlich
kam mit Rose
ein neues aufregendes Framework heraus, das
nicht nur flexibler sondern sogar performanter arbeitet.
Das vom CPAN erhältliche Framework spricht Datenbanken über die
Abstraktion Rose::DB
an und greift mit
Rose::DB::Object
auf Tabellenzeilen zu.
Dabei braucht man, wie Listing PhpbbDB.pm
zeigt, keineswegs die einzelnen
Tabellenspalten manuell durchzugehen, um zur Klassendarstellung zu gelangen. Mit
auto_initialize()
durchforstet Rose
Tabellen gängiger Datenbanksysteme (MySQL,
Postgres, ...) selbständig und legt automatisch die notwendige Methoden für
den Abstraktionslayer an.
Normalerweise verlinkt man Tabellen in relationalen Datenbanken
über einen Fremdschlüssel in einer extra Spalte, aber phpBB
implementiert dies recht eigenwillig mit zwei Primärschlüsseln,
die jeweils post_id
heißen (Abb. 8). Das ist schade, denn Rose
verfügt über einen sogenannten Convention-Manager, der
(ähnlich wie in Ruby on Rails)
Tabellenbeziehungen anhand von Konventionen errät. Ist alles nach
Standard angelegt, genügt es, Rose::DB::Object::Loader
aufzurufen,
das alles geradezu magisch erledigt.
Abbildung 8: Die ungewöhnliche Verlinkung zweier Tabellen in Forumsoftware phpBB. |
Findet Rose eine Tabelle namens phpbb_posts (Mehrzahl), legt es eine Klasse
PhpbbPost
(Einzahl) an. Aus Tabellennamen mit Unterstrichen werden
Klassennamen im sogenannten CamelCase. Den Aufruf von
__PACKAGE__->meta->table('phpbb_topics');
kann sich PhpbbDB.pm
so zum Beispiel
sparen, da Rose den Tabellennamen 'phpbb_topics'
automatisch aus dem Klassenamen PhpbbTopic
per Konvention errät.
Fände es einen Fremdschlüssel post_text
,
würde es nach einer weiteren Tabelle post_texts
suchen.
Existiert diese, verlinkt Rose die beiden Klassen PhpbbPost
und
PostText
mit einer ``many to one''-(N:1)-Beziehung. Zu
beachten ist, dass Tabellennamen immer in der Mehrzahl sind
(post_texts
), während Spaltennamen für Fremdschlüssel
in 1:1 und N:1-Beziehungen in der Einzahl (post_text
)
geschrieben werden.
01 package Phpbb::DB; 02 ########################################### 03 use base qw(Rose::DB); 04 __PACKAGE__->use_private_registry(); 05 __PACKAGE__->register_db( 06 driver => 'mysql', 07 database => 'forum_db', 08 host => 'forum.db.host.com', 09 username => 'db_user', 10 password => 'XXXXXX', 11 ); 12 13 ########################################### 14 package Phpbb::DB::Object; 15 ########################################### 16 use base qw(Rose::DB::Object); 17 sub init_db { Phpbb::DB->new(); } 18 19 ########################################### 20 package PhpbbTopic; 21 ########################################### 22 use base "Phpbb::DB::Object"; 23 __PACKAGE__->meta->auto_initialize(); 24 25 ########################################### 26 package PhpbbPostsText; 27 ########################################### 28 use base "Phpbb::DB::Object"; 29 __PACKAGE__->meta->table( 30 'phpbb_posts_text'); 31 __PACKAGE__->meta->auto_initialize(); 32 33 ########################################### 34 package PhpbbPost; 35 ########################################### 36 use base "Phpbb::DB::Object"; 37 38 __PACKAGE__->meta->table('phpbb_posts'); 39 __PACKAGE__->meta->add_relationships( 40 text => { 41 type => "one to one", 42 class => "PhpbbPostsText", 43 column_map => { post_id => 'post_id' }, 44 }, 45 topic => { 46 type => "one to one", 47 class => "PhpbbTopic", 48 column_map => { 49 topic_id => 'topic_id' }, 50 } 51 ); 52 53 __PACKAGE__->meta->auto_initialize(); 54 __PACKAGE__->meta->make_manager_class( 55 'phpbb_posts'); 56 57 1;
Entspricht das Datenbankschema einer Applikation allerdings nicht
diesem Standard, kann man entweder den Convention-Manager anpassen
oder Rose manuell unter die Arme greifen. So legt Listing PhpbbDB.pm
ab Zeile 39 mit add_relationships
die Beziehungen der Tabelle
phpbb_posts
zu den Tabellen phpbb_posts_text
und phpbb_topics
fest. Die Beziehung text
(ab Zeile 40) stellt zum Beispiel eine
``one to one''-(1:1)-Verbindung von der Tabelle phpbb_posts
zur Tabelle
phpbb_posts_text
her. ``One to one'' heisst hier, dass jede Reihe in
phpbb_posts
einer Reihe in phpbb_posts_text
zugeordnet ist und
umgekehrt. Entsprechend gibt es auch die Beziehungstypen "one to
many"
(1:N), "many to one"
(N:1) und "many to many"
(N:N).
Wichtig ist, dass
add_relationships
vor dem Aufruf von
auto_initialize()
erfolgt, sonst ignoriert Rose die Beziehungen
einfach kommentarlos (ein Bugfix ist hoffentlich bald verfügbar).
Mit dem so weitgehend automatisch angelegten Abstraktionswrapper
lässt sich nun zum Beispiel in einem Objekt der Klasse PhpbbPost
mit der Methode post_id()
der Wert der Spalte id
abfragen. Und
wegen der vorher angelegten Beziehung genügt es, die Methodenkette
text()->post_text()
aufzurufen, um von einem Objekt der
Klasse PhpbbPost
zum zugehörigen
Textstring des Postings zu gelangen, der in der Tabelle
phpbb_posts_text
liegt!
Eine ausführliche Beschreibung des objektorientierten
Datenbankwrappers Rose
und praktische Beispiele finden sich in der
CPAN-Distribution des Moduls, das Tutorial ist unter [4] erhältlich.
Um SELECT-ähnliche Sammelabfragen auf eine abstrahierte Tabelle zuzulassen,
wird die Klasse PhpbbPost
in Zeile 54 von PhpbbDB.pm
instruiert, die Klasse PhpbbPost::Manager
zu erzeugen, deren Methode
query()
die Abfrage ausführt. Die Syntax für die Abfrage ist ein
erstaunlich sauberes Konstrukt in reinem Perl (Zeile 73 in
posting-watcher
):
[ post_id => { gt => $latest } ]
entspricht einem ``SELECT ... WHERE post_id > $latest''. Bei einer
erfolgreichen Suche
liefert die Methode get_phpbb_posts()
eine
Referenz auf einen Array zurück, der Treffer-Objekte vom Typ PhpbbPost
enthält. Diese wiederum geben mittels den Methoden post_id()
,
text()->post_text()
und topic()->topic_title()
wegen der vorher
definierten Tabellenbeziehungen die ID des Postings und dessen Titel und
Text als Strings zurück. Das Skript posting-watcher
nutzt sie, um
die Email an den Moderator mit Inhalt zu füllen.
Den persistenten Cache, der die Posting-IDs den voher erwähnten
Zufallsschlüsseln für die Emails zuordnet, implementiert das CPAN-Modul
Cache::FileCache
. Wie in Zeile 21 festgelegt, verfallen Einträge nach
14 Tagen und werden als akzeptiert gehandelt. Die Methode purge()
(Zeile 28) räumt verfallene Cache-Einträge auf.
Die ID der zuletzt moderierten Nachricht speichert Zeile 92 einfach
unter dem Schlüssel _latest
im Cache, der eigentlich Post-IDs den
voher erwähnten Zufallsschlüsseln für die Emails zuordnet.
Die Funktion check()
in posting-watcher
holt die Nummer des zuletzt
untersuchten Postings aus dem Cache und setzt eine SQL-Abfrage
ab, die alle Postings mit neueren IDs liefert.
Die 32 Bytes langen Schlüsselstrings im Hexformat erzeugt die
Funktion genkey()
ab Zeile 96. Sie verwendet ein aus dem CPAN-Modul
Apache::Session kopiertes Verfahren, das als Zufallsparameter die
aktuelle Uhrzeit, eine Speicheradresse, eine Zufallszahl und die ID
des laufenden Prozesses zweimal durch ein MD5-Hashing laufen lässt.
So entsteht eine fast 100%ig eindeutiger und sehr schwer zu erratender
Schlüssel. Wer ihn kennt, darf das ihm im Cache zugeordnete Posting
aus dem Forum löschen. Das Skript posting-watcher
schickt den
Schlüssel an den Moderator, und wenn dieser ihn wieder zurück an das
Skript schickt, startet dieses den Agenten, der das Posting durch
simulierte Mausklicks auf der Admin-GUI des Forums aus diesem
verschwinden lässt.
001 #!/usr/bin/perl -w 002 use strict; 003 use PhpbbDB; 004 use Cache::FileCache; 005 use Digest::MD5; 006 use Mail::Mailer; 007 use Mail::Internet; 008 use Text::ASCIITable; 009 use Text::Wrap qw(wrap); 010 use Getopt::Std; 011 use WWW::Mechanize::Pluggable; 012 013 getopts("kc", \my %opts); 014 015 my $FORUM_URL = "http://foo.com/forum"; 016 my $FORUM_USER = "forum_user_id"; 017 my $FORUM_PASS = "XXXXXXXX"; 018 019 my $TO = 'moderator@foo.com'; 020 my $REPL = 'forumcleaner@foo.com'; 021 my $EXPIRE = 14*24*3600; 022 023 my $cache = Cache::FileCache->new({ 024 cache_root => "$ENV{HOME}/phpbb-cache", 025 namespace => "phpbb-watcher", 026 }); 027 028 $cache->purge(); 029 030 if($opts{k}) { 031 my @data = <>; 032 my $body = join '', @data; 033 if($body =~ /\[delete-key (.*?)\]/) { 034 my $id = kill_by_key($1); 035 my $mail = Mail::Internet->new(\@data); 036 if($mail) { 037 my $reply = $mail->reply(); 038 $reply->body( 039 ["Deleted posting $id.\n\n", @data]); 040 $reply->send() or 041 die "Reply mail failed"; 042 } 043 } 044 } elsif($opts{c}) { 045 check(); 046 } else { 047 die "Use -c or -k"; 048 } 049 050 ########################################### 051 sub kill_by_key { 052 ########################################### 053 my($key) = @_; 054 my $id = $cache->get("key$key"); 055 if(defined $id) { 056 msg_remove($id); 057 } else { 058 die "Invalid key $key"; 059 } 060 061 return $id; 062 } 063 064 ########################################### 065 sub check { 066 ########################################### 067 my $latest = $cache->get("_latest"); 068 $latest = -1 unless defined $latest; 069 070 my $new_posts = 071 PhpbbPost::Manager->get_phpbb_posts( 072 query => 073 [ post_id => { gt => $latest } ] 074 ); 075 076 foreach my $p (@$new_posts) { 077 my $id = $p->post_id(); 078 079 my $key = genkey(); 080 081 mail($id, format_post($id, 082 $p->text()->post_text(), 083 $p->topic()->topic_title(), 084 $key), $key 085 ); 086 087 $cache->set("key$key", $id, $EXPIRE); 088 089 $latest = $id; 090 } 091 092 $cache->set("_latest", $latest); 093 } 094 095 ########################################### 096 sub genkey { 097 ########################################### 098 return Digest::MD5::md5_hex( 099 Digest::MD5::md5_hex( 100 time(). {}. rand(). $$)); 101 } 102 103 ########################################### 104 sub mail { 105 ########################################### 106 my($id, $body, $key) = @_; 107 108 my $m = Mail::Mailer->new('sendmail'); 109 110 $m->open({ 111 To => $TO, 112 Subject => "Forum News (#$id) [delete-key $key]", 113 From => $REPL }); 114 115 print $m $body; 116 } 117 118 ########################################### 119 sub format_post { 120 ########################################### 121 my($id, $text, $topic, $key) = @_; 122 123 my $t = Text::ASCIITable->new( 124 {drawRowLine => 1}); 125 126 $t->setCols('Header', 'Content'); 127 $t->setColWidth("Header", 6); 128 129 $Text::Wrap::columns=60; 130 131 $text =~ s/[^[:print:]]/./g; 132 133 $t->addRow('post', "#$id"); 134 $t->addRow('topic', $topic); 135 $t->addRow('text', wrap("", "", $text)); 136 $t->addRow('key', "[delete-key $key]"); 137 138 return $t->draw(); 139 } 140 141 ########################################### 142 sub msg_remove { 143 ########################################### 144 my($post_id) = @_; 145 146 my $mech = 147 WWW::Mechanize::Pluggable->new(); 148 $mech->get($FORUM_URL); 149 150 $mech->phpbb_login($FORUM_USER, 151 $FORUM_PASS); 152 $mech->get( 153 "$FORUM_URL/viewtopic.php?p=$post_id"); 154 $mech->phpbb_post_remove($post_id); 155 }
Damit der Moderator die Nachricht über ein neu eingeganges Posting
schön formatiert erhält, presst die ab Zeile 119 definierte Funktion
format_post
ID, Titel und Text in die in Abbildung 4 gezeigten ASCII-
Kästen. Diese erzeugt das CPAN-Modul Text::ASCIITable
ohne großen
Heckmeck. Da der Text auch Tabs und andere schwer druckbare Sonderzeichen
enthalten kann, filtert sie der reguläre Ausdruck in Zeile 131 aus und
ersetzt sie durch Punkte.
Vor dem Einpressen des Texts wird dieser vom Modul Text::Wrap
auf
eine Zeilenbreite von 60 Zeichen formatiert. In der letzten
ASCII-Tabellenzeile schließlich hängt format_post
den geheimen
Schlüssel an. Außerdem wird er der Subject:
-Zeile der Email
angehängt, so dass der Moderator nur den Reply
-Knopf seines
Mailclients drücken muss, um die Antwort mitsamt dem Schlüssel
im Subject (Diesmal mit "Re: ..."
) an den Spammörder zu schicken.
Abbildung 9: Bestätigung der Löschung. |
Die Funktion msg_remove
ab Zeile 142 startet den Screen-Scraper
WWW::Mechanize
über den von meinem Yahoo-Kollegen Joe McMahon
entworfenen Plugin-
Mechanismus im CPAN-Modul WWW::Mechanize::Pluggable
. Mit diesem
Modul lassen sich einfach Plugin-Module bauen und aufs CPAN stellen,
die WWW::Mechanize
mit praktischen Befehlen erweitern. So fügt zum
Beispiel WWW::Mechanize::Pluggable::Phpbb
die Methoden
phpbb_login
und phpbb_post_remove
in WWW::Mechanize
ein, die
den virtuellen Browserbenutzer als Administrator in ein Phpbb-Forum
einloggen und die nötigen Knöpfe drücken, um ein mit seiner ID identifiziertes
Posting zu tilgen. Anschließend schickt msg_remove
eine Bestätigungs-Email
an den Auftraggeber (Abbildung 9).
Damit die vom Moderator an den Unix-Account des Spammörders
zurückgeschickten Emails
an posting-watcher
weitergeleitet
werden, legt man eine .forward
-Datei im Home-Verzeichnis mit folgendem
Inhalt an:
| /vollständiger/pfad/posting-watcher-kill.sh
Im ausführbaren Shellskript posting-watcher-kill.sh
steht dann die
vollständige Kommandozeile
#!/bin/sh /home/mschilli/bin/posting-watcher -k
Grund für diesen Umstand: Die .forward
-Datei kann keine
Kommandozeilenoptionen aufzurufender Skripts verarbeiten.
Weiter sind die Zeilen 15 bis 21 von posting-watcher
mit gültigen Emails, einer Forum-URL sowie mit dem
Benutzernamen und dem Passwort des Forumsadministrators auf die
individuellen Bedürfnisse anzupassen.
Der Cronjob, der die Forumsdatenbank alle 15 Minuten lang auf
neue Einträge hin untersucht, wird mit crontab -e
und dem
Eintrag
*/15 * * * * /vollständiger/pfad/posting-watcher -c
aufgerufen. Wichtig ist, dass die Moduldatei PhpbbDB.pm
vom
Skript gefunden wird. Falls PhpbbDB.pm
nicht bei den anderen
Perl-Modulen steht, kann man posting-watcher
mit
use lib '/pfad/zum/modulverzeichnis';
auf das Modulverzeichnis hinweisen. Wer möchte, kann das Skript erweitern und es Postings, die bestimmten Kriterien genügen (zum Beispiel mehr als eine bestimmte Anzahl Links oder eindeutige Schlüsselwörter enthalten) auch ohne Rückfrage automatisch zu löschen. Kampf den Spammern!
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. |