#!/usr/bin/perl
package filtersurf;

our $u; # Major Versionsnummer
our $v; # Minor Versionsnummer

# Version: 2.15
($u,$v)=(2,15);


################################################################################
#
# Dieser Squid-Redirector ist eine Komponente von FilterSurf
# Details und Verwendungshinweise gibt es unter http://www.filtersurf.de/
# 
# Bitte lesen Sie Informationen in dieser Datei sowie die ausführlichen
# Erklärungen in der Konfigurationsdatei.
# 
# Copyright (C) 2004-2006
# FilterSurf GbR: Christian Ludwig, Dominik Herrmann (mail@filtersurf.de)
#
# $Id: redirector.pl 744 2006-12-23 14:43:54Z luchr $
#
# 
# LIZENZBEDINGUNGEN: GNU GENERAL PUBLIC LICENSE (GPL)
# 
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place, Suite 330, Boston, MA  02111-1307  USA
#
#
# VERWENDUNG UND AUFRUF
#
# Der FilterSurf-Redirector wird als redirect_program im Proxy-Server Squid
# eingebunden. Er kann jedoch zu Testzwecken auch direkt in einer Shell
# gestartet werden (s. unten). Vor der Verwendung sollten Sie die Config-Datei
# an Ihre Bedürfnisse anpassen.
#
# a) ./redirector.pl
#    Verwendet die Konfigurationsdatei redirector.conf im aktuellen
#    Verzeichnis
# 
# b) ./redirector.pl /pfad/zur/redirector.conf
#    Verwendet die angegebene Konfigurationsdatei
#
#
# TEST DER FUNKTIONSFÄHIGKEIT (Z.B. UNTER LINUX):
# 
# * wget www.filtersurf.de/downloads/redirector.pl (redirector herunterladen)
# * chmod a+x redirector.pl (allen Benutzern das Ausführen erlauben)
# * ./redirector.pl (redirector starten)
#   - Wenn sich der redirector sofort wieder beendet, liegt ein Fehler vor! Mit
#     tail /var/log/messages oder tail /var/log/syslog in den Logs nachsehen.
#   - Mögliche Fehlerursachen: Ein Perl-Modul konnte nicht geladen werden, der
#     shared cache konnte nicht angelegt werden, Perl-Version ist zu alt,
#     DNS Anfrage für fsredir.dyndns.org war nicht möglich
# * Wenn der redirector läuft, eine Test-Anfrage von Hand stellen (z.B.):
#   http://www.google.de/
#   Wenn alles klappt, erscheint kurz darauf eine Zeile tiefer
#   http://www.google.de/
# * Dann noch eine Test-Anfrage stellen, die geblockt wird (z.B.):
#   http://www.sex.de
#   Es sollte eine URL erscheinen, die so ähnlich aussieht:
#   http://fsredir.dyndns.org/accessdenied.php?u=2&v=11&req=http%3a%2f%2fsex.de
# * Zum Beenden des Tests Ctrl-D drücken, nun wie in der Installationsanleitung
#   auf der FilterSurf-Homepage nachzulesen den redirector in squid integrieren
#
#
# CHANGELOG:
# * TODO neuer Konfigurationsparamter: deny_ip_hosts
#   -> Alle URLs mit IPs als Host sperren
# 
# Version 2.15 (22.12.2006)
# * Wenn Redirector beendet wird, gibt es einen log-Eintrag
# * Für den Bypass wird jetzt die URL bypass.filtersurf.de verwendet 
#   (die alte URL "filtersurf.bypass" konnte nicht aufgelöst werden, so dass
#   dieses Feature nur hinter einem Proxy funktionierte)
# 
# Version 2.14b - nicht-veröffentlichte Version (01.12.2006) 
#                 Klaus Thilman <kth@ic-team.de>
# * Bei Verwendung einer Squid-Auth-Loesung (z.B. an LDAP) wird 
#   jetzt bei "ACCDENY" der Benutzernamen mit in das LOG geschrieben
#
# Version 2.14 (12.03.06)
# * Redirector leert beim Start den shared cache (rc4)
# * Redirector loggt immer seine PID (rc3)
# * Beim Start loggt Redirector parent PID (rc3)
# * Bei Verwendung von fs_server_host wird kontrolliert,
#   ob fs_server_host leer ist und in diesem Fall 
#   conf::filtersurfserver verwendet (rc2)
# * Code aufräumen und dokumentieren (rc1)
# * fscached vollständig in Redirector integriert (rc1)
# * neuer Konfigurationsparameter: fscached_max_entries (rc1)
# 
# Version 2.13 - nicht-veröffentlichte Version (07.01.06)
# * Die conf-Datei kann ab V2.13 auch mit dem Tool unter der URL
#   http://www.filtersurf.de/configurator.php erstellt werden.
# * conf-File kann jetzt als 1. Command Line Argument übergeben werden.
# * Ein Fallback-Server kann im conf-File definiert werden. Dieser springt
#   dann ein, wenn der Haupt-Server nicht erreichbar ist oder ausgefallen ist.
# * Falls erforderlich, kann ein Username zusätzlich zur ID definiert werden.
# * Das Verbindungs-Timeout ist nun in der conf-Datei einstellbar.
# * Einstellbare default action, wenn kein FilterSurf-Server erreichbar sein
#   sollte oder die Lizenz ungültig ist.
# * Bugfixes beim Laden des conf-Files.
# * Voraussetzungen für die benutzerdefinierte Access-Denied-Seite geschaffen.
# * Ab jetzt wird das lokale Caching durch den fscached (FilterSurf Cache 
#   Daemon) durchgeführt (default: keinen fscached verwenden).
# * Der Pfad zum conf-File ist jetzt im Redirector einstellbar.
# * Lokale Blacklisten und Whitelisten können nun in eigene Dateien ausgelagert
#   werden. Hierzu muss im conf-File die includefile-Funktion verwendet werden.
#
# Version 2.12 (21.05.05)
# * Änderungen am DNS-Lookup-Code:
#   Wenn der initiale DNS-Lookup fehlschlägt (bei Konfiguration ohne
#   Proxy-Server), beendet sich der Redirector ab jetzt nicht mehr
#   sofort. Stattdessen versucht er nach kurzem Warten erneut. Erst
#   nach einigen Fehlversuchen gibt er auf und beendet sich.
# * Google SafeSearch Unterstützung:
#   Der Redirector unterstützt nun die Safesearch-Funktion von google.
#   Dadurch werden unerwünschte Seiten (üblicherweise Kategorie adult/porn)
#   schon gar nicht mehr in den Ergebnislisten angezeigt.
#   Standardmäßig ist die Verwendung von SafeSearch ab jetzt aktiviert. Sie
#   lässt sich jedoch über die Konfigurationsdatei ausschalten.
# * Bugfix: zwischen den Parametern m und i muss ein & stehen!
#
# Neues in 2.11 (01.02.05)
# Diese Version ist ein größeres Update mit vielen neuen Features...
# * Neues Caching-Verfahren mit Cache::FastMmap, falls auf dem System vorhanden
#   Andernfalls wird wie bisher ein lokaler, dedizierter Cache für jeden
#   Redirector verwendet.
# * Einfache Tests zur Überprüfung der Funktionsfähigkeit am Anfang des
#   Redirector-Skripts hinzugefügt.
# * Vereinheitlichte (englische) Log-Meldungen ausformuliert.
# * Einige einfache Sanity Checks vorm Absenden eines Requests an den Server.
# * Führendes "www." wird nun für die Filter-Entscheidung komplett ignoriert.
# * Integration der GNU Public License in den Source-Code.
# * Bei jedem DNS-Update wird der lokale Cache geleert.
# * Änderungen am Protokoll:
#   - Die maximale Länge des Pfads (inkl. Teil hinter dem ?) ist begrenzt
#   - Wenn ein ACCESS_GRANT_NOCACHE empfangen wird, ist die URL zwar erlaubt,
#     sie darf jedoch nicht gecached werden, weil einige Pfade unter dieser
#     URL geblockt werden sollen 
# * Neue Konfigurationsoption: Der Redirector kann alle Zugriffe in einer
#   Logdatei protokollieren (Zeit, Source-IP, Adresse, Filter-Ergebnis)
# * Die Syslog Facility kann konfiguriert werden (default: local0).
# * In der Konfigurationsdatei kann nun die User-ID eingetragen werden,
#   die nach Ablauf des Pilotprojekts zur Authentifizierung erforderlich ist.
# * Der zu verwendende FilterSurf-Server kann ebenfalls konfiguriert werden.
# 
# Neues in 2.10
# * Whitelist, die lokal gepflegt werden kann
# * Unterstützung für Kategorien
# 
# Neues in 2.9
# * Bugfix: Der Redirector beendet sich nun nicht mehr mit einer
#   Perl-Fehlermeldung, wenn die erste DNS-Abfrage nach dem Starten ergebnislos
#   blieb (z.B. wegen falschen Routen oder Internet-Einstellungen)
# * Zusätzlicher timeout in http_request, ob bei gelungem 
#   Verbindungsaufbau eine Antwort in annehmbarer Zeit kommt.
# * Bugfix: URLs mit Sonderzeichen werden nun lesbarer geloggt.
# 
# Neues in 2.8
# * Proxy-Support (wenn http-Anfragen über proxy laufen sollen)
# * redirector.conf Datei mit Konfigurationen für Zwangsproxy
# * lokale Liste mit zu sperrenden Domains (seit Version 2.6)
#   ist auch in dieses File gewandert.
#
# Neues in 2.7
# * Ein Bugfix für URLs mit Großbuchstaben wurde eingebaut.
# * Der Timeout für die Verbindung zum FilterSurf Server wurde auf 5 Sek.
#   hochgesetzt (wichtig für ISDN-Verbindungen).
#
# Neues in 2.6
# * Es wird nun eine in diesem File veränderbare Liste von
#   Domains unterstützt, die (ohne den FilterSurf-Server zu fragen)
#   gesperrt werden sollen. [Implementiert auf Wunsch von J. Hasselbeck] 
#
# Neues in 2.5
# * Im lokalen Cache werden nur noch die Hostnamen gespeichert
# * Suche im Cache durch Perl's grep ersetzt
# 
# Neues in 2.4
# * Kosmetische Änderung: $q entfernt
# * Verwendung von gethostbyname zur DNS-Abfrage, jetzt wird die IP auch
#   korrekt aktualisiert
# 
# Neues in 2.3
# * Der DNS-Lookup auf filtersurf.de wird ab jetzt nicht mehr nur beim Starten
#   einmalig durchgeführt, sondern alle 12 Stunden. Es wird also 2x am Tag ein
#   DNS-Lookup durchgeführt, der die aktuelle IP ermittelt. Wenn der Server also
#   wieder einmal seine IP ändert, dann bekommt der Redirector spätestens 1 Tag
#   danach dies automatisch mit und ist wieder voll funktionsfähig.
# * Für die URL-Requests wird nun nicht mehr www.filtersurf.de verwendet,
#   sondern fsredir.dyndns.org. Dadurch wird für die Betreiber des FilterSurf-
#   Servers und die Benutzer des FilterSurf-Redirectors das automatische
#   Umschalten auf einen zweiten Server erleichtert, um z.B. am ersten Server
#   in Ruhe Wartungsarbeiten durchführen zu können.
# * Die accessdenied-Seite wurde aktualisiert. Sie zeigt nun die Version des
#   verwendeten Redirectors an.
#
# Neues in 2.2 (nicht veröffentlicht)
# * Diese Version kommt mit möglichst wenigen Modulen aus. Dazu wurde der
#   EventLog-Support unter Windows wieder entfernt. Fehlermeldungen
#   werden stattdessen direkt auf STDERR ausgegeben. Sie werden dadurch
#   an die Squid Log-Datei unter <squid-verzeichnis>/log/cache.log angehängt
#   Unter Linux wird weiterhin der Syslog verwendet.
# * Die Version des Redirectors wird im HTTP-Request (User Agent) übermittelt
#   Dadurch kann auf dem Server ausgewertet werden, welche Versionen in Umlauf
#   sind.
# * Aufräumarbeiten und viele Kommentare eingefügt
# * Erste Version, die es als ausführbare EXE-Datei für Windows gibt.
#   (noch nicht veröffentlicht)
#
# Neues in 2.1 (nicht veröffentlicht)
# * Unterstützung für ActiveState ActivePerl hinzugefügt
#   Dazu: Perl-Modul Syslog wird nur unter Linux geladen, unter Windows
#   werden die Meldungen in die Windows Ereignisanzeige geschrieben
#   (dazu wird das Modul EventLog verwendet).
#
# Neues in 2.0
# * Vollkommener Verzicht auf das CGI-Modul. Das URL-Escapen erfolgt
#   jetzt durch eine Regexp (http://forums.devshed.com/archive/t-26950)
# * Der GET-Request-Teil (hinter dem ?) einer URL wird nicht mehr
#   zum filtersurf Server mitgeschickt. Spart Bandbreite und ist
#   aus Datenschutzgesichtspunkten sicherer.
# * Es wird nur beim Starten 1x ein DNS Lookup auf den filtersurf.de
#   Server durchgeführt, um Zeit zu sparen.
#
# Neues in 1.9
# * Bugfix: Bei Socketfehlern syslog Eintrag statt STDOUT
# * Timeout beim Connect auf 2 Sekunden reduziert
#
# Neues in 1.8:
# * Verzicht auf das LWP-Modul; Verwendung von IO::Socket stattdessen
# * Neue Methode http_request
# * => erheblicher Geschwindigkeitsvorteil (ca. Faktor 2 bis 3)
# * Cache von 50 auf 150 Domain-Cache-Entries erhöht
#
# Neues in 1.7:
# * Es wurde auf use strict; umgestellt,
#   um Fehler schneller entdecken zu können.
#
# Neues in 1.6:
# * CGI start ohne Kommandozeilenparameter von der Konsole
#
# Neues in 1.5:
# * Bug im Cache-Algorithmus entfernt
#
# Neues in 1.4:
# * Bei Anfrage wird Version des redirectors mitgeschickt:
#   Format: V1.4 => u=1&v=4
#
# Neues in 1.3:
# * lokaler Cache mit Arrays
#
# Neues in 1.2:
# * Deutsche Kommentare
#   Zeitmessung für Request; bei über 3 Sekunden => log
#
################################################################################


# Variablen müssen vor Benutzung deklariert werden
use strict;

# Zum Debuggen
# use warnings;

# Um Pfadangaben zu splitten
use File::Basename;

# Socket-Modul für Kommunikation mit FilterSurf-Server
use IO::Socket::INET;

# Socket-Modul für Kommunikation mit fscache daemon
use IO::Socket::UNIX;

# MD5-Modul für Bypass-Funktion
use Digest::MD5 qw(md5_hex md5_base64);



# BEGINN FUNKTIONEN ############################################################

### Konfiguration und Environment

# Herausfinden, auf welchem Betriebssystem das Skript ausgeführt wird
# Wichtig für Entscheidung, ob syslog für die Logs verwendet werden soll,
# oder Fehlermeldungen auf STDERR ausgegeben werden sollen.
# Wenn wir weder auf Linux noch auf Windows laufen, dann gibt es hier
# eventuell Probleme. Wir nehmen dann an, dass wir zumindest *nicht* unter
# Linux laufen.
sub isLinux {
  our $isLinux;
  my $os = $^O;

  if(! ($os =~ /[Ll]inux/)) { $isLinux=0; } else { $isLinux=1; }
  return $isLinux;
}

# load_config
# lädt die Config-Datei. Sofern beim Aufruf eine Datei übergebene wurde,
# wird diese verwendet, ansonsten wird nach der redirector.conf gesucht.
sub load_config {
  our $conffile;

  # initialisieren der conf-Variablen mit Standard-Werten
  $conf::proxy_server_ip = '';
  $conf::proxy_server_port ='';
  @conf::locallist = ();
  @conf::localwhitelist = ();
  $conf::categories = 3;
  $conf::googlesafesearch = 1;
  $conf::access_log = "0";
  $conf::syslog_facility = 'local0';
  $conf::fscached_sock = '/tmp/fscached';
  $conf::fscached_expiration_time = 24*60*60;
  $conf::fscached_max_entries = 10000;
  $conf::accessdenied_design = '';
  @conf::bypasscredentials = ();
  $conf::bypasstime = 60*60; # 1h
  $conf::filtersurfserver = 'srv1.filtersurf.de';
  $conf::fallbackserver= 'srv2.filtersurf.de';
  $conf::fallbackthreshold=3;
  $conf::resolveinterval=12*60*60;
  $conf::connectiontimeout=20;
  $conf::defaultaction='ACCESS_GRANT';
  $conf::userid = '';
  $conf::username = '';

  # Wenn beim Einlesen des Config-Files Fehler auftreten, dann werden diese hier
  # abgespeichert und ins Log geschrieben, sobald das Logging verfügbar ist.
  $conf::_conferror = '';

  # Wenn der User einen Dateinamen übergeben hat, diesen verwenden.
  # Wenn es dann aber keine Datei mit diesem Namen gibt, einen Fehler
  # ins Log schreiben.
  if($#ARGV>=0 && ( $ARGV[0] !~ /^--/ ) ) {
    $conffile=$ARGV[0];
    $conf::_conferror.="Could not read config file $conffile!\n" 
      unless -e $conffile;
  }

  # Falls weder Dateiname übergeben wurde (noch oben ein default eingetragen
  # wurde), wird jetzt die redirector.conf in dem Verzeichnis gesucht, wo
  # auch dieses Perl-File liegt.
  if($conffile eq '') {
    $conffile=dirname "$0";
    $conffile=$conffile."/redirector.conf";
  }

  # Config-Datei einlesen
  if(-e $conffile) {
    $conf::_conferror.="Reading config file $conffile\n";
    do "$conffile";
  } else {
    $conf::_conferror.="No config file found - using defaults\n";
  }

  # UserID für Transport vorbereiten
  $conf::userid.='.'.$conf::username if($conf::username ne '');
}

### Wrapper-Funktionen für das Logging

# startlog: muss immer vor dem 1. writelog aufgerufen werden!
sub startlog {
  our ($isLinux);

  openlog('redirector','',shift) if $isLinux;
}

# writelog - einen Log-Eintrag schreiben
# Achtung: level muss ein gültiger syslog level sein!
# Unter Windows werden nur Fehler (level eq "err") geloggt!
sub writelog {
  our $isLinux;
  my ($level,$msg) = @_;
  if($isLinux) {
    $msg =~ s/%/%%/g;
    syslog($level,"[$$] $msg"); 
  }
  else {
    # Fehlerausgabe (nur schwerwiegende Fehler!) auf STDERR
    if($level eq 'err') {print STDERR $msg . "\n";}
  }
}

# accesslog - einen Access-Log-Eintrag schreiben
# 0. Arg: ClientIP
# 1. Arg: URL
# 2. Arg: Result
# verwendet dazu writelog von oben
sub accesslog {
  my ($clientip,$url,$result)=@_;
  if(!defined($clientip)) {$clientip='0.0.0.0';}
  else { $clientip=substr($clientip,0,index($clientip,'/')); }
  writelog('info',"[$clientip] $result $url");
}

# endlog - vor dem Programmende aufzurufen
sub endlog {
  our $isLinux;

  closelog() if $isLinux;
}

### Cache 

# Der lokale Domain-Cache sollte regelmäßig gelöscht werden, damit
# sichergestellt ist, dass der Redirector keine Entscheidungen auf veralteter
# Datenbasis trifft
sub init_localcache {
  our ($sharedcache,@localcache);
  # Verwenden wir einen dedizierten Cache?
  if(!$sharedcache) {
    writelog('notice', "Cache: Flushing dedicated cache.");
    @localcache=();
  }
}


# query_fscached
# Eine Anfrage an den fscached schicken
# 1. Arg: Anfragetext
# 2. Arg: zu verwendende Socket (optional, default ist $g_fscached_sock)
# Returns: Das Result als String oder 0 im Fehlerfall
sub query_fscached {
  our $g_fscached_sock;
  my ($query,$sock)=(shift,shift);
  my $resp;

  $sock=$g_fscached_sock if (!defined($sock));
  
  $sock=new IO::Socket::UNIX(Peer=>$sock,Type=>SOCK_STREAM,Timeout=>1);
  if(!$sock) {
    writelog('err', "Error connecting to fscached: " . $@);
    return 0;
  }
  
  $sock->autoflush(1);

  print $sock $query."\n";
  $resp='';
  while (<$sock>) {
    last if /^\.\n$/;
    $resp.=$_;
  }
  close($sock); $sock=undef;
  return $resp;
}

# cache_get:
# Eine Domain im Cache suchen
# Das Suchen funktioniert je nach verwendetem Cache unterschiedlich
sub cache_get {
  our ($sharedcache,@localcache);
  my $entry=shift;

  if($sharedcache) {return (query_fscached("get $entry") =~ /^100 /);}
  else {return grep { $_ eq $entry } @localcache;}
}

# cache_set:
# Eine Domain in den Cache schreiben
# fscached nimmt als 2. Parameter die expiration time in Sekunden entgegen
sub cache_set {
  our ($sharedcache,@localcache,$max_cache_entries);
  my ($entry,$value)=@_;
  my $fscachedcmd;

  if($sharedcache) {
    $fscachedcmd="add $entry $value";
    query_fscached($fscachedcmd);
  } else {
    if (scalar(@localcache)>=$max_cache_entries) {shift(@localcache);}
    push(@localcache,$entry);
  }
}

### Access grant/deny

# access_denied:
# Browser auf die Zugriff-Verweigert-Seite umleiten
# 0. Arg: url
# 1. Arg: url (escaped)
# 2. Arg: Client-IP
# 3. Arg: Log-String
# 4. Arg: zusätzliche Parameter
sub access_denied {
  our ($fs_server_host,$sharedcache,$u,$v); 
  my $strdesign='';
  my $bypass='';

  my $params = $_[4] || '';$params='&'.$params if $params;
  
  # Wenn ein spezielles Access-Denied Design gewünscht ist, den in der
  # Konfigurationsdatei hinterlegten Bezeichner an die accessdenied.php
  # Seite übermitteln. Dies kann für OEM-Lösungen benötigt werden.
  if($conf::accessdenied_design ne '') {
    $strdesign="&d=".$conf::accessdenied_design;
  }

  # Wenn bypasscredentials gegeben sind, dann das der accessdenied-Seite
  # mitteilen, damit diese ggf. ein Formular für die Bypass-Funktion
  # darstellen kann. Darüber hinaus funktioniert die Bypass-Funktion nur,
  # wenn der fscached verwendet wird!
  if(@conf::bypasscredentials>=2 && length($conf::bypasscredentials[0])>0 &&
    $sharedcache) {
    $bypass="&by=1";
  }
  print 'http://'.($fs_server_host?$fs_server_host:$conf::filtersurfserver).
        '/accessdenied.php?' .
        "u=$u&v=$v&i=".$conf::userid . $strdesign . $bypass .
        "&bytime=".$conf::bypasstime."&req=$_[1]$params\n";
  accesslog($_[2],$_[0],$_[3]) if ($conf::access_log);
}

# access_grant:
# Die URL ist erlaubt und wird durchgelassen
# 0. Arg: URL
# 1. Arg: Client-IP
# 2. Arg: Log-String 
# Wenn gewünscht wird Google-Safe-Search aktiviert
# Falls Logging aktiv ist, wird geloggt
sub access_grant {
  my $url=shift;

  # Diese regex ist noch verbesserungswürdig:
  # !!! neue RegExp gesucht
  $url.='&safe=on' if 
    ($conf::googlesafesearch) && ($url =~ m#^http://[^/]*google\.[a-zA-Z]+/.*\?.*$#);

  print "$url\n";
  if ($conf::access_log) {
    my ($clientip,$msg)=(shift,shift);
    accesslog($clientip,$url,$msg);
  }
}

# default Action
# Eingestellte Default-Action ausführen
# 0. Arg: URL 
# 1. Arg: URL escaped für Deny-Seite
# 2. Arg: ClientIP
sub do_default_action {
  if($conf::defaultaction eq 'ACCESS_GRANT') {
    access_grant($_[0],$_[2],"ACCGRANT DEFAULTACTION");
  } else { # wenn $conf::defaultaction nicht 'ACCESS_GRANT', dann DENY
    access_denied($_[0],$_[1],$_[2],"ACCDENY DEFAULTACTION","l=2")
  }
}

# String escapen
# Es wird in situ escaped
# 0. Arg: String
sub escape_string {
  $_[0] =~ s/([^@\w.-])/ ($1 eq ' ')?'+':sprintf("%%%2.2x",ord($1)); /eg
}

# URL in ihre Bestandteile zerlegen
# 0. Arg: URL
# 1. Arg: Referenz auf Hash; 
# Im übergebenem Hash werden folgende Keys 
# gespeichert
# proto,     www,     host,     path,     sep
# proto_esc, www_esc, host_esc, path_esc, sep_esc
sub parse_url {
  my $ref=$_[1];

  $_[0]=~ m{^                                # Beginn der URL
            (?:                              # Protokoll:// nur Cluster 
              ([^:]*)://                     # Protokoll         -> $1
            )?                               # Protokoll gefolgt von :// optional
            (www\.)?                         # www. optional     -> $2
            ([^/]*)                          # host              -> $3
            (?:/                             # Pfad nur Cluster 
              (.*)                           # Pfad (ohne slash) -> $4
            )?                               # Pfad ist optional
            $                                # Ende der URL
           }x;
  escape_string($$ref{'proto_esc'} = $$ref{'proto'} = $1     || '' );
  escape_string($$ref{'www_esc'}   = $$ref{'www'}   = $2     || '' );
  escape_string($$ref{'host_esc'}  = $$ref{'host'}  = lc($3) || '' );  
  escape_string($$ref{'path_esc'}  = $$ref{'path'}  = $4     || '' );
  escape_string($$ref{'sep_esc'}   = $$ref{'sep'}   = '/'          );
}

# handle_bypass
# Behandelt bypass requests
# 0. Arg: URL
# 1. Arg: Client-IP
sub handle_bypass {
  my @result;        # Ergebnis von Suchen
  my $key_value;     # key=value String in Parameter
  my ($key,$value);  # key und value getrennt
  my %urlparams;     # Hash mit key,value Paaren
  my %URL;           # zerlegte URL

  # Den Hash überprüfen: URL nicht kopieren! 
  @result = $_[0] =~ m#.*/[^/]*\?([^?]*)$#;
  if (!scalar(@result)>0) { 
    # wenn überhaupt keine Parameter übertragen wurden, dann soll der
    # Browser mal sehn, was er davon hat -> nix!
    access_grant($_[0],$_[1],"ACCGRANT"); 
    return;
  }
  
  # An & auftrennen
  @result = split(/&/, $result[0]);

  # Der nachfolgende Code ist angelehnt an CGI_Lite,
  # wird für das Auslesen von URL-GET-Parametern verwendet.
  foreach $key_value (@result) {
    if($key_value eq '') { next; }
    
    ($key, $value) = split (/=/, $key_value, 2);
    
    $key   =~ s/%([\da-fA-F]{2})/chr (hex ($1))/eg if defined($key);
    $value =~ s/%([\da-fA-F]{2})/chr (hex ($1))/eg if defined($value);
    
    $urlparams{$key} = $value;
  }
  # ENDE des entlehnten Codes.

  # Defaults setzen
  $urlparams{'req'}="undefined" unless defined($urlparams{'req'});
  $urlparams{'hash'}='' unless defined($urlparams{'hash'});
  $urlparams{'bytime'} = $conf::bypasstime
    unless(defined($urlparams{'bytime'}) &&
    ($urlparams{'bytime'} =~ /^[0-9]+$/));
  
  # kein Parameter hash vorhanden -> dann interessiert uns das gar nicht
  # -> Erlauben!
  if(!$urlparams{'hash'}) {
    access_grant($_[0],$_[1],"ACCGRANT");
    return;
  }
  
  # Jetzt werden die Credentials geprüft
  if($urlparams{'hash'} eq md5_hex($conf::bypasscredentials[0],
     $conf::bypasscredentials[1],$urlparams{'req'})) {

    # credentials sind korrekt -> req cachen und dorthin umleiten
    # Negative Zeit, damit der Cache bei Speicherknappheit diesen
    # Eintrag auf keinen Fall löscht.

    %URL=();parse_url($urlparams{'req'},\%URL);
    cache_set($URL{'host'},-$urlparams{'bytime'});
    
    # Wichtig: Eine für den Browser erkennbare Umleitung durchführen 
    # (HTTP REDIRECT Permanently Moved). Details siehe Squid FAQ unter
    # http://www.squid-cache.org/Doc/FAQ/FAQ-15.html

    access_grant("301:".$urlparams{'req'},$_[1],"ACCGRANT BYPASSED");
    return;
  } else {
    # Falsche Credentials. Fehlerseite anzeigen:
    access_denied($urlparams{'req'},$urlparams{'req'},$_[1],"ACCDENY BYPASS_AUTHFAILED",
      "by=authfailed");
  }
  return;
}

# check_url
# Schickt eine URL an den FilterSurf-Server zur Überprüfung.
# Ausnahme: URL ist bereits im lokalen Cache oder der lokalen
# white- oder blacklist enthalten
# 0. Arg: url
# 1. Arg: Client-IP
# 2. Arg: Ident 
# 3. Arg: method
# url (0. Arg) wird NICHT kopiert
sub check_url {
  our ($u,$v);
  our ($maxdomainlength,$maxpathlength); 
  our ($fs_server_host,$fs_server_ip,$fs_server_port);
  our ($count_requests_failed);

  my $clientip = $_[1];    # Parameter abholen: Client-IP
  my $ident = $_[2] || ''; # Parameter abholen: Ident 
  my $url_request_trimmed; # URL escaped für Übertragung
  my $url_request;         # URL escaped für Access Denied Seite
  my $req;                 # HTTP-Request
  my $begintime;           # Zeitmessung: Anfragestart
  my $endtime;             # Zeitmessung: Anfrageende
  my %URL;                 # Hash mit folgenden Keys
                           # proto,     www,     host,     path,     sep
                           # proto_esc, www_esc, host_esc, path_esc, sep_esc
  
  do { print "\n";return; } if (!defined($_[0]) || (!$_[0]));
  
  # URL zerlegen und jeweils auch escape-Varianten speichern
  # host wird dabei in Kleinbuchstaben konvertiert
  %URL=();parse_url($_[0],\%URL);

  #### 0. War es ein Bypass-Request? Dann die GET-Parameter auswerten ####
  do {handle_bypass($_[0],$clientip); return;} if ($URL{'host'} eq 'bypass.filtersurf.de');

  #### 1. Kein Bypass-Request; Domain im lokalen Cache suchen ####
  
  # den lokalen Cache durchsuchen: ist die gesuchte Domain enthalten?
  
  # wenn die Domain im Cache enthalten ist, ersparen wir uns die Abfrage
  # des filtersurf-Servers. Die Domain wurde ja erst kürzlich mal erlaubt.
  # Andernfalls fragen wir den filtersurf-Server, was von der URL zu halten
  # ist...
  do {access_grant($_[0],$clientip,"ACCGRANT CACHED");return;} if cache_get($URL{'host'});
  
  #### 2. URL war nicht im lokalen Cache. Übertragung vorbereiten ####

  # Die URL zusammenbauen, die an den FilterSurf-Server geschickt wird.
  # Maximale Pfadlänge beachten
  $url_request_trimmed = 
    $URL{'host_esc'}.$URL{'sep_esc'}.substr($URL{'path_esc'},0,$maxpathlength);

  # url_request wird für Fehlermeldungen, Logs und accessdenied.php verwendet
  # => hier die original URL mit vollem Pfad merken, aber ALLES escaped
  $url_request.= ($URL{'proto'}?$URL{'proto_esc'}.':'.$URL{'sep_esc'}.$URL{'sep_esc'}:'').
                 $URL{'host_esc'}.$URL{'sep_esc'}.$URL{'path_esc'};

  # Sanity check der Domain (inkl. dem führenden www.)
  if(length($URL{'www'})+length($URL{'host'})>$maxdomainlength) {
    # Die URL wird gesperrt => umleiten auf die FilterSurf-Fehlerseite:
    writelog('warning', "URL=$_[0] violates RFC1035 (Domain length exceeds " . 
                        "$maxdomainlength characters). URL will be DENIED!");
    access_denied($_[0],$url_request,$clientip,"ACCDENY".(length($ident)?" ($ident)":''));
    return;
  }
   
  #### 3. URL in den lokalen Listen (black- und whitelist) suchen #### 

  # Die lokale Blacklist durchsuchen. Wenn die URL dort steht, wird sie sofort
  # gesperrt, ohne den Server zu fragen.
  do {access_denied($_[0],$url_request,$clientip,"ACCDENY LOCALLIST","l=1");
      return;} if (grep { $URL{'host'} =~ m/$_$/ } @conf::locallist);
   
  # die lokale whitelist durchsuchen
  # Wenn $URL{'host'} so endet wie ein Eintrag in der local whitelist, 
  # dann will der Benutzer diese URL explizit erlauben
  do {access_grant($_[0],$clientip,"ACCGRANT LOCALLIST");return;} if
     (grep { $URL{'host'} =~ m/$_$/ } @conf::localwhitelist);

  #### 4. URL an den FilterSurf-Server senden, Filter-Ergebnis abholen. ####

  # Zeitmessung für den HTTP-Request starten...
  $begintime=time();
   
  # Request an FilterSurf-Server schicken
  # Den DNS-Query haben wir ja schon beim Starten gemacht - das spart Zeit!
  # Die aktuelle Version der Redirectors wird übermittelt, um
  # abwärtskompatible Antworten am Server generieren zu können

  # Übermittelt wird:
  # $url_request_trimmed  (URL ohne www.; mit abgeschnittenem Pfad)
  # $u, $v                (FilterSurf-Redirector-Version)
  # $conf::categories     (die unterwünschten bzw. zu filternden Kategorien)
  # $conf::userid         (die User-ID zur Authentifizierung)
  
  my ($headers,$contents,$error) = http_request($fs_server_ip,$fs_server_host,
    $fs_server_port,"http://$fs_server_host/request.php?".
    "url=$url_request_trimmed".
    "&u=$u&v=$v&m=".$conf::categories.'&i='.$conf::userid);
   
  # Zeitmessung STOP!
  $endtime=time();
   
  # Fehlerbehandlung, evtl. umschalten auf Fallback-Server
  if($error) {
    if(!$fs_server_host) {
      # Wenn dieser Fall eintritt, ist derzeit überhaupt kein FS-Server verfügbar
      # Es macht also keinen Sinn, auf den Fallback-Server umzuschalten.
      writelog('err', "No FS-Server available presently. Using default action");
    } else {
      # Der angegebene FilterSurf-Server war nicht erreichbar oder
      # der Request konnte nicht bearbeitet werden.
      # Das kann auch bedeuten, dass keine Verbindung (connect)
      # innerhalb des Socket Timeouts zustande gekommen ist.
      $count_requests_failed++;
      if($count_requests_failed>=$conf::fallbackthreshold) {
        # Wenn wir derzeit nicht den Fallback-Server verwenden, auf diesen
        # umschalten
        # Wichtig: Wenn beim Umschalten auch der Fallback-Server einen Timeout
        # hat oder nicht eingetragen ist, wird fs_server_host auf "" gesetzt.
        # Bis zum nächsten Aufruf von regular_update wird dann nicht mehr 
        # versucht, einen FS-Server zu verwenden.
        if($conf::fallbackserver && $fs_server_host ne $conf::fallbackserver) {
          writelog('warning', "Switching over to Fallback-Server.");
          if(use_server($conf::fallbackserver)) {
            writelog('warning', "Fallback-Server is working.");
            # Fallback-Server vorhanden und erreichbar, Request noch
            # einmal senden
            $begintime=time();
            ($headers,$contents,$error) = http_request($fs_server_ip,
              $fs_server_host,$fs_server_port,"http://$fs_server_host".
              '/request.php?url=' .
              "$url_request_trimmed&u=$u&v=$v&m=".$conf::categories.'&i='.
              $conf::userid);
            $endtime=time();
          }
        } else {
          writelog('warning', "No FilterSurf-Server available. All requests ".
                   "will be served with default action");
          $fs_server_host='';
        }
      }
    }
  }

  # Fehlerbehandlung - DefaultAction
  do {do_default_action($_[0],$url_request,$clientip);return;} if $error;
  
  # Der Request war erfolgreich -> weiter
  $count_requests_failed=0;
  
  # wenn der gesamte Request länger als 3 Sekunden dauert,
  # dann gibt es evtl. ein Problem mit dem Netzwerk. Schreiben es mal ins Log.
  if (($endtime-$begintime)>3) {
    my $difference = $endtime-$begintime;
    writelog('warning',"URL=$url_request, Slow response from FilterSurf-Server!  ".
	     "Check your network setup! RESULT=$contents, " . 
	     "Duration=$difference");
  }
 
  #### 5. Ergebnis auswerten und Filter-Entscheidung treffen ####

  ## ACCESS_GRANT_NOAUTH
  # Keine Anmeldedaten übertragen oder diese sind falsch.
  # *Alle* URLs werden daher erlaubt.
  if(index($contents,"ACCESS_GRANT_NOAUTH")>=0) {
    access_grant($_[0],$clientip,"ACCGRANT_NOAUTH");
    return;
  }

  ## ACCESS_GRANT_NOLIC
  # Anmeldedaten übertragen. Diese sind korrekt, Lizenz ist jedoch abgelaufen.
  # *Alle* URLs werden daher erlaubt.
  if(index($contents,"ACCESS_GRANT_NOLIC")>=0) {
    access_grant($_[0],$clientip,"ACCGRANT_NOLIC");
    return;
  }
   
  ## ACCESS_GRANT_NOCACHE
  # Der angefragte Request darf *nicht* gecached werden (Es gibt Pfade in
  # dieser Domain, die gefiltert werden sollen)
  if(index($contents,"ACCESS_GRANT_NOCACHE")>=0) {
    access_grant($_[0],$clientip,"ACCGRANT_NOCACHE");
    return;
  }
   
  ## ACCESS_GRANT
  # Die URL wurde erlaubt.
  # 1. URL unverändert zurückliefern (wichtig bei Redirector-Verkettung)
  # 2. Cache-Eintrag anlegen
  if(index($contents,"ACCESS_GRANT")>=0) {
    access_grant($_[0],$clientip,"ACCGRANT");
    cache_set($URL{'host'},$conf::fscached_expiration_time);
    return;
  }

  ## ACCESS_DENY_NOLIC
  # Anmeldedaten übertragen. Diese sind korrekt, Lizenz ist jedoch abgelaufen.
  # *Alle* URLs werden daher gesperrt.
  if(index($contents,"ACCESS_DENY_NOLIC")>=0) {
    access_denied($_[0],$url_request,$clientip,"ACCDENY_NOLIC","l=3");
    return;
  }
  
  ## ACCESS_DENY_NOAUTH
  # Keine Anmeldedaten übertragen oder diese sind falsch.
  # *Alle* URLs werden daher gesperrt.
  if(index($contents,"ACCESS_DENY_NOAUTH")>=0) {
    access_denied($_[0],$url_request,$clientip,"ACCDENY_NOAUTH","l=4");
    return;
  }
  
  ## ACCESS_DENY 
  if(index($contents,"ACCESS_DENY")>=0) {
    # Die URL wird gesperrt => umleiten auf die FilterSurf-Fehlerseite:
    access_denied($_[0],$url_request,$clientip,"ACCDENY".(length($ident)?" ($ident)":''));
    return;
  }

  ## WAS NUN? KEINE GÜLTIGE ANTWORT GEFUNDEN...
  # -> Die default action schlägt zu!
  writelog('warning', "Invalid response from server. Using default action.");
  do_default_action($_[0],$url_request,$clientip);
}

# In regelmäßigen Zeitabständen DNS-Lookup und Cache leeren
# Den lokalen Cache leeren, damit keine Leichen (=veraltete 
# ACCESS_GRANT-Domains) im Cache liegen bleiben
sub regular_update {
  our ($fs_server_host,$fs_server_ip);
  our ($lastupdate);

  my $firstcall=0;
  my $res;
  my $new_date=time;
  
  # Wenn die letzte Query weniger als resolveinterval Sekunden zurück liegt,
  # gibt es nichts tun
  if(($new_date-$lastupdate) < $conf::resolveinterval ) { return;  }

  if(!$fs_server_ip) { $firstcall=1; }
  
  $lastupdate = $new_date;
  
  if($firstcall) {
    writelog('notice', 'Initializing...');
  } else {
    writelog('notice', 'Testing availability of FilterSurf-Server...');
  }
  
  $fs_server_host="";
  
  init_localcache();
  $res=use_server($conf::filtersurfserver);
  if(!$res) { # FilterSurf-Server not responding
    writelog('err', 'FilterSurf-Server '.$conf::filtersurfserver.' is not '.
             'responding. Trying fallbackserver...');
    $res=use_server($conf::fallbackserver);
    if(!$res) { # Fallback-Server not responding either
      writelog('err', 'Fallback server is not responding either. Using '.
               'default action ('.$conf::defaultaction.') for all requests');
    } else { # Fallback-Server is responding
      writelog('warning', 'Using fallbackserver '.$conf::fallbackserver);
    }
  } else { # FilterSurf-Server is responding
    writelog('warning', 'Using FilterSurf-Server '.$conf::filtersurfserver);
  }
}


# DNS Lookup auf FilterSurf Server durchführen
# Returns: IP-Adresse als String oder "" im Fehlerfall
sub dns_lookup {
  our $dns_norace_maxtries;
  my $server = shift;
  
  my ($packed_ip,$unpacked_ip,$result_ip);
  my ($i,$randval);

  $packed_ip=gethostbyname($server);$i=0;
  while (!$packed_ip) {
    last if $i>=$dns_norace_maxtries; 
    $randval=rand(13);
    writelog('warning', 'DNS lookup for '.$server.' ' .
          "failed. This happened already $i " .
          "times. Will try again in ".int($randval)." seconds...");
    sleep($randval);
    $packed_ip=gethostbyname($server);$i++;
  }

  # wenn auch mehrere Versuche erfolglos bleiben, dann Fehler zurückliefern
  if (!$packed_ip) {
    writelog('warning', 'DNS lookup for '.$server.' ' .
          "failed $i times ... got no IP");
    return '';
  }
  
  # Die IP konnte erfolgreich ermittelt werden -> weiter!
  
  $unpacked_ip = join(".",unpack("C4",$packed_ip));
  if(!$unpacked_ip) { return ''; }
  else { return $unpacked_ip; }
}


# Versucht, den angegebenen Server zu verwenden
# 1. Arg: Server-DNS-Name
# Return: 1 im Erfolgsfall, 0 im Fehlerfall
sub use_server {
  our ($u,$v);
  our ($fs_server_host,$fs_server_ip,$fs_server_port);

  my $server=shift;
  my $ip;
  # Wenn der Server nicht definiert ist, können wir auch nicht switchen!
  if($server eq '') {
    $fs_server_host=""; $fs_server_ip=""; return 0;
  }

  # Wenn Proxy verwendet wird, keine DNS-Abfrage durchführen
  if($conf::proxy_server_ip ne "") {
    writelog('notice', "DNS lookup: using a proxy server => skipping lookup.");
    $fs_server_ip=$conf::proxy_server_ip;
    $fs_server_port=$conf::proxy_server_port;
  } else {
    $ip=dns_lookup($server);
    if(!$ip) {
      $fs_server_host=""; $fs_server_ip=""; return 0; 
    }
    $fs_server_ip=$ip;
    writelog('notice',"DNS lookup: $server has $ip");
  }
  
  # Jetzt noch einen Test-Request an den Server schicken
  my ($headers,$content,$error)=http_request($fs_server_ip,
    $server,$fs_server_port,
    "http://$server/request.php?url=fsredirtest".
    "&u=$u&v=$v&i=".$conf::userid);
  if($error) {
    writelog('err', "Invalid or missing response from $server"); 
    $fs_server_host=""; $fs_server_ip=""; return 0;
  }

  $fs_server_host=$server;

  writelog('notice',"Received valid response. Set server to $server ".
           "successfully.");
  
  return 1;
}

# Hier wird der eigentliche HTTP Request abgeschickt
# 1. Arg: IP-Adresse
# 2. Arg: Servername
# 3. Arg: Port
# 2. Arg: URL
sub http_request {
  # syscall(&SYS_gettimeofday, $start, 0) != -1 or die "gettimeofday: $!";
  my ($host,$servername,$port,$target)=(shift,shift,shift,shift);
  my $cr="\015\012";
  my $socketerror=0;
  my ($contents,$headers)=(undef,undef);
  
  if(!$servername) {
    $socketerror = "No FilterSurf-Server available";
    return ($headers, $contents, $socketerror);
  }
  
  my $sock = new IO::Socket::INET (PeerAddr => $host,
                                   PeerPort => $port,
                                   Proto    => 'tcp',
                                   Type     => SOCK_STREAM,
                                   Timeout  => $conf::connectiontimeout # Sek.
                                  );
  
  # Wenn wir erfolgreich connected sind, haben wir ein Amt bekommen (Socket)
  if($sock)
  {
    $sock->autoflush(1); # So schnell wie möglich alles senden
    
    # HTTP-1.0-Anfrage basteln
    print $sock "GET $target HTTP/1.0$cr";
    print $sock "Host: $host$cr$cr"; 

    # Kommt innerhalb der Timeout-Zeit etwas auf dem Socket an?
    my $sel=new IO::Select($sock);
    if (!$sel->can_read($conf::connectiontimeout)) 
    {
      $socketerror = "HTTP error: Connected successfuly but request timed out!";
      return ($headers, $contents, $socketerror);
    }

    # HTTP-Header empfangen
    while (<$sock>)
    {
      last if /^\015?\012/; # Ende bei LF oder CRLF
      $headers.= $_ ;
    }

    # Header aufdröseln
    $headers=~ s/\015?\012[ \t]+/ /g ;
    
    # HTTP-Body empfangen
    while (<$sock>) {$contents.=$_;}
    
    # Status Code 200? => Request konnte verarbeitet werden
    if(! ($headers =~ /HTTP\/1\.[01] 200/))
    {
      $socketerror = "HTTP Error: HTTP Headers: $headers";
    }
    close $sock;
  } else {
    # Connect war nicht möglich => den Connection-Fehler melden
    $socketerror = $@;
  }
  return ($headers, $contents, $socketerror);
}

# fscached suchen und ggf. als Child starten
# 0. Arg: Socket-Path
# 1. Arg: maximale Anzahl von Cache-Entries
# Returns: 
#     0: fscached nicht gefunden, konnte auch nicht gestartet werden
#     1: fscached lebendig vorgefunden mit Version 1
#     2: fscached nicht gefunden oder tot, konnte aber gestartet werden
sub find_or_start_fscached {
  my ($sock,$maxentries)=(shift,shift);

  # Überprüfen, ob bereites ein Socket vorhanden ist
  if(-S $sock) {
    # Socket-File vorhanden; lauscht da auch jemand, der 
    # Version 1 spricht?
    if (query_fscached("ver",$sock) =~ /^1\n100 OK$/) {
      # Kommunikation erfolgreich => fscached verwenden!
      # fscached muss nicht gestartet werden
      # flush cache
      query_fscached("flush",$sock);
      return 1;
    } else {
      writelog('err',"Error: found socket file '$sock' for fscached, but ".  
               "I got no response or the fscached has wrong version!");
    }
  } 

  writelog('notice',"I [$$] try to start fscached");
  # fscached muss also gestartet werden
  unlink $sock;

  # forken
  $SIG{CHLD}="IGNORE";
  if (!fork) {
    # Child Process
    package fscached;
    main_loop($sock,$maxentries);
    exit(0);
  } else {
    # Parent Process
    sleep(1);
  }

  # Überprüfen, ob fscached auch anwesend
  return 2 if (-S $sock) && (query_fscached("ver",$sock) =~ /^1\n100 OK$/);

  return 0;
}

# Signal-Handler
sub handleSignals {
  exit 0;
}


# ENDE FUNKTIONEN ##############################################################



# WEITERE PAKETE ###############################################################

### Konfiguration 

package conf;

# nachfolgende Funktionen werden aus dem Config-File heraus aufgerufen

use strict;
# use warnings;


# Eine Textdatei einlesen und den Inhalt als Zeilen-Array zurückliefern
# 1. Param: Filename inkl. Pfad
# Return:   Array mit dem Dateiinhalt
sub includefile {
  our $_conferror;
  my @lines; my $fname = shift;

  if(!open(FH, "<".$fname)) {
    $_conferror.= "Could not open file '$fname' for inclusion!\n";return;
  }
  @lines = <FH>; close(FH);
  chomp(@lines);
  return @lines;
}

### Zentraler Cache
package fscached;

use strict;
use Socket;

# global Errorcodes:
# <100 : Error 
# >=100: OK
#
# description cf. sendResult
# 
#     1 cannot parse command-line
#     2 unknown command
#     3 not enough arguments for command
#   100 Object found/OK
#   101 Object not found
#   102 Object expired

# cache: Hash-Array: 
# key is URL
# value is reference to hash-array containing
#    timestamp  : expires at this time
#    dontremove : flag: do not automatically 
#                 remove before expiring
our %cache=();

# maxsize: maximal size of cache
our $cachemax;

# Notes to maxsize:
# if an element is added to the cache and 
# cachemax is exceeded, then the the oldest
# entry (the one which expires next) is removed
# (unless the dontremove flag is set).
# Hence, it is possible that the cache size
# is bigger than cachemax (if for example all
# entries have the dontremove flag).

# Path to socket filehandle
our $socketfile;

# Removes all out-of-date entries
sub removeOldEntries {
  our %cache;
  my $now=time();

  foreach (grep { ($cache{$_}->{'timestamp'})<$now } keys %cache) {
    delete $cache{$_};  
  }
}

# Removes oldest entry that is not marked as dontremove
# (regardless if's out-of-date or not)
# oldest: that's the entry that is next to expire
# Returns the key that was deleted or undef.
sub removeOldestEntry {
  our %cache;
  my ($key,$ref,$ts);
  my ($min,$minkey)=(0,undef);

  while (($key,$ref)=each %cache) {
    unless ($cache{$key}->{'dontremove'}) {
      $ts=$cache{$key}->{'timestamp'};
      if (($min==0) || ($ts<$min)) {
        $min=$ts;$minkey=$key;  
      }
    }
  }
  if (defined($minkey)) {
    delete $cache{$minkey};
    return (wantarray?($minkey):$minkey);
  } else {
    return (wantarray?():undef);
  }
}

# checks cache size
# removeOldEntries and (if necessary) the oldest entry.
sub checkCacheSize {
  our (%cache,$cachemax);

  return if scalar(keys %cache)<=$cachemax;
  removeOldEntries;
  while ((scalar(keys %cache)>$cachemax) && (removeOldestEntry)) {};
}

# send result
# 1. Arg: filehandle for result
# 2. Arg: errorcode
# 3. Arg: user defined text
sub sendResult {
  my ($filehandle,$code,$text)=(shift,shift,shift);
  if (!defined($text) || (!$text)) {
    $_=$code;
    /^1$/   && do {$text="cannot parse command-line";};
    /^2$/   && do {$text="unknown command";};
    /^3$/   && do {$text="not enough arguments for command";};
    /^100$/ && do {$text="OK";};
    /^101$/ && do {$text="Object not found";};
    /^102$/ && do {$text="Object expired";};
  }
  print $filehandle $code," ",$text,"\n";
  print $filehandle ".\n";
}

# Prints version 
# 1. Arg: filehandle for result
sub printVersion {
  my ($filehandle)=(shift);

  print $filehandle "1\n";
  sendResult($filehandle,100,undef);
}

# Prints current time
# 1. Arg: filehandle for result
sub printTime {
  my ($filehandle)=(shift);

  print $filehandle time,"\n";
  sendResult($filehandle,100,undef);
}

# Prints whole cache
# (after removing out-of-date entries)
# 1. Arg: filehandle for result
sub printCache {
  our %cache;
  my ($filehandle)=(shift);
  my ($key,$hashref); 

  removeOldEntries;
  while (($key,$hashref)=each %cache) {
    print $filehandle $key,' ',($$hashref{'timestamp'});
    print $filehandle '  (*)' if ($$hashref{'dontremove'});
    print $filehandle "\n";
  }
  sendResult($filehandle,100,undef);
}

# Add item to cache
# 1. Arg: filehandle for result
# 2. Arg: key
# 3. Arg: expires after ... seconds (delay)
#         if (3. Arg)<0 then the dontremove flag ist set
sub addToCache {
  our %cache;
  my ($filehandle,$key,$delay)=(shift,shift,shift);
  my $rflag=0;
  
  $rflag=1 if ($delay<0);
  $cache{$key}={'timestamp'=>time()+abs($delay), 'dontremove'=>$rflag};
  checkCacheSize;
  sendResult($filehandle,100,undef);
}

# Remove item from cache
# 1. Arg: filehandle for result
# 2. Arg: key
sub removeFromCache {
  our %cache;
  my ($filehandle,$key)=(shift,shift);

  delete $cache{$key};
  sendResult($filehandle,100,undef);
}

# Removes all items form cache
# 1. Arg: filehandle for result
sub clearCache {
  our %cache;
  my ($filehandle)=(shift);

  %cache=();
  sendResult($filehandle,100,undef);
}

# Search item in cache
# 1. Arg: filehandle for result
# 2. Arg: key
sub isInCache {
  our %cache;
  my ($filehandle,$key)=(shift,shift);
  my ($now,$hashref);

  if (defined($cache{$key})) {
    $now=time();$hashref=$cache{$key};
    if ($$hashref{'timestamp'}<$now) {
      delete $cache{$key};
      sendResult($filehandle,102,undef);
    } else {
      sendResult($filehandle,100,"Object found");
    }
  } else {
    sendResult($filehandle,101,undef);
  }
}

# Tries to execute the given command
# 1. Arg: filehandle for result
# 2. Arg: command
sub execCommand {
  my ($filehandle,$line)=(shift,shift);
  my (@parts,$len);
  chomp $line;
  @parts=split /\s+/,$line;
  $len=scalar(@parts);
  if ($len<1) {
    sendResult($filehandle,1,undef);
    return;
  }
  $_=$parts[0];

  /^ver$/   && do { printVersion $filehandle; return;};
  /^stat$/  && do { printCache $filehandle; return;};
  /^time$/  && do { printTime $filehandle; return;};
  /^flush$/ && do { clearCache $filehandle; return;};
  /^add$/   && do {
    if ($len<3) { sendResult($filehandle,3,undef);return;} else {
      addToCache $filehandle,$parts[1],$parts[2];return;
    }
  };
  /^del$/   && do {
    if ($len<2) { sendResult($filehandle,3,undef);return;} else {
      removeFromCache $filehandle,$parts[1];return;
    }
  };
  /^get$/   && do {
    if ($len<2) { sendResult($filehandle,3,undef);return;} else {
      isInCache $filehandle,$parts[1];return;
    }
  };

  sendResult($filehandle,2,undef);
}

# Close socket before termination
sub catch_term {
  our $socketfile;

  close SOCKHANDLE;
  unlink $socketfile;
  exit(0);
}

sub main_loop {
  our $socketfile = shift;
  our $cachemax = shift;
  my ($socketaddr,$line,$socket_exists);
  
  $socketfile='/tmp/fscached' if ((!defined($socketfile)) || (!$socketfile));
  $cachemax=1000 if ((!defined($cachemax)) || ($cachemax<2));

  $socket_exists=(-e $socketfile);
  
  #### TODO!
  # if($socket_exists && <connect an socket geht schief>) {
  #   unlink($socketfile)
  # }
  
  socket(SOCKHANDLE,PF_UNIX,SOCK_STREAM,0) or exit(2);
  
  $socketaddr=sockaddr_un($socketfile);
  bind(SOCKHANDLE,$socketaddr) or exit(3);
  
  $SIG{'TERM'}=\&catch_term;
  
  listen(SOCKHANDLE,SOMAXCONN) or catch_term(4); 
  
  while (1) {
    while (accept(CLIENTHANDLE,SOCKHANDLE)) {
      select((select(CLIENTHANDLE),$|=1)[0]);
      $line=<CLIENTHANDLE>;
      execCommand(*CLIENTHANDLE,$line) if ($line);
      close CLIENTHANDLE;
    }
  }
  
  exit(0);
}

# ENDE WEITERE PAKETE ##########################################################

package filtersurf;

# BEGINN HAUPTPROGRAMM #########################################################

# Alle Warnungen einschalten? Nur fürs Debugging!
# use warnings;

use sigtrap 'handler' => \&handleSignals, 'INT','TERM','QUIT';

# Hier kann der (absolute) Pfad zur redirector.conf-Datei eingetragen werden
# Default: ./redirector.conf (im gleichen Verzeichnis wie dieses Skript)
our $conffile='';

# Config-Datei einlesen
# Eventuell aufgetretetene Fehler melden, sobald Logging verfügbar ist, s. unten
load_config();


our $fs_server_host;           # DNS-Name des aktuellen FS-Servers
our $fs_server_ip;             # IP-Adresse des FilterSurf Servers
our $fs_server_port=80;        # Port des FilterSurf Servers
our @localcache;               # normaler dedizierter Cache für erl. URLs
our $max_cache_entries=150;    # max. Anzahl Einträge für @localcache
our $url;                      # zu überprüfende URL
our $isLinux=isLinux();        # Laufen wir unter Linux?
our $lastupdate=-1;            # Uhrzeit/Datum des letzten regular_updates
our $maxpathlength=30;         # Nur $maxpathlength Zeichen des Pfads übertragen
our $maxdomainlength=255;      # RFC 1035, max. Länge einer Domain
our $dns_norace_maxtries=3;    # wie oft soll ein DNS-Lookup versucht werden?
our $count_requests_failed=0;  # fehlgeschl. Req. seit letztem erfolgr. Request
our $g_fscached_sock;          # globale Variable für query_fscached
our $sharedcache=0;            # Boolean: fscached verwenden oder nicht?

srand(time); # Für den zufälligen Aufruf der DNS-Lookups

$|=1;           # Wenn möglich, alle Ausgaben sofort ohne Pufferung durchführen

# ENDE VARIABLEN DEKLARATIONEN #################################################


# Syslog oder EventLog Modul verwenden und initialisieren
if($isLinux) {
  eval ("use Sys::Syslog;");
  Sys::Syslog::setlogsock('unix');
}

startlog($conf::syslog_facility);
writelog('notice', "FilterSurf Redirector V$u.$v [$$] STARTED by ".getppid());

# Jetzt ggf. die Fehler im conf-file loggen
if($conf::_conferror ne "") {
  foreach (split("\n",$conf::_conferror)) {
    writelog('notice', "Config file: ".$_); 
  }
}


# Caching-Methode bestimmen
# Wenn fscached verwendet werden soll, dann jetzt die Verbindung testen:

# Wurde ein Pfad zum fscached socket angegeben?
if ($conf::fscached_sock ne '') {
  writelog('notice','Looking for fscached');
  my $erg=find_or_start_fscached($conf::fscached_sock,$conf::fscached_max_entries);
  if (!$erg) {
    writelog('err',"Error: I [$$] didn't find fscached at '$conf::fscached_sock' or its ".
             "version was wrong. I was not able to start a new fscached daemon");
    $sharedcache=0;$g_fscached_sock=undef;
  } 
  if ($erg==1) {
    writelog('notice',"I [$$] found fscached at '$conf::fscached_sock'");  
    $sharedcache=1;$g_fscached_sock=$conf::fscached_sock;
  } 
  if ($erg==2) {
    writelog('notice',"I [$$] started new fscached daemon at '$conf::fscached_sock'");
    $sharedcache=1;$g_fscached_sock=$conf::fscached_sock;
  }
}

if($sharedcache==0) {
  writelog('warning', "Warning: Using low-performance local cache!");
} else {
  writelog('notice', "fscached: expiration time $conf::fscached_expiration_time secs.");
}

## cacheonly gesetzt?
if($#ARGV>=0) {
  if($ARGV[$#ARGV] eq "--cacheonly") {
    writelog('notice', "cacheonly mode was requested; exiting...");
    exit(0);
  }
}

# Proxy-Server verwenden?
if($conf::proxy_server_ip ne "") {
  $fs_server_ip=$conf::proxy_server_ip;
  $fs_server_port=$conf::proxy_server_port;
  writelog('notice', "Using proxy server $fs_server_ip:$fs_server_port.");
}

# Lokalen Cache initialisieren und testen, ob FilterSurf-Server erreichbar ist
regular_update();


# Die URLs vom STDIN lesen, jede einzeln in einer eigenen Zeile...
while (<STDIN>) {
  # Argumente (vom Squid übergeben): URL, IP
  check_url(split);
  regular_update();
}

# Wenn wir hier ankommen: <EOF> wurde gesendet. Jetzt beenden!
# Log vorher ggf. noch schließen
exit(0);

## End-Block 
END {
  writelog('notice',"Terminating");
  endlog();
}


# ENDE HAUPTPROGRAMM ###########################################################



# SOFTWARE-LIZENZ ##############################################################
#
#
# 		    GNU GENERAL PUBLIC LICENSE
# 		       Version 2, June 1991
# 
#  Copyright (C) 1989, 1991 Free Software Foundation, Inc.
#                        59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#  Everyone is permitted to copy and distribute verbatim copies
#  of this license document, but changing it is not allowed.
# 
# 			    Preamble
# 
#   The licenses for most software are designed to take away your
# freedom to share and change it.  By contrast, the GNU General Public
# License is intended to guarantee your freedom to share and change free
# software--to make sure the software is free for all its users.  This
# General Public License applies to most of the Free Software
# Foundation's software and to any other program whose authors commit to
# using it.  (Some other Free Software Foundation software is covered by
# the GNU Library General Public License instead.)  You can apply it to
# your programs, too.
# 
#   When we speak of free software, we are referring to freedom, not
# price.  Our General Public Licenses are designed to make sure that you
# have the freedom to distribute copies of free software (and charge for
# this service if you wish), that you receive source code or can get it
# if you want it, that you can change the software or use pieces of it
# in new free programs; and that you know you can do these things.
# 
#   To protect your rights, we need to make restrictions that forbid
# anyone to deny you these rights or to ask you to surrender the rights.
# These restrictions translate to certain responsibilities for you if you
# distribute copies of the software, or if you modify it.
# 
#   For example, if you distribute copies of such a program, whether
# gratis or for a fee, you must give the recipients all the rights that
# you have.  You must make sure that they, too, receive or can get the
# source code.  And you must show them these terms so they know their
# rights.
# 
#   We protect your rights with two steps: (1) copyright the software, and
# (2) offer you this license which gives you legal permission to copy,
# distribute and/or modify the software.
# 
#   Also, for each author's protection and ours, we want to make certain
# that everyone understands that there is no warranty for this free
# software.  If the software is modified by someone else and passed on, we
# want its recipients to know that what they have is not the original, so
# that any problems introduced by others will not reflect on the original
# authors' reputations.
# 
#   Finally, any free program is threatened constantly by software
# patents.  We wish to avoid the danger that redistributors of a free
# program will individually obtain patent licenses, in effect making the
# program proprietary.  To prevent this, we have made it clear that any
# patent must be licensed for everyone's free use or not licensed at all.
# 
#   The precise terms and conditions for copying, distribution and
# modification follow.
#  
# 		    GNU GENERAL PUBLIC LICENSE
#    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
# 
#   0. This License applies to any program or other work which contains
# a notice placed by the copyright holder saying it may be distributed
# under the terms of this General Public License.  The "Program", below,
# refers to any such program or work, and a "work based on the Program"
# means either the Program or any derivative work under copyright law:
# that is to say, a work containing the Program or a portion of it,
# either verbatim or with modifications and/or translated into another
# language.  (Hereinafter, translation is included without limitation in
# the term "modification".)  Each licensee is addressed as "you".
# 
# Activities other than copying, distribution and modification are not
# covered by this License; they are outside its scope.  The act of
# running the Program is not restricted, and the output from the Program
# is covered only if its contents constitute a work based on the
# Program (independent of having been made by running the Program).
# Whether that is true depends on what the Program does.
# 
#   1. You may copy and distribute verbatim copies of the Program's
# source code as you receive it, in any medium, provided that you
# conspicuously and appropriately publish on each copy an appropriate
# copyright notice and disclaimer of warranty; keep intact all the
# notices that refer to this License and to the absence of any warranty;
# and give any other recipients of the Program a copy of this License
# along with the Program.
# 
# You may charge a fee for the physical act of transferring a copy, and
# you may at your option offer warranty protection in exchange for a fee.
# 
#   2. You may modify your copy or copies of the Program or any portion
# of it, thus forming a work based on the Program, and copy and
# distribute such modifications or work under the terms of Section 1
# above, provided that you also meet all of these conditions:
# 
#     a) You must cause the modified files to carry prominent notices
#     stating that you changed the files and the date of any change.
# 
#     b) You must cause any work that you distribute or publish, that in
#     whole or in part contains or is derived from the Program or any
#     part thereof, to be licensed as a whole at no charge to all third
#     parties under the terms of this License.
# 
#     c) If the modified program normally reads commands interactively
#     when run, you must cause it, when started running for such
#     interactive use in the most ordinary way, to print or display an
#     announcement including an appropriate copyright notice and a
#     notice that there is no warranty (or else, saying that you provide
#     a warranty) and that users may redistribute the program under
#     these conditions, and telling the user how to view a copy of this
#     License.  (Exception: if the Program itself is interactive but
#     does not normally print such an announcement, your work based on
#     the Program is not required to print an announcement.)
#  
# These requirements apply to the modified work as a whole.  If
# identifiable sections of that work are not derived from the Program,
# and can be reasonably considered independent and separate works in
# themselves, then this License, and its terms, do not apply to those
# sections when you distribute them as separate works.  But when you
# distribute the same sections as part of a whole which is a work based
# on the Program, the distribution of the whole must be on the terms of
# this License, whose permissions for other licensees extend to the
# entire whole, and thus to each and every part regardless of who wrote it.
# 
# Thus, it is not the intent of this section to claim rights or contest
# your rights to work written entirely by you; rather, the intent is to
# exercise the right to control the distribution of derivative or
# collective works based on the Program.
# 
# In addition, mere aggregation of another work not based on the Program
# with the Program (or with a work based on the Program) on a volume of
# a storage or distribution medium does not bring the other work under
# the scope of this License.
# 
#   3. You may copy and distribute the Program (or a work based on it,
# under Section 2) in object code or executable form under the terms of
# Sections 1 and 2 above provided that you also do one of the following:
# 
#     a) Accompany it with the complete corresponding machine-readable
#     source code, which must be distributed under the terms of Sections
#     1 and 2 above on a medium customarily used for software interchange; or,
# 
#     b) Accompany it with a written offer, valid for at least three
#     years, to give any third party, for a charge no more than your
#     cost of physically performing source distribution, a complete
#     machine-readable copy of the corresponding source code, to be
#     distributed under the terms of Sections 1 and 2 above on a medium
#     customarily used for software interchange; or,
# 
#     c) Accompany it with the information you received as to the offer
#     to distribute corresponding source code.  (This alternative is
#     allowed only for noncommercial distribution and only if you
#     received the program in object code or executable form with such
#     an offer, in accord with Subsection b above.)
# 
# The source code for a work means the preferred form of the work for
# making modifications to it.  For an executable work, complete source
# code means all the source code for all modules it contains, plus any
# associated interface definition files, plus the scripts used to
# control compilation and installation of the executable.  However, as a
# special exception, the source code distributed need not include
# anything that is normally distributed (in either source or binary
# form) with the major components (compiler, kernel, and so on) of the
# operating system on which the executable runs, unless that component
# itself accompanies the executable.
# 
# If distribution of executable or object code is made by offering
# access to copy from a designated place, then offering equivalent
# access to copy the source code from the same place counts as
# distribution of the source code, even though third parties are not
# compelled to copy the source along with the object code.
#  
#   4. You may not copy, modify, sublicense, or distribute the Program
# except as expressly provided under this License.  Any attempt
# otherwise to copy, modify, sublicense or distribute the Program is
# void, and will automatically terminate your rights under this License.
# However, parties who have received copies, or rights, from you under
# this License will not have their licenses terminated so long as such
# parties remain in full compliance.
# 
#   5. You are not required to accept this License, since you have not
# signed it.  However, nothing else grants you permission to modify or
# distribute the Program or its derivative works.  These actions are
# prohibited by law if you do not accept this License.  Therefore, by
# modifying or distributing the Program (or any work based on the
# Program), you indicate your acceptance of this License to do so, and
# all its terms and conditions for copying, distributing or modifying
# the Program or works based on it.
# 
#   6. Each time you redistribute the Program (or any work based on the
# Program), the recipient automatically receives a license from the
# original licensor to copy, distribute or modify the Program subject to
# these terms and conditions.  You may not impose any further
# restrictions on the recipients' exercise of the rights granted herein.
# You are not responsible for enforcing compliance by third parties to
# this License.
# 
#   7. If, as a consequence of a court judgment or allegation of patent
# infringement or for any other reason (not limited to patent issues),
# conditions are imposed on you (whether by court order, agreement or
# otherwise) that contradict the conditions of this License, they do not
# excuse you from the conditions of this License.  If you cannot
# distribute so as to satisfy simultaneously your obligations under this
# License and any other pertinent obligations, then as a consequence you
# may not distribute the Program at all.  For example, if a patent
# license would not permit royalty-free redistribution of the Program by
# all those who receive copies directly or indirectly through you, then
# the only way you could satisfy both it and this License would be to
# refrain entirely from distribution of the Program.
# 
# If any portion of this section is held invalid or unenforceable under
# any particular circumstance, the balance of the section is intended to
# apply and the section as a whole is intended to apply in other
# circumstances.
# 
# It is not the purpose of this section to induce you to infringe any
# patents or other property right claims or to contest validity of any
# such claims; this section has the sole purpose of protecting the
# integrity of the free software distribution system, which is
# implemented by public license practices.  Many people have made
# generous contributions to the wide range of software distributed
# through that system in reliance on consistent application of that
# system; it is up to the author/donor to decide if he or she is willing
# to distribute software through any other system and a licensee cannot
# impose that choice.
# 
# This section is intended to make thoroughly clear what is believed to
# be a consequence of the rest of this License.
#  
#   8. If the distribution and/or use of the Program is restricted in
# certain countries either by patents or by copyrighted interfaces, the
# original copyright holder who places the Program under this License
# may add an explicit geographical distribution limitation excluding
# those countries, so that distribution is permitted only in or among
# countries not thus excluded.  In such case, this License incorporates
# the limitation as if written in the body of this License.
# 
#   9. The Free Software Foundation may publish revised and/or new versions
# of the General Public License from time to time.  Such new versions will
# be similar in spirit to the present version, but may differ in detail to
# address new problems or concerns.
# 
# Each version is given a distinguishing version number.  If the Program
# specifies a version number of this License which applies to it and "any
# later version", you have the option of following the terms and conditions
# either of that version or of any later version published by the Free
# Software Foundation.  If the Program does not specify a version number of
# this License, you may choose any version ever published by the Free Software
# Foundation.
# 
#   10. If you wish to incorporate parts of the Program into other free
# programs whose distribution conditions are different, write to the author
# to ask for permission.  For software which is copyrighted by the Free
# Software Foundation, write to the Free Software Foundation; we sometimes
# make exceptions for this.  Our decision will be guided by the two goals
# of preserving the free status of all derivatives of our free software and
# of promoting the sharing and reuse of software generally.
# 
# 			    NO WARRANTY
# 
#   11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
# FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
# OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
# PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
# OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
# TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
# PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
# REPAIR OR CORRECTION.
# 
#   12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
# WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
# REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
# INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
# OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
# TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
# YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
# PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGES.
# 
# 		     END OF TERMS AND CONDITIONS
#  
# 	    How to Apply These Terms to Your New Programs
# 
#   If you develop a new program, and you want it to be of the greatest
# possible use to the public, the best way to achieve this is to make it
# free software which everyone can redistribute and change under these terms.
# 
#   To do so, attach the following notices to the program.  It is safest
# to attach them to the start of each source file to most effectively
# convey the exclusion of warranty; and each file should have at least
# the "copyright" line and a pointer to where the full notice is found.
# 
#     <one line to give the program's name and a brief idea of what it does.>
#     Copyright (C) <year>  <name of author>
# 
#     This program is free software; you can redistribute it and/or modify
#     it under the terms of the GNU General Public License as published by
#     the Free Software Foundation; either version 2 of the License, or
#     (at your option) any later version.
# 
#     This program is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU General Public License for more details.
# 
#     You should have received a copy of the GNU General Public License
#     along with this program; if not, write to the Free Software
#     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
# 
# 
# Also add information on how to contact you by electronic and paper mail.
# 
# If the program is interactive, make it output a short notice like this
# when it starts in an interactive mode:
# 
#     Gnomovision version 69, Copyright (C) year name of author
#     Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
#     This is free software, and you are welcome to redistribute it
#     under certain conditions; type `show c' for details.
# 
# The hypothetical commands `show w' and `show c' should show the appropriate
# parts of the General Public License.  Of course, the commands you use may
# be called something other than `show w' and `show c'; they could even be
# mouse-clicks or menu items--whatever suits your program.
# 
# You should also get your employer (if you work as a programmer) or your
# school, if any, to sign a "copyright disclaimer" for the program, if
# necessary.  Here is a sample; alter the names:
# 
#   Yoyodyne, Inc., hereby disclaims all copyright interest in the program
#   `Gnomovision' (which makes passes at compilers) written by James Hacker.
# 
#   <signature of Ty Coon>, 1 April 1989
#   Ty Coon, President of Vice
# 
# This General Public License does not permit incorporating your program into
# proprietary programs.  If your program is a subroutine library, you may
# consider it more useful to permit linking proprietary applications with the
# library.  If this is what you want to do, use the GNU Library General
# Public License instead of this License.

# DATEI-ENDE ###################################################################

