5 # is a subprogram of fexsrv! do not run it directly!
 
   7 # Author: Ulli Horlacher <framstag@rus.uni-stuttgart.de>
 
  11 use CGI::Carp   qw(fatalsToBrowser);
 
  12 use Fcntl       qw(:flock :seek :mode);
 
  13 use POSIX       qw(strftime locale_h);
 
  14 use Cwd         qw(getcwd abs_path);
 
  18 our ($bs,$tmpdir,@doc_dirs);
 
  22 # POSIX time format needed for HTTP header
 
  23 setlocale(LC_TIME,'POSIX');
 
  30   my ($link,$host,$path,$range);
 
  32   our $error = 'F*EX document output ERROR';
 
  37   if ($range = $ENV{HTTP_RANGE}) {
 
  38     $seek = $1 if $range =~ /^bytes=(\d+)-/i;
 
  39     $stop = $1 if $range =~ /^bytes=\d*-(\d+)/i;
 
  42   # redirect on relative symlinks without "../"
 
  43   if ($link = readlink($doc) and
 
  44       $link !~ m:^/: and $link !~ m:\.\./: and $link !~ /^:.+:$/) {
 
  45     $path = $ENV{REQUEST_URI};
 
  60   if (@wdd and $wdd and grep { $doc =~ /$_/ } @wdd) { &$wdd($doc) }
 
  62   my $dir = untaint(getcwd());
 
  64   http_output($doc,$seek,$stop);
 
  69   my ($file,$seek,$stop) = @_;
 
  70   my ($filename,$files,$streamfile,$size,$total_size);
 
  79   my $http_client = $ENV{HTTP_USER_AGENT} || '';
 
  82   # extra security check: document must not be in lib or spool directory
 
  83   if (path_match($file,$FEXLIB) or path_match($file,$spooldir)) {
 
  87   security_check($file);
 
  88   $htauth = dirname($file).'/.htauth';
 
  89   require_auth($htauth,$file) if -f $htauth;
 
  93     open $file,'<',$file or http_error(400);
 
  94     security_check($file);
 
  95   } elsif ($file =~ /(.+)\.gz$/ and -f $1) {
 
  97     open $file,'-|',qw'gzip -c',@files or http_error(503);
 
  98   } elsif ($file =~ /(.+)\.tgz$/ and -f "$1.tar") {
 
 100     open $file,'-|',qw'gzip -c',@files or http_error(503);
 
 101   } elsif ($file =~ /(.+)\.(tar|tgz|zip)$/ and
 
 102            @s = lstat($streamfile = "$1.stream") and
 
 103            ($s[4] == $< or $s[4] == 0))
 
 106     chdir dirname($file);
 
 107     security_check($file);
 
 108     if (-l $streamfile and readlink($streamfile) =~ /^:(.+):$/) {
 
 109       # special symlink pointer file for streaming
 
 110       @files = split(/:/,$1);
 
 111     } elsif (open $streamfile,$streamfile) {
 
 112       # special streaming file
 
 113       while (<$streamfile>) {
 
 127       if (/^\// or /\.\.\//) {
 
 128         # absolute path or relative path with parent directory is not allowed
 
 131       if (@s = stat($_) and not($s[2] & S_IRGRP) or not -r $_) {
 
 132         # file must be readable by user and group
 
 136     http_error(416) if $ENV{HTTP_RANGE};
 
 138     if    ($file =~ /\.tar$/) { @a = qw'tar --exclude *~ --exclude .* -cf -' }
 
 139     elsif ($file =~ /\.tgz$/) { @a = qw'tar --exclude *~ --exclude .* -czf -' }
 
 140     elsif ($file =~ /\.zip$/) { @a = qw'zip -x *~ */.*/* @ -rq -' }
 
 141     else { http_error(400) }
 
 142     open $file,'-|',@a,@files or http_error(503);
 
 147   $type = 'application/octet-stream';
 
 148   if    ($file =~ /\.html$/)    { $type = 'text/html' }
 
 149   # elsif ($file =~ /\.txt$/)   { $type = 'text/plain' }
 
 150   elsif ($file =~ /\.css$/)     { $type = 'text/css' }
 
 151   elsif ($file =~ /\.js$/)      { $type = 'text/javascript' }
 
 152   elsif ($file =~ /\.ps$/)      { $type = 'application/postscript' }
 
 153   elsif ($file =~ /\.pdf$/)     { $type = 'application/pdf' }
 
 154   elsif ($file =~ /\.jpg$/)     { $type = 'image/jpeg' }
 
 155   elsif ($file =~ /\.png$/)     { $type = 'image/png' }
 
 156   elsif ($file =~ /\.gif$/)     { $type = 'image/gif' }
 
 157   elsif ($file !~ /\.(tar|tgz|zip|jar|rar|arj|7z|bz2?|gz)$/) {
 
 158     my $qfile = untaint(abs_path($file));
 
 159     $qfile =~ s/([^\/\.\+\w!=,_-])/\\$1/g;
 
 163     } elsif (/text/i and not -x $file) {
 
 164       $type = 'text/plain';
 
 165       if    (/\sASCII\s/)    { $type .= "; charset=us-ascii" }
 
 166       elsif (/(ISO-[\w-]+)/) { $type .= "; charset=".lc($1) }
 
 167       else                   { $type .= "; charset=utf-8" }
 
 171   # show sourcecode if URL ends with '!'
 
 172   # to avoid this for a HTML file, simple do a: chmod o-r file
 
 173   if ($type eq 'text/html') {
 
 175       if (@s = stat($file) and $s[2] & S_IROTH) {
 
 176         $type = 'text/plain';
 
 181   } elsif ($ENV{'QUERY_STRING'} eq '!') {
 
 182     $type = 'text/plain';
 
 186   if ($type eq 'text/html') {
 
 191     while ($htmldoc =~ s/\n##.*?\n/\n/) {};
 
 192     # evaluate #if ... #else ... #elseif ... #endif blocks
 
 193     my $mark = randstring(16);
 
 194     while ($htmldoc =~ s/\n(#if\s+(.+?)\n.+?\n)#endif/\n$mark/s) {
 
 200         $htmldoc =~ s/$mark/$_/;
 
 203         while (s/.*?\n#elseif\s+(.+?)\n//s) {
 
 206             $htmldoc =~ s/$mark/$_/;
 
 210         if ($htmldoc =~ /$mark/) {
 
 211           s/.*\n#else\s*\n//s or $_ = '';
 
 212           $htmldoc =~ s/$mark/$_/;
 
 217     while ($htmldoc =~ s/\n#include "(.*?)"/\n$mark/s) {
 
 220       if (open $file,$file) {
 
 224       $dynamic = $htmldoc =~ s/$mark/$include/;
 
 226     # evaluate <<perl-code>> or <<<perl-code>>>
 
 229       local $SIG{ALRM} = sub { $timeout = '<h3>TIMEOUT!</h3>' };
 
 231       while ($htmldoc =~ /<<(.+?>?)>>/s) {
 
 233         if ($pc =~ s/^<(.+)>$/$1/) {
 
 234           # eval code without output substitution
 
 235           eval('package DOP;' . $pc);
 
 237           $dynamic = $htmldoc =~ s/<<<(.+?)>>>//s;
 
 239           # eval code with output substitution
 
 241           tie *STDOUT => "Buffer",\$__;
 
 242           $__ .= eval('package DOP;' . $pc);
 
 245           $dynamic = $htmldoc =~ s/<<(.+?)>>/$__/s;
 
 249       $dynamic = $htmldoc =~ s/<<(.+?>?)>>/$timeout/sg if $timeout;
 
 251     # substitute $variable$ with value from environment (if present)
 
 252     while ($htmldoc =~ /\$([\w_]+)\$/g) {
 
 254       if (defined($env = $ENV{$var})) {
 
 255         $htmldoc =~ s/\$$var\$/$env/g;
 
 258     $total_size = $size = $s = length($htmldoc);
 
 263       $total_size = -s $file || 0;
 
 264       $size = $total_size - $seek - ($stop ? $total_size-$stop-1 : 0);
 
 269     http_header('416 Requested Range Not Satisfiable');
 
 275   if ($seek or $stop) {
 
 278       $range = sprintf("bytes %s-%s/%s",$seek,$stop,$total_size);
 
 280       $range = sprintf("bytes %s-%s/%s",$seek,$total_size-1,$total_size);
 
 283       'HTTP/1.1 206 Partial Content',
 
 285       "Content-Length: $size",
 
 286       "Content-Range: $range",
 
 287       "Content-Type: $type",
 
 296         "Content-Type: $type",
 
 299       # Java (clients) needs Last-Modified header!
 
 300       # if there are locale versions, use actual time for Last-Modified
 
 301       # to enforce reload of page
 
 302       $file =~ m{/htdocs/(.+)};
 
 303       my @lfiles = glob("$FEXHOME/locale/*/htdocs/$1");
 
 304       my $date = ($dynamic or @lfiles > 1) ?
 
 305                  strftime("%a, %d %b %Y %T GMT",gmtime(time)) :
 
 310         "Last-Modified: $date",
 
 312         "Content-Length: $size",
 
 313         "Content-Type: $type",
 
 315       # nvt_print("Set-Cookie: locale=$locale") if $use_cookies and $locale;
 
 318   nvt_print($_) foreach(@extra_header);
 
 321   if ($ENV{REQUEST_METHOD} eq 'GET') {
 
 322     if ($type eq 'text/html') {
 
 327       # binary data # can be stream!
 
 328       seek $file,$seek,0 if $seek;
 
 329       while ($b = read($file,$data,$bs)) {
 
 330         if ($stop and $s+$b > $size) {
 
 332           $data = substr($data,0,$b)
 
 339     fdlog($log,$file,$s,$size) if $s;
 
 344   exit if @files; # streaming end
 
 349 # show directory index
 
 356   my $uri = $ENV{REQUEST_URI};
 
 358   my ($htindex,$htauth);
 
 364   security_check($dir);
 
 366   $htindex = "$dir/.htindex";
 
 367   $htauth  = "$dir/.htauth";
 
 369   open $htindex,$htindex or http_error(403);
 
 370   require_auth($htauth,$dir) if -f $htauth;
 
 372   # .htindex may contain listing regexp
 
 373   chomp ($allowed = <$htindex>||'.');
 
 376   opendir $dir,$dir or http_error(503);
 
 377   while (defined($_ = readdir $dir)) {
 
 378     next if /^[.#]/ or /~$/;
 
 379     if (@s = lstat "$dir/$_" and ($s[2] & (S_IRGRP|S_IROTH))) {
 
 380       if    (-l _) { push @links,$_ }
 
 381       elsif (-d _) { push @dirs,$_ }
 
 382       elsif (-f _) { push @files,$_ }
 
 387   # parent directory listable?
 
 388   if ($uri =~ m:(/.+)/.+: and -f "$dir/../.htindex") {
 
 392   # first the (sub)directories
 
 393   $htmldoc = "<HTML>\n<h1>$uri/</h1>\n";
 
 394   foreach my $d (sort @dirs) {
 
 395     if ($d =~ m:^/: and -f "$d/.htindex") {
 
 396       $htmldoc .= "<h3><a href=\"$d/\">$d/</a></h3>\n";
 
 397     } elsif (-f "$dir/$d/.htindex") {
 
 398       $htmldoc .= "<h3><a href=\"$uri/$d/\">$uri/$d/</a></h3>\n";
 
 402 #  # then the symlinks
 
 403 #  $htmldoc .= "\n<pre>\n";
 
 405 #  foreach my $l (sort @links) {
 
 406 #    if ($l =~ /$allowed/ and $link = readlink "$dir/$l" and $link =~ /^[^.\/]/) {
 
 407 #      $htmldoc .= "$l -> <a href=\"$link\">$dir/$link</a>\n";
 
 412   $htmldoc .= "\n<pre>\n";
 
 413   foreach my $f (sort @files) {
 
 414     if ($f =~ /$allowed/) {
 
 415       $htmldoc .= sprintf "%20s %20s <a href=\"%s/%s\">%s</a>\n",
 
 416                           isodate(mtime("$dir/$f")),
 
 418                           $uri,urlencode($f),$f;
 
 421   $htmldoc .= "</pre>\n</HTML>\n";
 
 423   $size = length($htmldoc);
 
 427     "Content-Length: $size",
 
 428     "Content-Type: text/html",
 
 432   fdlog($log,"$dir/",$size,$size);
 
 438   while (s/(\d)(\d\d\d\b)/$1,$2/) {};
 
 447   if (@stat = stat($file)) {
 
 448     return strftime("%a, %d %b %Y %T GMT",gmtime($stat[9]));
 
 456   my $p1 = abs_path(shift);
 
 457   my $p2 = abs_path(shift);
 
 459   if (defined $p1 and defined $p2) {
 
 460     return 1 if $p1          =~ /^\Q$p2/;
 
 461     return 2 if dirname($p1) =~ /^\Q$p2/;
 
 467 # return real file name (from symlink)
 
 471   return '' unless -e $file;
 
 474     return realfilename(readlink($file));
 
 482   my $file = shift; # can be directory, too!
 
 488   # documents with leading . are not allowed
 
 489   if (abs_path($file) =~ /\/\./) {
 
 490     errorlog("$file with leading .");
 
 496     # document filename must not contain @
 
 497     if (realfilename($file) =~ /@/ or abs_path($file) =~ /@/) {
 
 498       errorlog("$file contains @");
 
 502     # document filename must not end with ~
 
 503     if (realfilename($file) =~ /~$/) {
 
 504       errorlog("$file ends with ~");
 
 508     # file must be group or world readable
 
 509     if (@s = stat($file) and not($s[2] & (S_IRGRP|S_IROTH))) {
 
 510       errorlog("$file not group or world readable");
 
 514     # symlink to regular file and symlink owned by root or fex? ==> ok!
 
 515     if (-l $file and path_match(dirname($file),$docdir)) {
 
 517       return if $s[4] == 0 or $s[4] == $<;
 
 522   # file in allowed directory? ==> ok!
 
 523   foreach my $dir (@doc_dirs) {
 
 524     return if path_match($file,$dir);
 
 527   errorlog("$file not in \@doc_dirs");
 
 531 # security check: client ip allowed?
 
 533   my $file = abs_path(shift);
 
 538   $dir .= '/x' if -d $dir;
 
 540   while ($dir = dirname($dir) and $dir ne '/') {
 
 541     $af = "$dir/.htaccessfrom";
 
 550       errorlog("no access to $file by $af");
 
 557 # HTTP Basic authentication
 
 563   my $uri = $ENV{REQUEST_URI} || '/';
 
 565   $uri =~ s/\/index\.html$//;
 
 568   if (-d $doc or $doc =~ /\/index\.html$/) {
 
 571     $realm = dirname($uri);
 
 574   $auth = slurp($htauth);
 
 575   unless ($auth and $realm) {
 
 576     http_header("200 OK");
 
 577     print html_header("$ENV{SERVER_NAME} no authentication");
 
 579       '<h3><code>$htauth</code> missing</h3>'
 
 586   if ($ENV{HTTP_AUTHORIZATION} and $ENV{HTTP_AUTHORIZATION} =~ /Basic\s+(.+)/)
 
 587   { @http_auth = split(':',decode_b64($1)) }
 
 588   if (@http_auth != 2 or $http_auth[1] ne $auth) {
 
 590       '401 Authorization Required',
 
 591       "WWW-Authenticate: Basic realm=\"$realm\"",
 
 594     # control back to fexsrv for further HTTP handling
 
 600 # function for <<perl-code>> inside HTML documents
 
 606 # tie STDOUT to buffer variable (redefining print)
 
 610   my ($class,$buffer) = @_;
 
 611   bless $buffer,$class;
 
 616   $$buffer .= $_ foreach @_;
 
 622   $$buffer .= sprintf($fmt,@_);