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:

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:

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:

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: 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">

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: Es können bis zu 255 Relaiskarten kaskadiert werden (255 Relaiskarten * 8 Relais = 2040 schaltbare Relais insgesamt). Wer sich weiter informieren will, kann sich 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
-statStatus der Relais als Dezimalzahl. Bit = 1: Relais an, Bit = 0: Relais aus. Es sind keine weiteren Parameter möglich
-offalle Relais aus
-onalle Relais an
-sxRelais x einschalten (1 <= x <= 8)
-rxRelais 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:

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:

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:

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