upload.pl
changeset 51 1700cf720315
parent 50 8de14266312f
child 52 b8d25524650e
equal deleted inserted replaced
50:8de14266312f 51:1700cf720315
     1 #! /usr/bin/perl -T
       
     2 # FIXME: UPDATE {{
       
     3 # Example .htaccess
       
     4 # | Options -Indexes
       
     5 # | <Files upload.pl>
       
     6 # | AuthType Basic
       
     7 # | AuthName upload
       
     8 # | Require valid-user
       
     9 # | AuthUserFile /home/heiko/public_html/.passwd
       
    10 # | </Files>
       
    11 #
       
    12 # Je nach Permission-Lage kann es gut sein, daß das upload.d-Verzeichnis
       
    13 # mit der Hand angelegt werden muß und dem Webserver-Nutzer „geschenkt“
       
    14 # werden muß.
       
    15 #
       
    16 # Das Upload-Verzeichnis sollte natuerlich vor der Indizierung geschuetzt
       
    17 # werden - siehe Beispiel .htaccess.
       
    18 #
       
    19 # Eventuell in der Apache-Config sowas wie
       
    20 #   ScriptAlias /ud	    /home/ud/XXX/upload.pl
       
    21 #   Alias	/d	    /home/ud/XXX/d/
       
    22 # gesetzt werden.
       
    23 #
       
    24 # }}
       
    25 #
       
    26 
       
    27 # STATUS: Proof of Concept!
       
    28 # NEEDS: Security review!
       
    29 
       
    30 use 5.014;
       
    31 use strict;
       
    32 use warnings;
       
    33 use CGI qw(:all *table);
       
    34 use CGI::Carp qw(fatalsToBrowser);
       
    35 use CGI::Pretty;
       
    36 use IO::File;
       
    37 use File::Basename;
       
    38 use File::Path qw(remove_tree make_path);
       
    39 use File::Spec::Functions;
       
    40 use File::MimeInfo qw(mimetype);
       
    41 use Cwd qw(getcwd realpath);
       
    42 use Digest::MD5 qw(md5_hex);
       
    43 use OSSP::uuid;
       
    44 
       
    45 sub human;        # convert numbers to human readable format
       
    46 sub deletedir;    # safely delete directories
       
    47 sub confirm;      # ask for user confirmation (HTML)
       
    48 sub deslash;      # cleanup a path name
       
    49 
       
    50 my $uuid = qr/[[:xdigit:]-]{36}/;
       
    51 my $hash = qr/[[:xdigit:]]{32}/;
       
    52 
       
    53 umask 077;
       
    54 
       
    55 # The working (var) directory gets passed to us via ONCE_VAR environment
       
    56 # FIXME: Should we allow the current directory as an alternative?
       
    57 
       
    58 my $ONCE_VAR = do {
       
    59     $ENV{ONCE_VAR} =~ /^(\/\S+)/;
       
    60     die "Please define (correct) env ONCE_VAR\n"
       
    61       if not defined $1;
       
    62     $1;
       
    63 };
       
    64 
       
    65 my @footer = (hr,
       
    66     div(
       
    67         { -align => "right" },
       
    68         a(
       
    69             {
       
    70                 -href =>
       
    71                   "https://ssl.schlittermann.de/hg/anon-upload/file/once/"
       
    72             } => "Scripting"
       
    73         ),
       
    74         " &copy; 2010,2011,2015 ",
       
    75         a({ -href => "http://www.schlittermann.de/" } => "Heiko Schlittermann"),
       
    76         " &copy; 2014 ",
       
    77         a({ -href => "http://www.schlittermann.de/" } => "Matthias Förste")
       
    78     )
       
    79 );
       
    80 
       
    81 MAIN: {
       
    82 
       
    83     # Download?
       
    84     if ($ENV{PATH_INFO} =~
       
    85         m{(?<path>(?<store>(?<view>/$hash|$uuid)/$uuid-\d+-.)/(?<base>.*))})
       
    86     {
       
    87         my $view  = deslash realpath catfile $ONCE_VAR, $+{view};
       
    88         my $store = deslash realpath catfile $ONCE_VAR, $+{store};
       
    89         my $file  = deslash realpath catfile $ONCE_VAR, $+{path};
       
    90         my $base  = $+{base};
       
    91 
       
    92         unless (-f $file) {
       
    93             print header('text/plain', '404 Not found'), 'Not found';
       
    94             exit 0;
       
    95         }
       
    96 
       
    97         my $mimetype = mimetype($file);
       
    98         confirm $base, $mimetype
       
    99           if $store =~ /-d$/ and not defined param('confirmed');
       
   100 
       
   101         open my $f, '<', $file or die "Can't open <`$file`: $!\n";
       
   102         remove_tree $1 if $store =~ m(^(/.*-d)$);
       
   103         rmdir $1 if $view =~ m(^(/.*));
       
   104 
       
   105         print header(-type => $mimetype, -charset => 'UTF-8');
       
   106         if (request_method() ~~ [qw(GET POST)]) {
       
   107             local $/ = \do { 1 * 2**20 };    # 1 MB Buffer
       
   108             print while <$f>;
       
   109         }
       
   110         exit 0;
       
   111 
       
   112     }
       
   113 
       
   114     # UPLOAD / VIEW request
       
   115     # per view (user) we have an own directory
       
   116 
       
   117     # pre condition checks
       
   118     -d $ONCE_VAR
       
   119       or mkdir $ONCE_VAR => 0777
       
   120       or die "Can't mkdir $ONCE_VAR: $! (your admin should have created it)\n";
       
   121 
       
   122     -x -w $ONCE_VAR
       
   123       or die "Can't write to $ONCE_VAR: $!\n";
       
   124 
       
   125     my ($view, $user_dir) = do {
       
   126         my ($v, $d);
       
   127         if ($ENV{REMOTE_USER} =~ /^\s*(.+)\s*$/) {
       
   128             $v = $1;
       
   129             $d = md5_hex($1);
       
   130         }
       
   131         else {
       
   132             tie $d => 'OSSP::uuid::tie', 'v4';
       
   133             $v = 'anonymous';
       
   134         }
       
   135         $v, deslash catfile($ONCE_VAR, $d);
       
   136     };
       
   137 
       
   138     if (param('delete') =~ m{(?<store>(?<view>$uuid|$hash)/$uuid-\d+-./?)}) {
       
   139 
       
   140         # FIXME: sanitization
       
   141         my $store = deslash catfile $ONCE_VAR, $+{store};
       
   142         my $view  = deslash catfile $ONCE_VAR, $+{view};
       
   143         remove_tree $1 if $store =~ m(^(/.*));
       
   144         rmdir $1 if $view =~ m(^(/.*));
       
   145         print redirect(-uri => url(-path_info => 1));
       
   146         exit 0;
       
   147     }
       
   148 
       
   149     print header(-charset => "UTF-8"),
       
   150       start_html(-title => "once"),
       
   151       h1 "Ansicht: $view";
       
   152 
       
   153     # print Dump;
       
   154 
       
   155     if (length(my $file = param('upload'))) {
       
   156         my $days = param('expires');
       
   157         my ($delete, $expires);
       
   158         tie my $uuid => 'OSSP::uuid::tie', 'v4';
       
   159 
       
   160         # sanitize expires
       
   161         $days =~ /.*?([+-]?\d+).*/;
       
   162         $days = defined $1 ? $1 : 10;
       
   163 
       
   164         $expires = time + $days * 86400;
       
   165         $delete  = 'l';                    # on file[l]ist
       
   166         if ($days == 0) {
       
   167             $delete = 'd';                 # on first [d]ownload
       
   168         }
       
   169         elsif ($days == -1) {
       
   170             $delete = 'm';                 # only [m]anually
       
   171         }
       
   172 
       
   173         # sanitizing the filename
       
   174         (my $filename = $file) =~ tr /\\/\//;
       
   175         $filename =~ /(.*)/;
       
   176         $filename = $1;
       
   177 
       
   178         my $dir = catfile($user_dir, "$uuid-$expires-$delete");
       
   179         make_path($dir);
       
   180         my $outfh = new IO::File ">$dir/$filename"
       
   181           or die "Can't create $dir/$filename: $!\n";
       
   182         print {$outfh} <$file>;
       
   183 
       
   184         if (not $delete ~~ [qw(d m)]
       
   185             and my $atfh = new IO::File("|at now + $days days"))
       
   186         {
       
   187             print {$atfh}
       
   188               "rm -f \"$dir/$filename\"\n",
       
   189               "rmdir \"$dir\"\n";
       
   190             close $atfh;
       
   191             system("cat /tmp/log");
       
   192         }
       
   193 
       
   194     }
       
   195     print hr;
       
   196 
       
   197     # List the current content
       
   198     if (my @files = map { deslash $_ } glob "$user_dir/*-*/*") {
       
   199 
       
   200         print p <<__;
       
   201         <@files>
       
   202 Der gültige Download-Link ist die Link-Adresse, die sich hinter
       
   203 dem Datei-Namen verbirgt. (Firefox: Rechte Maustaste, Link-Location).
       
   204 Nach Ablauf des <a href="http://de.wikipedia.org/wiki/Mindesthaltbarkeitsdatum">MHD</a>
       
   205 wird die Datei automatisch gelöscht.
       
   206 __
       
   207 
       
   208         print start_table,
       
   209           Tr(th { align => "left" }, [qw/Name Größe Hochgeladen Löschung/]);
       
   210 
       
   211         foreach (map { /(.*)/ } sort { -M $a <=> -M $b } @files) {
       
   212             my ($file, $dir) = fileparse($_);
       
   213             $dir = substr $dir,
       
   214               length $ONCE_VAR;    # make it relative to $ONCE_VAR
       
   215 
       
   216             $dir =~ /(\S+)-(\d+)-(.)\/?$/ or next;
       
   217             my ($hash, $expires, $delete) = ($1, $2, $3);
       
   218             if (${expires} <= time and $delete eq 'l') {
       
   219                 /(.*)/;
       
   220                 unlink $_  or die "Can't unlik $_: $!\n";
       
   221                 rmdir $dir or die "Can't rmdir $dir: $!\n";
       
   222                 next;
       
   223             }
       
   224 
       
   225             my $d;
       
   226             if ($delete eq 'l') {
       
   227                 $d = localtime ${expires};
       
   228             }
       
   229             elsif ($delete eq 'd') {
       
   230                 $d = 'unmittelbar nach Download';
       
   231             }
       
   232             else {
       
   233                 $d = 'nur manuell';
       
   234             }
       
   235 
       
   236             print Tr(
       
   237                 td(a { href => "$ENV{PATH_INFO}/$dir$file" }, $file),
       
   238                 td({ align => "right" }, human((stat $_)[7])),
       
   239                 td(scalar localtime +(stat $_)[9]),
       
   240                 td($d),
       
   241                 td(a({ href => "?delete=$dir" }, 'remove'))
       
   242             );
       
   243         }
       
   244 
       
   245         print end_table, hr;
       
   246     }
       
   247 
       
   248     print start_multipart_form, start_table,
       
   249       Tr(td("Dateiname: "),
       
   250         td(filefield(-name => "upload", -default => "nothing")),
       
   251       ),
       
   252       Tr(
       
   253         td("Löschen in: "),
       
   254         td(textfield(-name => "expires", -default => 0)),
       
   255         td("Tagen (0: unmittelbar nach Download; -1: nur manuell)")
       
   256       ),
       
   257       Tr(td(), td(submit(-value => "Hochladen")),),
       
   258       end_table,
       
   259       end_multipart_form,
       
   260       @footer,
       
   261       end_html;
       
   262 }
       
   263 
       
   264 sub deletedir {
       
   265     remove_tree
       
   266       map  { /^(\/.*)/ }
       
   267       grep { /^\Q$ONCE_VAR\E/ } @_;
       
   268 }
       
   269 
       
   270 sub human {
       
   271     my $_     = shift;
       
   272     my @units = qw(B K M G T);
       
   273     while (length int > 3 and @units) {
       
   274         $_ = sprintf "%.1f" => $_ / 1024;
       
   275         shift @units;
       
   276     }
       
   277     croak "filesize is too big (can't convert to human readable number)"
       
   278       if !@units;
       
   279     return "$_$units[0]";
       
   280 }
       
   281 
       
   282 sub deslash { $_[0] =~ s{/+}{/}gr }
       
   283 
       
   284 sub confirm {
       
   285     my ($base, $mimetype) = @_;
       
   286     print header(-charset => "UTF-8"),
       
   287       start_html(-title => "once"),
       
   288       h1 "Download bestätigen";
       
   289     print hr, p <<__;
       
   290         Die Datei `$base' ($mimetype), die Sie herunterladen möchten, wird nach Abschluß des
       
   291         Downloads gelöscht. Virenscanner oder andere Programme, die den Link
       
   292         möglicherweise automatisiert aufrufen, könnten eine versehentliche
       
   293         Löschung der Datei auslösen. Bestätigen Sie deshalb bitte den Download
       
   294         per Knopfdruck.
       
   295 __
       
   296     print start_form,
       
   297       hidden('confirmed', 'yes'),
       
   298       submit(-value => 'Bestätigung'),
       
   299       end_form,
       
   300       @footer,
       
   301       end_html;
       
   302     exit 0;
       
   303 }