#!/usr/bin/perl -T

# fexsrv : web server for F*EX service
#
# Author: Ulli Horlacher <framstag@rus.uni-stuttgart.de>
#

use 5.008;
use Socket;
use IO::Handle;
use Fcntl qw':flock :seek';
use warnings;

BEGIN {
  # stunnel workaround
  $SIG{CHLD} = "DEFAULT";
  $ENV{PERLINIT} = q{
    $| = 1;
    ## open STDERR,">>/tmp/fex.log";
    $ENV{LC_ALL} = 'en_US.utf8';
    unshift @INC,(getpwuid($<))[7].'/perl';
    # web error handler
    $SIG{__DIE__} = $SIG{__WARN__} = sub {
      my $info = '';
      my $url = $ENV{REQUEST_URL}||'';
      my @d = localtime time;
      my $time = sprintf('%d-%02d-%02d %02d:%02d:%02d',
                 $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]);
      if ($admin) {
        my $mailto = "mailto:$admin?subject=fex%20bug";
        $info = "<h3>send this error to <a href=\"$mailto\">$admin</a></h3>";
      }
      $_ = join("\n",@_);
      debuglog($_) if exists &debuglog;
      chomp;
      s/&/&amp;/g;
      s/</&lt;/g;
      my $html = join("\n",
        "<html><body>",
        "<h1>INTERNAL ERROR in $0</h1>",
        "<pre>\n$_\n</pre>\n<p>",
        "$url\n<p>",
        "$time\n<p>",
        "$info\n<p>",
        "</body></html>\n"
      );
      $length = length($html);
      local $SIG{ALRM} = sub { exit 99 };
      alarm(3);
      unless ($HTTP_HEADER) {
        print "HTTP/1.0 200 INTERNAL ERROR\r\n";
        print "Content-Type: text/html\r\n";
        print "Content-Length: $length\r\n";
        print "\r\n";
      }
      print $html;
      sleep 1;
      close STDOUT;
      exit 99;
    }
  };
  eval $ENV{PERLINIT};
}

# use BSD::Resource;
# setrlimit(RLIMIT_CPU,999,999) or die "$0: $!\n";

# SSL remote address provided by stunnel
if (@ARGV and $ARGV[0] eq 'stunnel' and $ENV{REMOTE_HOST} =~ /(.+)/) {
  $ssl_ra = $1;
  # stunnel 5.40 bug workaround
  $ssl_ra =~ s/.*:// if $ssl_ra =~ /\./;
}

# KEEP_ALIVE <== callback from CGI or fexsrv reexec
unless ($keep_alive = $ENV{KEEP_ALIVE}) {
  %ENV = ( PERLINIT => $ENV{PERLINIT} );   # clean environment
}

$ENV{STUNNEL} = $ssl_ra if $ssl_ra;

$ENV{HOME} = (getpwuid($<))[7] or die "no HOME";

# fexsrv MUST be run with full path!
if ($0 =~ m:^(/.+)/bin/fexsrv:) {
  $FEXHOME = $1;
  $FEXHOME =~ s:/+:/:g;
  $FEXHOME =~ s:/$::;
  $ENV{FEXHOME} = $FEXHOME;
}

foreach my $lib (
  $FEXHOME,
  '/usr/local/fex',
  '/usr/local/share/fex',
  '/usr/share/fex',
) {
  $ENV{FEXLIB} = $FEXLIB = $lib       and last if -f "$lib/fex.pp";
  $ENV{FEXLIB} = $FEXLIB = "$lib/lib" and last if -f "$lib/lib/fex.pp";
}

# import from fex.pp
our ($hostname,$debug,$timeout,$max_error,$max_error_handler,$keep_archives);
our ($spooldir,@logdir,$docdir,$xkeydir,$akeydir,$lockdir,$encryption);
our ($force_https,$default_locale,$bs,$MB,$adlm,@forbidden_user_agents);
our (@locales);

# load common code (local config: $FEXHOME/lib/fex.ph)
require "$FEXLIB/fex.pp" or die "cannot load $FEXLIB/fex.pp - $!\n";

chdir $spooldir or http_die("$0: $spooldir - $!\n");

our $log = 'fexsrv.log';
our $error = 'F*EX ERROR';
our $htmlsource;
our $hid = ''; # header ID
our @log = ();
our $connect = '';
our $request = '';
my $sin = ''; # STDIN input buffer, filled by getaline()

$0 = untaint($0);

$ENV{GATEWAY_INTERFACE} = 'CGI/1.1f';
$ENV{SERVER_NAME} = $hostname;
$ENV{REQUEST_METHOD} = '';
$ENV{QUERY_STRING} = '';
$ENV{HTTP_COOKIE} = '';
$ENV{PATH_INFO} = '';
$ENV{RANDOM} = randstring(8);
$ENV{FEATURES} =
  'SID,CHECKRECIPIENT,GROUPS,QUOTA,FILEID,MULTIPOST,XKEY,FILEQUERY,FILESTREAM,'.
  'NOSTORE,AXEL,FEXMAIL,FILELINK,LISTARCHIVE,PREFETCH,CCCSYNC,FUPWATCH,MUP';

$port = 0;

# continue session?
if ($keep_alive) {
  if ($ENV{HTTP_HOST} =~ /(.+):(.+)/) {
    $hostname = $1;
    $port = $2;
  } else {
    $hostname = $ENV{HTTP_HOST};
    if ($ENV{PROTO} eq 'https') { $port = 443 }
    else                        { $port = 80 }
  }
  $ra = $ENV{REMOTE_ADDR};
  $rh = $ENV{REMOTE_HOST};
}

# new session
else {
  my $iaddr;

  # HTTPS connect
  if ($ssl_ra) {
    $ENV{PROTO} = 'https';
    $ra = $ssl_ra;
    if ($ssl_ra =~ /[\w:]:\w/) {
      # ($rh) = `host $ssl_ra 2>/dev/null` =~ /name pointer (.+)\.$/;
      $^W = 0; eval 'use Socket6'; $^W = 1;
      http_error(503) if $@;
      $iaddr = inet_pton(AF_INET6,$ssl_ra) and
      $rh = gethostbyaddr($iaddr,AF_INET6);
    } else {
      $rh = gethostbyaddr(inet_aton($ra),AF_INET);
    }
    $port = 443;
    # print {$log} "X-SSL-Remote-Host: $ssl_ra\n";
  }

  # HTTP connect
  else {
    $ENV{PROTO} = 'http';
    my $sa = getpeername(STDIN) or die "no network stream on STDIN\n";
    if (sockaddr_family($sa) == AF_INET) {
      ($ENV{REMOTE_PORT},$iaddr) = sockaddr_in($sa);
      $ra = inet_ntoa($iaddr);
      $rh = gethostbyaddr($iaddr,AF_INET);
      ($port) = sockaddr_in(getsockname(STDIN));
    } elsif (sockaddr_family($sa) == AF_INET6) {
      $^W = 0; eval 'use Socket6'; $^W = 1;
      http_error(503) if $@;
      ($ENV{REMOTE_PORT},$iaddr) = unpack_sockaddr_in6($sa);
      $ra = inet_ntop(AF_INET6,$iaddr);
      $rh = gethostbyaddr($iaddr,AF_INET6);
      ($port) = unpack_sockaddr_in6(getsockname(STDIN));
    } else {
      die "unknown IP version\n";
    }
    $port ||= 80;
  }

  $rh ||= '-';
  $ENV{REMOTE_ADDR} = $ra;
  $ENV{REMOTE_HOST} = $rh;
  $ENV{PORT} = $port;
  $ENV{HTTP_HOST} = ($port == 80 or $port == 443)
                  ? $hostname : "$hostname:$port";

  if (@allowed_domains) {
    my $connect = sprintf "CONNECT:%s %s %s %s [%s_%s]",
                  $port,isodate(time),$rh,$ra,$$,0;
    if ($rh eq '-') {
      fexlog($connect,"no DNS");
      while (<>) { last if /^\s*$/ }
      http_error(403,"You have no DNS entry.");
    }
    if ("@allowed_domains" ne '*') {
      my $allowed = 0;
      foreach my $ad (@allowed_domains) {
        $allowed = $ad if $rh =~ /(^|\.)\Q$ad\E$/i;
      }
      unless ($allowed) {
        fexlog($connect,"$rh not in \@allowed_domains");
        while (<>) { last if /^\s*$/ }
        http_error(403,"You ($rh) are not in the allowed domains list.");
      }
    }
  }
}

$ra = untaint($ra);
$rh = untaint($rh);
$port = untaint($port);

$ra =~ s/^::ffff:// if $ra =~ /\./;

if ($reverse_proxy_ip and $reverse_proxy_ip eq $ra) {
  $ENV{FEATURES} =~ s/SID,//;
}

if (@anonymous_upload and ipin($ra,@anonymous_upload)) {
  $ENV{FEATURES} .= ',ANONYMOUS';
}

$ENV{FEATURES} .= ',FAS' if -x "$FEXHOME/cgi-bin/fas";
$ENV{FEATURES} .= ',DOX' if -x "$FEXHOME/cgi-bin/dox";

$| = 1;

$SIG{CHLD} = "DEFAULT"; # stunnel workaround

$SIG{ALRM} = sub {
  # printf {$log} "\nTIMEOUT %s %s\n",isodate(time),$connect;
  $request ||= $sin;
  exit if $keep_alive and not $request;
  $request ||= '(NOTHING)';
  @log = ($request) unless @log;
  debuglog('TIMEOUT',isodate(time));
  fexlog($connect,@log,"TIMEOUT");
  &$header_hook($connect,$request,$ra) if $header_hook;
  exit;
};

REQUEST:

$sin = $request = $HTTP_HEADER = '';
@log = ();

while (*STDIN) {

  if (defined $ENV{REQUESTCOUNT}) { $ENV{REQUESTCOUNT}++ }
  else                            { $ENV{REQUESTCOUNT} = 0 }

  $connect = sprintf "%s:%s %s %s %s [%s_%s]",
                     $keep_alive ? 'CONTINUE' : 'CONNECT',
                     $port,
                     isodate(time),
                     $rh||'-',
                     $ra,
                     $$,$ENV{REQUESTCOUNT};
  $hid = sprintf("%s %s\n",$rh||'-',$ra);

  @header = @log = ();
  $header = '';

  # read complete HTTP header
  while (defined ($_ = &getaline)) {
    last if /^\s*$/;
    $hl += length;
    $header .= $_;
    s/[\r\n]+$//;
    # URL-encode non-printable chars
    s/([\x00-\x08\x0E-\x1F\x7F-\x9F])/sprintf "%%%02X",ord($1)/ge;
    s/%21/!/g;
    if (@header and s/^\s+/ /) {
      $header[-1] .= $_;
    } else {
      push @header,$_;
      $header{$1} = $2 if /(.+)\s*:\s*(.+)/;
      push @log,$_;
    }
    if ($hl > $MB) {
      fexlog($connect,@log,"OVERRUN");
      http_error(413);
    }

    if (/^(GET \/|\S*Forwarded|\S*Client-IP|\S*Coming-From|User-Agent)/i) {
      $hid .= $_."\n";
    }

    # reverse-proxy?
    # (only IPv4 support!)
    if ($reverse_proxy_ip and $reverse_proxy_ip eq $ra and
       /^\S*(Forwarded|Client-IP|Coming-From)\S*: ([\da-f:.]+)/i
    ) {
      $ENV{REMOTE_ADDR} = $ra = $2;
      if ($ra =~ /:/) {
        $rh = untaint(gethostbyaddr(inet_atop($ra),AF_INET6)) || '';
      } else {
        $rh = untaint(gethostbyaddr(inet_aton($ra),AF_INET)) || '';
      }
      $ENV{REMOTE_HOST} = $rh;
      $ENV{HTTP_HOST} = $hostname;
      if ($ENV{PROTO} eq 'https') { $port = 443 }
      else                        { $port = 80 }
    }
  }

  exit unless @header;
  exit if $header =~ /^\s*$/;

  $ENV{HTTP_HEADER} = $header;
  debuglog($header);
  # http_die("<pre>$header</pre>");

  $ENV{'HTTP_HEADER_LENGTH'} = $hl;
  $ENV{REQUEST_URI} = $uri = '';
  $cgi = '';

  # is it a HTTP-request at all?
  $request = shift @header;
  if ($request !~ /^(GET|HEAD|POST|OPTIONS).*HTTP\/\d\.\d$/i) {
    fexlog($connect,$request,"DISCONNECT: no HTTP request");
    badlog("no HTTP request: $request");
    exit;
  }

  if ($force_https and $port != 443
      and $request =~ /^(GET|HEAD|POST)\s+(.+)\s+(HTTP\/[\d\.]+$)/i) {
    $request = $2;
    nvt_print(
      "HTTP/1.1 301 Moved Permanently",
      "Location: https://$hostname$request",
      "Content-Length: 0",
      ""
    );
    fexlog($connect,@log);
    exit;
  }

  $request =~ s{^(GET|HEAD|POST) https?://$hostname(:\d+)?}{$1 }i;

  if ($request =~ m"^(GET|HEAD) /fop/\w+/") {
    # no header inquisition on regular fop request
    # $header_hook = '';
  } else {
    &$header_hook($connect,$request,$ra) if $header_hook;
  }

  unless ($keep_alive) {
    if ($request =~ m:(HTTP/1.(\d)): and $2) {
      $ENV{KEEP_ALIVE} = $keep_alive = $ra
    } else {
      $ENV{KEEP_ALIVE} = $keep_alive = '';
    }
  }

  if ($request =~ /^OPTIONS \/?FEX HTTP\/[\d\.]+$/i) {
    fexlog($connect,@log);
    nvt_print(
      "HTTP/1.1 201 OK",
      "X-Features: $ENV{FEATURES}",
      "X-Timeout: $timeout",
      ''
    );
#    goto REQUEST if $keep_alive;
    &reexec;
    exit;
  }

  if ($request =~ m:^GET /?SID HTTP/[\d\.]+$:i) {
    if (1 and $ENV{FEATURES} =~ /\bSID\b/) {
      $ENV{SID} = randstring(8);
      nvt_print(
        "HTTP/1.1 201 ".$ENV{SID},
        "Server: fexsrv",
        "X-SID: ".$ENV{SID},
        "X-Features: ".$ENV{FEATURES},
        "X-Timeout: ".$timeout,
        'Content-Length: 0',
        ''
      );
    } else {
      nvt_print(
        "HTTP/1.1 501 Not Implemented",
        "Server: fexsrv",
        "X-Features: ".$ENV{FEATURES},
        "X-Timeout: ".$timeout,
        'Content-Length: 0',
        ''
      );
    }
    fexlog($connect,@log);
#    goto REQUEST if $keep_alive;
    &reexec;
    exit;
  }

  if ($request =~ /^(GET|HEAD|POST)\s+(.+)\s+(HTTP\/[\d\.]+$)/i) {
    $ENV{REQUEST}	 = $_;
    $ENV{REQUEST_METHOD} = uc($1);
    $ENV{REQUEST_URI}    = $uri = $cgi = $2;
    $ENV{HTTP_VERSION}   = $protocol = $3;
    $ENV{QUERY_STRING}   = $1               if $cgi =~ s/\?(.*)//;
    $ENV{PATH_INFO}      = $1               if $cgi =~ m:/.+?(/.+?)(\?|$):;
    $ENV{KEEP_ALIVE}     = $keep_alive = '' if $protocol =~ /1\.0/;
    $ENV{REQUEST_URL}    = "$ENV{PROTO}://$ENV{HTTP_HOST}$ENV{REQUEST_URI}";
    if ($request !~ m:^(GET|HEAD) /(fop|dox)/:) {
      if ($uri =~ /<|%3c/i)  { badchar("&lt;") }
      if ($uri =~ />|%3e/i)  { badchar(">") }
      if ($uri =~ /\||%7c/i) { badchar("|") }
      if ($uri =~ /\\|%5c/i) { badchar("\\") }
    }
    $uri =~ s:/+:/:g;
    &$request_hook if $request_hook;
  }

  my $fua = join('|',@forbidden_user_agents);

  while ($_ = shift @header) {

    # header inquisition!
    &$header_hook($connect,$_,$ra) if $header_hook;

    # mega stupid "Download Manager" FlashGet
    if ($uri =~ m{^/fop/} and m{^Referer: https?://.*\Q$uri$}) {
      fexlog($connect,@log,"NULL: FlashGet");
      debuglog("NULL: FlashGet");
      exec qw'cat /dev/zero' or sleep 30;
      exit;
    }

    if ($fua and /^User-Agent: ($fua)/) {
      disconnect($1,"499 User Agent $1 Not Supported",30);
    }

    if (/^Range:.*,/) {
      disconnect("Range a,b","416 Requested Range Not Satisfiable",30);
    }
    if (/^Range:.*(\d+)-(\d+)/) {
      if ($1 > $2) {
        disconnect("Range a>b","416 Requested Range Not Satisfiable",0);
      }
      if (($header{'User-Agent'}||'') !~ /$adlm/ ) {
        disconnect("Range a-b","416 Requested Range Not Satisfiable",30);
      }
    }

    if (/^Range:.*\d+-$/ and $hid) {
      my $lock = untaint($lockdir.'/'.md5_hex($hid));
      if (open $lock,'+>>',$lock) {
        if (flock($lock,LOCK_EX|LOCK_NB)) {
          seek $lock,0,0;
          truncate $lock,0;
          print {$lock} $hid;
        } else {
          disconnect(
            "multiple Range request",
            "400 Multiple Requests Not Allowed",
            10,
          );
        }
      }
    }

    # client signed int bug
    if (/^Range:.*-\d+-/) {
      disconnect("Range -a-","416 Requested Range Not Satisfiable",0);
    }

#    if (/^Range:/ and $protocol =~ /1\.0/) {
#      &$header_hook($connect,$_,$ra) while ($header_hook and $_ = shift @header);
#      fexlog($connect,@log,"DISCONNECT: Range + HTTP/1.0");
#      debuglog("DISCONNECT: Range + HTTP/1.0");
#      http_error(416);
#      exit;
#    }

    if (/^Connection:\s*close/i) {
      $ENV{KEEP_ALIVE} = $keep_alive = '';
    }

    # HTTP header ==> environment variables
    if (/^([\w\-_]+):\s*(.+)/s) {
      $http_var = $1;
      $http_val = $2;
      $http_var =~ s/-/_/g;
      $http_var = uc($http_var);
      $http_val =~ s/^\s+//;
      $http_val =~ s/\s+$//;
      if ($http_var =~ /^X_(FEX_\w+|CONTENT_LENGTH)$/) {
        $http_var = $1;
      } else {
        $http_val =~ s/\s+/ /g;
        if ($http_var =~ /^HTTP_(HOST|VERSION)$/) {
          $http_var = 'HTTP_X_'.$1;
        } elsif ($http_var =~ /^PROXY/) {
          # http://cert.at/warnings/all/20160718.html
          $http_var = 'HTTP_X_'.$http_var;
        } elsif ($http_var !~ /^CONTENT_/) {
          $http_var = 'HTTP_'.$http_var;
        }
      }
      $http_val =~ s/[^\w.:-]/_/g if $http_var eq 'HTTP_HOST';
      $ENV{$http_var} = $http_val;
    }
  }

  # multiline header inquisition
  &$header_hook($connect,$header,$ra) if $header_hook;

  exit unless $cgi;

  # extra download request? (request http://fexserver//xkey)
  if ($cgi =~ m{^//([^/]+)$}) {
    my $xkey = $1;
    my $dkey;
    if ($xkey =~ /^afex_\d/) {
      $dkey = readlink "$xkeydir/$xkey" and $dkey =~ s/^\.\.\///;
    } else {
      $dkey = readlink "$xkeydir/$xkey/dkey" and $dkey .= "/$xkey";
    }
    if ($dkey) {
      # xkey downloads are only one time possible - besides afex
      if ($xkey !~ /^afex_\d/) {
        unlink "$xkeydir/$xkey/xkey";
        unlink "$xkeydir/$xkey";
      }
      nvt_print(
        "HTTP/1.1 301 Moved Permanently",
        "Location: $ENV{PROTO}://$ENV{HTTP_HOST}/fop/$dkey",
        "Content-Length: 0",
        ""
      );
      fexlog($connect,@log);
      exit;
    }
    fexlog($connect,@log);
    http_error(404);
    exit;
  }

  # get locale
  if (($ENV{QUERY_STRING} =~ /.*locale=([\w-]+)/ or
       $ENV{HTTP_COOKIE}  =~ /.*locale=([\w-]+)/)
      and -d "$FEXHOME/locale/$1") {
    $ENV{LOCALE} = $locale = $1;
  } else {
    $ENV{LOCALE} = $locale = $default_locale;
  }

  # for dynamic HTML documents
  if ($ENV{HTTP_COOKIE} =~ /akey=(\w+)/) {
    my $akey = $1;
    my ($user,$id);
    if ($user = readlink "$akeydir/$akey") {
      $user =~ s:.*/::;
      $user = untaint($user);
      if ($id = slurp("$spooldir/$user/@")) {
        chomp $id;
        $ENV{AKEY} = $akey;
        $ENV{USER} = $user;
        $ENV{ID}   = $id;
      }
    }
  }

  # check for name based virtual host
  $vhost = vhost($ENV{'HTTP_HOST'});

  if ($debug) {
    debuglog("ENV:\n");
    foreach $var (sort keys %ENV) {
      if (defined($ENV{$var})) {
        debuglog(sprintf "  %s = >%s<\n",$var,$ENV{$var});
      }
    }
    debuglog("\n");
  }

  # locale definitions in fex.ph?
  if (@locales) {
    if (@locales == 1) {
      $locale = $locales[0];
    } elsif (not grep /^$locale$/,@locales) {
      $locale = $default_locale;
    }
  }

  # prepare document file name
  if ($ENV{REQUEST_METHOD} =~ /^GET|HEAD$/) {
    if (%redirect) {
      foreach my $r (keys %redirect) {
        if ($uri =~ /^\Q$r/) {
          redirect($uri,$r);
          exit;
        }
      }
    }
    $doc = untaint($uri);
    $doc =~ s/%([\dA-F]{2})/unpack("a",pack("H2",$1))/ge;
    $doc =~ m:/\.\./: and http_error(403);
    $doc =~ s:^/+::;
    $doc =~ s/\?.*//;
    if ($locale and $locale ne 'english' and -e "$docdir/locale/$locale/$doc") {
      $doc = "$docdir/locale/$locale/$doc";
    } else {
      $doc = "$docdir/$doc";
    }
  }

  # CGI or document request?
  if ($cgi =~ s:^/+::) {
    $cgi =~ s:/.*::;
    unless ($cgi) {
      my $login = "$FEXHOME/cgi-bin/login";
      if (-x $login) {
        $cgi = untaint(readlink($login) || $login);
        $cgi =~ s:.*/::;
      }
    }

    $ENV{SCRIPT_NAME} = $cgi;

    # locale CGIs? (vhost comes already with own FEXLIB)
    if ($locale and $locale ne 'english'
        and -f "$FEXHOME/locale/$locale/cgi-bin/$cgi") {
      $ENV{SCRIPT_FILENAME} = $cgi = "$FEXHOME/locale/$locale/cgi-bin/$cgi";
      $ENV{FEXLIB} = $FEXLIB = "$FEXHOME/locale/$locale/lib" unless $vhost;
    } else {
      $ENV{SCRIPT_FILENAME} = $cgi = "$FEXHOME/cgi-bin/$cgi";
      $ENV{FEXLIB} = $FEXLIB = "$FEXHOME/lib" unless $vhost;
    }

    $status = '';
    if (-x $cgi and -f $cgi) {
      if (@forbidden_hosts and ipin($ra,@forbidden_hosts)) {
        fexlog($connect,@log,"FORBIDDEN");
        http_error(403);
      }
      if ($header !~ /^Host: ./im) {
        fexlog($connect,@log,"BADREQUEST: no Host header");
        badlog("no Host header");
        http_error(400);
      }
      unlink "$spooldir/.error/$ra";
      # push @log,"DEBUG: locale=$locale locales=(@locales)";
      fexlog($connect,@log,"EXEC $cgi");
      eval { local $^W = 0; exec $cgi };
      $status = "$! or bad interpreter";
      fexlog($connect,@log,"FAILED to exec $cgi : $status");
      http_error(555);
    } elsif ($ENV{REQUEST_METHOD} =~ /^GET|HEAD$/) {
      if (-f "$doc/.htindex") {
        require "$FEXLIB/dop";
        fexlog($connect,@log);

        showindex($doc);
        STDOUT->flush;
#        goto REQUEST if $keep_alive;
        &reexec;
        exit;
      }
      if (-f "$doc/index.html") {
        # force redirect if trailing / is missing
        # this is mandatory for processing further HTTP request!
        if ($doc !~ m{/$}) {
          nvt_print(
            "HTTP/1.1 301 Moved Permanently",
            "Location: $ENV{REQUEST_URL}/",
            "Content-Length: 0",
            ""
          );
          fexlog($connect,@log);
#          goto REQUEST if $keep_alive;
          &reexec;
          exit;
        }
        $doc .= '/index.html';
        $doc =~ s:/+:/:g;
      }
      $doc =~ s/#.*//; # ignore HTML anchors (stupid msnbot)

      # special request for F*EX UNIX clients
#      if ($ENV{SCRIPT_NAME} eq 'xx.tar') {
#        bintar(qw'fexget fexsend xx fexpush fexpull xxx');
#      }
#      if ($ENV{SCRIPT_NAME} eq 'sex.tar') {
#        bintar(qw'sexsend sexget sexxx');
#      }
#      if ($ENV{SCRIPT_NAME} eq 'afex.tar') {
#        bintar(qw'afex fexget fexsend');
#      }
#      if ($ENV{SCRIPT_NAME} eq 'afs.tar') {
#        bintar(qw'fexget fexsend xx fexpush fexpull xxx',
#               qw'afex asex sexsend sexget sexxx');
#      }
      # URL ends with ".html!" or ".html?!"
      if ($doc =~ s/(\.html)!$/$1/ or
          $doc =~ /\.html$/ and $ENV{'QUERY_STRING'} eq '!')
      { $htmlsource = $doc } else { $htmlsource = '' }

      if (-f $doc
          or $doc =~ /(.+)\.(tar|tgz|zip)$/ and lstat("$1.stream")
          or $doc =~ /(.+)\.tgz$/           and -f "$1.tar"
          or $doc =~ /(.+)\.gz$/            and -f $1)
      {
        unlink "$spooldir/.error/$ra";
        delete $ENV{SCRIPT_FILENAME};
        $ENV{DOCUMENT_FILENAME} = $doc;
        require "$FEXLIB/dop";
        fexlog($connect,@log);
        dop($doc);
        STDOUT->flush;
#        goto REQUEST if $keep_alive;
        &reexec;
        exit;
      } elsif ($uri eq '/bunny') {
        fexlog($connect,@log);
        nvt_print(
          'HTTP/1.1 200 OK',
          'Server: fexsrv',
          "Content-Type: text/plain",
          '',
          '=:3',
        );
        exit;
      } elsif ($uri eq '/camel') {
        fexlog($connect,@log);
        nvt_print(
          'HTTP/1.1 200 OK',
          'Server: fexsrv',
          "Content-Type: text/plain",
          '',
        );
        local $/;
        print unpack('u',<DATA>);
        exit;
      } elsif (-e $cgi) {
        $status = 'not executable';
      }

    }

  }

  # neither document nor CGI ==> error

  if ($status) {
    fexlog($connect,@log,"FAILED to exec $cgi : $status");
    http_error(666);
  } else {
    fexlog($connect,@log,"UNKNOWN URL");
    badlog($request);
    http_error(404);
  }
  exit;
}


# read one text line unbuffered from STDIN
sub getaline {
  my $n = 0;
  my $c;
  $sin = ''; # input buffer, must be global for timeout()

  alarm(10);

  # must use sysread to avoid perl line buffering
  # (later exec would destroy line buffer)
  while (sysread STDIN,$c,1) {
    $sin .= $c;
    $n++;
    last if $c eq "\n";
    if ($n > $bs) {
      fexlog($connect,@log,$sin,"OVERRUN");
      http_error(413);
    }
  }

  alarm(0);

  return $sin;
}


sub fexlog {
  my @log = @_;
  local $SIG{ALRM} = sub { return };

  foreach my $logdir (@logdir) {
    if (open $log,'>>',"$logdir/$log") {
      alarm(2);
      flock $log,LOCK_EX;
      alarm(0);
      seek $log,0,SEEK_END;
      print {$log} "\n",join("\n",@log),"\n";
      close $log;
    } else {
      http_die("$0: cannot write to $logdir/$log - $!\n");
    }
  }
}


sub badchar {
  my $bc = shift;

  fexlog($connect,@log,"DISCONNECT: bad characters in URL ($bc)");
  debuglog("DISCONNECT: bad characters in URL $uri ($bc)");
  badlog($request);
  http_die("\"$bc\" is not allowed in URL");
}


sub bintar {
  my $tmpdir = "$FEXHOME/tmp";
  my $fs = "$ENV{PROTO}://$ENV{HTTP_HOST}";

  if (chdir "$FEXHOME/bin") {
    fexlog($connect,@log);
    chdir $fstb if $fstb;
    mkdir $tmpdir;
    foreach my $f (@_) {
      copy($f,"$tmpdir/$f","fexserver = ''","fexserver = '$fs'");
      chmod 0755,"$tmpdir/$f";
    }
    chdir $tmpdir or http_die("internal error: $tmpdir - $!");
    my $tar = `tar cf - @_ 2>/dev/null`;
    unlink @_;
    nvt_print(
      'HTTP/1.1 200 OK',
      'Server: fexsrv',
      "Content-Length: ".length($tar),
      "Content-Type: application/x-tar",
      '',
    );
    print $tar;
    exit;
  }
}


sub disconnect {
  my $info = shift;
  my $error = shift;
  my $wait = shift||0;

  # &$header_hook($connect,$_,$ra) while ($header_hook and $_ = shift @header);
  fexlog($connect,@log,"DISCONNECT: $info");
  debuglog("DISCONNECT: $info");
  errorlog("$ENV{REQUEST_URI} ==> $error");
  badlog("$ENV{REQUEST_URI} ==> $error ($info)");

  sleep 5;
  nvt_print("HTTP/1.0 $error");
  exit;
}


sub redirect {
  my $uri = shift;
  my $r = shift;
  my $rr = $redirect{$r};
  my $newurl;

  $uri =~ s/\Q$r//;

  if ($rr =~ s/^!// or $header =~ /\nUser-Agent: (wget|curl)/i) {
    $newurl = $rr.$uri;
    nvt_print(
      "HTTP/1.1 301 Moved Permanently",
      "Location: $newurl",
      "Content-Length: 0",
      ""
    );
  } else {
    if ($rr =~ /^http/) {
      $newurl = $rr.$uri;
    } else {
      $newurl = "$ENV{PROTO}://$ENV{HTTP_HOST}$rr$uri";
    }

    http_header("200 OK");
    print html_header("$hostname page has moved");
    pq(qq(
      '<h3>Please use new URL: <a href="$newurl">$newurl</a></h3>'
      '</body></html>'
    ));
  }
  fexlog($connect,@log,"REDIRECT $newurl");
  if ($rr =~ /^http/) {
    exit;
  } else {
    &reexec;
    exit;
  }
}


sub badlog {
  my $request = shift;
  my @n;
  my $ed = "$spooldir/.error";
  local $_;

  if (@ignore_error) {
    foreach (@ignore_error) {
      return if $request =~ /$_/;
    }
  }

  if ($ra and $max_error and $max_error_handler) {
    mkdir($ed) unless -d $ed;

    if (open $ra,"+>>$ed/$ra") {
      flock($ra,LOCK_EX);
      seek $ra,0,SEEK_SET;
      @n = <$ra>;
      printf {$ra} "%s %s\n",isodate(time),$request;
      close $ra;
      &$max_error_handler($ra,'bad request',@n) if scalar(@n) > $max_error;
    }
  }
}


__END__
M("`@("`@("`@("`@("`@("`@("`@("`@("`@("`@("`@("PM)R(G+5P*("`@
M("`@("`@("`@("`@("`@("`@("`@("]@8&`M+B`@(&!<("`@("Q=+B`@("`@
M("`@("`@("`@("`@("`@("`L+BY?"B`@("`@("`@("`@("`@("`@("`@("`@
M+&`@("`@(&`B+B`@72X@("`@(&`N("`@("`@("`@("`@("`@7U]?+BY>(&!?
M)V`B(B(*("`@("`@("`@("`@("`@("`@("`@("`I("`@("`@("`G+"TG+2T@
M+B`@("!@+B`@("`@(%]?7RPN+2<@("XN("T@("`G("TP("Y?"B`@("`@("`@
M("`@+"X@("`@("`@("`@?%]?+"Y?("!?("`N+5\@("!<("`\("Y@(B<G)V`L
M7R`@("`@("`@+2!?("TN("`@("`@("<M+BX*("`@("`@("`@('P@(&8M+2TM
M+2TM+2<O("`@("!@)RTM+BXL("`@("`M("`@("`@("`@("T@("`@("`@("`@
M("`@("`G("XL7U\N7%\N)PH@("`@("`@("`@8"TM)R<G)R<G8"TM7"`@("`@
M("`@("`@("!@)RTM+B`@("`@("`@("`@("`@("`@("`@7RPN)RTM)R<*("`@
M("`@("`@("`@("`@("`@("`@("\@("`@("`@("`@("`@("`@("!@8#T@+2`@
M+2`@("!?+2`@7RXM)PH@("`@("`@("`@("`@("`@("`@("!\("`@("`@("`@
M("`@("`@("`@("`@("!?)V`@("`G)U\M+2<*("`@("`@("`@("`@("`@("`@
M("`@?"`@("`@("`@("`@("`@("`@("`O("`@?"T]+BXM+2<*("`@("`@("`@
M("`@("`@(%\@("`@("<N("`@("`@("`N7R`@(%\N+2=@)R`@(&!?7PH@("`@
M("`@("`@("`@("PG("`N("PG("TL3%]?+WP@(%P@8"<G+&\N("`@+5\@("`@
M)V`M+2X*("`@("`@("`@("`@("`@+BPG(BT@("`O("`@("`@("`@?"`@8'8@
M+R`N("PG8"TM+BXG7R`G+@H@("`@("`@("`@("`@("`@("TN+E\L)R`@("`@
M('P@("!\("`@)RTM+E]@("!?+R`@('P@("!\"B`@("`@("`@("`@("`@("`@
M("`@("`@("`@("=@8"<@6R`@("`@("`@("=@)R`@("`L+RXN+PH@("`@("`@
M("`@("`@("`@("`@("`@("`@('P@("`M+B<@("`@("`@("`@("`@("Y;("`@
M+PH@("`@("`@("`@("`@("`@("`@(%]?7U]?7RX@("`L.E]?7U]?7U]?7U]?
M7U]?7RQ@("PG"B`@("TM+2TM+2TM+2TM+2TM+2TM+2TM+2TM+2T@+2(M+2TM
M+2TM+2TM+2TM+2TM+2T]+0H@+2TM+2TM+2TM+2TM+2TM+2TM+2TM+2TM+2TM
6+2TM+2TM+2TM+2TM+2TM+2TM+2TM"@``
