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