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);
19 my $log = "$logdir/dop.log";
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};
49 $host = $ENV{HTTP_HOST} || $hostname;
51 "HTTP/1.1 301 Moved Permanently",
52 "Location: $ENV{PROTO}://$host/$doc",
61 if (@wdd and $wdd and grep { $doc =~ /$_/ } @wdd) { &$wdd($doc) }
63 my $dir = untaint(getcwd());
65 http_output($doc,$seek,$stop);
70 my ($file,$seek,$stop) = @_;
71 my ($filename,$files,$streamfile,$size,$total_size);
80 my $http_client = $ENV{HTTP_USER_AGENT} || '';
83 # extra security check: document must not be in lib or spool directory
84 if (path_match($file,$FEXLIB) or path_match($file,$spooldir)) {
88 security_check($file);
89 $htauth = dirname($file).'/.htauth';
90 require_auth($htauth,$file) if -f $htauth;
94 open $file,'<',$file or http_error(400);
95 security_check($file);
96 } elsif ($file =~ /(.+)\.gz$/ and -f $1) {
98 open $file,'-|',qw'gzip -c',@files or http_error(503);
99 } elsif ($file =~ /(.+)\.tgz$/ and -f "$1.tar") {
101 open $file,'-|',qw'gzip -c',@files or http_error(503);
102 } elsif ($file =~ /(.+)\.(tar|tgz|zip)$/ and
103 @s = lstat($streamfile = "$1.stream") and $s[4] == $<)
105 # streaming file (only if it is owned by user fex)
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') {
311 # binary data # can be stream!
312 seek $file,$seek,0 if $seek;
313 while ($b = read($file,$data,$bs)) {
314 if ($stop and $s+$b > $size) {
316 $data = substr($data,0,$b)
323 fdlog($log,$file,$s,$size) if $s;
328 exit if @files; # streaming end
333 # show directory index
340 my $uri = $ENV{REQUEST_URI};
342 my ($htindex,$htauth);
348 security_check($dir);
350 $htindex = "$dir/.htindex";
351 $htauth = "$dir/.htauth";
353 open $htindex,$htindex or http_error(403);
354 require_auth($htauth,$dir) if -f $htauth;
356 # .htindex may contain listing regexp
357 chomp ($allowed = <$htindex>||'.');
360 opendir $dir,$dir or http_error(503);
361 while (defined($_ = readdir $dir)) {
362 next if /^[.#]/ or /~$/;
363 if (@s = lstat "$dir/$_" and ($s[2] & (S_IRGRP|S_IROTH))) {
364 if (-l _) { push @links,$_ }
365 elsif (-d _) { push @dirs,$_ }
366 elsif (-f _) { push @files,$_ }
371 # parent directory listable?
372 if ($uri =~ m:(/.+)/.+: and -f "$dir/../.htindex") {
376 # first the (sub)directories
377 $htmldoc = "<HTML>\n<h1>$uri/</h1>\n";
378 foreach my $d (sort @dirs) {
379 if ($d =~ m:^/: and -f "$d/.htindex") {
380 $htmldoc .= "<h3><a href=\"$d/\">$d/</a></h3>\n";
381 } elsif (-f "$dir/$d/.htindex") {
382 $htmldoc .= "<h3><a href=\"$uri/$d/\">$uri/$d/</a></h3>\n";
386 # # then the symlinks
387 # $htmldoc .= "\n<pre>\n";
389 # foreach my $l (sort @links) {
390 # if ($l =~ /$allowed/ and $link = readlink "$dir/$l" and $link =~ /^[^.\/]/) {
391 # $htmldoc .= "$l -> <a href=\"$link\">$dir/$link</a>\n";
396 $htmldoc .= "\n<pre>\n";
397 foreach my $f (sort @files) {
398 if ($f =~ /$allowed/) {
399 $htmldoc .= sprintf "%20s %20s <a href=\"%s/%s\">%s</a>\n",
400 isodate(mtime("$dir/$f")),
402 $uri,urlencode($f),$f;
405 $htmldoc .= "</pre>\n</HTML>\n";
407 $size = length($htmldoc);
411 "Content-Length: $size",
412 "Content-Type: text/html",
416 fdlog($log,"$dir/",$size,$size);
421 return (lstat shift)[9];
427 while (s/(\d)(\d\d\d\b)/$1,$2/) {};
436 if (@stat = stat($file)) {
437 return strftime("%a, %d %b %Y %T GMT",gmtime($stat[9]));
445 my $p1 = abs_path(shift);
446 my $p2 = abs_path(shift);
448 if (defined $p1 and defined $p2) {
449 return 1 if $p1 =~ /^\Q$p2/;
450 return 2 if dirname($p1) =~ /^\Q$p2/;
456 # return real file name (from symlink)
460 return '' unless -e $file;
463 return realfilename(readlink($file));
471 my $file = shift; # can be directory, too!
477 # documents with leading . are not allowed
478 if (abs_path($file) =~ /\/\./) {
479 errorlog("$file with leading .");
485 # document filename must not contain @
486 if (realfilename($file) =~ /@/ or abs_path($file) =~ /@/) {
487 errorlog("$file contains @");
491 # document filename must not end with ~
492 if (realfilename($file) =~ /~$/) {
493 errorlog("$file ends with ~");
497 # file must be group or world readable
498 if (@s = stat($file) and not($s[2] & (S_IRGRP|S_IROTH))) {
499 errorlog("$file not group or world readable");
503 # symlink to regular file and symlink owned by root or fex? ==> ok!
504 if (-l $file and path_match(dirname($file),$docdir)) {
506 return if $s[4] == 0 or $s[4] == $<;
511 # file in allowed directory? ==> ok!
512 foreach my $dir (@doc_dirs) {
513 return if path_match($file,$dir);
516 errorlog("$file not in \@doc_dirs");
520 # security check: client ip allowed?
522 my $file = abs_path(shift);
527 $dir .= '/x' if -d $dir;
529 while ($dir = dirname($dir) and $dir ne '/') {
530 $af = "$dir/.htaccessfrom";
539 errorlog("no access to $file by $af");
546 # HTTP Basic authentication
552 my $uri = $ENV{REQUEST_URI} || '/';
554 $uri =~ s/\/index\.html$//;
557 if (-d $doc or $doc =~ /\/index\.html$/) {
560 $realm = dirname($uri);
563 $auth = slurp($htauth);
564 unless ($auth and $realm) {
565 http_header("200 OK");
566 print html_header("$ENV{SERVER_NAME} no authentication");
568 '<h3><code>$htauth</code> missing</h3>'
575 if ($ENV{HTTP_AUTHORIZATION} and $ENV{HTTP_AUTHORIZATION} =~ /Basic\s+(.+)/)
576 { @http_auth = split(':',decode_b64($1)) }
577 if (@http_auth != 2 or $http_auth[1] ne $auth) {
579 '401 Authorization Required',
580 "WWW-Authenticate: Basic realm=\"$realm\"",
583 # control back to fexsrv for further HTTP handling
589 # function for <<perl-code>> inside HTML documents
595 # tie STDOUT to buffer variable (redefining print)
599 my ($class,$buffer) = @_;
600 bless $buffer,$class;
605 $$buffer .= $_ foreach @_;
611 $$buffer .= sprintf($fmt,@_);