5 # is a subprogram of fexsrv! do not run it directly!
7 # Author: Ulli Horlacher <framstag@rus.uni-stuttgart.de>
11 use Fcntl qw(:flock :seek :mode);
12 use POSIX qw(strftime locale_h);
13 use Cwd qw(getcwd abs_path);
15 # use CGI::Carp qw(fatalsToBrowser);
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
129 errorlog("$streamfile: $_ is not allowed for streaming");
133 errorlog("$streamfile: $_ does not exist");
136 if (@s = stat($_) and not($s[2] & S_IRGRP) or not -r $_) {
137 # file must be readable by user and group
138 errorlog("$streamfile: $_ is not readable by user and group");
142 http_error(416) if $ENV{HTTP_RANGE};
144 if ($file =~ /\.tar$/) { @a = qw'tar --exclude *~ --exclude .* -cf -' }
145 elsif ($file =~ /\.tgz$/) { @a = qw'tar --exclude *~ --exclude .* -czf -' }
146 elsif ($file =~ /\.zip$/) { @a = qw'zip -x *~ */.*/* @ -rq -' }
147 else { http_error(400) }
148 open $file,'-|',@a,@files or http_error(503);
153 $type = 'application/octet-stream';
154 if ($file =~ /\.html$/) { $type = 'text/html' }
155 # elsif ($file =~ /\.txt$/) { $type = 'text/plain' }
156 elsif ($file =~ /\.css$/) { $type = 'text/css' }
157 elsif ($file =~ /\.js$/) { $type = 'text/javascript' }
158 elsif ($file =~ /\.ps$/) { $type = 'application/postscript' }
159 elsif ($file =~ /\.pdf$/) { $type = 'application/pdf' }
160 elsif ($file =~ /\.jpg$/) { $type = 'image/jpeg' }
161 elsif ($file =~ /\.png$/) { $type = 'image/png' }
162 elsif ($file =~ /\.gif$/) { $type = 'image/gif' }
163 elsif ($file !~ /\.(tar|tgz|zip|jar|rar|arj|7z|bz2?|gz)$/) {
164 my $qfile = untaint(abs_path($file));
165 $qfile =~ s/([^\/\.\+\w!=,_-])/\\$1/g;
169 } elsif (/text/i and not -x $file) {
170 $type = 'text/plain';
171 if (/\sASCII\s/) { $type .= "; charset=us-ascii" }
172 elsif (/(ISO-[\w-]+)/) { $type .= "; charset=".lc($1) }
173 else { $type .= "; charset=utf-8" }
177 # show sourcecode if URL ends with '!'
178 # to avoid this for a HTML file, simple do a: chmod o-r file
179 if ($type eq 'text/html') {
181 if (@s = stat($file) and $s[2] & S_IROTH) {
182 $type = 'text/plain';
187 } elsif ($ENV{'QUERY_STRING'} eq '!') {
188 $type = 'text/plain';
192 if ($type eq 'text/html') {
197 while ($htmldoc =~ s/\n##.*?\n/\n/) {};
198 # evaluate #if ... #else ... #elseif ... #endif blocks
199 my $mark = randstring(16);
200 while ($htmldoc =~ s/\n(#if\s+(.+?)\n.+?\n)#endif/\n$mark/s) {
206 $htmldoc =~ s/$mark/$_/;
209 while (s/.*?\n#elseif\s+(.+?)\n//s) {
212 $htmldoc =~ s/$mark/$_/;
216 if ($htmldoc =~ /$mark/) {
217 s/.*\n#else\s*\n//s or $_ = '';
218 $htmldoc =~ s/$mark/$_/;
223 while ($htmldoc =~ s/\n#include "(.*?)"/\n$mark/s) {
226 if (open $file,$file) {
230 $dynamic = $htmldoc =~ s/$mark/$include/;
232 # evaluate <<perl-code>> or <<<perl-code>>>
235 local $SIG{ALRM} = sub { $timeout = '<h3>TIMEOUT!</h3>' };
237 while ($htmldoc =~ /<<(.+?>?)>>/s) {
239 if ($pc =~ s/^<(.+)>$/$1/) {
240 # eval code without output substitution
241 eval('package DOP;' . $pc);
243 $dynamic = $htmldoc =~ s/<<<(.+?)>>>//s;
245 # eval code with output substitution
248 tie *STDOUT => "Buffer",\$__;
249 my $r .= eval('package DOP;' . $pc);
250 $__ .= $r if $pc !~ /;\s*$/;
253 $dynamic = $htmldoc =~ s/<<(.+?)>>/$__/s;
257 $dynamic = $htmldoc =~ s/<<(.+?>?)>>/$timeout/sg if $timeout;
259 # substitute $variable$ with value from environment (if present)
260 while ($htmldoc =~ /\$([\w_]+)\$/g) {
262 if (defined($env = $ENV{$var})) {
263 $htmldoc =~ s/\$$var\$/$env/g;
266 $total_size = $size = $s = length($htmldoc);
271 $total_size = -s $file || 0;
272 $size = $total_size - $seek - ($stop ? $total_size-$stop-1 : 0);
277 http_header('416 Requested Range Not Satisfiable');
283 if ($seek or $stop) {
286 $range = sprintf("bytes %s-%s/%s",$seek,$stop,$total_size);
288 $range = sprintf("bytes %s-%s/%s",$seek,$total_size-1,$total_size);
291 'HTTP/1.1 206 Partial Content',
293 "Content-Length: $size",
294 "Content-Range: $range",
295 "Content-Type: $type",
304 "Content-Type: $type",
307 # Java (clients) needs Last-Modified header!
308 # if there are locale versions, use actual time for Last-Modified
309 # to enforce reload of page
310 $file =~ m{/htdocs/(.+)};
311 my @lfiles = glob("$FEXHOME/locale/*/htdocs/$1");
312 my $date = ($dynamic or @lfiles > 1) ?
313 strftime("%a, %d %b %Y %T GMT",gmtime(time)) :
318 "Last-Modified: $date",
320 "Content-Length: $size",
321 "Content-Type: $type",
323 # nvt_print("Set-Cookie: locale=$locale") if $use_cookies and $locale;
326 nvt_print($_) foreach(@extra_header);
329 if ($ENV{REQUEST_METHOD} eq 'GET') {
330 if ($type eq 'text/html') {
335 # binary data # can be stream!
336 seek $file,$seek,0 if $seek;
337 while ($b = read($file,$data,$bs)) {
338 if ($stop and $s+$b > $size) {
340 $data = substr($data,0,$b)
347 fdlog($log,$file,$s,$size) if $s;
352 exit if @files; # streaming end
357 # show directory index
364 my $uri = $ENV{REQUEST_URI};
366 my ($htindex,$htauth);
372 security_check($dir);
374 $htindex = "$dir/.htindex";
375 $htauth = "$dir/.htauth";
377 open $htindex,$htindex or http_error(403);
378 require_auth($htauth,$dir) if -f $htauth;
380 # .htindex may contain listing regexp
381 chomp ($allowed = <$htindex>||'.');
384 opendir $dir,$dir or http_error(503);
385 while (defined($_ = readdir $dir)) {
386 next if /^[.#]/ or /~$/;
387 if (@s = lstat "$dir/$_" and ($s[2] & (S_IRGRP|S_IROTH))) {
388 if (-l _) { push @links,$_ }
389 elsif (-d _) { push @dirs,$_ }
390 elsif (-f _) { push @files,$_ }
395 # parent directory listable?
396 if ($uri =~ m:(/.+)/.+: and -f "$dir/../.htindex") {
400 # first the (sub)directories
401 $htmldoc = "<HTML>\n<h1>$uri/</h1>\n";
402 foreach my $d (sort @dirs) {
403 if ($d =~ m:^/: and -f "$d/.htindex") {
404 $htmldoc .= "<h3><a href=\"$d/\">$d/</a></h3>\n";
405 } elsif (-f "$dir/$d/.htindex") {
406 $htmldoc .= "<h3><a href=\"$uri/$d/\">$uri/$d/</a></h3>\n";
410 # # then the symlinks
411 # $htmldoc .= "\n<pre>\n";
413 # foreach my $l (sort @links) {
414 # if ($l =~ /$allowed/ and $link = readlink "$dir/$l" and $link =~ /^[^.\/]/) {
415 # $htmldoc .= "$l -> <a href=\"$link\">$dir/$link</a>\n";
420 $htmldoc .= "\n<pre>\n";
421 foreach my $f (sort @files) {
422 if ($f =~ /$allowed/) {
423 $htmldoc .= sprintf "%20s %20s <a href=\"%s/%s\">%s</a>\n",
424 isodate(mtime("$dir/$f")),
426 $uri,urlencode($f),$f;
429 $htmldoc .= "</pre>\n</HTML>\n";
431 $size = length($htmldoc);
435 "Content-Length: $size",
436 "Content-Type: text/html",
440 fdlog($log,"$dir/",$size,$size);
446 while (s/(\d)(\d\d\d\b)/$1,$2/) {};
455 if (@stat = stat($file)) {
456 return strftime("%a, %d %b %Y %T GMT",gmtime($stat[9]));
464 my $p1 = abs_path(shift);
465 my $p2 = abs_path(shift);
467 if (defined $p1 and defined $p2) {
468 return 1 if $p1 =~ /^\Q$p2/;
469 return 2 if dirname($p1) =~ /^\Q$p2/;
475 # return real file name (from symlink)
479 return '' unless -e $file;
482 return realfilename(readlink($file));
490 my $file = shift; # can be directory, too!
496 # documents with leading . are not allowed
497 if (abs_path($file) =~ /\/\./) {
498 errorlog("$file with leading .");
504 # document filename must not contain @
505 if (realfilename($file) =~ /@/ or abs_path($file) =~ /@/) {
506 errorlog("$file contains @");
510 # document filename must not end with ~
511 if (realfilename($file) =~ /~$/) {
512 errorlog("$file ends with ~");
516 # file must be group or world readable
517 if (@s = stat($file) and not($s[2] & (S_IRGRP|S_IROTH))) {
518 errorlog("$file not group or world readable");
522 # symlink to regular file and symlink owned by root or fex? ==> ok!
523 if (-l $file and path_match(dirname($file),$docdir)) {
525 return if $s[4] == 0 or $s[4] == $<;
530 # file in allowed directory? ==> ok!
531 foreach my $dir (@doc_dirs) {
532 return if path_match($file,$dir);
535 errorlog("$file not in \@doc_dirs");
539 # security check: client ip allowed?
541 my $file = abs_path(shift);
546 $dir .= '/x' if -d $dir;
548 while ($dir = dirname($dir) and $dir ne '/') {
549 $af = "$dir/.htaccessfrom";
558 errorlog("no access to $file by $af");
565 # HTTP Basic authentication
571 my $uri = $ENV{REQUEST_URI} || '/';
573 $uri =~ s/\/index\.html$//;
576 if (-d $doc or $doc =~ /\/index\.html$/) {
579 $realm = dirname($uri);
582 $auth = slurp($htauth);
583 unless ($auth and $realm) {
584 http_header("200 OK");
585 print html_header("$ENV{SERVER_NAME} no authentication");
587 '<h3><code>$htauth</code> missing</h3>'
594 if ($ENV{HTTP_AUTHORIZATION} and $ENV{HTTP_AUTHORIZATION} =~ /Basic\s+(.+)/)
595 { @http_auth = split(':',decode_b64($1)) }
596 if (@http_auth != 2 or $http_auth[1] ne $auth) {
598 '401 Authorization Required',
599 "WWW-Authenticate: Basic realm=\"$realm\"",
602 # control back to fexsrv for further HTTP handling
608 # function for <<perl-code>> inside HTML documents
614 # tie STDOUT to buffer variable (redefining print and printf)
618 my ($class,$buffer) = @_;
619 bless $buffer,$class;
624 $$buffer .= $_ foreach @_;
630 $$buffer .= sprintf($fmt,@_);