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>>
227 while ($htmldoc =~ /<<(.+?)>>/s) {
230 tie *STDOUT => "Buffer",\$__;
233 $dynamic = $htmldoc =~ s/<<(.+?)>>/$__/s;
235 # substitute $variable$ with value from environment (if present)
236 while ($htmldoc =~ /\$([\w_]+)\$/g) {
238 if (defined($env = $ENV{$var})) {
239 $htmldoc =~ s/\$$var\$/$env/g;
242 $total_size = $size = $s = length($htmldoc);
247 $total_size = -s $file || 0;
248 $size = $total_size - $seek - ($stop ? $total_size-$stop-1 : 0);
253 http_header('416 Requested Range Not Satisfiable');
259 if ($seek or $stop) {
262 $range = sprintf("bytes %s-%s/%s",$seek,$stop,$total_size);
264 $range = sprintf("bytes %s-%s/%s",$seek,$total_size-1,$total_size);
267 'HTTP/1.1 206 Partial Content',
269 "Content-Length: $size",
270 "Content-Range: $range",
271 "Content-Type: $type",
281 "Content-Type: $type",
285 # Java (clients) needs Last-Modified header!
286 # if there are locale versions, use actual time for Last-Modified
287 # to enforce reload of page
288 $file =~ m{/htdocs/(.+)};
289 my @lfiles = glob("$FEXHOME/locale/*/htdocs/$1");
290 my $date = ($dynamic or @lfiles > 1) ?
291 strftime("%a, %d %b %Y %T GMT",gmtime(time)) :
296 "Last-Modified: $date",
298 "Content-Length: $size",
299 "Content-Type: $type",
301 nvt_print("Set-Cookie: locale=$locale") if $use_cookies and $locale;
306 if ($ENV{REQUEST_METHOD} eq 'GET') {
307 if ($type eq 'text/html') {
312 # binary data # can be stream!
313 seek $file,$seek,0 if $seek;
314 while ($b = read($file,$data,$bs)) {
315 if ($stop and $s+$b > $size) {
317 $data = substr($data,0,$b)
324 fdlog($log,$file,$s,$size) if $s;
329 exit if @files; # streaming end
334 # show directory index
341 my $uri = $ENV{REQUEST_URI};
343 my ($htindex,$htauth);
349 security_check($dir);
351 $htindex = "$dir/.htindex";
352 $htauth = "$dir/.htauth";
354 open $htindex,$htindex or http_error(403);
355 require_auth($htauth,$dir) if -f $htauth;
357 # .htindex may contain listing regexp
358 chomp ($allowed = <$htindex>||'.');
361 opendir $dir,$dir or http_error(503);
362 while (defined($_ = readdir $dir)) {
363 next if /^[.#]/ or /~$/;
364 if (@s = lstat "$dir/$_" and ($s[2] & (S_IRGRP|S_IROTH))) {
365 if (-l _) { push @links,$_ }
366 elsif (-d _) { push @dirs,$_ }
367 elsif (-f _) { push @files,$_ }
372 # parent directory listable?
373 if ($uri =~ m:(/.+)/.+: and -f "$dir/../.htindex") {
377 # first the (sub)directories
378 $htmldoc = "<HTML>\n<h1>$uri/</h1>\n";
379 foreach my $d (sort @dirs) {
380 if ($d =~ m:^/: and -f "$d/.htindex") {
381 $htmldoc .= "<h3><a href=\"$d/\">$d/</a></h3>\n";
382 } elsif (-f "$dir/$d/.htindex") {
383 $htmldoc .= "<h3><a href=\"$uri/$d/\">$uri/$d/</a></h3>\n";
387 # # then the symlinks
388 # $htmldoc .= "\n<pre>\n";
390 # foreach my $l (sort @links) {
391 # if ($l =~ /$allowed/ and $link = readlink "$dir/$l" and $link =~ /^[^.\/]/) {
392 # $htmldoc .= "$l -> <a href=\"$link\">$dir/$link</a>\n";
397 $htmldoc .= "\n<pre>\n";
398 foreach my $f (sort @files) {
399 if ($f =~ /$allowed/) {
400 $htmldoc .= sprintf "%20s %20s <a href=\"%s/%s\">%s</a>\n",
401 isodate(mtime("$dir/$f")),
403 $uri,urlencode($f),$f;
406 $htmldoc .= "</pre>\n</HTML>\n";
408 $size = length($htmldoc);
412 "Content-Length: $size",
413 "Content-Type: text/html",
417 fdlog($log,"$dir/",$size,$size);
423 while (s/(\d)(\d\d\d\b)/$1,$2/) {};
432 if (@stat = stat($file)) {
433 return strftime("%a, %d %b %Y %T GMT",gmtime($stat[9]));
441 my $p1 = abs_path(shift);
442 my $p2 = abs_path(shift);
444 if (defined $p1 and defined $p2) {
445 return 1 if $p1 =~ /^\Q$p2/;
446 return 2 if dirname($p1) =~ /^\Q$p2/;
452 # return real file name (from symlink)
456 return '' unless -e $file;
459 return realfilename(readlink($file));
467 my $file = shift; # can be directory, too!
473 # documents with leading . are not allowed
474 if (abs_path($file) =~ /\/\./) {
475 errorlog("$file with leading .");
481 # document filename must not contain @
482 if (realfilename($file) =~ /@/ or abs_path($file) =~ /@/) {
483 errorlog("$file contains @");
487 # document filename must not end with ~
488 if (realfilename($file) =~ /~$/) {
489 errorlog("$file ends with ~");
493 # file must be group or world readable
494 if (@s = stat($file) and not($s[2] & (S_IRGRP|S_IROTH))) {
495 errorlog("$file not group or world readable");
499 # symlink to regular file and symlink owned by root or fex? ==> ok!
500 if (-l $file and path_match(dirname($file),$docdir)) {
502 return if $s[4] == 0 or $s[4] == $<;
507 # file in allowed directory? ==> ok!
508 foreach my $dir (@doc_dirs) {
509 return if path_match($file,$dir);
512 errorlog("$file not in \@doc_dirs");
516 # security check: client ip allowed?
518 my $file = abs_path(shift);
523 $dir .= '/x' if -d $dir;
525 while ($dir = dirname($dir) and $dir ne '/') {
526 $af = "$dir/.htaccessfrom";
535 errorlog("no access to $file by $af");
542 # HTTP Basic authentication
548 my $uri = $ENV{REQUEST_URI} || '/';
550 $uri =~ s/\/index\.html$//;
553 if (-d $doc or $doc =~ /\/index\.html$/) {
556 $realm = dirname($uri);
559 $auth = slurp($htauth);
560 unless ($auth and $realm) {
561 http_header("200 OK");
562 print html_header("$ENV{SERVER_NAME} no authentication");
564 '<h3><code>$htauth</code> missing</h3>'
571 if ($ENV{HTTP_AUTHORIZATION} and $ENV{HTTP_AUTHORIZATION} =~ /Basic\s+(.+)/)
572 { @http_auth = split(':',decode_b64($1)) }
573 if (@http_auth != 2 or $http_auth[1] ne $auth) {
575 '401 Authorization Required',
576 "WWW-Authenticate: Basic realm=\"$realm\"",
579 # control back to fexsrv for further HTTP handling
585 # function for <<perl-code>> inside HTML documents
591 # tie STDOUT to buffer variable (redefining print)
595 my ($class,$buffer) = @_;
596 bless $buffer,$class;
601 $$buffer .= $_ foreach @_;
607 $$buffer .= sprintf($fmt,@_);