5 use Fcntl qw':flock :seek :mode';
9 use Digest::MD5 qw'md5_hex';
12 use Symbol qw'gensym';
14 # set and untaint ENV if not in CLI (fexsrv provides clean ENV)
16 foreach my $v (keys %ENV) {
17 ($ENV{$v}) = ($ENV{$v} =~ /(.*)/s) if defined $ENV{$v};
19 $ENV{PATH} = '/usr/local/bin:/bin:/usr/bin';
24 unless ($FEXLIB = $ENV{FEXLIB} and -d $FEXLIB) {
25 die "$0: found no FEXLIB - fexsrv needs full path\n"
31 # $FEXHOME is top-level directory of F*EX installation or vhost
32 # $ENV{HOME} is login-directory of user fex
33 # in default-installation both are equal, but they may differ
34 $FEXHOME = $ENV{FEXHOME} or $ENV{FEXHOME} = $FEXHOME = dirname($FEXLIB);
39 $hostname = gethostname();
40 $tmpdir = $ENV{TMPDIR} || '/var/tmp';
41 $spooldir = $FEXHOME.'/spool';
42 $docdir = $FEXHOME.'/htdocs';
46 $limited_download = 'YES'; # multiple downloads only from same client
47 $fex_yourself = 'YES'; # allow SENDER = RECIPIENT
49 $recipient_quota = 0; # MB
50 $sender_quota = 0; # MB
51 $timeout = 30; # seconds
52 $bs = 2**16; # I/O blocksize
53 $DS = 60*60*24; # seconds in a day
54 $MB = 1024*1024; # binary Mega
56 $sendmail = '/usr/lib/sendmail';
57 $sendmail = '/usr/sbin/sendmail' unless -x $sendmail;
65 @forbidden_user_agents = ('FDM');
67 # https://securityheaders.io/
68 # https://scotthelme.co.uk/hardening-your-http-response-headers/
69 # http://content-security-policy.com/
71 # "Content-Security-Policy: sandbox allow-forms allow-scripts",
72 "Content-Security-Policy: script-src 'self' 'unsafe-inline'",
73 "X-Frame-Options: SAMEORIGIN",
74 "X-XSS-Protection: 1; mode=block",
75 "X-Content-Type-Options: nosniff",
78 $FHS = -f '/etc/fex/fex.ph' and -d '/usr/share/fex/lib';
81 $ENV{FEXHOME} = $FEXHOME = '/usr/share/fex';
82 $spooldir = '/var/spool/fex';
83 $logdir = '/var/log/fex';
84 $docdir = '/var/lib/fex/htdocs';
85 $notify_newrelease = '';
88 # allowed download managers (HTTP User-Agent)
89 $adlm = '^(Axel|fex)';
92 require "$FEXLIB/fex.ph" or die "$0: cannot load $FEXLIB/fex.ph - $!";
94 $fop_auth = 0 if $fop_auth =~ /no/i;
95 $mail_authid = 0 if $mail_authid =~ /no/i;
96 $force_https = 0 if $force_https =~ /no/i;
97 $debug = 0 if $debug =~ /no/i;
99 @logdir = ($logdir) unless @logdir;
100 $logdir = $logdir[0];
102 # allowed multi download recipients: from any ip, any times
103 if (@mailing_lists) {
104 $amdl = '^('.join('|',map { quotewild($_) } @mailing_lists).')$';
109 # check for name based virtual host
110 $vhost = vhost($ENV{'HTTP_HOST'});
112 $RB = 0; # read POST bytes
114 push @doc_dirs,$docdir;
115 foreach my $ld (glob "$FEXHOME/locale/*/htdocs") {
119 $nomail = ($mailmode =~ /^MANUAL|nomail$/i);
121 if (not $nomail and not -x $sendmail) {
122 http_die("found no sendmail");
124 http_die("cannot determine the server hostname") unless $hostname;
126 $ENV{PROTO} = 'http' unless $ENV{PROTO};
127 $keep = $keep_default ||= $keep || 5;
129 $fra = $ENV{REMOTE_ADDR} || '';
130 $sid = $ENV{SID} || '';
132 $dkeydir = "$spooldir/.dkeys"; # download keys
133 $ukeydir = "$spooldir/.ukeys"; # upload keys
134 $akeydir = "$spooldir/.akeys"; # authentification keys
135 $skeydir = "$spooldir/.skeys"; # subuser authentification keys
136 $gkeydir = "$spooldir/.gkeys"; # group authentification keys
137 $xkeydir = "$spooldir/.xkeys"; # extra download keys
138 $lockdir = "$spooldir/.locks"; # download lock files
140 if (my $ra = $ENV{REMOTE_ADDR} and $max_fail) {
141 mkdirp("$spooldir/.fail");
142 $faillog = "$spooldir/.fail/$ra";
146 $admin = $ENV{SERVER_ADMIN} ? $ENV{SERVER_ADMIN} : 'fex@'.$hostname;
149 # $ENV{SERVER_ADMIN} may be set empty in fex.ph!
150 $ENV{SERVER_ADMIN} = $admin unless defined $ENV{SERVER_ADMIN};
155 if (my $cookie = $ENV{HTTP_COOKIE}) {
156 if ($cookie =~ /\bakey=(\w+)/) { $akey = $1 }
157 # elsif ($cookie =~ /\bskey=(\w+)/) { $skey = $1 }
162 if ($default_locale and not grep /^$default_locale$/,@locales) {
163 push @locales,$default_locale;
166 $default_locale = $locales[0];
170 $default_locale ||= 'english';
172 # $durl is first default fop download URL
173 # @durl is optional mandatory fop download URL list (from fex.ph)
177 my $xinetd = '/etc/xinetd.d/fex';
181 } elsif ($ENV{HTTP_HOST} and $ENV{PROTO}) {
183 ($host,$port) = split(':',$ENV{HTTP_HOST}||'');
188 if (open $xinetd,$xinetd) {
190 if (/^\s*port\s*=\s*(\d+)/) {
199 # use same protocal as uploader for download
200 if ($ENV{PROTO} eq 'https' and $port == 443 or $port == 80) {
201 $durl = "$ENV{PROTO}://$host/fop";
203 $durl = "$ENV{PROTO}://$host:$port/fop";
206 if (open $xinetd,$xinetd) {
208 if (/^\s*port\s*=\s*(\d+)/) {
216 $durl = "http://$hostname/fop";
218 $durl = "http://$hostname:$port/fop";
222 @durl = ($durl) unless @durl;
226 exec($FEXHOME.'/bin/fexsrv') if $ENV{KEEP_ALIVE};
233 $cont = shift || 'request accepted: continue';
235 http_header('200 ok');
236 print html_header($head||$ENV{SERVER_NAME});
238 '<script type="text/javascript">'
239 ' window.location.replace("$url");'
242 ' <h3><a href="$url">$cont</a></h3>'
251 print header(),"<pre>\n";
252 print "file = $file\n";
253 foreach $v (keys %ENV) {
254 print $v,' = "',$ENV{$v},"\"\n";
261 foreach (@_) { syswrite STDOUT,"$_\r\n" }
282 return if $HTTP_HEADER;
283 $HTTP_HEADER = $status;
287 nvt_print("HTTP/1.1 $status");
288 nvt_print("X-Message: $msg");
289 # nvt_print("X-SID: $ENV{SID}") if $ENV{SID};
290 nvt_print("Server: fexsrv");
291 nvt_print("Expires: 0");
292 nvt_print("Cache-Control: no-cache");
294 # https://www.owasp.org/index.php/HTTP_Strict_Transport_Security
295 # https://scotthelme.co.uk/hsts-the-missing-link-in-tls/
296 nvt_print("Strict-Transport-Security: max-age=2851200; preload");
298 nvt_print($_) foreach(@extra_header);
300 $akey = md5_hex("$from:$id") if $id and $from;
302 nvt_print("Set-Cookie: akey=$akey; path=/; Max-Age=9999; Discard");
305 # nvt_print("Set-Cookie: skey=$skey; Max-Age=9999; Discard");
308 nvt_print("Set-Cookie: locale=$locale");
311 unless (grep /^Content-Type:/i,@_) {
312 # nvt_print("Content-Type: text/html; charset=ISO-8859-1");
313 nvt_print("Content-Type: text/html; charset=UTF-8");
322 my $header = 'header.html';
325 binmode(STDOUT,':utf8'); # for text/html !
327 # http://www.w3.org/TR/html401/struct/global.html
328 # http://www.w3.org/International/O-charset
332 ' <meta http-equiv="expires" content="0">'
333 ' <meta http-equiv="Content-Type" content="text/html;charset=utf-8">'
334 ' <title>$title</title>'
337 # '<!-- <style type="text/css">\@import "/fex.css";</style> -->'
339 if ($0 =~ /fexdev/) { $head .= "<body bgcolor=\"pink\">\n" }
340 else { $head .= "<body>\n" }
342 $title =~ s:F\*EX:<a href="/index.html">F*EX</a>:;
344 if (open $header,'<',"$docdir/$header") {
345 $head .= $_ while <$header>;
349 $head .= &$prolog($title) if defined($prolog);
353 '<h1><a href="%s"><img align=center src="%s" border=0></a>%s</h1>',
354 $H1_extra[0],$H1_extra[1]||'',$title
357 $head .= "<h1>$title</h1>";
369 my $isodate = isodate(time);
371 $msg =~ s/[\s\n]+/ /g;
372 $msg =~ s/<.+?>//g; # remove HTML
373 map { s/<script.*?>//gi } @msg;
378 $SIG{__DIE__} = 'DEFAULT';
383 # cannot send standard HTTP Status-Code 400, because stupid
384 # Internet Explorer then refuses to display HTML body!
385 http_header("666 Bad Request - $msg");
386 print html_header($error);
387 print 'ERROR: ',join("<p>\n",@msg),"\n";
393 ' <a href="mailto:$ENV{SERVER_ADMIN}">$ENV{SERVER_ADMIN}</a>'
404 unless ($ENV{GATEWAY_INTERFACE}) {
405 warn "$0: @_\n"; # must not die, because of fex_cleanup!
411 # create special error file on upload
413 my $ukey = "$spooldir/.ukeys/$uid";
414 $ukey .= "/error" if -d $ukey;
416 if (open $ukey,'>',$ukey) {
417 print {$ukey} join("\n",@_),"\n";
422 html_error($error||'',@_);
427 if (my $status = readlink '@MAINTENANCE') {
428 my $isodate = isodate(time);
429 http_header('666 MAINTENANCE');
430 print html_header($head||'');
433 "<h1>Server is in maintenance mode</h1>"
437 "<address>$ENV{HTTP_HOST} $isodate</address>"
449 $user .= '@'.$mdomain if $mdomain and $user !~ /@/;
451 if (-e "$user/\@DISABLED") {
452 my $isodate = isodate(time);
453 http_header('666 DISABLED');
454 print html_header($head);
456 "<h3>$user is disabled</h3>"
457 "Contact $ENV{SERVER_ADMIN} for details"
459 "<address>$ENV{HTTP_HOST} $isodate</address>"
468 my @d = localtime shift;
469 return sprintf('%d-%02d-%02d %02d:%02d:%02d',
470 $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]);
476 $s =~ s{([\=\x00-\x20\x7F-\xA0])}{sprintf("=%02X",ord($1))}eog;
481 # from MIME::Base64::Perl
490 return '' unless length;
492 for ($i = 0; $i <= $l; $i += 60) {
493 $uu .= "M" . substr($_,$i,60);
496 $uu .= chr(32+(length)*3/4) . $_ if $_;
497 return unpack ("u",$uu);
501 # short base64 encoding
507 $_ = join '',map(pack('u',$_)=~ /^.(\S*)/, ($_[0]=~/(.{1,45})/gs));
508 tr|` -_|AA-Za-z0-9+/|;
509 $x = (3 - length($_[0]) % 3) % 3;
516 # simulate a "rm -rf", but never removes '..'
517 # return number of removed files
526 next if /(^|\/)\.\.$/;
528 if (-d $file and not -l $file) {
530 opendir D,$dir or next;
531 while ($file = readdir D) {
532 next if $file eq '.' or $file eq '..';
533 $dels += rmrf("$dir/$file");
536 rmdir $dir and $dels++;
538 unlink $file and $dels++;
546 my $hostname = hostname;
551 $_ = `hostname 2>/dev/null`;
552 $hostname = /(.+)/ ? $1 : '';
554 if ($hostname !~ /\./ and open my $rc,'/etc/resolv.conf') {
556 if (/^\s*domain\s+([\w.-]+)/) {
560 if (/^\s*search\s+([\w.-]+)/) {
565 $hostname .= ".$domain" if $domain;
567 if ($hostname !~ /\./ and $admin and $admin =~ /\@([\w.-]+)/) {
575 # strip off path names (Windows or UNIX)
579 s/.*\\// if /^([A-Z]:)?\\/;
586 # substitute all critcal chars
590 return '' unless defined $_;
592 # we need perl native utf8 (see perldoc utf8)
593 $_ = decode_utf8($_) unless utf8::is_utf8($_);
596 s/[\x00-\x1F\x80-\x9F]/_/g;
600 return encode_utf8($_);
604 # substitute all critcal chars
608 return '' unless defined $_;
618 # substitute all critcal chars with underscore
619 sub normalize_filename {
624 # we need native utf8
625 $_ = decode_utf8($_) unless utf8::is_utf8($_);
629 # substitute all critcal chars with underscore
630 s/[^a-zA-Z0-9_=.+-]/_/g;
633 return encode_utf8($_);
637 sub normalize_email {
640 s/[^\w_.+=!~#^\@\-]//g;
650 $user = lc(urldecode(despace($user)));
651 $user .= '@'.$mdomain if $mdomain and $user !~ /@/;
652 checkaddress($user) or http_die("$user is not a valid e-mail address");
653 return untaint($user);
659 s/%([a-f0-9]{2})/chr(hex($1))/gie;
676 http_die("\"$1\" is not allowed at beginning of $input");
678 if (/([\/\"\'\\<>;])/) {
679 http_die(sprintf("\"&#%s;\" is not allowed in %s",ord($1),$input));
682 http_die("\"$1\" is not allowed at end of $input");
685 http_die("control characters are not allowed in $input");
696 local ($domain,$dns);
698 $a =~ s/:\w+=.*//; # remove options from address
700 return $a if $a eq 'anonymous';
702 $a .= '@'.$mdomain if $mdomain and $a !~ /@/;
704 $re = '^[.@-]|@.*@|local(host|domain)$|["\'\`\|\s()<>/;,]';
706 debuglog("$a has illegal syntax ($re)");
709 $re = '^[!^=~#_:.+*{}\w\-\[\]]+\@(\w[.\w\-]*\.[a-z]+)$';
713 local $SIG{__DIE__} = sub { die "\n" };
716 $dns = Net::DNS::Resolver->new->query($domain)||mx($domain);
717 unless ($dns or mx('uni-stuttgart.de')) {
718 http_die("Internal error: bad resolver");
725 debuglog("no A or MX DNS record found for $domain");
729 debuglog("$a does not match e-mail regexp ($re)");
735 # check forbidden addresses
741 $a .= '@'.$mdomain if $mdomain and $a !~ /@/;
742 return $a if -d "$spooldir/$a"; # ok, if user already exists
743 if (@forbidden_recipients) {
744 foreach (@forbidden_recipients) {
746 # skip public recipients
747 if (@public_recipients) {
748 foreach $pr (@public_recipients) {
749 return $a if $a eq lc $pr;
752 return '' if $a =~ /^$fr$/i;
761 my @rc = ('A'..'Z','a'..'z',0..9 );
765 for (1..$n) { $rs .= $rc[int(rand($rn))] };
777 http_die("cannot mkdir /") unless $dir;
779 if ($pdir =~ s:/[^/]+$::) {
780 mkdirp($pdir) unless -d $pdir;
783 mkdir $dir,0770 or http_die("mkdir $dir - $!");
792 if ($rid and $ENV{SID} and $id =~ /^MD5H:/) {
793 $rid = 'MD5H:'.md5_hex($rid.$ENV{SID});
799 # test if ip is in iplist (ipv4/ipv6)
800 # iplist is an array with ips and ip-ranges
809 if ($ip =~ /\./ and $i =~ /\./ or $ip =~ /:/ and $i =~ /:/) {
810 if ($i =~ /(.+)-(.+)/) {
814 return $ip if $ipe ge $ia and $ipe le $ib;
816 return $ip if $ipe eq ipe($i);
823 # ip expand (ipv4/ipv6)
827 if (/^\d+\.\d+\.\d+\.\d+$/) {
828 s/\b(\d\d?)\b/sprintf "%03d",$1/ge;
829 } elsif (/^[:\w]+:\w+$/) {
830 s/\b(\w+)\b/sprintf "%04s",$1/ge;
832 while (s/::/::0000:/) { last if length > 39 }
845 if (open $file,'<',"$file/filename") {
846 $filename = <$file>||'';
853 $filename =~ s:.*/::;
862 s/(^[.~]|[^\w.,=:~^+-])/sprintf "%%%X",ord($1)/ge;
867 # file and document log
869 my ($log,$file,$s,$size) = @_;
870 my $ra = $ENV{REMOTE_ADDR}||'-';
873 $ra .= '/'.$ENV{HTTP_X_FORWARDED_FOR} if $ENV{HTTP_X_FORWARDED_FOR};
876 $msg = sprintf "%s [%s_%s] %s %s %s/%s\n",
877 isodate(time),$$,$ENV{REQUESTCOUNT},$ra,encode_Q($file),$s,$size;
888 return unless $debug and @_;
889 unless ($debuglog and fileno $debuglog) {
890 my $ddir = "$spooldir/.debug";
891 mkdir $ddir,0770 unless -d $ddir;
893 $prg = untaint($prg);
894 $debuglog = sprintf("%s/%s_%s_%s.%s",
895 $ddir,time,$$,$ENV{REQUESTCOUNT}||0,$prg);
896 $debuglog =~ s/\s/_/g;
897 # http://perldoc.perl.org/perlunifaq.html#What-is-a-%22wide-character%22%3f
898 # open $debuglog,'>>:encoding(UTF-8)',$debuglog or return;
899 open $debuglog,'>>',$debuglog or return;
900 # binmode($debuglog,":utf8");
901 autoflush $debuglog 1;
902 # printf {$debuglog} "\n### %s ###\n",isodate(time);
904 while ($_ = shift @_) {
905 $_ = encode_utf8($_) if utf8::is_utf8($_);
907 s/<.+?>//g; # remove HTML
908 print {$debuglog} $_;
909 print "DEBUG: $_" if -t;
918 my $ra = $ENV{REMOTE_ADDR}||'-';
920 $ra .= '/'.$ENV{HTTP_X_FORWARDED_FOR} if $ENV{HTTP_X_FORWARDED_FOR};
923 $msg =~ s/[\r\n]+$//;
924 $msg =~ s/[\r\n]+/ /;
925 $msg =~ s/\s*<p>.*//;
926 $msg = sprintf "%s %s %s %s\n",isodate(time),$prg,$ra,$msg;
928 writelog('error.log',$msg);
936 foreach my $logdir (@logdir) {
937 if (open $log,'>>',"$logdir/$log") {
939 seek $log,0,SEEK_END;
947 # failed authentification log
952 if ($faillog and $max_fail_handler and open $faillog,"+>>$faillog") {
953 flock($faillog,LOCK_EX);
954 seek $faillog,0,SEEK_SET;
955 $n++ while <$faillog>;
956 printf {$faillog} "%s %s\n",isodate(time),$request;
958 &$max_fail_handler($ENV{REMOTE_ADDR}) if $n > $max_fail;
962 # remove all white space
974 my $q = "[\'\"]"; # quote delimiter chars " and '
976 # remove first newline and look for default indention
980 # remove trailing spaces at end
985 # first line have a quote delimiter char?
987 # remove heading spaces and delimiter chars
993 # find the line with the fewest heading spaces (and count them)
997 if (/^( *)\S/ and length($1) < $s) { $s = length($1) };
1005 return join("\n",@s)."\n";
1013 if (@_ > 1 and defined fileno $_[0]) { $H = shift }
1014 binmode($H,':utf8');
1019 # check sender quota
1020 sub check_sender_quota {
1022 my $squota = $sender_quota||0;
1024 my ($file,$size,%file,$data,$upload);
1027 if (open $qf,'<',"$sender/\@QUOTA") {
1030 $squota = $1 if /sender.*?(\d+)/i;
1035 foreach $file (glob "*/$sender/*") {
1036 $data = "$file/data";
1037 $upload = "$file/upload";
1038 if (not -l $data and $size = -s $data) {
1039 # count hard links only once (= same inode)
1040 my $i = (stat($data))[1]||0;
1041 unless ($file{$i}) {
1045 } elsif (-f $upload) {
1046 # count hard links only once (= same inode)
1047 my $i = (stat($upload))[1]||0;
1048 unless ($file{$i}) {
1049 $size = readlink "$file/size" and $du += $size;
1055 return($squota,int($du/1024/1024));
1059 # check recipient quota
1060 sub check_recipient_quota {
1061 my $recipient = shift;
1062 my $rquota = $recipient_quota||0;
1067 if (open my $qf,'<',"$recipient/\@QUOTA") {
1070 $rquota = $1 if /recipient.*?(\d+)/i;
1075 foreach $file (glob "$recipient/*/*") {
1076 if (-f "$file/upload" and $size = readlink "$file/size") {
1078 } elsif (not -l "$file/data" and $size = -s "$file/data") {
1083 return($rquota,int($du/1024/1024));
1090 chomp($_ = <$file>||'');
1095 # (shell) wildcard matching
1098 my $p = quotemeta shift;
1111 if ($skey) { $logout = "/fup?logout=skey:$skey" }
1112 elsif ($gkey) { $logout = "/fup?logout=gkey:$gkey" }
1113 elsif ($akey) { $logout = "/fup?logout=akey:$akey" }
1114 else { $logout = "/fup?logout" }
1117 '<form name="logout" action="$logout">'
1118 ' <input type="submit" name="logout" value="logout">'
1125 # print data dump of global or local variables in HTML
1126 # input musst be a string, eg: '%ENV'
1132 $_ = eval(qq(use Data::Dumper;Data::Dumper->Dump([\\$v])));
1136 print "<pre>\n$_\n</pre>\n";
1141 my ($file,$link) = @_;
1143 return symlink untaint($link),$file;
1147 # copy file (and modify) or symlink
1148 # returns chomped file contents or link name
1149 # preserves permissions and time stamps
1151 my ($from,$to,$mod) = @_;
1156 $to .= '/'.basename($from) if -d $to;
1158 if (defined($link = readlink $from)) {
1159 mksymlink($to,$link);
1162 open $from,'<',$from or return;
1163 open $to,'>',$to or return;
1168 close $to or http_die("internal error: $to - $!");
1169 if (my @s = stat($from)) {
1171 utime @s[8,9],$to unless $mod;
1184 if (open $file,$file) {
1193 # read one line from STDIN (net socket) and assign it to $_
1194 # return number of read bytes
1195 # also set global variable $RB (read bytes)
1199 if (defined ($_ = <STDIN>)) {
1209 # read forward to given pattern
1211 my $pattern = shift;
1213 while (&nvt_read) { return if /$pattern/ }
1217 # HTTP GET and POST parameters
1219 # fills global variable %PARAM :
1220 # normal parameter is $PARAM{$parameter}
1221 # file parameter is $PARAM{$parameter}{filename} $PARAM{$parameter}{data}
1222 sub parse_parameters {
1223 my $cl = $ENV{X_CONTENT_LENGTH} || $ENV{CONTENT_LENGTH} || 0;
1228 if ($cl > 128*$MB) {
1229 http_die("request too large");
1232 binmode(STDIN,':raw');
1234 foreach (split('&',$ENV{QUERY_STRING})) {
1235 if (/(.+?)=(.*)/) { $PARAM{$1} = $2 }
1236 else { $PARAM{$_} = $_ }
1238 $_ = $ENV{CONTENT_TYPE}||'';
1239 if ($ENV{REQUEST_METHOD} eq 'POST' and /boundary=\"?([\w\-\+\/_]+)/) {
1241 while ($RB<$cl and &nvt_read) { last if /^--\Q$boundary/ }
1242 # continuation lines are not checked!
1243 while ($RB<$cl and &nvt_read) {
1245 if (/^Content-Disposition:.*\s*filename="(.+?)"/i) {
1248 if (/^Content-Disposition:\s*form-data;\s*name="(.+?)"/i) {
1250 # skip rest of mime part header
1251 while ($RB<$cl and &nvt_read) { last if /^\s*$/ }
1254 if ($p =~ /password/i) {
1255 debuglog('*' x length)
1260 last if /^--\Q$boundary/;
1263 unless (defined $_) { die "premature end of HTTP POST\n" }
1264 $data =~ s/\r?\n$//;
1266 $PARAM{$p}{filename} = $filename;
1267 $PARAM{$p}{data} = $data;
1271 last if /^--\Q$boundary--/;
1278 # name based virtual host?
1280 my $hh = shift; # HTTP_HOST
1282 my $locale = $ENV{LOCALE};
1284 # memorized vhost? (default is in fex.ph)
1285 %vhost = split(':',$ENV{VHOST}) if $ENV{VHOST};
1287 if (%vhost and $hh and $hh =~ s/^([\w\.-]+).*/$1/) {
1288 if ($vhost = $vhost{$hh} and -f "$vhost/lib/fex.ph") {
1289 $ENV{VHOST} = "$hh:$vhost"; # memorize vhost for next run
1290 $ENV{FEXLIB} = $FEXLIB = "$vhost/lib";
1291 $logdir = $spooldir = "$vhost/spool";
1292 $docdir = "$vhost/htdocs";
1293 @logdir = ($logdir);
1294 if ($locale and -e "$vhost/locale/$locale/lib/fex.ph") {
1295 $ENV{FEXLIB} = $FEXLIB = "$vhost/locale/$locale/lib";
1297 require "$FEXLIB/fex.ph" or die "$0: cannot load $FEXLIB/fex.ph - $!";
1298 $ENV{SERVER_NAME} = $hostname;
1299 @doc_dirs = ($docdir);
1300 foreach my $ld (glob "$FEXHOME/locale/*/htdocs") {
1310 my ($plain,$to,$keyring,$from) = @_;
1311 my ($pid,$pi,$po,$pe,$enc,$err);
1316 $pid = open3($po,$pi,$pe,
1317 "gpg --batch --trust-model always --keyring $keyring".
1318 " -a -e -r $bcc -r $to"
1321 print {$po} "\n",$plain,"\n";
1324 $enc .= $_ while <$pi>;
1325 $err .= $_ while <$pe>;
1326 errorlog("($from --> $to) $err") if $err;
1337 my @s = stat(shift) or return;
1342 # wildcard * to perl regexp
1344 local $_ = quotemeta shift;
1345 s/\\\*/.*/g; # allow wildcard *
1350 # extract locale functions into hash of subroutine references
1351 # e.g. \&german ==> $notify{german}
1352 sub locale_functions {
1357 if ($locale and open my $fexpp,"$FEXHOME/locale/$locale/lib/fex.pp") {
1359 s/.*\n(\#\#\# locale functions)/$1/s;
1360 # sub xx {} ==> xx{$locale} = sub {}
1361 s/\nsub (\w+)/\n\$$1\{$locale\} = sub/gs;
1370 my $status = shift || 'new';
1371 my ($to,$keep,$locale,$file,$filename,$comment,$autodelete,$replyto,$mtime);
1374 if ($dkey =~ m:/.+/.+/:) {
1376 $dkey = readlink("$file/dkey");
1378 $file = readlink("$dkeydir/$dkey")
1379 or http_die("internal error: no DKEY $DKEY");
1382 $filename = filename($file);
1385 $mtime = mtime("$file/data") or http_die("internal error: no $file/data");
1386 $comment = slurp("$file/comment") || '';
1387 $replyto = readlink "$file/replyto" || '';
1388 $autodelete = readlink "$file/autodelete"
1389 || readlink "$to/\@AUTODELETE"
1391 $keep = readlink "$file/keep"
1392 || readlink "$to/\@KEEP"
1395 $locale = readlink "$to/\@LOCALE" || readlink "$file/locale" || 'english';
1396 $_ = untaint("$FEXHOME/locale/$locale/lib/lf.pl");
1398 unless ($notify{$locale}) {
1399 $locale = 'english';
1400 $notify{$locale} ||= \¬ify;
1402 return &{$notify{$locale}}(
1405 filename => $filename,
1406 keep => $keep-int((time-$mtime)/$DS),
1407 comment => $comment,
1408 autodelete => $autodelete,
1409 replyto => $replyto,
1413 ########################### locale functions ###########################
1414 # Will be extracted by install process and saved in $FEXHOME/lib/lf.pl #
1415 # You cannot modify them here without re-installing! #
1416 ########################################################################
1420 # my ($status,$dkey,$filename,$keep,$warn,$comment,$autodelete) = @_;
1422 my ($to,$from,$file,$mimefilename,$receiver,$warn,$comment,$autodelete);
1423 my ($size,$bytes,$days,$header,$data,$replyto,$uurl);
1424 my ($mfrom,$mto,$dfrom,$dto);
1429 my $fua = $ENV{HTTP_USER_AGENT}||'';
1431 my $disclaimer = '';
1434 my $boundary = randstring(16);
1435 my ($body,$enc_body);
1439 $warn = $P{warn}||2;
1440 $comment = $P{comment}||'';
1441 $comment = encode_utf8($P{comment}||'') if utf8::is_utf8($comment);
1442 $comment =~ s/^!\*!//; # multi download allow flag
1443 $autodelete = $P{autodelete}||$::autodelete;
1445 $file = untaint(readlink("$dkeydir/$P{dkey}"));
1446 $file =~ s/^\.\.\///;
1447 # make download protocal same as upload protocol
1448 if ($uurl = readlink("$file/uurl") and $uurl =~ /^(\w+):/) {
1450 $durl =~ s/^\w+::/$proto::/;
1452 $index = "$proto://$hostname/index.html";
1453 ($to,$from,$file) = split('/',$file);
1454 $filename = strip_path($P{filename});
1457 $mfrom .= '@'.$mdomain if $mdomain and $mfrom !~ /@/;
1458 $mto .= '@'.$mdomain if $mdomain and $mto !~ /@/;
1459 $keyring = $to.'/@GPG';
1460 # $to = '' if $to eq $from; # ???
1461 $replyto = $P{replyto}||$mfrom;
1462 $header = "From: <$mfrom> ($mfrom via F*EX service $hostname)\n";
1463 $header .= "Reply-To: <$replyto>\n" if $replyto ne $mfrom;
1464 $header .= "To: <$mto>\n";
1465 $data = "$dkeydir/$P{dkey}/data";
1466 $size = $bytes = -s $data;
1467 return unless $size;
1469 "We recommend fexget or fexit for download,\n".
1470 "because these clients can resume the download after an interruption.\n".
1471 "See $proto://$hostname/tools.html";
1476 # "Please avoid download with Internet Explorer, ".
1477 # "because it has too many bugs.\n\n";
1479 if ($filename =~ /\.(tar|zip|7z|arj|rar)$/) {
1481 "$filename is a container file.\n".
1482 "You can unpack it for example with 7zip ".
1483 "(http://www.7-zip.org/download.html)";
1485 if ($limited_download =~ /^y/i) {
1487 'This download link only works for you, you cannot distribute it.';
1490 $size = "$size Bytes";
1491 } elsif ($size/1024 < 2048) {
1492 $size = int($size/1024)." kB";
1494 $size = int($size/1024/1024)." MB";
1496 if ($autodelete eq 'YES') {
1497 $autodelete = "WARNING: After download (or view with a web browser!), "
1498 . "the file will be deleted!";
1499 } elsif ($autodelete eq 'DELAY') {
1500 $autodelete = "WARNING: When you download the file it will be deleted "
1501 . "soon afterwards!";
1509 $mimefilename = $filename;
1510 if ($mimefilename =~ s/([_\?\=\x00-\x1F\x7F-\xFF])/sprintf("=%02X",ord($1))/eog) {
1511 $mimefilename =~ s/ /_/g;
1512 $mimefilename = '=?UTF-8?Q?'.$mimefilename.'?=';
1516 unless ($fileid = readlink("$dkeydir/$P{dkey}/id")) {
1517 my @s = stat($data);
1518 $fileid = @s ? $s[1].$s[9] : 0;
1521 if ($P{status} eq 'new') {
1523 $header .= "Subject: F*EX-upload: $mimefilename\n";
1526 $header .= "Subject: reminder F*EX-upload: $mimefilename\n";
1528 $header .= "X-FEX-Client-Address: $fra\n" if $fra;
1529 $header .= "X-FEX-Client-Agent: $fua\n" if $fua;
1530 foreach my $u (@durl?@durl:($durl)) {
1531 my $durl = sprintf("%s/%s/%s",$u,$P{dkey},normalize_filename($filename));
1532 $header .= "X-FEX-URL: $durl\n" unless -s $keyring;
1533 $download .= "$durl\n";
1536 "X-FEX-Filesize: $bytes\n".
1537 "X-FEX-File-ID: $fileid\n".
1538 "X-FEX-Fexmaster: $ENV{SERVER_ADMIN}\n".
1540 "MIME-Version: 1.0\n";
1541 if ($comment =~ s/^\[(\@(.*?))\]\s*//) {
1542 $receiver = "group $1";
1543 if ($_ = readlink "$from/\@GROUP/$2" and m:^../../(.+?)/:) {
1544 $receiver .= " (maintainer: $1)";
1549 if ($days == 1) { $days .= " day" }
1550 else { $days .= " days" }
1552 # explicite sender set in fex.ph?
1554 map { s/^From: <$mfrom/From: <$sender_from/ } $header;
1555 open $sendmail,'|-',$sendmail,$mto,$bcc
1556 or http_die("cannot start sendmail - $!");
1558 # for special remote domains do not use same domain in From,
1559 # because remote MTA will probably reject this e-mail
1560 $dfrom = $1 if $mfrom =~ /@(.+)/;
1561 $dto = $1 if $mto =~ /@(.+)/;
1562 if ($dfrom and $dto and @remote_domains and
1564 $dfrom =~ /(^|\.)$_$/ and $dto =~ /(^|\.)$_$/
1567 $header =~ s/(From: <)\Q$mfrom\E(.*?)\n/$1$admin$2\nReply-To: $mfrom\n/;
1568 open $sendmail,'|-',$sendmail,$mto,$bcc
1569 or http_die("cannot start sendmail - $!");
1571 open $sendmail,'|-',$sendmail,'-f',$mfrom,$mto,$bcc
1572 or http_die("cannot start sendmail - $!");
1575 $comment = "\n$comment\n" if $comment;
1576 if ($comment =~ s/\n!(shortmail|\.)!\s*//i
1577 or (readlink("$to/\@NOTIFICATION")||'') =~ /short/i
1585 $disclaimer = slurp("$from/\@DISCLAIMER") || qqq(qq(
1588 'F*EX is not an archive, it is a transfer system for personal files.'
1589 'For more information see $index'
1591 'Questions? ==> F*EX admin: $admin'
1593 $disclaimer .= "\n$::disclaimer\n" if $::disclaimer;
1596 '$from has uploaded the file'
1598 '($size) for $receiver. Use'
1601 'to download this file within $days.'
1608 $body =~ s/\n\n+/\n\n/g;
1610 $enc_body = gpg_encrypt($body,$to,$keyring,$from);
1615 'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";'
1616 '\tboundary="$boundary"'
1617 'Content-Disposition: inline'
1621 'Content-Type: application/pgp-encrypted'
1622 'Content-Disposition: attachment'
1627 'Content-Type: application/octet-stream'
1628 'Content-Disposition: inline; filename="fex.pgp"'
1635 "Content-Type: text/plain; charset=UTF-8\n".
1636 "Content-Transfer-Encoding: 8bit\n";
1638 print {$sendmail} $header,"\n",$body;
1639 close $sendmail and return $to;
1640 http_die("cannot send notification e-mail (sendmail error $!)");
1646 my ($expire,$user) = @_;
1647 my $fexsend = "$FEXHOME/bin/fexsend";
1648 my $reactivation = "$FEXLIB/reactivation.txt";
1654 my $lr = "$FEXHOME/locale/$locale/lib/reactivation.txt";
1655 $reactivation = $lr if -f $lr and -s $lr;
1657 $fexsend .= " -M -D -k 30 -C"
1658 ." 'Your F*EX account has been inactive for $expire days,"
1659 ." you must download this file to reactivate it."
1660 ." Otherwise your account will be deleted.'"
1661 ." $reactivation $user";
1662 # on error show STDOUT and STDERR
1663 my $fo = `$fexsend 2>&1`;
1664 warn $fexsend.'\n'.$fo if $?;
1666 warn "$0: cannot execute $fexsend for reactivation()\n";