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);
17 our ($bs,$tmpdir,@doc_dirs);
21 # POSIX time format needed for HTTP header
22 setlocale(LC_TIME,'POSIX');
29 my ($link,$host,$path,$range);
31 our $error = 'F*EX document output ERROR';
36 if ($range = $ENV{HTTP_RANGE}) {
37 $seek = $1 if $range =~ /^bytes=(\d+)-/i;
38 $stop = $1 if $range =~ /^bytes=\d*-(\d+)/i;
41 # redirect on relative symlinks without "../"
42 if ($link = readlink($doc) and
43 $link !~ m:^/: and $link !~ m:\.\./: and $link !~ /^:.+:$/) {
44 $path = $ENV{REQUEST_URI};
59 if (@wdd and $wdd and grep { $doc =~ /$_/ } @wdd) { &$wdd($doc) }
61 my $dir = untaint(getcwd());
63 http_output($doc,$seek,$stop);
68 my ($file,$seek,$stop) = @_;
69 my ($filename,$files,$streamfile,$size,$total_size);
78 my $http_client = $ENV{HTTP_USER_AGENT} || '';
81 # extra security check: document must not be in lib or spool directory
82 if (path_match($file,$FEXLIB) or path_match($file,$spooldir)) {
86 security_check($file);
87 $htauth = dirname($file).'/.htauth';
88 require_auth($htauth,$file) if -f $htauth;
92 open $file,'<',$file or http_error(400);
93 security_check($file);
94 } elsif ($file =~ /(.+)\.gz$/ and -f $1) {
96 open $file,'-|',qw'gzip -c',@files or http_error(503);
97 } elsif ($file =~ /(.+)\.tgz$/ and -f "$1.tar") {
99 open $file,'-|',qw'gzip -c',@files or http_error(503);
100 } elsif ($file =~ /(.+)\.(tar|tgz|zip)$/ and
101 @s = lstat($streamfile = "$1.stream") and $s[4] == $<)
103 # streaming file (only if it is owned by user fex)
104 chdir dirname($file);
105 security_check($file);
106 if (-l $streamfile and readlink($streamfile) =~ /^:(.+):$/) {
107 # special symlink pointer file for streaming
108 @files = split(/:/,$1);
109 } elsif (open $streamfile,$streamfile) {
110 # special streaming file
111 while (<$streamfile>) {
125 if (/^\// or /\.\.\//) {
126 # absolute path or relative path with parent directory is not allowed
129 if (@s = stat($_) and not($s[2] & S_IRGRP) or not -r $_) {
130 # file must be readable by user and group
134 http_error(416) if $ENV{HTTP_RANGE};
136 if ($file =~ /\.tar$/) { @a = qw'tar --exclude *~ --exclude .* -cf -' }
137 elsif ($file =~ /\.tgz$/) { @a = qw'tar --exclude *~ --exclude .* -czf -' }
138 elsif ($file =~ /\.zip$/) { @a = qw'zip -x *~ */.*/* @ -rq -' }
139 else { http_error(400) }
140 open $file,'-|',@a,@files or http_error(503);
145 $type = 'application/octet-stream';
146 if ($file =~ /\.html$/) { $type = 'text/html' }
147 # elsif ($file =~ /\.txt$/) { $type = 'text/plain' }
148 elsif ($file =~ /\.css$/) { $type = 'text/css' }
149 elsif ($file =~ /\.js$/) { $type = 'text/javascript' }
150 elsif ($file =~ /\.ps$/) { $type = 'application/postscript' }
151 elsif ($file =~ /\.pdf$/) { $type = 'application/pdf' }
152 elsif ($file =~ /\.jpg$/) { $type = 'image/jpeg' }
153 elsif ($file =~ /\.png$/) { $type = 'image/png' }
154 elsif ($file =~ /\.gif$/) { $type = 'image/gif' }
155 elsif ($file !~ /\.(tar|tgz|zip|jar|rar|arj|7z|bz2?|gz)$/) {
156 my $qfile = untaint(abs_path($file));
157 $qfile =~ s/([^\/\.\+\w!=,_-])/\\$1/g;
161 } elsif (/text/i and not -x $file) {
162 $type = 'text/plain';
163 if (/\sASCII\s/) { $type .= "; charset=us-ascii" }
164 elsif (/(ISO-[\w-]+)/) { $type .= "; charset=".lc($1) }
165 else { $type .= "; charset=utf-8" }
169 # show sourcecode if URL ends with '!'
170 # to avoid this for a HTML file, simple do a: chmod o-r file
171 if ($type eq 'text/html') {
173 if (@s = stat($file) and $s[2] & S_IROTH) {
174 $type = 'text/plain';
179 } elsif ($ENV{'QUERY_STRING'} eq '!') {
180 $type = 'text/plain';
184 if ($type eq 'text/html') {
189 while ($htmldoc =~ s/\n##.*?\n/\n/) {};
190 # evaluate #if ... #else ... #elseif ... #endif blocks
191 my $mark = randstring(16);
192 while ($htmldoc =~ s/\n(#if\s+(.+?)\n.+?\n)#endif/\n$mark/s) {
198 $htmldoc =~ s/$mark/$_/;
201 while (s/.*?\n#elseif\s+(.+?)\n//s) {
204 $htmldoc =~ s/$mark/$_/;
208 if ($htmldoc =~ /$mark/) {
209 s/.*\n#else\s*\n//s or $_ = '';
210 $htmldoc =~ s/$mark/$_/;
215 while ($htmldoc =~ s/\n#include "(.*?)"/\n$mark/s) {
218 if (open $file,$file) {
222 $dynamic = $htmldoc =~ s/$mark/$include/;
224 # evaluate <<perl-code>>
225 while ($htmldoc =~ /<<(.+?)>>/s) {
228 tie *STDOUT => "Buffer",\$__;
231 $dynamic = $htmldoc =~ s/<<(.+?)>>/$__/s;
233 # substitute $variable$ with value from environment (if present)
234 while ($htmldoc =~ /\$([\w_]+)\$/g) {
236 if (defined($env = $ENV{$var})) {
237 $htmldoc =~ s/\$$var\$/$env/g;
240 $total_size = $size = $s = length($htmldoc);
245 $total_size = -s $file || 0;
246 $size = $total_size - $seek - ($stop ? $total_size-$stop-1 : 0);
251 http_header('416 Requested Range Not Satisfiable');
257 if ($seek or $stop) {
260 $range = sprintf("bytes %s-%s/%s",$seek,$stop,$total_size);
262 $range = sprintf("bytes %s-%s/%s",$seek,$total_size-1,$total_size);
265 'HTTP/1.1 206 Partial Content',
267 "Content-Length: $size",
268 "Content-Range: $range",
269 "Content-Type: $type",
279 "Content-Type: $type",
283 # Java (clients) needs Last-Modified header!
284 # if there are locale versions, use actual time for Last-Modified
285 # to enforce reload of page
286 $file =~ m{/htdocs/(.+)};
287 my @lfiles = glob("$FEXHOME/locale/*/htdocs/$1");
288 my $date = ($dynamic or @lfiles > 1) ?
289 strftime("%a, %d %b %Y %T GMT",gmtime(time)) :
294 "Last-Modified: $date",
296 "Content-Length: $size",
297 "Content-Type: $type",
299 nvt_print("Set-Cookie: locale=$locale") if $use_cookies and $locale;
304 if ($ENV{REQUEST_METHOD} eq 'GET') {
305 if ($type eq 'text/html') {
310 # binary data # can be stream!
311 seek $file,$seek,0 if $seek;
312 while ($b = read($file,$data,$bs)) {
313 if ($stop and $s+$b > $size) {
315 $data = substr($data,0,$b)
322 fdlog($log,$file,$s,$size) if $s;
327 exit if @files; # streaming end
332 # show directory index
339 my $uri = $ENV{REQUEST_URI};
341 my ($htindex,$htauth);
347 security_check($dir);
349 $htindex = "$dir/.htindex";
350 $htauth = "$dir/.htauth";
352 open $htindex,$htindex or http_error(403);
353 require_auth($htauth,$dir) if -f $htauth;
355 # .htindex may contain listing regexp
356 chomp ($allowed = <$htindex>||'.');
359 opendir $dir,$dir or http_error(503);
360 while (defined($_ = readdir $dir)) {
361 next if /^[.#]/ or /~$/;
362 if (@s = lstat "$dir/$_" and ($s[2] & (S_IRGRP|S_IROTH))) {
363 if (-l _) { push @links,$_ }
364 elsif (-d _) { push @dirs,$_ }
365 elsif (-f _) { push @files,$_ }
370 # parent directory listable?
371 if ($uri =~ m:(/.+)/.+: and -f "$dir/../.htindex") {
375 # first the (sub)directories
376 $htmldoc = "<HTML>\n<h1>$uri/</h1>\n";
377 foreach my $d (sort @dirs) {
378 if ($d =~ m:^/: and -f "$d/.htindex") {
379 $htmldoc .= "<h3><a href=\"$d/\">$d/</a></h3>\n";
380 } elsif (-f "$dir/$d/.htindex") {
381 $htmldoc .= "<h3><a href=\"$uri/$d/\">$uri/$d/</a></h3>\n";
385 # # then the symlinks
386 # $htmldoc .= "\n<pre>\n";
388 # foreach my $l (sort @links) {
389 # if ($l =~ /$allowed/ and $link = readlink "$dir/$l" and $link =~ /^[^.\/]/) {
390 # $htmldoc .= "$l -> <a href=\"$link\">$dir/$link</a>\n";
395 $htmldoc .= "\n<pre>\n";
396 foreach my $f (sort @files) {
397 if ($f =~ /$allowed/) {
398 $htmldoc .= sprintf "%20s %20s <a href=\"%s/%s\">%s</a>\n",
399 isodate(mtime("$dir/$f")),
401 $uri,urlencode($f),$f;
404 $htmldoc .= "</pre>\n</HTML>\n";
406 $size = length($htmldoc);
410 "Content-Length: $size",
411 "Content-Type: text/html",
415 fdlog($log,"$dir/",$size,$size);
421 while (s/(\d)(\d\d\d\b)/$1,$2/) {};
430 if (@stat = stat($file)) {
431 return strftime("%a, %d %b %Y %T GMT",gmtime($stat[9]));
439 my $p1 = abs_path(shift);
440 my $p2 = abs_path(shift);
442 if (defined $p1 and defined $p2) {
443 return 1 if $p1 =~ /^\Q$p2/;
444 return 2 if dirname($p1) =~ /^\Q$p2/;
450 # return real file name (from symlink)
454 return '' unless -e $file;
457 return realfilename(readlink($file));
465 my $file = shift; # can be directory, too!
471 # documents with leading . are not allowed
472 if (abs_path($file) =~ /\/\./) {
473 errorlog("$file with leading .");
479 # document filename must not contain @
480 if (realfilename($file) =~ /@/ or abs_path($file) =~ /@/) {
481 errorlog("$file contains @");
485 # document filename must not end with ~
486 if (realfilename($file) =~ /~$/) {
487 errorlog("$file ends with ~");
491 # file must be group or world readable
492 if (@s = stat($file) and not($s[2] & (S_IRGRP|S_IROTH))) {
493 errorlog("$file not group or world readable");
497 # symlink to regular file and symlink owned by root or fex? ==> ok!
498 if (-l $file and path_match(dirname($file),$docdir)) {
500 return if $s[4] == 0 or $s[4] == $<;
505 # file in allowed directory? ==> ok!
506 foreach my $dir (@doc_dirs) {
507 return if path_match($file,$dir);
510 errorlog("$file not in \@doc_dirs");
514 # security check: client ip allowed?
516 my $file = abs_path(shift);
521 $dir .= '/x' if -d $dir;
523 while ($dir = dirname($dir) and $dir ne '/') {
524 $af = "$dir/.htaccessfrom";
533 errorlog("no access to $file by $af");
540 # HTTP Basic authentication
546 my $uri = $ENV{REQUEST_URI} || '/';
548 $uri =~ s/\/index\.html$//;
551 if (-d $doc or $doc =~ /\/index\.html$/) {
554 $realm = dirname($uri);
557 $auth = slurp($htauth);
558 unless ($auth and $realm) {
559 http_header("200 OK");
560 print html_header("$ENV{SERVER_NAME} no authentication");
562 '<h3><code>$htauth</code> missing</h3>'
569 if ($ENV{HTTP_AUTHORIZATION} and $ENV{HTTP_AUTHORIZATION} =~ /Basic\s+(.+)/)
570 { @http_auth = split(':',decode_b64($1)) }
571 if (@http_auth != 2 or $http_auth[1] ne $auth) {
573 '401 Authorization Required',
574 "WWW-Authenticate: Basic realm=\"$realm\"",
577 # control back to fexsrv for further HTTP handling
583 # function for <<perl-code>> inside HTML documents
589 # tie STDOUT to buffer variable (redefining print)
593 my ($class,$buffer) = @_;
594 bless $buffer,$class;
599 $$buffer .= $_ foreach @_;
605 $$buffer .= sprintf($fmt,@_);