Grafische Oberflächen mit Gtk2 muss der Entwickler nicht als Spaghetti-Code im Skript definieren. Mit dem Werkzeug Glade entstehen durch Drag-and-Drop Schritt für Schritt selbst ausgefeilte GUIs. Das Ergebnis liegt anschließend als XML-Beschreibung vor und wird vom Skript zur Laufzeit eingelesen.
Abbildung 1: Die Schnüffel-GUI im Einsatz |
Wer wissen möchte, welche Rechner auf dem lokalen Netzwerk ihr Unwesen
treiben und die Anzeige auch gerne noch in einer
Gtk2-Oberfläche hätte, dem dürfte das heute vorgestellte Skript
capture.pl
gefallen. Es schnüffelt die vorbeisausenden Pakete
mittels des CPAN-Moduls Net::Pcap
von der Leitung oder
aus dem drahtlosen Netzwerk, dekodiert sie, stellt fest, welche
von einer Adresse auf dem lokalen Subnetz stammen und stellt die gefundenen
IP-Adressen in einem TextView-Widget dar (Abbildung 1).
Neu gefundene Adressen platziert
es oben und baut so dynamisch die Anzeige auf. Im File
-Dropdown des
Menübalkens findet sich ein Reset
-Eintrag zur Löschung der bisher
gefundenen IPs und ein Quit
-Eintrag zum Verlassen des Programms.
Dabei definiert das Skript die GUI gar nicht auf programmatischem Weg,
sondern liest zur Laufzeit eine vorher mit dem Tool glade-2
([4])
erstellte
XML-Beschreibung ein. Anschließend stellt es
die Oberfläche dar und
verarbeitet hereinkommende Events. Dieser Ansatz unterscheidet sich
vom typischen Anwendungsfall grafischer GUI-Tools: Diese lassen
den Entwickler zwar auch mittels Drag-and-Drop Widgets platzieren
und Events definieren,
wandeln das Ergebnis aber in Code um, der dann vom Entwickler noch den
letzten Schliff erhält. Nachteil: Einmal von Hand nachbearbeiteter Code
ist im allgemeinen nicht mehr maschinell änderbar.
glade-2
beherrscht beide Verfahren, es generiert wahlweise C- (und
auch anderen) Code oder eben eine XML-Beschreibung, die dann das Skript
mit der libglade
-Bibliothek verarbeitet. Gtk2::GladeXML
vom
CPAN ist der zugehörige Perl-Wrapper.
Abbildung 2: Mit Glade konstruieren sich GUIs ganz einfach visuell. |
Abbildung 2 zeigt glade-2
im Einsatz: Links oben ist das Hauptfenster,
mit dem man ein neues Projekt anlegt. Unten links ist der Werkzeugkasten
mit verschiedenen Widgets. In der Mitte steht die fertige Applikation,
rechts oben ein Fenster mit zusätzlichen Optionen für das gerade
ausgewählte Widget. Hier lassen sich der Name, unter dem das Widget
später im Code referenziert wird einstellen und auch sonstige
Widget-Eigenschaften, wie Maße, Editierbarkeit, verarbeitete Signale
und vieles mehr.
Das Widget-Tree-Window rechts unten zeigt hingegen die hierarchische
Ordnung bisher definierter Widgets.
Um eine neue GUI-Beschreibung zu erzeugen, klickt man zunächst auf das Hauptfenster-Icon im Werkzeugkasten (links oben mit blauem Streifen) und erhält sofort ein leeres Applikationsfenster, wie in Abbildung 3 gezeigt. In dieses kommt dann ein Container in Form einer V-Box, damit später der Menübalken oben schwebt und das Textfeld unten. Um neue Widgets in der Applikation zu platzieren, klickt man sie zunächst im Werkzeugkasten an und klickt anschließend noch einmal an die entsprechende Stelle in der aufzubauenden Applikation. Nicht ganz Drag-and-Drop, aber fast.
Abbildung 4 zeigt den Dialog nach dem Auswählen der V-Box (Symbol mit den drei horizontalen Streifen in der Toolbox), die mit zwei Reihen konfiguriert wird. In das obere Feld wird in Abbildung 5 ein Menü platziert, in Abbildung 6 kommt unten ein Scrolled Window hinzu und in Abbildung 7 schließlich kommt darauf noch ein TextView-Widget. Das vorher erzeugte und in Abbildung 7 probehalber ausgeklappte Standard-Menü ist für die Zwecke unserer Mini-Applikation zu komplex. Um das zu korrigieren, reicht ein Klick auf ``Edit Menus'' im ``Properties''-Window (siehe Abbildung 2), um mittels des Dialogs in Abbildung 8 die Einträge drastisch zu reduzieren.
Auch die anderen Widgets lassen sich konfigurieren, so wurde zum
Beispiel Länge und Breite des Hauptfensters der capture
-GUI
im Property-Window manuell auf 300 und 120 gesetzt.
Abbildung 3: Zuerst das Hauptfenster ... |
Abbildung 4: ... darin eine zweireihige V-Box ... |
Abbildung 5: ... ein Menü oben ... |
Abbildung 6: ... ein Scrolled Window unten ... |
Abbildung 7: ... und ein TextView-Widget darin. Das Standard-Menü ist allerdings zu ausführlich ... |
Abbildung 8: ... und deswegen wird es noch vereinfacht. |
Ein Klick auf den ``Save''-Knopf im Hauptfenster von glade-2
sichert
schließlich nach Angabe des Projektnamens capture
zwei Dateien: capture.glade
und capture.pglade
. Die zweite ist
für unsere Zwecke irrelevant, die erste ist die XML-Beschreibung der GUI.
Das Skript capture.pl
liest diese Beschreibung in Zeile 24 beim Aufruf
des Konstruktors zu Gtk2::GladeXML
ein.
Im XML stehen Definitionen der einzelnen Widgets, deren Platzierung
relativ zur GUI, und ihre eingestellten Attribute: So stehen
beispielsweise in der XML-Beschreibung zum TextView-Widget die
Attribute
<property name="editable">False</property> <property name="cursor_visible">False</property>
Diese Werte stammen aus der Konstruktionsphase der GUI mittels glade-2
.
Dort wurde das Widget mittels entsprechender Knöpfe
als nicht-editierbar und mit unsichtbarem Cursor definiert.
Zum gleichen Ergebnis hätten auch folgende Codezeilen geführt:
$text->set_editable(0); $text->set_cursor_visible(0);
Den dynamischen Teil der statisch definierten GUI definiert die
signal_autoconnect_all
-Methode in Zeile 47. Sie verbindet die in
der XML-Beschreibung mit den Widgets assoziierten Signale wie
on_quit1_activate
(File/Quit-Menü gewählt) und
on_reset1_activate
(File/Reset-Menü angeklickt) mit entsprechenden
Perl-Funktionen (Abbildung 8). Die gewählten Namen entsprechen den von
glade-2
automatisch zugeordneten, wer möchte, kann sie während
der GUI-Konstruktion in glade-2
verändern.
Wenn capture.pl
in Zeile 67 dann mit main_loop()
in die Hauptschleife
springt, geht alles seinen vorprogrammierten Gang: Die GUI erscheint
auf dem Bildschirm und der Benutzer darf nach Belieben darauf herumklicken.
Abbildung 9: Die XML-Beschreibung der GUI in capture.glade. |
Das Schnüffeln im Netzwerk beschlagnamt allerdings die CPU, die sich
währenddessen nicht mehr um die Oberfläche kümmern kann. Das stößt dem
Anwender freilich sauer auf, denn niemand schätzt graue Löcher im
Bildschirm. Um gleichzeitig die GUI in Schuss zu halten und mittels
des Perl-Moduls Net::Pcap
das Ethernet abzuschnüffeln, erzeugt
capture.pl
in Zeile 32 mittels fork()
einen Kindprozess. Eine
vorher mit pipe
erzeugte Pipe stellt den Kommunikationskanal zwischen
dem Abkömmling und dem Elternteil. Findet das Kind eine neue IP-Adresse
im Netz, sendet es sie als String über das WRITEHANDLE der Pipe an
den elterlichen GUI-Verwalter, der die Nachricht
am anderen Ende der Röhre über READHANDLE
aufschnappt.
Damit die grafische Oberfläche nicht aktiv auf Ereignisse aus der Pipe warten muss, sondern sich um Benutzereingaben kümmern kann, definiert sie ab Zeile 62 mittels
Glib::IO->add_watch( fileno(READHANDLE), 'in', \&watch_callback);
einen ``Watch'', der immer dann die ab Zeile 70 definierte Callback-Funktion
watch_callback
aufruft, falls Daten auf READHANDLE
Daten
eintrudeln. Gtk2
baut auf der Glib
auf und kann deswegen deren
Low-Level-Dienste in Anspruch nehmen. Da add_watch()
einen File-Deskriptor
und kein File-Handle erwartet, wandelt die Perl-Funktion fileno
das Handle READHANDLE
entsprechend um.
Mit Net::Pcap
vom CPAN
steht in Perl eine Schnittstelle zur libpcap
-Bibliothek
zur Verfügung. Letztere schnupft Pakete vom Netzwerk, analysiert sie und
filtert sie performant nach voreingestellten Bedingungen. Auch Programmpakete
wie Ethereal basieren darauf.
Die Funktion snooper()
ab Zeile 90 sucht zunächst mit
Net::Pcap::lookupdev
das erste aktive Netzwerkinterface des aktuellen Hosts
heraus. Das ist üblicherweise "eth0"
, und das darauffolgende
Net::Pcap::lookupnet
findet die zugehörige Netzwerkadresse und -maske.
Net::Pcap::open_live()
ab Zeile 102 öffnet ein Live-Capture und schnappt
sich bis zu 1024 Bytes pro Paket zur Analyse. Weil der dritte Parameter
auf 1 steht, schaltet es die Netzwerkkarte in den promiscuous mode,
veranlasst sie also dazu, nicht nur für sie bestimmte Pakete zu
erforschen, sondern neugierig alle vorbeiflitzenden zu begaffen.
-1
als vierter Parameter bestimmt, dass kein Timeout in
Millisekunden vorgegeben ist.
Net::Pcap::loop
ab Zeile 105 springt in eine Schleife, die jedesmal,
wenn sie ein Paket findet, die eingestellte Callback-Funktion
snooper_callback
anspringt. Der zweite Parameter
bestimmt mit -1
, dass der Reigen endlos weitergeht und nicht nach
einer voreingestellten Anzahl von Paketen Feierabend ist.
Der letzte Parameter im Net::Pcap::loop
-Aufruf bestimmt eine Referenz
auf einen Array mit nützlichen Daten, die snooper_callback
bei
jedem Aufruf als
ersten Parameter eingetrichtert bekommt. Mit
[$fd, $addr, $netmask]
stehen dort drei Werte: Ein File-Deskriptor $fd
,
um durch die Pipe an den Elternprozess zu schreiben, sowie die vorher
ermittelte Netzwerkadresse und -maske.
Net::Pcap::loop
sorgt dafür, dass nicht nur diese Daten, sondern
auch die Header- und Content-Information des aktuell aufgeschnappten
Pakets an snooper_callback
weitergeleitet werden.
Dort extrahiert C<NetPacket::Ethernet::strip> die Ethernet-Information aus dem Paket, C<NetPacket::IP-E<gt>decode()> nimmt sich den IP-Layer vor und gibt eine Referenz auf einen Hash zurück, der unter dem Schlüssel C<src_ip> die IP-Adresse des Senders enthält. Diesen String im Format "xx.xx.xx.xx" wandelt C<inet_aton()> aus dem C<Socket>-Modul ins Binärformat mit Network-Byteorder um. Die vorher ermittelten Werte für Netzwerkadresse (C<$addr>) und -maske (C<$netmask>) liegen im Binärformat des aktuell benutzten Prozessors (big/little Endian) vor und müssen deswegen noch mittels C<pack 'N', ...> ins maschinenunabhänige Netzwerkformat umgewandelt werden, bevor C<capture.pl> mit
($ip & $mask) eq $network_addr
prüfen kann, ob die IP-Adresse $ip
aus dem Netzwerk $network_addr
stammt. Nur falls obige Bedingung erfüllt ist, wurde das gerade analysierte
Paket irgendwo vom Subnet des aktuellen Hosts abschickt und muss deshalb
berücksichtigt werden.
syswrite
in Zeile 125 sorgt dafür, dass die Nachricht an den
Elternprozess ohne Zwischenpufferung geschickt wird und die gefundene
IP-Adresse als String im Format xx.xx.xx.xx mit einem nachfolgenden
Newline enthält.
Die Nachricht wandert durch die in Zeile 28 definierte Pipe und löst in
der GUI wegen des in Zeile 62 aufgesetzten Watches einen Event aus, der
watch_callback()
aufruft. Dort wird der globale Array @IPS
aufgefrischt,
der alle bislang bekannten IP-Addressen enthält. Neu gefundene IPs sind
noch nicht im Hash %IPS
gespeichert und landen mit unshift
am
Anfang des Arrays @IPS
. Zeile 81 stellt einen Textstring mit allen
bekannten IP-Adressen zusammen, die durch Newlines getrennt sind.
Zeile 83 frischt das TextView-Widget damit auf.
001 #!/usr/bin/perl 002 ########################################### 003 # capture -- Gtk2 GUI observing the network 004 # Mike Schilli, 2004 (m@perlmeister.com) 005 ########################################### 006 use warnings; 007 use strict; 008 009 use Gtk2 -init; 010 use Gtk2::GladeXML; 011 use Glib; 012 use Net::Pcap; 013 use NetPacket::IP; 014 use NetPacket::Ethernet; 015 use Socket; 016 017 our @IPS = (); 018 our %IPS = (); 019 020 die "You need to be root to run this.\n" if 021 $> != 0; 022 023 # Load GUI XML description 024 my $g = Gtk2::GladeXML->new( 025 'capture.glade'); 026 027 # Child/Parent communication pipe 028 pipe READHANDLE,WRITEHANDLE or 029 die "Cannot open pipe"; 030 031 # Fork off a child 032 our $pid = fork(); 033 die "failed to fork" unless defined $pid; 034 035 if($pid == 0) { 036 # Child, never returns 037 snooper(\*WRITEHANDLE); 038 } 039 040 # Parent, init text window 041 my $buf = Gtk2::TextBuffer->new(); 042 $buf->set_text("No activity yet.\n"); 043 044 my $text = $g->get_widget('textview1'); 045 $text->set_buffer($buf); 046 047 $g->signal_autoconnect_all( 048 on_quit1_activate => sub { 049 # Stop snooper 050 kill('KILL', $pid); 051 wait(); 052 Gtk2->main_quit; 053 }, 054 on_reset1_activate => sub { 055 # Reset display 056 @IPS = (); 057 %IPS = (); 058 $buf->set_text(""); 059 } 060 ); 061 062 Glib::IO->add_watch( 063 fileno(READHANDLE), 064 'in', \&watch_callback); 065 066 # Enter main loop 067 Gtk2->main(); 068 069 ########################################### 070 sub watch_callback { 071 ########################################### 072 chomp(my $ip = <READHANDLE>); 073 074 # Register IP if unknown 075 unshift @IPS, $ip 076 unless exists $IPS{$ip}; 077 $IPS{$ip}++; 078 079 my $text = ""; 080 081 $text .= "$_\n" for @IPS; 082 083 $buf->set_text($text); 084 085 # Return true to keep watch 086 1; 087 } 088 089 ########################################### 090 sub snooper { 091 ########################################### 092 my($fd) = @_; 093 094 my($err, $addr, $netmask); 095 my $dev = Net::Pcap::lookupdev(\$err); 096 097 if(Net::Pcap::lookupnet($dev, \$addr, 098 \$netmask, \$err)) { 099 die "lookupnet on $dev failed"; 100 } 101 102 my $object = Net::Pcap::open_live($dev, 103 1024, 1, -1, \$err); 104 105 Net::Pcap::loop($object, -1, 106 \&snooper_callback, 107 [$fd, $addr, $netmask]); 108 } 109 110 ########################################### 111 sub snooper_callback { 112 ########################################### 113 my($user_data, $header, $packet) = @_; 114 115 my($fd, $addr, $netmask) = @$user_data; 116 117 my $edata = 118 NetPacket::Ethernet::strip($packet); 119 120 my $ip = NetPacket::IP->decode($edata); 121 122 if((inet_aton($ip->{src_ip}) & 123 pack('N', $netmask)) eq 124 pack('N', $addr)) { 125 syswrite($fd, "$ip->{src_ip}\n"); 126 } 127 }
Das Skript benötigt wegen der verwendeten Gtk2-Oberfläche einen ganzen
Rattenschwanz an Modulen, hier sind die wichtigsten:
ExtUtils::Depends
,
ExtUtils::PkgConfig
,
Glib
,
Gtk2
,
Gtk2::GladeXML
,
Net::Pcap (fails)
,
NetPacket
. Am besten installiert man sie mit einer CPAN-Shell, manchmal
sind aber manuelle Korrekturen notwendig.
Ist libglade
noch nicht auf dem Rechner, kann man sie von
[3] beziehen. Das Tool glade-2
steht auf [4] zur Verfügung
und kann einfach kompiliert werden.
Bei der Installation von Net::Pcap
ist darauf zu achten, dass die
Testphase (make test
) unter root
laufen muss, auch wenn die
eigentliche Installation gar keine root
-Rechte bräuchte. Tritt trotzdem
ein Fehler auf, hilft ein ``Augen zu und durch'' mit einem ``make install''
im Build-Verzeichnis.
Vor dem Starten von capture.pl
ist sicherzustellen, dass die XML-Beschreibung
der Oberfläche in capture.glade
verfügbar ist. Wer alles gerne unter
einem Dach führt, der kann Zeile 24 in
my $xml = join "\n", <DATA>; my $g = Gtk2::GladeXML-> new_from_buffer($xml);
umändern und den XML-Salat aus capture.glade
einfach
am Ende von capture.pl
in einer DATA
-Sektion anhängen:
# ... Ende von capture.pl __DATA__ <?xml version="1.0" ... <!DOCTYPE glade-interface ...
Wegen des promiscuous mode
, in den die Netzwerkkarte versetzt wird, muss
capture.pl
als root
laufen.
Mit glade-2
lassen sich auch weit kompliziertere Oberflächen erstellen.
Drag-and-Drop und WYSIWYG versüßen die Pfrümelarbeit.
Die plattformunabhängige XML-Repräsentation ist elegant und hält
platzraubene statische Widget-Definitionen vom Code fern, der sich
auf die wesentlichen dynamischen Aspekte konzentrieren kann.
(Bitte sowohl capture.pl als auch die XML-Beschreibung capture.glade dort ablegen.)
libglade
: http://ftp.gnome.org/pub/GNOME/sources/libglade
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. |