Open Source im professionellen Einsatz

Komponentenkleber

Was geht nun hinter den Kulissen in den beiden POE-Komponenten vor? Abbildung 1 zeigt das Diagramm mit den Server- und Client-Komponenten sowie den jeweils genutzten Portnummern. Der im Port-Forwarder auf Port 25 lauschende TCP-Server spannt pro Client jeweils eine TCP-Client-Komponente auf, um sie mit dem Tunnel zu verbinden.

Abbildung 1: Der Mailclient unterhält sich mit dem Port 25 des Forwarders, dessen Client wiederum mit dem Tunnel kommuniziert.

Abbildung 1: Der Mailclient unterhält sich mit dem Port 25 des Forwarders, dessen Client wiederum mit dem Tunnel kommuniziert.

Als Parameter erwartet die Klasse den Port »port_from« (auf dem der Server lauscht), »port_to« (den Tunnelport) sowie zwei Callback-Routinen. Die Komponente springt die unter »port_bound« abgelegte Subroutinen-Referenz an, sobald der Server sich mit Port 25 verbunden hat und damit keine Root-Rechte mehr benötigt.

Beim Aufgeben der Root-Rechte ist darauf zu achten, dass dies in der richtigen Reihenfolge für effektive und reale User-IDs geschieht, sonst kann der Daemon hinterher die Root-Rechte wiederherstellen [2]. Wären hier mehrere Threads gleichzeitig zugange, müsste »PoCoTimedProcess« intern aufpassen, dass keine Race Condition den Tunnel zweimal startet. Aber in der von POE bereitgestellten Ein-Prozess-ein-Thread-Umgebung genügt ein einfacher Variablencheck ohne jedes Locking - robust, einfach hinzuschreiben und später leicht zu begreifen!

Der zweite Callback des Forwarders, »client_connect«, wird jedes Mal angesprungen, wenn ein Mailclient an Port 25 andockt. Die im Callback ausgeführte Methode »launch()« der Komponente »PoCoTimedProcess« fährt daraufhin den Tunnel hoch, falls dieser noch nicht besteht. Intern stellt »PoCoForwarder« jeder Clientverbindung eine eigene POE-Komponente vom Typ PoCo::Client::TCP zur Seite, die mit dem Tunnelport Kontakt aufnimmt. Während also PoCo::Server::TCP beliebig viele Clients betreut, nutzt es für jede Clientverbindung eine separate PoCo::Client::TCP-Komponente.

Closures: Verwirrend schön

Zeile 23 in Listing 2 zeigt, wie die Komponente den Callback »port_bound« implementiert. Der in Zeile 17 erzeugte POE-TCP-Server springt nach erfolgreichem Start den Zustand »Started« an. Dort kramt »PoCoForwarder« die von »minimail« definierte Subroutinen-Referenz aus dem Objekthash »$self« und verzweigt dorthin. Der in Minimail definierte Callback-Code tut den Rest.

Listing 2:
»PoCoForwarder.pm«

01 ###########################################
02 package PoCoForwarder;
03 use strict;
04 use Log::Log4perl qw(:easy);
05 use POE::Component::Server::TCP;
06 use POE::Component::Client::TCP;
07 use POE;
08
09 ###########################################
10 sub new {
11 ###########################################
12   my($class, %options) = @_;
13
14   my $self = { %options };
15
16   my $server_session =
17   POE::Component::Server::TCP->new(
18     ClientArgs     => [$self],
19     Port           => $self->{port_from},
20     ClientConnected  => &client_connect,
21     ClientInput      => &client_request,
22     Started          => sub {
23       $self->{port_bound}->(@_) if
24         defined $self->{port_bound};
25     },
26   );
27
28   return bless $self, $class;
29 }
30
31 ###########################################
32 sub client_connect {
33 ###########################################
34   my ($kernel, $heap, $session, $self) =
35           @_[ KERNEL, HEAP, SESSION, ARG0];
36
37   $self->{client_connect}->(@_) if
38     defined $self->{client_connect};
39
40   my $client_session =
41   POE::Component::Client::TCP->new(
42     RemoteAddress => "localhost",
43     RemotePort    => $self->{port_to},
44     ServerInput   => sub {
45       my $input = $_[ARG0];
46         # $heap is the tcpserver's (!) heap
47       $heap->{client}->put( $_[ARG0] );
48     },
49     Connected => sub {
50         $_[HEAP]->{connected} = 1; },
51     Disconnected => sub {
52      $kernel->post( $session, "shutdown" );
53     },
54     ConnectError => sub {
55      $_[HEAP]->{connected} = 0;
56      $kernel->delay('reconnect', 1);
57     },
58     ServerError => sub {
59      ERROR $_[ARG0] if $_[ARG1];
60      $kernel->post( $session, "shutdown" );
61     },
62   );
63
64   $heap->{client_heap} = $kernel->
65     ID_id_to_session( $client_session )->
66     get_heap();
67 }
68
69 ###########################################
70 sub client_request {
71 ###########################################
72   my ($kernel, $heap, $request) =
73      @_[ KERNEL, HEAP, ARG0 ];
74
75   return if  # tunnel not up yet, discard
76     ! $heap->{client_heap}->{connected};
77
78   $heap->{client_heap}->
79          {server}->put( $request );
80 }
81
82 1;

Zu beachten ist, dass »$self« sich gar nicht im Scope des dem Zustand »Started« zugewiesenen Handlers befindet. Vielmehr stammt sie aus dem Konstruktor »new()« der Klasse »PoCoForwarder«, aber die Subroutine verwandelt sich dadurch in eine so genannte Closure, die die lexikalische Variable »$self« umschließt und die daher auch nach dem Verlassen des Konstruktor-Scope (aber nur innerhalb des Callback) gültig bleibt.

Andererseits sorgt der Parameter »Client-Args« in Zeile 18 dafür, dass die Serverkomponente den Objekthash »$self« auch als Argument »ARG0« mitliefert, falls sie die Callback-Funktion »client_connect()« anspringt. Dort ruft Zeile 37 den von Minimail gesetzten Callback »client_connect()« auf, der den Tunnel hochfährt. Nun ergibt sich allerdings ein Timingproblem, denn es ist schwer vorherzusagen, wann der Tunnel steht. Deswegen versucht der Client eventuell sich mit einem Port zu verbinden, auf dem niemand lauscht.

Doch das ist kein Problem, denn in diesem Fall springt der TCP-Client den Zustand »ConnectError« ab Zeile 54 an, der mit der POE-Kernelfunktion »delay()« einfach einen Reconnect-Event für eine Sekunde später in POEs Terminkalender einträgt. Dieses Spielchen setzt sich eventuell ein paar Runden fort, doch irgendwann steht der Tunnel, der TCP-Client verbindet sich erfolgreich mit dem nun arbeitenden Port und wechselt deshalb in den Zustand »Connected« (ab Zeile 49).

Falls Minimail ein Kommando sendet, verzweigt der TCP-Server zum Zustand »client_request« und damit zum gleichnamigen Handler ab Zeile 70. Der prüft, ob der Tunnel bereits steht, und verwirft den Client-Text, falls die Verbindung down ist. Im SMTP-Protokoll meldet sich der Server zuerst mit einer Grußmeldung - deswegen wird ein wohlerzogener Client nicht losplappern, bevor der Tunnel steht. Bei anderen Protokollen (zum Beispiel HTTP) wäre dies anders, in diesem Fall müsste die Forwarder-Komponente die Clientkommandos puffern, bis die Verbindung steht, und sie dann im Auftrag des Clients gebündelt an den Server weiterleiten.

Ist der Tunnel hingegen betriebsbereit, wurde vorher im Zustand »Connected« die Heap-Variable »connected« auf 1 gesetzt. Um die Nachricht an den Tunnel weiterzuleiten, kramt Zeile 78 aus dem in »client_heap« zwischengespeicherten Heap des TCP-Clients die Tunnelreferenz hervor und schickt ihr mit »put()« den eingegangenen Text. An dieser Stelle ist allerdings zu beachten, dass die Funktion »client_request()« ein Callback der Serversession ist, die vom Client, der in einer anderen Session arbeitet, und seinem Heap nichts weiß. Die Heap-Variable »client_heap« in der Serversession löst das Problem.

Kommen jetzt Nachrichten aus dem Tunnel zurück, dann wechselt der TCP-Client in den Zustand »ServerInput« ab Zeile 43, der daraufhin seinerseits den Text mit der im Heap gespeicherten Clientreferenz und der Methode »put()« an Minimail zurückgibt.

Für den Fall, dass Minimail sich von dem TCP-Server abkoppeln sollte, springt dieser daraufhin in den Zustand »Disconnected«. Das wiederum bewirkt, dass der Handler dort einen Shutdown-Event an die laufende Session schickt, was die Client-Server-Verbindung schließlich trennt. Die Komponente »PoCoTimedProcess.pm« in Listing 3 hingegen kümmert sich um nichts anderes als um den Auf- und Abbau des Tunnels.

Listing 3:
»PoCoTimedProcess.pm«

001 ###########################################
002 package PoCoTimedProcess;
003 use strict;
004 use warnings;
005 use POE;
006 use POE::Wheel::Run;
007 use Log::Log4perl qw(:easy);
008
009 ###########################################
010 sub new {
011 ###########################################
012   my($class, %options) = @_;
013
014   my $self = { %options };
015   bless $self, $class;
016 }
017
018 ###########################################
019 sub launch {
020 ###########################################
021   my($self) = @_;
022
023   $poe_kernel->post($self->{session},'up');
024 }
025
026 ###########################################
027 sub spawn {
028 ###########################################
029   my($self) = @_;
030
031   $self->{session} =
032   POE::Session->create(
033     inline_states => {
034       _start => sub {
035         my($h,$kernel) = @_[HEAP, KERNEL];
036
037         $h->{is_up} = 0;
038         $h->{command} = $self->{command};
039         $h->{timeout} = $self->{timeout};
040         $h->{heartbeat} =
041                        $self->{heartbeat};
042         $kernel->yield('keep_alive');
043         $kernel->yield('heartbeat');
044       },
045       sig_child => sub {
046           delete $_[HEAP]->{wheel};
047       },
048       heartbeat => &heartbeat,
049       up   => &up,
050       down => &down,
051       keep_alive => sub {
052         $_[HEAP]->{countdown} =
053           $_[HEAP]->{timeout};
054       },
055       closing => sub {
056         $_[HEAP]->{is_up} = 0;
057       },
058    })->ID();
059 }
060
061 ###########################################
062 sub heartbeat {
063 ###########################################
064   my($kernel, $heap) = @_[KERNEL, HEAP];
065
066   $kernel->delay( "heartbeat",
067                   $heap->{heartbeat});
068
069   if( $heap->{is_up} ) {
070     INFO "Process is up for another ",
071       $heap->{countdown}, " seconds";
072
073     $heap->{countdown} -=
074       $heap->{heartbeat};
075
076     if($heap->{countdown} <= 0) {
077         INFO "Time's up. Shutting down";
078         $kernel->yield("down");
079         return;
080     }
081   }
082 }
083
084 ###########################################
085 sub up {
086 ###########################################
087   my ($heap, $kernel) = @_[ HEAP, KERNEL ];
088
089   if($heap->{is_up}) {
090       INFO "Is already up";
091       $_[KERNEL]->yield('keep_alive');
092       return 1;
093   }
094
095   my($prog, @args) = @{ $heap->{command} };
096
097   $heap->{wheel} =
098     POE::Wheel::Run->new(
099       Program     => $prog,
100       ProgramArgs => [@args],
101       CloseEvent  => "closing",
102       ErrorEvent  => "closing",
103       StderrEvent => "ignore",
104   );
105
106   my $pid = $heap->{wheel}->PID();
107   INFO "Started process $pid";
108
109   $kernel->sig_child($pid, "sig_child");
110   $kernel->sig( "INT"  => "down" );
111   $kernel->sig( "TERM" => "down" );
112
113   $_[KERNEL]->yield('keep_alive');
114   $heap->{is_up} = 1;
115 }
116
117 ###########################################
118 sub down {
119 ###########################################
120   my ($heap, $kernel) = @_[ HEAP, KERNEL ];
121
122   if(! $heap->{is_up}) {
123       INFO "Process already down";
124       return 1;
125   }
126
127   INFO "Killing pid ", $heap->{wheel}->PID;
128   $heap->{wheel}->kill();
129   $heap->{is_up} = 0;
130   $kernel->sig_handled();
131 }
132
133 1;

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 6 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Als digitales Abo

Als PDF im Abo bestellen

comments powered by Disqus

Ausgabe 07/2013

Preis € 6,40

Insecurity Bulletin

Insecurity Bulletin

Im Insecurity Bulletin widmet sich Mark Vogelsberger aktuellen Sicherheitslücken sowie Hintergründen und Security-Grundlagen. mehr...

Linux-Magazin auf Facebook