CGI und Perl
Description: Einführung in die CGI-Programmierung mit Perl im Internet. Vorkenntnisse der Programmierung sind notwendig.
Keywords: Multimedia WWW Internet Webdesign CGI Perl Programmierung

| ### Grundlagen CGI-Programmierung mit Perl von Prof. Jürgen Plate

# 7 Fortgeschrittene CGI-Programmierung

## 7.1 Session-Tracking

Ein Problem bei CGI-Programmen ist, daß es sich bei HTTP um ein zustandsloses Protokoll handelt. Bei jeder Interaktion wird ein CGI-Programm aufs Neue aufgerufen. Ohne besondere Maßnahmen ist nur eine Anfrage des Browsers, gefolgt von einer Antwort des CGI-Programms, möglich. So lassen sich viele Anwendungen nicht realisieren. Stellen Sie sich ein Bestellsystem vor. Dieses erlaubt es, Waren in einen virtuellen Warenkorb zu stellen. Dieser Vorgang kann sich über mehrere Aufrufe des CCI-Programms erstrecken und wird entweder mit einem Abbruch der Aktion oder mit dem "Bezahlen" an der "Kasse" abgeschlossen. Ein solches System ist jedoch nicht durch einfache Frage/Antwort-Folgen zu realisieren.

Man muß also Methoden finden, mit denen der Zustand zwischen mehreren Aufrufen des CGI-Programms erhalten bleibt, und die ein zustandsloses (stateless) Protokoll nutzen um ein zustandsbehaftetes (stateful) Protokoll darüber implementieren. Ein CGI-Programm selbst kann zunächst keinen Zustand halten. Wir wollen aber die mehrfache Ausführung eines CGI-Programms als ein virtuelles Programm auffassen, das bei jedem CGI-Aufruf in einen neuen Zustand übergeht.

Ein einfaches Beispiel:
Bei jedem Aufruf des CGI-Programms soll eine Zahl (Session-Kennung) angezeigt werden. Diese Zahl ist bei jedem neuen Aufruf zu inkrementieren. Jeder Browser bzw. Benutzer soll eine eigene Kennung besitzen, so daß es nicht ausreicht, einen globalen Zähler serverseitig zu erhöhen.

Der Zähler repräsentiert den Session-Zustand. Diesen müssen wir jeweils zwischen zwei aufeinanderfolgenden Aufrufen des CCI-Programms erhalten. Da der Zustand pro Browser bzw. User gilt, muß dieser mehrfach gespeichert werden. Zur Speicherung des Zustands gibt es zwei Möglichkeiten:

- Der komplette Zustand wird clientseitig gespeichert. Hierbei kann er in einer Browsersitzung versteckt sein oder aber auch in Form von Cookies auf der Festplatte des Browsers liegen.
- Der Zustand aller Clients wird serverseitig gespeichert. Es ist dabei notwendig, bei den Clients noch eine minimale Zustandsinformation zu hinterlegen. Diese Informationsmenge wird auch Sitzungsschlüssel genannt.

Die clientseitige Speicherung des gesamten Zustandes hat den Vorteil, daß die Informationen vieler Clientsitzungen auch bei den Clients gespeichert sind und somit für den Server keinen Speicherbedarf bedeuten. Ein Nachteil ist, daß die Information auf der Clientseite eingesehen oder sogar verändert werden kann. Da der Zustand bei jeder Interaktion übertragen werden muß, kann dies bei vielen gleichzeitig aktiven Clients Server und Netz belasten. Ein dritter Nachteil ist, daß keine Statistiken über die einzelnen Zustände herzustellen sind, da die Info bei den Clients gespeichert ist.

Ein Vorteil der serverseitigen Speicherung ist, daß die gesamte Information über alle Clients sicher beim Server aufgehoben ist und auch analysiert werden kann. Positiv ist weiterhin, daß die Zustandsinformation nicht mehr komplett beim User gespeichert wird. Im Folgenden werden einige Möglichkeiten der Zustandsspeicherung vorgestellt.

### Zustandsspeicherung über PATH_INFO

Eine Möglichkeit, den Zustand zu erhalten, benutzt die URL. Am Anschluß an den Namen des CGI-Scriptes wird die URL fortgeführt. In obigem Beispiel wäre dies:

    http://www.netzmafia.de/cgi-bin/state.pl/1
    http://www.netzmafia.de/cgi-bin/state.pl/2
    http://www.netzmafia.de/cgi-bin/state.pl/3

usw.

Hier sind `/l, /2` und `/3` zusätzliche Pfadinformationen, die den Zustand repräsentieren. Innerhalb derselben Browsersitzung kann jeweils durch Anklicken der nächsten dynamisch erzeugten URL zum nächsten Zustand gewechselt werden. Ein Programm gelangt an diese Pfadinformation durch das Auslesen der Umgebungsvariablen `PATH_INFO`. Wie das geschieht, zeigt folgendes Programm, das auch noch einen weiteren Kniff demonstriert. Über die Umgebungsvariablen SERVER_NAME und SCRIPT_NAME kann man automatisch den WWW-Server und den Pfad zum Skript ermitteln. Damit sind diesbezüglich keine Änderungen von Hand nötig, wenn das Skript auf einem anderen Server laufen soll. Die komplette URL des Skripts ergibt sich aus `'http://' . $ENV{'SERVER_NAME'} . $ENV{'SCRIPT_NAME'};`

    #!/usr/bin/perl
    # Zustandserhaltung mit PATH_INFO ohne CGI-Modul.

    use strict;

    use constant INITSTATE => 1;

    my $url = $ENV{'SERVER_NAME'} . $ENV{'SCRIPT_NAME'};
    my $state      = retrieve_state();
    my $nextstate  = compute_next_state($state);
    my $saveaction = save_state($nextstate);

    # Tue etwas abhaengig von $state:
    print "Content-type: text/html\n\n";
    print "<HTML>", "\n";
    print "<HEAD><TITLE>Status mit Pathinfo</TITLE></HEAD>\n";
    print "<BODY>\n";
    print "<B>Zustand: ", $state,"</B><P>";
    print $saveaction;
    print "</BODY></HTML>\n";

    sub retrieve_state
      {
      my $state = $ENV{'PATH_INFO'} || INITSTATE;
      $state =~ s/^\///;
      return $state;
      }

    sub compute_next_state
      {
      my $current_state = shift;
      return $current_state + 1;
      }

    sub save_state
      {
      my $newstate = shift;
      my $send_me_back = "<A HREF=\"http://$url/$newstate\"> [Weiter] </A>";
      return $send_me_back;
      }

Verwendet man das Perl-Modul CGI, wird das Programm einfacher zu schreiben, man sieht aber nicht mehr so genau, was geschieht:

    #!/usr/bin/perl
    #Zustandserhaltung mit PATH_INFO.

    use strict;
    use CGI qw(:standard);

    use constant INITSTATE => 1;

    my $state      = retrieve_state();
    my $nextstate  = compute_next_state($state);
    my $saveaction = save_state($nextstate);

    # Tue etwas abhaengig von $state:
    print header;
    print start_html('Status mit Pathinfo');
    print '<B>Zustand: ', $state,'</B><P>';
    print $saveaction;
    print end_html;

    sub retrieve_state
      {
      my $state = $ENV{'PATH_INFO'} || INITSTATE;
      $state =~ s/^\///;
      return $state;
      }

    sub compute_next_state
      {
      my $current_state = shift;
      return $current_state + 1;
      }

    sub save_state
      {
      my $newstate = shift;
      my $send_me_back = a({-href => url() . "/$newstate"}, , " [Weiter] ");
      return $send_me_back;
      }

Nachteil dieser Lösung ist, daß beim Beenden (oder Absturz) des Browsers alle Pfad-Info verloren ist. Weiterhin kann der Benutzer in der URL-Zeile des Browser beliebige Angaben machen und so einen beliebigen Zustand eingeben. Durch diese Methode des Session-Tracking können die statischen HTML-Seiten der weiterhin von Internet-Suchmaschinen verarbeitet (indexiert) werden, was eventuell auch nicht erwünscht ist.

### URL-Coding per Parameter

Bei dieser Methode wird die SessionID an jede Internet-Adresse (Link) als Parameter angehängt (z. B. `http://www.server.de/cgi-bin/forum.cgi?state=4`). Der Wert des Parameters wird dann per GET-Methode geholt. Die Seitenabrufe sehen dann beispielsweise so aus:

    http://www.netzmafia.de/cgi-bin/state.pl
    http://www.netzmafia.de/cgi-bin/state.pl?state=2
    http://www.netzmafia.de/cgi-bin/state.pl?state=3

usw.

Das Programm gleicht fast dem vorhergehenden, nur dass hier statt einer Pfadangabe ein Query-String angehängt wird - im Prinzip das, was bei einer Formulareingabe geschieht.

    #!/usr/bin/perl
    # Zustandserhaltung mit QUERY_STRING.

    use strict;

    use constant INITSTATE => 1;

    my %FORM = ();
    my $url = $ENV{'SERVER_NAME'} . $ENV{'SCRIPT_NAME'};
    my $state      = retrieve_state();
    my $nextstate  = compute_next_state($state);
    my $saveaction = save_state($nextstate);

    # Tue etwas abhaengig von $state:
    print "Content-type: text/html\n\n";
    print "<HTML>", "\n";
    print "<HEAD><TITLE>Status mit Query-String</TITLE></HEAD>\n";
    print "<BODY>\n";
    print "<B>Zustand: ", $state, "</B><P>";
    print $saveaction;
    print "</BODY></HTML>\n";

    sub retrieve_state
      {
      &parse_form;
      my $state = $FORM{'state'} || INITSTATE;
      return $state;
      }

    sub compute_next_state
      {
      my $current_state = shift;
      return $current_state + 1;
      }

    sub save_state
      {
      my $newstate = shift;
      my $send_back = "<A HREF=\"http://$url?state=$newstate\"> [Weiter] </A>";
      return $send_back;
      }

    sub parse_form
      {
      my ($buffer, @pairs, $pair, $name, $value);
      $buffer = $ENV{'QUERY_STRING'};
      @pairs = split(/&/, $buffer);
      foreach $pair (@pairs)
        {
        ($name, $value) = split(/=/, $pair);
        $value =~ tr/+/ /;
        $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
        $FORM{$name} = $value;
        }
      }

Auch hier die Alternative mit dem CGI-Modul. Nun fällt z. B. die Parse-Routine weg:

    #!/usr/bin/perl
    # Zustandserhaltung mit QUERY_STRING.

    use strict;
    use CGI qw(:standard);

    use constant INITSTATE => 1;

    my $state      = retrieve_state();
    my $nextstate  = compute_next_state($state);
    my $saveaction = save_state($nextstate);

    # Tue etwas abhaengig von $state:
    print header;
    print start_html('Status mit Query-String');
    print '<B>Zustand: ', $state,'</B><P>';
    print $saveaction;
    print end_html;

    sub retrieve_state
      {
      my $state = param('state') || INITSTATE;
      return $state;
      }

    sub compute_next_state
      {
      my $current_state = shift;
      return $current_state + 1;
      }

    sub save_state
      {
      my $newstate = shift;
      my $send_back = a({-href => url() . "?state=$newstate"}, " [Weiter] ");
      return $send_back;
      }

Auch bei dieser Methode kann der Benutzer den Zustand beliebig ändern. Trotzdem wird sie häufig verwendet. Außerdem ist bei einem Beenden/Absturz des Browsers der Status verloren.

### Zustand über Hidden-Felder in Formularen

Man codiert den Zustand innerhalb eines Formulars über Felder, die nicht angezeigt werden (`<INPUT TYPE="HIDDEN" ...>>`). Sie werden vom Browser generiert und mit Information versehen (`VALUE=...`). Sendet der Benutzer das Formular zurück, wird auch der neue Zustand geliefert. Das folgende Programm zeigt, wie es geht:

    #!/usr/bin/perl
    #Zustandserhaltung mit Hidden-Feldern.

    use strict;

    use constant INITSTATE => 1;

    my %FORM = ();
    my $url = 'http://' . $ENV{'SERVER_NAME'} . $ENV{'SCRIPT_NAME'};
    my $state      = retrieve_state();
    my $nextstate  = compute_next_state($state);
    my $saveaction = save_state($nextstate);

    # Tue etwas abhaengig von $state:
    print "Content-type: text/html\n\n";
    print "<HTML>", "\n";
    print "<HEAD><TITLE>Status mit hidden fields</TITLE></HEAD>\n";
    print "<BODY>\n";
    print "<B>Zustand: $state </B><P>";
    print "<FORM METHOD=\"POST\" ACTION=\"$url\">\n";
    print $saveaction;
    print '<INPUT TYPE="submit" NAME="submit" VALUE=" Abschicken ">';
    print "</FORM>\n";
    print "</BODY></HTML>\n";

    sub retrieve_state
      {
      &parse_form;
      my $state = $FORM{'state'} || INITSTATE;
      return $state;
      }

    sub compute_next_state
      {
      my $current_state = shift;
      return $current_state + 1;
      }

    sub save_state
      {
      my $newstate = shift;
      my $send_back = "<INPUT TYPE=\"hidden\" NAME=\"state\" VALUE=\"$newstate\">";
      return $send_back;
      }

    sub parse_form
      {
      my ($buffer, @pairs, $pair, $name, $value);
      if ($ENV{'REQUEST_METHOD'} eq 'GET')
        { @pairs = split(/&/, $ENV{'QUERY_STRING'}); }
      elsif ($ENV{'REQUEST_METHOD'} eq 'POST')
        {
        @pairs = split(/&/, $buffer);
        read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
        }
      @pairs = split(/&/, $buffer);
      foreach $pair (@pairs)
        {
        ($name, $value) = split(/=/, $pair);
        $value =~ tr/+/ /;
        $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
        $FORM{$name} = $value;
        }
      }

Die Version mit dem CGI-Modul ist auch hier wieder kürzer:

    #!/usr/bin/perl
    #Zustandserhaltung mit Hidden-Feldern.

    use strict;
    use CGI qw(:standard);

    use constant INITSTATE => 1;

    my $state      = retrieve_state();
    my $nextstate  = compute_next_state($state);
    my $saveaction = save_state($nextstate);

    # Tue etwas abhaengig von $state:
    print header;
    print start_html('Status mit Hidden Fields');
    print '<B>Zustand: ', $state,'</B><P>';
    print '<FORM METHOD="POST">\n';
    print $saveaction;
    print '<INPUT TYPE="submit" NAME="submit" VALUE=" Abschicken ">';
    print '</FORM>\n';
    print end_html;

    sub retrieve_state
      {
      my $state = param('state') || INITSTATE;
      return $state;
      }

    sub compute_next_state
      {
      my $current_state = shift;
      return $current_state + 1;
      }

    sub save_state
      {
      my $newstate = shift;

      my $send_back = '<INPUT TYPE="hidden" NAME="state" VALUE=';
      $send_back .= "\"$newstate\">";
      return $send_back;
      }

Auch bei dieser Methode kann der Benutzer den Zustand beliebig ändern. Trotzdem wird sie ebenso häufig wie die vorstehende verwendet. Außerdem ist auch bei einem Beenden/Absturz des Browsers der Status verloren. Man kann zur Not den Formularcharakter verbergen, wenn der Submit-Button durch ein Bild getarnt wird. Ein Vorteil ist, daß sich mehr Info in der Statusvariablen verbergen läßt.

Man kann das Verfahren verbessern, indem man die Session-Ids nicht voraussagbar macht, also nicht 1,2, ... wie in den vorhergehenden Beispielen. Man erzeugt vielmehr eine möglichst zufällige Session-Kennung. Da die random-Funktion von Perl nur Zufallszahlen liefert, stützt sich das folgende Beispiel auf das Random-Device von Linux (`/dev/urandom`). Der Aufruf zum Öffnen des Devices wird mittels `eval` gekapselt, damit im Fehlerfall nicht das ganze Programm abstürzt.

    sub make_session_id
      {
      # $id = &make_session_id;
      # Erzeugt eine zufaellige Session ID.
      # Zurueckgegeben werden 24 Zeichen aus dem 64-Zeichen-Satz [A-Za-z0-9.-]
      # oder undef im Fehlerfall.
      # Plattformen ohne /dev/urandom koennen diese Routine nicht benutzen

      my $len = 24;
      my @session_chars = ('A' .. 'Z', 'a' .. 'z', 0 .. 9, '.', '-');
      my $id;
      eval
        { open(RANDOM, "/dev/urandom") or die; };
      return (-1) if ($@);
      return (-1) unless (read(RANDOM, $id, $len) == $len);
      close(RANDOM);
      $id =~ s/(.)/$session_chars[ord($1) & 63]/esg;
      return $id;
      }

### Zustand über Cookies

Cookies sind Informationen in Form einfacher ASCII-Texte, die von einem WWW-Server auf dem Rechner des Clients gespeichert und später wieder abgerufen werden können. Der Zustand kann clientseitig gespeichert werden und bei Bedarf von der jeweiligen Dienst-Applikation vom Client wieder abgerufen werden. Cookies sind aber in Verruf geraten, weil sie für User-Tracking verwendet wurden. Sie werden deshalb gerne im Browser deaktiviert und garantieren somit kein durchgängiges Session-Tracking, obwohl sie von Netscape eigens für diesen Zweck konzipiert worden sind. Sie stellen jedoch kein Sicherheitsrisiko dar. Session-Tracking über Cookies stellt die einfachste Methode dar und findet auch bei vielen Anwendungen Verwendung. Das folgende Programm zeigt, wie Cookies eingesetzt werden. Die Cookies werden netterweise schon in der Umgebungsvariablen HTTP_COOKIE bereitgestellt. Das Setzen eines Cookies erfolgt im HTTP-Header. Es muß daher vor der Zeile `Content-type: text/html` stehen! Die Zeile zum Setzen eines Cookies hat folgenden Aufbau:

    Set-Cookie: Name=Wert; expires=Verfallsdatum; path=Pfad; domain=Domain

Die Kombination Name=Wert wird beim Client gespeichert und ist abfragbar. Das Verfallsdatum ist ein Standard-Datumsstring (Beispiel siehe unten im Listing), als Pfad kann man in der Regel "/" verwenden und die Domain legt fest, von wo aus das Cookie abgerufen werden darf. Hier kann ein einzelner Rechnername oder eine Domain (z. B. ".netzmafia.de") stehen.

    #!/usr/bin/perl
    #Zustandserhaltung mit Cookies.

    use strict;

    use constant INITSTATE => 1;

    my $url = $ENV{'SERVER_NAME'} . $ENV{'SCRIPT_NAME'};
    my $Cookie_Domain = $ENV{'SERVER_NAME'};
    my $ExpDate = "Monday, 31-Dec-2035 23:59:59 GMT"; # cookie expire date

    my $state      = retrieve_state();
    my $nextstate  = compute_next_state($state);
    save_state($nextstate);

    # Tue etwas abhaengig von $state:
    print "Content-type: text/html\n\n";
    print "<HTML>", "\n";
    print "<HEAD><TITLE>Status mit Cookies</TITLE></HEAD>\n";
    print "<BODY>\n";
    print '<B>Zustand: ', $state,'</B><P>';
    print "Bitte 'Reload Page' betätigen";
    print "</BODY></HTML>\n";

    sub retrieve_state
      {
      # must be before "content-type"-line
      my $state = &GetMakeCookie;
      return $state;
      }

    sub compute_next_state
      {
      my $current_state = shift;
      return $current_state + 1;
      }

    sub save_state
      {
      my $newstate = shift;
      SetCookie("State"; $newstate; $ExpDate; "/"; $Cookie_Domain);
      }

    sub SetCookie
      {
      my ($name, $val, $exp, $path, $dom) = @_;
      print "Set-Cookie: ";
      print "$name=$val; expires=$exp; path=$path; domain=$dom\n";
      }

    sub GetCookies
      {
      my %cookies;
      my $cookie;
      foreach $cookie (split (/; /,$ENV{'HTTP_COOKIE'}))
        {
        my($key) = split(/=/, $cookie);
        $cookies{$key} = substr($cookie, index($cookie, "=")+1);
        }
      return(%cookies);
      }

    sub GetMakeCookie
      {
      my $State = '';
      my %Cookies = GetCookies();
      $State = $Cookies{'State'};
      $State =~ s/,.*//;
      # No Cookie Data? Establish one!
      if ($State eq '')
        {
        $State = INITSTATE;
        SetCookie("State", $State, $ExpDate, "/", $Cookie_Domain);
        }
      return($State);
      }

Will man nicht nur unseren Beispiel-Status, sondern etwa eine Kunden-Kennung speichern, sollte der Wert anders gewählt werden. Da sich der Kunde eventuell noch gar nicht angemeldet hat, fallen Kundennummern etc. weg. Andererseits sollten die vergebenen Kennungen einmalig sein, sonst vermischen sich zwei Bestellungen. Die folgende Variation der Funktion GetMakeCookie schliesst so etwas nicht aus, ist aber hinreichend variabel. Sie nimmt den aktuellen UNIX-Zeitstempel und zwei Zufallszahlen. Damit sind auch Kunden unterscheidbar, die sich in der gleichen Sekunde anmelden. Ausserdem verschleiern die Zufallszahlen den Zeitstempel:

    sub GetMakeCookie
      {
      my %Cookies = ();

      $Customer = '';
      %Cookies = GetCookies();
      $Customer = $Cookies{'Customer'};
      $Customer =~ s/,.*//;
      # No Cookie Data? Establish one!
      if ($Customer eq '')
        {
        srand(time % 1000);
        $Customer = int(rand(999)) . time() . $$ . int(rand(999));
        SetCookie("Customer", $Customer, $ExpDate, "/", $Cookie_Domain);
        }
      }

Das war jetzt die händische Lösung. Dank des Moduls `CGI::Cookie` ist die Cookie-Programmierung aber genauso einfach, wie die anderen gezeigten Methoden. Das folgende Programm zeigt die Anwendung des Moduls:

    #!/usr/bin/perl
    #Zustandserhaltung mit Cookies.

    use strict;
    use CGI qw(:standard);
    use CGI::Cookie;

    use constant INITSTATE => 1;

    my $state      = retrieve_state();
    my $nextstate  = compute_next_state($state);
    my $saveaction = save_state($nextstate);

    # Tue etwas abhaengig von $state:
    print header(-cookie => $saveaction);
    print start_html('Status mit Cookies');
    print '<B>Zustand: ', $state,'</B><P>';
    print "Bitte 'Reload Page' bet&auml;tigen";
    print end_html;

    sub retrieve_state
      {
      my $state = cookie(-name => 'state') || INITSTATE;
      return $state;
      }

    sub compute_next_state
      {
      my $current_state = shift;
      return $current_state + 1;
      }

    sub save_state
      {
      my $newstate = shift;
      my $cookie   = new CGI::Cookie(-name    => 'state',
    				                 -value   => $newstate,
    			                     -expires => '+5m');
    			                     # diesmal nur 5 Minuten Haltbarkeit
      return $cookie;
      }

Das Cookie wird im Header der HTTP-Antwort des Servers übermittelt. Es darf auch höchstens einige hundert Bytes lang werden. Auch hat jeder Bowser eine Obergrenze für die Anzahl der verwalteten Cookies. Auch können erfahrene Benutzer durch Editieren der Datei `cookies.txt` auch die Statusinformation im Cookie verändern. Da würde es nur helfen, die Cookie-Information zu verschlüsseln oder mit einer Signatur zu versehen.

Serverseitiges Session-Tracking erfordert eine Datenbankanbindung und wird im Rahmen dieser Vorlesung nicht besprochen.

## 7.2 Fileupload per Perl/CGI

Eine Datei soll über ein Formular und per HTTP auf einen Server geladen werden, da ein FTP-Zugang nicht zur Verfügung steht oder nicht eingesetzt werden soll. Für den Upload wird beim Formular die Codierung `multipart/form-data` verwendet, die es erlaubt, ein mehrteiliges Formular per `POST`-Methode zum Server zu senden. Der eine Teil sind die normalen Formulareingaben, der andere Teil ist der Inhalt der hochgeladenen Datei. Selbst mehrere Dateien können so auf einmal hochgeladen werden. Auf den Server wird die hochgeladene Datei zunächst nur temporär gespeichert, denn Sie gehört ja zu der Formularantwort, die der Browser an den Webserver sendet. Deshalb müssen die Daten vom CGI-Script verarbeitet werden.

Das folgende Beispiel-Formular hat zwei Eingabefelder, eines zur Auswahl der lokalen Datei und eines zur Angabe des Namens der Datei auf dem Server (letztere könnte auch aus dem Originalnamen abgeleitet oder automatisch generiert werden):

    <HTML>
    <HEAD>
    <TITLE>Upload</TITLE>
    </HEAD>
    <BODY>
    <H1>Datei-Upload</H1>
    <form action="/cgi-bin/upload.pl" method="post" enctype="multipart/form-data">
    Lokaler Dateiname: <input type="file" name="datei" size=40><BR><BR>
    Dateiname auf dem Server: <input type="text" name="dateiname" size="40"><BR>
    <input type="Submit" value="Upload">
    </form>
    </BODY>
    </HTML>

Betrachtet man sich das Formular mit dem Browser, sieht man einen Button "Durchsuchen" bzw. "Browse" neben der ersten Eingabezeile mit dem amn im Dateisystem navigieren kann:

Lokaler Dateiname:
Dateiname auf dem Server:

Zu beachten sind folgende Besonderheiten:

- Im Formular muss (im Gegensatz zu einfachen Eingabeformularen) unbedingt `enctype="multipart/form-data"` angegeben werden.
- Durch den Eingabetyp `input type="file" name="datei"` wird durch Anklicken des Buttons "durchsuchen" bzw. "browse" ein Fenster zum Durchsuchen der Platte geöffnet, wie man es von anderen Programmen her kennt.
- Nach Eingabe der Daten und Anklicken des "Submit"-Buttons erfolgt der Upload, der etwas dauern kann. Man erkennt den Uploadvorgang nur an der Aktivitätsanzeige des Browsers.

Wie man eine sehr einfache Ladeanzeige realisieren kann, ist schon im Kapitel Fortschritts-/Ladeanzeige besprochen worden.

Der folgende Perl-Quellcode enthält ein Beispiel für ein Upload-Script. Die Parameter stellen sowohl ein Filehandle, als auch den Dateinamen der hochgeladenen Datei zur Verfügung. In der `while`-Schleife wird die Datei in 1024 KByte großen Blöcken eingelesen und in die angegebene Datei geschrieben. Beide sind im Binär-Modus geöffnet, weil nicht bekannt ist, ob die Daten, die ankommen, Binär- oder Text-Daten sind. Zuletzt wird noch eine Bestätigung an den User zurückgegeben, daß die Datei erfolgreich gesichert wurde.

    !/usr/bin/perl
    # Einfaches Script fuer File-Upload

    use strict;
    use CGI qw(:standard);

    # Upload Dir, kein Slash am Ende!
    my $updir = "/home/httpd/htdocs/upload";

    # Referer (mit .htaccess geschuetztes Verzeichnis wo das Formular liegt)
    my $ref = "http://myhost.tld/upload/upload.html";

    my $data; # Lesepuffer

    ##### BITTE BEACHTEN ######################################################
    # Die Felder des Formulars
    #   <form action="/cgi-bin/ups.pl" method="post" enctype="multipart/form-data">
    #   Lokaler Dateiname:
    #   <input type="file" name="datei" size=40><BR>
    #   Dateiname auf dem Server:
    #   <input name="dateiname" size="40">
    #   <input type="Submit" value="Upload">
    #   <input type="reset" value="L&ouml;schen">
    #   </form>

    print header;
    print start_html('Datei-Upload');

    if ($ENV{'HTTP_REFERER'} ne $ref)
      {
      print "<H1>Fehler!</H1>\n";
      print "Es wurde versucht, ohne Erlaubnis hochzuladen! Abbruch...";
      print $ENV{'HTTP_REFERER'};
      print end_html;
      exit;
      }

    my $datei = param('datei');
    my $dateiname = param('dateiname');

    if (! $datei or ! $dateiname)
      {
      print "<H1>Fehler!</H1>\n";
      print "Datei oder Dateiname fehlt! Abbruch...";
      print end_html;
      exit;
      }

    if (! open WF, ">$updir/$dateiname")
      {
      print "<H1>Fehler!</H1>\n";
      print "Datei kann nicht geschrieben werden! Abbruch...";
      print end_html;
      exit;
      }

    binmode $datei;
    binmode WF;
    while(read $datei,$data,1024)
      { print WF $data; }
    close WF;

    print "<H1>Upload O. K.</H1>\n";
    print "Die Datei wurde hochgeladen.<BR>\n";
    print "Remote Path and Filename: $updir/$dateiname\n";

    print end_html;
    exit;

Dieses Script ist nicht für den praktischen Gebrauch gedacht, es dient lediglich als Beispiel. Sonst müssten die Sicherheits-Vorkehrungen viel strenger sein. Ein Datei-Upload auf den Server bedeutet immer ein Sicherheits-Risiko. Darum muß die Datei auch in einem "harmlosen" Verzeichnis landen, keinesfalls in `cgi-bin` Auch sollte man die Maximalgröße der Datei begrenzen, sonst fürt das Hochladen riesiger Dateien zu einer Denial-of-Service-Situation. Deshalb sollten von Anfang an einige Sicherheits-Vorkehrungen getroffen werden. Dazu zählen unter anderem:

- Das Definieren einer maximalen Dateigröße
  Das kann durch Setzen der Variablen `$POST_MAX` geschehen. Hier wird die Maximalgröße in Bytes angegeben, z. B. für ein Maximum von 400 KByte: `$CGI::POST_MAX = 400 * 1024;`
  Übrigens kann man durch Setzen der Variablen `DISABLE_UPLOADS` auf einen Wert ungleich 0 den Upload komplett unterbinden.
- Berechtigung für Aufrufer vergeben
  Der Aufruf des Scripts darf nur aus einem durch Paßwort geschützten Verzeichnis erfolgen. Dieser Fall wird im Beispiel abgehandelt. Die Umgebungsvariable `HTTP_REFERER` enthält die URL der aufrufenden Seite. Wenn jemand versucht, das Script von einer anderen Seite aus aufzurufen, wird er abgewiesen.
- Die Überprüfung des Dateinamens auf Integrität
  Dies könnte z. B. so aussehen:

      my @extensions = "txt dat gif jpg";

      if(param('dateiname') !~ /^[a-z\.\-_]+?\.([a-z]{3})$/)
        {
        print "Ungültiger Dateiname</body></html>\n";
        exit;
        }
      else
        {
        my $extension = $1;
        if(!grep($extension,@extensions))
         {
         print "Ungültige Dateiendung"</body></html>\n";
         exit;
         }
       }

  In dem obigen Code-Schnipsel wird ein Array `@extensions` mit gültigen Datei-Endungen definiert. Natürlich wird damit nicht verhindert, dass jemand eine Datei mit passender Endung hochlädt, die ganz etwas anderes enthält. Um ein anschließendes Scannen auf Viren und Schadcode ist auf jeden Fall Pflicht.

  Bei Bilddateien kommt man dann auch um eine "Sichtkontrolle" nicht herum (z. B. gibt es ja eine Sorte von Bildern, die einen hohen Rosaanteil haben).

  Als nächste Aktion "entgiften" wir die Dateinamen selbst - und zwar beide Namen, den der Daten selbst und den "Wunschnamen". Das folgende Unterprogramm findent die gravierensten Probleme:

      sub untaint # ($dateiname)
        {
        my $filename = shift;
        my $untainted_filename;
        # zuerst \ in / umwandeln
        $filename =~ s|\\|\/|g;
        # auf erlaubte Zeichen pruefen
        if ($filename =~ /^([-_\/.A-Za-z0-9]+)$/)
          { $untainted_filename = $1; }
        else
          {
          print qq~
          Nicht zugelassene Zeichen im Dateinamen "$filename".
          Der Dateiname darf nur Buchstaben, Ziffern und
          die Sonderzeichen '_', '-', '/' und '.' enthalten.
          </body></html>
          ~;
          exit;
          }

        if ($untainted_filename =~ m/\.\./)
          {
          print qq~
          Nicht zugelassene Zeichen im Dateinamen "$filename".
          Der Dateiname enthält die Zeichenfolge '..'.
          </body></html>
          ~;
          exit;
          }

        return($untainted_filename);
        }

  Jetz können Sonderzeichen, Pipe-Symbole oder ähnliches darin nicht mehr vorkommen. Man könnte auch den "Wunschnamen" eliminieren, indem man den serverseitigen Dateinamen im Script selbst generiert, z. B. aus Referrer, Datum und Uhrzeit.

## 7.3 CGI und geschützte Verzeichnisse

Den Zugriff auf einzelne Verzeichnisse eines Webangebots kann eingeschränkt werden. Der Benutzer muß sich dann mit Benutzerkennung und Passwort anmelden, bevor er an die Daten des Verzeichnisses kommt. Wenn nun in so einem geschützten Verzeichnis ein Formular liegt, dann ist zwar das Verzeichnis geschützt, das CGI-Script kann jedoch jederzeit aufgerufen werden, ohne daß User und Passwort angegeben werden müssen. Um CGIs zu schützen kann man zwei Wege einschlagen:

- Benutzerauthentisierung ins CGI-Skript aufnehmen. Das geht recht einfach, indem man die Benutzerverwaltung selbst in die Hand nimmt. Das Schema der Apache-Basic-Authentisierung kann dabei beibehalten werden. Die Benutzerdatei enthät Zeilen mit den durch Doppelpunkt getrennten Paaren von Username und Passwort. Das eingegebene Passwort wird verschlüsselt und mit dem gespeicherten verschlüsselten Passwort verglichen:

      $username = substr($username,0,8);
      $cryptedpass = crypt($password,substr($username,0,2));

      open DAT, $PASSWORDFILE;
      while ()
        {
        chomp; $_ =~ s/\r//;            #  am Ende weg
        ($uid,$pass) = split(":", $_);  # user:passwort
        if ($uid eq $username)          # userid "passt"
          {
          fail("Falsches Passwort!") unless ($pass eq $cryptedpass);
          last;
          }
        }
      close DAT;

- Die andere Möglichkeit ist es, im Skript festzustellen, von wo es aufgerufen wurde (das verhindert das Einbinden des Scriptes in fremde Seiten). Die Environment-Variable `HTTP_REFERER` liefert die Referer-Adresse, also die Adresse der Seite, von der das Skript aufgerufen wurde. Zuerst wird definiert, wie die Datei mit dem HTML-Formular heißt und später im Programm `HTTP_REFERER` damit verglichen:

      my $referer = "http://myhost.tld/upload/upload.html";

      ...

      fail ("Programmaufruf ohne Erlaubnis!"> if ($ENV{'HTTP_REFERER'} ne $referer);

Das folgende Datei-Upload-Programm verwendet die zweite Methode.

Ein Anmeldeskript für eine geschlossene Benutzergruppe, das Formular dazu finden Sie in den Beispielen.

Infos zum Anlegen von geschützten Verzeichnissen finden Sie unter http://www.netzmafia.de/skripten/webdesign/mm6.html#6.5.

## 7.4 Skripten im Browser automatisch ausführen

### Web-Bugs

Sicherlich kennen viele von Ihnen das folgende Szenario: Auf irgendeiner beliebigen Webseite abonnieren Sie einen Newsletter. schon bald landet die erste Ausgabe im eigenen E-Mail-Postfach - natürlich schick formatiert in HTML. Wenn Sie später mal wieder auf die Mail schauen, geschieht etwas: Das E-Mail-Programm möchte online gehen. Warum sollte es das tun, die Mail ist doch schon da? Dasselbe kann beim Ansehen einer lokal gespeicherten Webseite geschehen.

Des Rätsels Lösung sind die "Web-Bugs". Dabei handelt es sich in der Regel um eine kleine Grafik, die nur 1 mal 1 Bildpunkt groß und zudem auch noch transparent ist. Sichtbar ist diese Grafiken nicht, dennoch wird Ihr E-Mail-Programm dieses Bild darstellen wollen. Nun ist es so, dass es aber nicht mitgeschickt wurde, es liegt noch auf dem Server, das Programm greift auf das Internet zu.

Wird die Grafik vom Server geladen, so wird dies dort mitprotokolliert. So kann der Betreiber des Newsletters sehen, wann und wie viele Leser den Newsletter geöffnet haben. Seriöse Argumente für solche "Web-Bugs" sind daher die Anfertigung von Statistiken. Letztlich möchten die meisten Webmaster Geld mit ihrer Site verdienen und ein potentieller Werbekunde möchte wissen, wie oft die E-Mail denn nun wirklich geöffnet wird. Da der Besuch von Webseiten derzeit i.A. kostenlos ist, ist dies ein durchaus legitimes Argument. Printmedien wissen schließlich auch, wie viele Zeitschriften verkauft wurden.Als Leser können Sie aber nicht feststellen, ob einfach nur gezählt wird, oder ob Ihre Schritte mitprotokolliert werden.

Mit "Web Bugs" in E-Mails läßt sich also feststellen, ob und wann eine Mail geöffnet wurde, was auch feststellbar macht, wann und ob Werbemails (SPAM) gelesen wurden. Sie lassen sich auch verwenden, um den Cookie des Browsers mit einer bestimmten Mailadresse zu verknüpfen, so daß ein Besucher bekannt ist, wenn er später eine Website aufruft. Wenn jemand mit dem Outlook Express oder dem Netscape Messenger Mitteilungen in einer Newsgroup liest, so lassen sich mit einem "Web Bug" auch diese Leser identifizieren.

Man kann aber noch eins draufsetzen. Statt der Grafik wir ein Skript aufgerufen, welches seinerseits die Grafikdaten zurückgibt und so das E-Mail-Programm oder den Browser zufriedenstellt. Das Skript kann nun alle möglichen Daten über den User ermitteln oder aus einer Menge von Grafiken eine bestimmte auswählen (per Zufallsgenerator oder nach anderen Kriterien). Selbst wer Cookies und Skriptsprachen abgeschaltet hat, entkommt dem "Web-Bug" nicht. Der Code in einer HTML-Seite könnte beispielsweise so aussehen:

    <B>Diese Seite zeigt am unteren Ende eine  Grafik,
    die von einen CGI-Skript generiert wird.</B>
    <p>
    <hr><center>
    <img src="/cgi-bin/bug.cgi"></center>

Man kann Verzeichnis und Dateiname natürlich noch "unverfänglicher" gestalten. Ein Demoscript ist ebenfalls schnell gemacht:

    #!/usr/bin/perl
    use strict;

    my $now = localtime(time);

    # HTTP-Vorspann
    print "Content-Type: image/gif\n";
    print "\n";

    # GIF schicken
    open(DAT,"/var/www/htdocs/1pix.gif");
    print while (<DAT>);
    close(DAT);

    # Irgendwas protokollieren
    open(DAT,">>","/var/www/htdocs/bug.log");
    print DAT $now,"\n";
    close(DAT);

Wenn man jetzt die Namen der Grafik dynamisch (z. B. benutzerbezogen) generiert, kann man alleine über den Namen der Grafik schon ein Benutzerprofil erstellen und - falls jede Webseite einen "Bug" enthält - sogar den Weg durch das eigene Angebot verfolgen.

Sie können das ausprobieren, denn in dieser Seite ist so ein Link versteckt: → ← Das Ergebnis finden Sie in bug.log.

### Aufruf über einen iframe

Die "normalen" Frames kommen bei Webseiten immer seltener vor. Abgelöst werden sie von den sogenannten iFrames. Der iFrame ist ein klassisches Gestaltungselement innerhalb von HTML. Er hat die Aufgabe, innerhalb einer Webseite ausgelöst durch einen Mausklick oder eine andere Aktion des Nutzers innerhalb der Seite einen neuen Inhalt darzustellen, ohne die restlichen Bestandteile der Seite zu verändern. Ein iFrame ist somit in gewisser Weise ein Browser-Fenster innerhalb eines anderen Browser-Fensters. Der iFrame kann in Größe, Position und Erscheinung frei bestimmt werden.

Hier unterhalb ist der folgende iFame in die Seite eingebunden:

    <iframe src="http://www.netzmafia.de/cgi-bin/bofhserver.cgi"
        name="harhar" scrolling="auto" frameborder="0"
        marginheight="0px" marginwidth="0px"
        height="640" width="480">
    </iframe>

Beim Aufruf der Seite wird automatisch das Script `bofhserver.cgi` aufgerufen und dessen Ausgabe hier dargestellt.

## 7.5 Skripten für spezielle Zwecke

### Ad-Click-Counter

Viele Webseiten mit sogenannten Werbebannern leben davon, dass sie die Werbung verkaufen. Um nun festzustellen, wie oft ein Banner angeklickt wurde, kann man einen "Ad-Click-Counter" programmieren. Das Bannerbild (oder auch nur ein Text) wird von einem speziellen Link eingerahmt. Statt des normalen `<A HREF="...">Bild/Text</A>` wird nun ein CGI-Script angsprochen. Es hat die Form:

    <A HREF="/cgi-bin/jump.cgi?url=[Link]&cnt=[Name]">Bild/Text</A>

`[Link]` ist dabei eine beliebig URL, die zum Server des Werbenden führt. `[Name]` ist eine eindeutige Kennung des Links bzw. der Firma (bitte wie Dateinamen behandeln, also keine Umlaute, Leerzeichen, etc., sonst wird eine Konvertierung notwendig).

Für jeden Namen (cnt=...) wird ein Datenbankeintrag mit Zaehler, Monat und Jahr angelegt. Die MySQL-Datenbanktabelle hat folgenden Aufbau:

    CREATE TABLE `AdKlick` (
      `Id` INT NOT NULL AUTO_INCREMENT ,
      `Name` VARCHAR( 50 ) NOT NULL ,
      `Count` INT NOT NULL ,
      `Mon` INT NOT NULL ,
      `Year` INT NOT NULL ,
      `Last_Change` TIMESTAMP NOT NULL ,
       PRIMARY KEY ( `Id` )
       );

Es wird also jeden Monat ein neuer Zähler-Eintrag angelegt, so dass man auch eine monatliche Abruf-Statistik erhält. Das Perl-Script dazu ist relativ kurz:

    #!/usr/bin/perl

    use strict;
    use DBI;

    my %FORM = ();
    my @pairs = ();
    my $buffer = '';
    my $script_url = $ENV{'SCRIPT_NAME'};

    my $dsn = 'DBI:mysql:de:localhost';                    # DBI/Modul/Datenbank/Host

    # Variablen fuer Datendankabfragen und -eintraege
    my ($dbh,$sql,$row,$sth,$id);
    my ($name,$count,$mon,$year);
    my ($url,$uname,$aktmon,$aktyear,$dummy);

    ($dummy,$dummy,$dummy,$dummy,$aktmon,$aktyear,$dummy,$dummy,$dummy) = gmtime(time);
    $aktyear = $aktyear + 1900;
    $aktmon++;

    # Formulardaten auswerten - wie schon oft gezeigt; es wird nur GET beruecksichtigt
    @pairs = split(/&/, $ENV{'QUERY_STRING'});
    foreach my $pair (@pairs)
      {
      my ($name, $value) = split(/=/, $pair);
      $value =~ tr/+/ /;
      $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
      $FORM{$name} = $value;
      }
    $url = $FORM{'url'};
    $uname = $FORM{'cnt'};
    # Nur Buchstaben und Zahlen bei 'cnt'
    $uname =~ s/[^a-zA-Z0-9]//g;

    # Datenbank-Verbindung aufbauen
    $dbh = DBI->connect($dsn, "debuser", "dbpass",
                        {RaiseError => 1, AutoCommit => 1});

    # Daten abfragen fuer den Namen sowie aktuellen Monat und aktuelles Jahr
    $sql = "SELECT * FROM AdKlick WHERE Name='" . $uname . "' AND Mon='" .
           $aktmon . "' AND Year='" . $aktyear . "';";
    $sth = $dbh->prepare($sql);
    $sth->execute();
    ($id,$name,$count,$mon,$year) = $sth->fetchrow_array();

    # Wenn Datensatz vorhanden, Counter erhoehen
    if ($id > 0)
      {
      $count++;
      $sql = "UPDATE AdKlick SET Count='" . $count . "' WHERE Id='" . $id . "';";
      $dbh->do($sql);
      }
    # in allen anderen Faellen: Neuanlage
    else
      {
      $sql ="INSERT INTO AdKlick (Name,Count,Mon,Year) VALUES(" .
            "'" . $uname . "', '1', '" . $aktmon . "', '" . $aktyear . "');";
      $dbh->do($sql);
      }
    # und per Umleitung zur Ziel-URL wechseln
    print "Location: $url\n\n";

Alternativ könnte man den Link auch noch in die Datenbank verlagern. Um missbrächlicher "Klick-Vermehrung" vorzubeugen, könnte man die IP-Adresse des Browsers in der Datenbank speichern und nur "neue" Klicks registrieren (was aber bei Usern, die über einen Proxy surfen zu einen Nachteil führt, da der Proxy nur einmal registriert wird, obwohl ja unterschiedliche User den Link angeklickt haben könnten).

### Feedback-Script

Das folgende Feedback-Script wertet ein beliebiges Formular aus, in dem sich der Betrachter der Webseite per Mail an den Betreiber wenden kann. Im Gegensatz zu einem speziell auf eine Anwendung zugeschnittenem Script, kann dieses Script nahezu jedes Formular verarbeiten. Die Formularfelder werden in der E-Mail einfach in der Form "Key: Value" eins zu eins wiedergegeben.

Es gibt jedoch einige spezielle Formularvariablen, welche die E-Mail selbst beeinflussen und als Hidden-Felder definiert werden, z. B.: `<input type="hidden" name="FORM_SUBJECT" value="Achtung! Wichtige Nachricht">`

- `FORM_SUBJECT` erlaubt das Ändern des Standard-Betreffs,
- `FORM_SENTFROM` erlaubt eine alternative Absenderangabe,
- `FORM_NEXT` erlaubt die Angabe einer URL, die nach dem Absenden aufgerufen wird (anstelle der Standard-Dankeschoen-Seite).

Damit Missbrauch weitestgehend ausgeschlossen wird, kann man über das `@referers`-Array festlegen, von welchen Domains aus das Script aufgerufen werden darf. Aus diesem Grund ist auch die Emfängeradresse fest einprogrammiert.

    #!/usr/bin/perl
    use strict;
    use warnings;

    # CONFIG SECTION -------------------------------------------------------
    my $SENDMAIL = '/usr/lib/sendmail -t -oi';         # Sendmail-Konfig.
    my $SENDTO   = 'webmaster@netzmafia.de';           # Default-Empfaenger
    my $SENTFROM = 'feedback-formular@netzmafia.de';   # Standard-Absender

    # Dankeschoen-Seite und Fehlerseiten
    my $ANTWORT       = 'http://www.netzmafia.de/dialog/antwort.html';
    my $REFERER_ERROR = 'http://www.netzmafia.de/dialog/referer_error.html';
    my $SYSTEM_ERROR  = 'http://www.netzmafia.de/dialog/system_error.html';
    my @referers =                                     # zugelassene Referrer
        ( "www.netzmafia.de",
          "www.ee.hm.edu",
        );
    # ----------------------------------------------------------------------

    my $SUBJECT = 'Ausgabe des FEEDBACK-Formulars';
    my $query_string = '';  # Formulareingabe auswerten
    my ($key, $value);      #     -"-
    my $EINGABE = '';       # Alle Formulardaten aufbereitet
    my $referer = '';       # fuer Pruefung Referrer

    # Formular einlesen, diesmal aber Spezialvariablen gesondert ueberpruefen
    if ($ENV{'REQUEST_METHOD'} eq "GET")
      { $query_string = $ENV{'QUERY_STRING'}; }
    elsif ($ENV{'REQUEST_METHOD'} eq "POST")
      { read(STDIN, $query_string, $ENV{'CONTENT_LENGTH'}); }

    foreach (split(/\&/,$query_string))
      {
      ($key,$value) = split(/=/,$_);
      $key =~ tr/\+/ /;
      $key =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;

      $value =~ tr/+/ /;
      $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
      $value =~ s/<!--(.|\n)*-->//g;

      if ("$key" eq "FORM_SUBJECT")   { $SUBJECT   = $value; next; }
      if ("$key" eq "FORM_SENTFROM")  { $SENTFROM  = $value; next; }
      if ("$key" eq "FORM_NEXT")      { $ANTWORT   = $value; next; }
      $EINGABE .= "$key: $value\n";
      }

    # Ueberpruefen des Referrers,  nur Domain betrachten
    $referer = $ENV{'HTTP_REFERER'};
    $referer =~ s|^.*http://||;
    $referer =~ s|/.*$||;
    unless (($referer ne "") && grep(/$referer/,@referers))
      { print "Location: $REFERER_ERROR\n\n"; exit 0; }

    # noch ein paar Statusinfos hinzufuegen
    $EINGABE .= "\n\n
    HTTP-Referer:   $ENV{'HTTP_REFERER'}
    Remote-Host:    $ENV{'REMOTE_HOST'}
    Remote-Address: $ENV{'REMOTE_ADDR'}
    ";

    # Absenden per Email
    if (! open(MAIL, "| $SENDMAIL"))
      { print "Location: $SYSTEM_ERROR\n\n"; exit 0; }
    print MAIL "From: $SENTFROM\n";
    print MAIL "To: $SENDTO\n";
    print MAIL "Subject: $SUBJECT\n";
    print MAIL "Mime-Version: 1.0\n";
    print MAIL "Content-Type: text/plain; charset=iso-8859-1\n";
    print MAIL "Content-Transfer-Encoding: 8bit\n";
    print MAIL "\n";
    print MAIL $EINGABE;
    close(MAIL);

    # Alles ging gut - Dankeschoen-Seite aufrufen
    print "Location: $ANTWORT\n\n";

### Fehlerseiten

Oft werden in der Datei `.htaccess` oder in der Apache-Konfiguration eigene Fehler-Seiten anstelle der Standard-Meldungen des Webservers definiert. In der `.htaccess`-Datei steht dann jeweils eine "ErrorDocument"-Zeile, welche die Fehlernummer und die anzuzeigende Datei angibt, z. B.:

    ErrorDocument 401 /Fehler401.html
    ErrorDocument 403 /Fehler403.html
    ErrorDocument 404 /Fehler404.html
    ErrorDocument 500 /Fehler500.html

Diese Umleitung muss aber nicht statisch erfolgen, sondern es kann natürlich auch ein Script als Zeiel angegeben werden, das beispielsweise einige Status-Daten in einer Datenbanktabelle ablegt und anschliessend ein Suchformular oder eine Sitemap ausgibt.

## 7.6 Komplexes Beispiel: Steuern per Internet

Dieses Beispiel soll in mehreren Schritten zeigen, wie das Ein- und Ausschalten von Geräten über das Internet realisiert werden kann. Es beginnt mit dem Entwurf eines sicheren Protokolls zwischen Client und Server (der dann die Steuerungsaufgabe übernimmt).

Im weiteren Ausbau wird der Client zu einem CGI-Skript erweitert, das dann über ein Webformular mit dem Benutzer kommuniziert. Die gesamte Konfiguration stellt sich folgendermaßen dar:

### Hardware

Technische Daten der Relaiskarte:

- Hersteller: Conrad Electronic
- Bestellnummer: 967720
- Betriebsspannung: 11 ... 15 V DC
- Maximale Stromaufnahme: 200mA bei 12 V (8 Relais eingeschaltet)
- Relais Schaltleistung: 230 V AC/ 4 A, ohne gesonderte Funktions- und Sicherheitsprüfung der Gesamtapplikation sind maximal 24 V/ 2 A zulässig!
- Außenmaße: 160 x 100 mm (Europlatine)
- Serielle Schnittstelle: RS232, 19200 Baud, 8 Datenbits, 1 Stopbit, kein Paritätsbit, kein Handshake, Nullmodemkabel zum Anschluß an den PC verwenden

Es können bis zu 255 Relaiskarten kaskadiert werden (255 Relaiskarten * 8 Relais = 2040 schaltbare Relais insgesamt). Wer sich weiter informieren will, kann sich

- den Schaltplan (PDF) und
- die Beschreibung (PDF) der Ansteuerung

ansehen.
Anschluß des Netzteiles: Zuerst die richtige Spannung am Netzteil einstellen (9 V sind trotz anders angegebener technischer Daten völlig ausreichend). Dann den am Netzteil vorhandenen Klinkenstecker abschneiden und beide Adern des Kabels abisolieren. Nun das Kabel an Klemme K9 anschliessen (richtige Polung beachten).

Nun wird das Nullmodemkabel angeschlossen. Man kann auch selbst ein Seriellkabel löten. Dies bietet sich an, wenn die Relaiskarte in ein Gehäse eingebaut wird, da dort kein Platz mehr für die Steckverbindung ist. Die Steckerbelegung zeigt das Bild unten. Die Klemmen auf der Relaiskarte befinden sich neben der seriellen Buchse. Beachten Sie die Einstellung von Jumper JP3. Beim Einzelkartenbetrieb muß sich der Jumper in der Position 1-2 befinden.

Die Karte wurde in den Conrad Power-Manager (Best. Nr. 998575) eingebaut. Das Gerät besteht aus einem Stahlblechgehäuse, das auf der Vorderseite sieben Schalter und auf der Rückseite sieben korrespondierende Schuko-Steckdosen besitzt. Die Relaiskontakte 1 - 7 der Relaiskarte sind in Reihe zu den Schaltern verdrahtet, so daß die im Schalter eingebauten Glimmlampen als Betriebsanzeige wirken (Schalter eingeschaltet). Ist ein Schalter in Stellung "aus", bleibt die entsprechende Steckdose unabhängig von der Relais-Stellung stromlos. Für die Versorgung der Relais-Karte wurde noch ein passendes 12-V-Netzteil eingebaut. Der Power Manager erfüllt so gleichzeitig drei Funktionen: Anzeige, Berührschutz und Gehäse für alle Komponenten. Da nur sieben Steckdosen vorhanden sind, wurde das achte Relais zum Schalten eines Gleichstromsummers verwendet.

Achtung: Die Abschaltung im Power Manager erfolgt nur einpolig. Je nachdem, wie der Netzstecker des Power Managers eingesteckt wird, schaltet er Phase oder Nulleiter. Lediglich der Hauptschalter ist zweipolig ausgeführt. Bei Arbeiten an angeschlossenen Geräten ist auf alle Fälle deren Netzstecker zu ziehen.

Anmerkung: Genauso gut kann natürlich auch eine andere Relaiskarte Verwendung finden. Die Ansteuerung muss nicht unbedingt seriell realisiert sein, sondern kann beispielsweise auch über die Druckerschnitt erfolgen.

### Software

### Das Programm `relais`

Syntax der Parameter von `relais`:
Das Ansteuerprogramm für die serielle Relaiskarte von Conrad wird mit dem Kommando `relais <Parameter>` aufgerufen. Als Parameter sind folgende Eingaben möglich:

Parameter | Bedeutung
--- | ---
`-stat` | Status der Relais als Dezimalzahl. Bit = 1: Relais an, Bit = 0: Relais aus. Es sind keine weiteren Parameter möglich
`-off` | alle Relais aus
`-on` | alle Relais an
`-sx` | Relais x einschalten (1 <= x <= 8)
`-rx` | Relais x ausschalten (1 <= x <= 8)

Beispiele:
`relais -off -s1 -s3:` Relais 1 und 3 einschalten
`relais -s4 -r3:` Relais 4 ein- und 3 ausschalten
`relais -on -r8:` alle Relais ausser 8 einschalten

Rückgabewert:
Normalerweise wird `OK: <Kontaktstellung dezimal>` z. B. 'OK: 5' --> Relais 1 und 3 on auf der Standardausgabe zurückgegeben.
Bei Fehler wird `FAIL:` und der komplette Status zurückgegeben (Antwortcode Adresse Daten/Info).

Quellcode:
Der C-Quellcode des Programms basiert auf dem Serial-HOWTO von Linux. Das Programm sollte auf allen Linux-Versionen lauffähig sein.
Über `#define PORT 0` wird die erste serielle Schnittstelle (`ttyS0`) ausgewählt; hier sind die Werte 0 bis 3 für `ttyS0` bis `ttyS3` möglich.

### Entwurf eines Protokolls

Es wird ein Protokoll für die Anwendungsschicht entworfen und beschrieben, das zwischen einem Server mit angeschlossener Relais-Steuereinheit und einem Client benutzt werden soll. Da die Relais nicht von Jedermann geschaltet werden sollen, müssen sich Client und Server gegenseitig authentisieren. Das Protokoll soll natürlich abhörsicher sein. Eine Übertragung eines Passworts im Klartext scheidet daher aus. Aber auch die Übertragung eines verschlüsselten Passworts führt diesmal nicht weiter (warum?).

Daher wird diesmal ein Challenge-Response-Protokoll verwendet. Das bedeutet, daß sich Client und Server im Dialog gegenseitig authentisieren. Damit die Übertragung abhörsicher wird, verschlüsselt man die Daten mit einer sogenannten Einweg-Funktion.
Eine Einwegfunktion ist eine mathematische Funktion, die (vorwärts) deutlich leichter zu berechnen ist, als die zugehörige Umkehrfunktion (rückwärts). Ein Rechner braucht beispielsweise nur einige Sekunden, um die Funktion für einen Wert zu berechnen, für die Umkehrung braucht er jedoch möglicherweise Monate oder sogar Jahre.
Je größer die Eingabedaten der Einwegfunktion (und damit der Schlüssel) gewählt werden, desto größer ist auch der Unterschied in der Rechenzeit für die Hin- und Rückrichtung. Alle praktisch verwendbaren asymmetrischen Kryptosysteme basieren auf angenommenen Einwegfunktionen, d.h. Funktionen, von denen man glaubt, daß es Einwegfunktionen sind, dieses jedoch bisher nicht bewiesen wurde.

#### Einwegfunktionen

Einwegfunktionen dienen in der Kryptografie zur Generierung von nicht manipulierbaren Fingerabdrücken aus Nachrichten. Sie erzeugen aus einer Nachricht mit beliebiger Länge nach einem vorbestimmten Verfahren ein Komprimat (gewissermaßen eine kryptografische Prüfziffer). Man nennt sie auch Hashfunktionen. Eine Hashfunktion, welche die Eigenschaft hat, daß es sehr lange dauert eine Nachricht zu finden für die hash(m) = hash(n) gilt, nennt man kryptografische Hashfunktion. Anders als bei Prüfsummenverfahren müssen dabei nicht nur zufällig auftretende Fehler, sondern auch vorsätzliche Manipulationen sicher erkannt werden. Die wichtigsten Anforderungen an solche Funktionen lauten:

- Effizienz
  Ein Hashwert muss "einfach", d.h. leicht und schnell, zu berechnen sein.

- Kollisionsfreiheit
  Eine Hashfunktion muss "kollisionsfrei" sein, das heißt es darf nicht möglich sein, zwei Nachrichten zu konstruieren, die den gleichen Hashwert haben, da es sonst möglich wäre, eine Originalnachricht unerkannt durch eine Fälschung auszutauschen (Geburtstagsangriff).

- Repräsentativität
  Jedes Bit eines Textes muss den Hashwert beeinflussen, d. h. die Änderung auch nur eines Buchstaben eines Textes muss zu einem anderen Hashwert führen. Daher müssen auch ähnliche Texte vollkommen unterschiedliche Hashwerte ergeben.

- Kryptografische Sicherheit
  Für einen gegebenen Hashwert muss es praktisch unmöglich sein den zugehörigen Text herzustellen bzw. zurückzugewinnen.

Hashfunktionen die diese Eigenschaften erfüllen werden im Englischen als "Message Digest" (MD) bezeichnet. (Digest: Auszug, Zusammenfassung) Ein Message Digest ist der (digitale) Fingerabdruck einer Nachricht, bei der mit Hilfe von einfach berechenbaren Funktionen ein Wert ermittelt wird, der kürzer ist als die Originalnachricht. Die verwendete Funktion muss so beschaffen sein, dass es relativ schwierig ist eine zweite Nachricht zu erzeugen, die den gleichen Fingerabdruck hat. Die Chance, aus zwei unterschiedlichen Texten einen identischen Fingerabdruck zu generieren, sollte eins zu unendlich sein, kann aber nie völlig ausgeschlossen werden. Eine kryptographische Hash-Funktion hat also folgende Eigenschaften:

- Die Eingabedaten können beliebig gross sein.
- Die Ausgabe ist normalerweise von konstanter Länge.
- Die Funktion muss schnell berechenbar sein.
- Die Funktion muss kollisionsfrei sein, d.h. für verschiedene Input-Daten darf nie derselbe Hashwert errechnet werden.
- Es muss eine Einweg-Funktion sein, d.h. aus dem Hashwert können keine Schlüsse auf die Inputdaten gezogen werden.
- Die Länge der Nachricht beeinflusst die kryptographische Prüsumme.
- Jedes Bit der Nachricht beeinflusst jedes Bit der kryptographischen Prüsumme. Bei einer Änderung eines Bits in der Nachricht besteht eine Änderungswahrscheinlichkeit von 50% für jedes Bit der Prüfsumme.
- Der Algorithmus der Hash-Funktion muss öffentlich bekannt sein. Die Sicherheit darf nicht von der Kenntnis oder Unkenntnis des verwendeten Verfahrens abhängen.

Ron Rivest entwickelte - zusammen mit anderen Mitarbeitern der RSA Data Security - eine Reihe von Hashfunktionen MD1(?), MD2, MD3, MD4 bis MD5, die gemeinhin auch als Synonym für den Message Digest gelten. Die Algorithmen akzeptieren als Eingabe eine Botschaft beliebiger Länge und erzeugen einen "digitalen Fingerabdruck" von 128 Bit Länge als Ausgabe. Die Chance, aus zwei unterschiedlichen Texten einen identischen Fingerabdruck zu generieren, ist beinahe unendlich, kann aber nicht völlig ausgeschlossen werden.

MD5 ist wohl zur Zeit die am weitesten verbreitete Hashfunktion. Sie ist aus MD4 entstanden und dabei in erster Linie um deren Unsicherheiten auszuräumen. Wie bei MD4 wird zu Beginn die Länge der Nachricht auf ein Vielfaches von 512 Bit gebracht, indem eine 1 und entsprechend Nullen sowie die Länge der Ursprungsnachricht - im 64 Bit Format - angehängt werden. Auch der Puffer und dessen Initialisierung sind gleich.
Eine genaue Beschreibung finden Sie in RFC 1321: MD5 Message Digest Algorithm; R. Rivest, April 1992. In Perl steht MD5 im Modul `Digest::MD5` bereit (`use Digest::MD5 qw(md5_hex`). Erzeugt wird ein Hash mit `$digest = md5_hex($string);`.

Es werden nun ein Server und ein Client in Perl vorgestellt. Die Authentisierung läuft folgendermaßen ab:

1. Nach dem Connect meldet sich der Server mit der Zeichenkette "AUTH", gefolgt von Newline. Danach folgen T und Z1 in jeweils einer neuen Zeile. T ist die aktuelle Zeit (GMT), die in Perl mit der Funktion `gmtime()` zu bekommen ist. Z1 ist eine vierstellige Zufallszahl (z. B. mit `substr(rand(),2,4)` zu finden). Dies ist die "Challenge".
2. Nachdem er die drei Zeilen vom Server erhalten hat, sendet der Client zwei Zeilen. Zuerst einen MD5-Hash von (T,Z1,P), wobei P das Passwort ist und authentisiert sich damit gegenüber dem Server. In der zweiten Zeile sendet er seine Zufallszahl Z2.
3. Der Server antwortet mit einem MD5-Hash von (T,Z2,P) und authentisiert sich so gegenüber dem Client.
4. Tritt im Verlauf dieser Authentisierung ein Fehler auf (nicht übereinstimmende Hashes, etc.) wird die Verbindung mit einer entsprechenden Meldung beendet.
5. Sind beide Partner glücklich, sendet der Client eine Zeile mit Relais-Steuerkommandos, die den Parametervereinbarungen des Programms `relais` entsprechen. Dieses Programm wird aus dem Perl-Server als Systemaufruf (Backquotes) gestartet.
6. Der Server sendet die Ausgaben von `relais` an den Client zurück, worauf beide die Verbindung beenden.

#### Das Client-Programm in Perl

    #!/usr/bin/perl
    # Client fuer die Relaissteuerung
    use warnings;
    use strict;

    use IO::Socket;
    use Digest::MD5  qw(md5 md5_hex md5_base64);

    my $DEBUG = 1;                 # 1 -> Debug-Infos, 0 -> Normalbetrieb
    my $SERVER = "localhost";      # Da haengt die Schaltbox dran
    my $PORT = 666;                # wird sonst von Doom verwendet
    my $PASS= "geheim";            # das geheime Wort
    my $TIMEOUT = 50;

    my ($eingabe, $ausgabe, $rs, $com);

    my $sock = new IO::Socket::INET(PeerAddr => $SERVER,
                                    PeerPort => $PORT,
                                    Proto    => 'tcp',
                                    Timeout  => $TIMEOUT)
               || die "Can't connect to server: $@\n";

    $eingabe = <$sock>; chomp($eingabe);
    if ($eingabe ne "AUTH")
      { print "Authentication failed\n"; close($sock); exit(1); }
    $eingabe = <$sock>; chomp($eingabe);
    print "Got: $eingabe\n" if($DEBUG);
    $rs = <$sock>; chomp($rs);
    print "Got: $rs\n" if($DEBUG);
    $ausgabe = md5_hex($eingabe, $rs, $PASS);
    print $sock "$ausgabe\n";
    print "Sent: $ausgabe\n" if($DEBUG);
    $ausgabe = substr(rand(),2,4);
    print $sock "$ausgabe\n";
    print "Sent: $ausgabe\n" if($DEBUG);
    $ausgabe = md5_hex($eingabe, $ausgabe, $PASS);
    $eingabe = <$sock>; chomp($eingabe);
    print "Got: $eingabe\n" if($DEBUG);
    if($eingabe ne $ausgabe)
      { print "Authentication failed\n"; close($sock); exit(1); }
    print $sock "@ARGV\n";
    $com = <$sock>;
    print "Returned $com\n" if($DEBUG);

#### Das Server-Programm

    #!/usr/bin/perl -w
    # Server fuer Relaissteuerung

    use strict;
    use IO::Socket;
    use Digest::MD5  qw(md5 md5_hex md5_base64);

    my $PORT = 666;
    my $PASS= "geheim";

    my ($rc, $sock, $client, $tim, $rs, $hs, $p, $com, $res);

    $sock = new IO::Socket::INET(LocalPort => $PORT,
                                 Reuse => 1,
                                 Listen => 5)
              || die "Can't create local socket : $@\n";

    print "Accepting connections on port ",$PORT, "...\n";
    while ($client = $sock->accept())
      {
      print "Accepted connection from ",
            $client->peerhost(), ":", $client->peerport(), "\n";

      $tim = gmtime();
      $rs = substr(rand(),2,4);
      print $client "AUTH\n$tim\n$rs\n";

      next if (<$client> ne (md5_hex($tim, $rs, $PASS))."\n");
      $rc = <$client>; chomp ($rc);
      print $client md5_hex($tim, $rc, $PASS)."\n";

      next if (! defined($com = <$client>));
      chomp ($com);
      print "Commandline: $com\n";             # Fuer den Test
      $res = `/usr/local/bin/relais $com`;
      print $client "$res\n";
      }
      continue { $client->close(); }

### CGI-Schnittstelle

Die Übertragung zwischen Relais-Server und Client ist nun möglich (sogar mit gesichertem Protokoll). Das CGI-Programm enthält natürlich den Client und wertet zusätzlich die Eingaben eines Formulars aus. Wenn zwischen HTTP-Client (nicht zu verwechseln mit unserem Relais-Client) und Wevserver das HTTPS-Protokoll verwendet wird, ist die gesamte Übertragungsstrecke gesichert.

Das Formular hat folgenden Quellcode:

    <TABLE BGCOLOR="#FFFFFF" ALIGN=CENTER BORDER=0 CELLPADDING=0 CELLSPACING=0 WIDTH="80%">
    <TR><TD VALIGN=TOP ALIGN=LEFT><IMG SRC="relais1.jpg"></TD>
    <TD>
    <CENTER>
    <H2>Relais-Steuerung</H2>
    <FORM ACTION="/cgi-bin/relais.cgi" METHOD="POST">
    <TABLE BGCOLOR="#EEEEEE" BORDER=0 CELLSPACING=0 CELLPADDING=5>
    <TR>
    <TD BGCOLOR="#EEFF99" VALIGN=TOP>Relais 1</TD>
    <TD BGCOLOR="#EEFF99" VALIGN=TOP>
    <INPUT TYPE="radio" NAME="1" VALUE="1">On
    <INPUT TYPE="radio" NAME="1" VALUE="0">Off </TD>
    </TR><TR>
    <TD BGCOLOR="#EEEEEE" VALIGN=TOP>Relais 2</TD>
    <TD BGCOLOR="#EEEEEE" VALIGN=TOP>
    <INPUT TYPE="radio" NAME="2" VALUE="1">On
    <INPUT TYPE="radio" NAME="2" VALUE="0">Off </TD>
    </TR><TR>
    <TD BGCOLOR="#EEFF99" VALIGN=TOP>Relais 3</TD>
    <TD BGCOLOR="#EEFF99" VALIGN=TOP>
    <INPUT TYPE="radio" NAME="3" VALUE="1">On
    <INPUT TYPE="radio" NAME="3" VALUE="0">Off </TD>
    </TR><TR>
    <TD BGCOLOR="#EEEEEE" VALIGN=TOP>Relais 4</TD>
    <TD BGCOLOR="#EEEEEE" VALIGN=TOP>
    <INPUT TYPE="radio" NAME="4" VALUE="1">On
    <INPUT TYPE="radio" NAME="4" VALUE="0">Off </TD>
    </TR><TR>
    <TD BGCOLOR="#EEFF99" VALIGN=TOP>Relais 5</TD>
    <TD BGCOLOR="#EEFF99" VALIGN=TOP>
    <INPUT TYPE="radio" NAME="5" VALUE="1">On
    <INPUT TYPE="radio" NAME="5" VALUE="0">Off </TD>
    </TR><TR>
    <TD BGCOLOR="#EEEEEE" VALIGN=TOP>Relais 6</TD>
    <TD BGCOLOR="#EEEEEE" VALIGN=TOP>
    <INPUT TYPE="radio" NAME="6" VALUE="1">On
    <INPUT TYPE="radio" NAME="6" VALUE="0">Off </TD>
    </TR><TR>
    <TD BGCOLOR="#EEFF99" VALIGN=TOP>Relais 7</TD>
    <TD BGCOLOR="#EEFF99" VALIGN=TOP>
    <INPUT TYPE="radio" NAME="7" VALUE="1">On
    <INPUT TYPE="radio" NAME="7" VALUE="0">Off </TD>
    </TR><TR>
    <TD BGCOLOR="#EEEEEE" VALIGN=TOP>Summer</TD>
    <TD BGCOLOR="#EEEEEE" VALIGN=TOP>
    <INPUT TYPE="radio" NAME="8" VALUE="1">On
    <INPUT TYPE="radio" NAME="8" VALUE="0">Off </TD>
    </TR><TR>
    <TD BGCOLOR="#EEEE99" VALIGN=TOP>Passwort:
        <INPUT TYPE="PASSWORD" NAME="PASS" LENGTH=8></TD>
    <TD BGCOLOR="#EEEE99" VALIGN=TOP>
        <INPUT TYPE="SUBMIT" VALUE=" Absenden "></TD>
    </TR>
    </TABLE>
    </FORM>
    </CENTER>
    </TD>
    <TD VALIGN=BOTTOM ALIGN=RIGHT><IMG SRC="relais2.jpg"></TD>
    </TR></TABLE>

Das Formular stellt sich dann wie folgt auf dem Bildschirm dar (und ist, bei Kenntnis des Passworts, auch von hier aus nutzbar):

| ## Relais-Steuerung
Relais 1 | On Off
Relais 2 | On Off
Relais 3 | On Off
Relais 4 | On Off
Relais 5 | On Off
Relais 6 | On Off
Relais 7 | On Off
Summer | On Off
Passwort: |

### Der Server als Daemon

Bisher wurde zu Testzwecken der Server immer von der Konsole oder vom Terminalfenster aus gestartet. Das Programm läßt sich mit den Erkenntnissen aus der Vorlesung aber recht einfach zum Daemon umgestalten. Der Server wird als Kindprozeß in den Hintergrund verlagert und die Standard-Ausgabe/Standard-Fehlerausgabe in eine Log-Datei umgeleitet. Auf das Anlegen einer PID-Datei wird im folgenden Beispiel verzichtet.

    #!/usr/bin/perl
    # Server fuer Relaissteuerung

    use strict;
    use warnings;
    use POSIX 'setsid';
    use POSIX ':sys_wait_h';
    use IO::Socket;
    use Digest::MD5  qw(md5 md5_hex md5_base64);

    # Configuration section ----------------------------
    my $DEBUG = 1;                    # 1 -> Debug-Infos, 0 -> Normalbetrieb
    my $PORT = 666;                   # wird sonst von Doom verwendet
    my $PASS= "geheim";               # das geheime Wort
    my $LOGFILE = "/var/log/rel.log"; # Protokolldatei
    # --------------------------------------------------

    my ($child, $rc, $sock, $client, $tim, $rs, $hs, $pid, $com, $res);

    # Exit-Handler setzen
    $SIG{TERM} = $SIG{INT} = sub { exit(0); };

    # Daemon werden
    $child = fork();
    if ($child < 0) { die "Cannot fork!\n"; }
    exit(0) if ($child > 0);          # Eltenprozess beendet sich
    &setsid();                        # Abtrennen
    open(STDIN, "</dev/null");        # Standarddateien umlenken
    open(STDOUT, ">$LOGFILE");
    open(STDERR, ">&STDOUT");
    chdir('/tmp');                    # Arbeitsverzeichnis /tmp
    umask(0);                         # UMASK definieren
                                      # Pfad definiert setzen:
    $ENV{PATH} = '/bin; /sbin; /usr/bin; /usr/sbin; /usr/local/bin;';

    $sock = new IO::Socket::INET(LocalPort => $PORT,
                                 Reuse => 1,
                                 Listen => 5)
              || die "Can't create local socket : $@";

    # Ins Logfile schreiben (Startmeldung):
    print "Accepting connections on port ",$PORT,"...\n";
    while ($client = $sock->accept())
      {
      # Ins Logfile schreiben:
      print "Accepted connection from ",
            $client->peerhost(), ": ", $client->peerport(), "\n";

      $client->autoflush;
      # ----------- Hier wird jetzt die eigentliche Arbeit gemacht

      $tim = gmtime();
      $rs = substr(rand(),2,4);
      print $client "AUTH\n$tim\n$rs\n";

      next if (<$client> ne (md5_hex($tim, $rs, $PASS))."\n");
      $rc = <$client>; chomp ($rc);
      print $client md5_hex($tim, $rc, $PASS)."\n";

      next if (! defined($com = <$client>));
      chomp ($com);
      print "Commandline: $com\n" if ($DEBUG);
      $res = `/usr/local/bin/relais $com`;     # Kommandoaufruf
      print $client "$res\n";

      # ---------- Ende-Behandlung: Verbindung beenden, Prozess beenden
      $client->close;
      }
    continue { $client->close(); }

Die Programme und Dateien können über folgende Links heruntergeladen werden:

- Steuerprogramm 'relais.c'
- Client-Programm
- Server-Programm
- Relais-Daemon
- CGI-Steuerprogramm
- HTML-Formular

Um die Sache schliesslich vollständig zu automatisieren, kann man in `/etc/init.d` noch ein Start/Stopp-Skript anlegen. Die folgende Version arbeitet unter der Debian-Distribution:

    #!/bin/sh
    # Start/stop the relais daemon.

    test -f /root/bin/relais-daemon.pl || exit 0

    case "$1" in
    start)  echo -n "Starting Relais-Daemon"
            start-stop-daemon --start --quiet --exec /root/bin/relais-daemon.pl
            echo "."
            ;;
    stop)   echo -n "Stopping Relais-Daemon"
            kill -HUP `ps ax | grep relais-daemon.pl | grep -v grep | awk '{print $1}'`
            echo "."
            ;;
    restart) echo -n "Restarting Relais-Daemon"
            $0 stop
            sleep 2
            $0 start
            echo "."
            ;;
    *)      echo "Usage: /etc/init.d/relais start|stop|restart"
            exit 1
            ;;
    esac
    exit 0

In den entsprechenden Runlevel-Verzeichnissen müssen dann noch die symbolischen Links (`S99relais` bzw. `K01relais`) angelegt werden.

Zum vorhergehenden Abschnitt | Zum Inhaltsverzeichnis | Zum nächsten Abschnitt

###### Copyright © FH München, FB 04, Prof. Jürgen Plate

###### Letzte Aktualisierung: 25. Feb 2013