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,@_);