once.pl
changeset 55 1f4bf7fe870e
parent 54 e139241262c3
child 56 39c4a5f4ac33
equal deleted inserted replaced
54:e139241262c3 55:1f4bf7fe870e
    37 use File::Basename;
    37 use File::Basename;
    38 use File::Path qw(remove_tree make_path);
    38 use File::Path qw(remove_tree make_path);
    39 use File::Spec::Functions;
    39 use File::Spec::Functions;
    40 use File::MimeInfo qw(mimetype);
    40 use File::MimeInfo qw(mimetype);
    41 use Cwd qw(getcwd realpath);
    41 use Cwd qw(getcwd realpath);
    42 use Digest::MD5 qw(md5_hex);
    42 use Digest::MD5 qw(md5_hex md5);
       
    43 use experimental qw(smartmatch lexical_topic);
    43 
    44 
    44 sub humanize;     # convert numbers to human readable format
    45 sub humanize;     # convert numbers to human readable format
    45 sub deletedir;    # safely delete directories
    46 sub deletedir;    # safely delete directories
    46 sub confirm;      # ask for user confirmation (HTML)
    47 sub confirm;      # ask for user confirmation (HTML)
    47 sub deslash;      # cleanup a path name
    48 sub deslash;      # cleanup a path name
    48 sub gen_uuid;
    49 sub gen_uuid;     # create a uniq identifier
    49 
    50 sub base62;
    50 my $uuid = qr/[[:xdigit:]-]{36}/;
    51 sub md5_base62 { ... }
    51 my $hash = qr/[[:xdigit:]]{32}/;
    52 
       
    53 my $rxVIEW = qr/[a-z\d]{6}-[a-z\d]+/i; # date-userhash
       
    54 my $rxFILE = qr/[a-z\d]{6}-[a-z\d]+-./i; # date-filehash-deletemode
    52 
    55 
    53 umask 077;
    56 umask 077;
    54 
    57 
    55 # The working (var) directory gets passed to us via ONCE_VAR environment
    58 # The working (var) directory gets passed to us via ONCE_VAR environment
    56 # FIXME: Should we allow the current directory as an alternative?
    59 # FIXME: Should we allow the current directory as an alternative?
    76         " © 2014 ",
    79         " © 2014 ",
    77         a({ -href => "http://www.schlittermann.de/" } => "Matthias Förste")
    80         a({ -href => "http://www.schlittermann.de/" } => "Matthias Förste")
    78     )
    81     )
    79 );
    82 );
    80 
    83 
    81 MAIN: {
    84 exit main() if not caller;
       
    85 
       
    86 sub main {
    82 
    87 
    83     # Download?
    88     # Download?
       
    89     # PATH_INFO is something like
       
    90     # /once/hDOPzMJMY0p/3fLjYmbYlGk-1450284310-d/boot.dot
       
    91     #       |-VIEW-----|                         |-BASE-|
       
    92     #       |-STORE----------------------------|
       
    93     # …     |-PATH--------------------------------------|
    84     if ($ENV{PATH_INFO} =~
    94     if ($ENV{PATH_INFO} =~
    85         m{(?<path>(?<store>(?<view>/$hash|$uuid)/$uuid-\d+-.)/(?<base>.*))})
    95         m{/(?<path>(?<store>(?<view>$rxVIEW)/$rxFILE)/(?<base>.*))})
    86     {
    96     {
       
    97 #        use Data::Dumper;
       
    98 #        die Dumper \%+;
    87         my $view  = deslash realpath catfile $ONCE_VAR, $+{view};
    99         my $view  = deslash realpath catfile $ONCE_VAR, $+{view};
    88         my $store = deslash realpath catfile $ONCE_VAR, $+{store};
   100         my $store = deslash realpath catfile $ONCE_VAR, $+{store};
    89         my $file  = deslash realpath catfile $ONCE_VAR, $+{path};
   101         my $file  = deslash realpath catfile $ONCE_VAR, $+{path};
    90         my $base  = $+{base};
   102         my $base  = $+{base};
    91 
   103 
   109         }
   121         }
   110         exit 0;
   122         exit 0;
   111 
   123 
   112     }
   124     }
   113 
   125 
   114     # UPLOAD / VIEW request
   126     # Handle the UPLOAD / VIEW request
   115     # per view (user) we have an own directory
   127     # per view (user) we have an own directory
   116 
   128 
   117     # pre condition checks
   129     # pre condition checks
   118     -d $ONCE_VAR
   130     -d $ONCE_VAR
   119       or mkdir $ONCE_VAR => 0777
   131       or mkdir $ONCE_VAR => 0777
   121 
   133 
   122     -x -w $ONCE_VAR
   134     -x -w $ONCE_VAR
   123       or die "Can't write to $ONCE_VAR: $!\n";
   135       or die "Can't write to $ONCE_VAR: $!\n";
   124 
   136 
   125     my ($view, $user_dir) = do {
   137     my ($view, $user_dir) = do {
       
   138         # view: display name
       
   139         #       anonymous | hans | …
       
   140         # user_dir: the directory name, becomes part of the
       
   141         #           link, later
       
   142         #       /var/lib/once/1AaIF9-1KF
       
   143         #                            `--> base62 of a random value, may
       
   144         #                            be shorter than 3 digits
       
   145         #                     `-----> base62 of a unix time stamp,
       
   146         #                     number of digits will be 6 for the
       
   147         #                     forseeable future
       
   148         # NOTE: if you change the generated user_dir string here, you may need
       
   149         # to adapt the patterns $rxVIEW and $rxFILE at the beginning of
       
   150         # the script.
       
   151         #
   126         my ($v, $d);
   152         my ($v, $d);
   127         if ($ENV{REMOTE_USER} =~ /^\s*(.+)\s*$/) {
   153         if ($ENV{REMOTE_USER} =~ /^\s*(.+)\s*$/) {
       
   154             # Known users get a directory name based user name.
       
   155             # Yes, if somebody can guess the user names, they can guess
       
   156             # the directory names too. But they can't guess the
       
   157             # completly randomly named files in there.
       
   158             $d = join '-' => base62(time), md5_base62($1);
   128             $v = $1;
   159             $v = $1;
   129             $d = md5_hex($1);
       
   130         }
   160         }
   131         else {
   161         else {
   132             $d = gen_uuid();
   162             # Anonymous get an timestamp()-rand(1000) directory
       
   163             $d = join '-' => base62(time), base62(rand(10_000));
   133             $v = 'anonymous';
   164             $v = 'anonymous';
   134         }
   165         }
   135         $v, deslash catfile($ONCE_VAR, $d);
   166         $v, deslash catfile($ONCE_VAR, $d);
   136     };
   167     };
   137 
   168 
   138     if (param('delete') =~ m{(?<store>(?<view>$uuid|$hash)/$uuid-\d+-./?)}) {
   169     if (param('delete') =~ m{(?<store>(?<view>$rxVIEW)/$rxFILE/?)}) {
   139 
   170 
   140         # FIXME: sanitization
   171         # FIXME: sanitization
   141         my $store = deslash catfile $ONCE_VAR, $+{store};
   172         my $store = deslash catfile $ONCE_VAR, $+{store};
   142         my $view  = deslash catfile $ONCE_VAR, $+{view};
   173         my $view  = deslash catfile $ONCE_VAR, $+{view};
   143         remove_tree $1 if $store =~ m(^(/.*));
   174         remove_tree $1 if $store =~ m(^(/.*));
   148 
   179 
   149     print header(-charset => "UTF-8"),
   180     print header(-charset => "UTF-8"),
   150       start_html(-title => "once"),
   181       start_html(-title => "once"),
   151       h1 "Ansicht: $view";
   182       h1 "Ansicht: $view";
   152 
   183 
   153     # print Dump;
   184     # calculate the file name for the uploaded file
   154 
       
   155     if (length(my $file = param('upload'))) {
   185     if (length(my $file = param('upload'))) {
   156         my $uuid = gen_uuid();
   186         my $uuid = gen_uuid();
   157         my $days = param('expires');
   187         my ($delete, $expires, $days) = do {
   158         my ($delete, $expires);
   188             my ($d, $e);
   159         # sanitize expires
   189             my $days = param('expires');
   160         $days =~ /.*?([+-]?\d+).*/;
   190             # sanitize expires
   161         $days = defined $1 ? $1 : 10;
   191             $days =~ /.*?([+-]?\d+).*/;
   162 
   192             $days = $1 // 10;
   163         $expires = time + $days * 86400;
   193             $e = base62 time + $days * 86400;
   164         $delete  = 'l';                    # on file[l]ist
   194 
   165         if ($days == 0) {
   195             if ($days == 0) { $d = 'd' }       # at first [d]ownload
   166             $delete = 'd';                 # on first [d]ownload
   196             elsif ($days < 0) { $d = 'm' }     # only [m]anually
   167         }
   197             else { $d = 'e' }                  # if expired
   168         elsif ($days == -1) {
   198             ($d, $e, $days);
   169             $delete = 'm';                 # only [m]anually
   199         };
   170         }
   200 
   171 
   201         # sanitize the filename
   172         # sanitizing the filename
   202         my $filename = do {
   173         (my $filename = $file) =~ tr /\\/\//;
   203             $file =~ tr /\\/\//;
   174         $filename =~ /(.*)/;
   204             $file =~ /(.*)/;
   175         $filename = $1;
   205             $1;
   176 
   206         };
   177         my $dir = catfile($user_dir, "$uuid-$expires-$delete");
   207 
       
   208         my $dir = catfile($user_dir, "$expires-$uuid-$delete");
   178         make_path($dir);
   209         make_path($dir);
   179         my $outfh = new IO::File ">$dir/$filename"
   210         my $outfh = new IO::File "$dir/$filename", 'w'
   180           or die "Can't create $dir/$filename: $!\n";
   211           or die "Can't create $dir/$filename: $!\n";
   181         print {$outfh} <$file>;
   212         print {$outfh} <$file>;
   182 
   213 
   183         if (not $delete ~~ [qw(d m)]
   214         if (not $delete ~~ [qw(d m)]
   184             and my $atfh = new IO::File("|at now + $days days"))
   215             and my $atfh = new IO::File("|at now + $days days"))
   185         {
   216         {
   186             print {$atfh}
   217             print {$atfh}
   187               "rm -f \"$dir/$filename\"\n",
   218               "rm -f \"$dir/$filename\"\n",
   188               "rmdir \"$dir\"\n";
   219               "rmdir \"$dir\"\n";
   189             close $atfh;
   220             close $atfh;
   190             system("cat /tmp/log");
       
   191         }
   221         }
   192 
   222 
   193     }
   223     }
   194     print hr;
   224     print hr;
   195 
   225 
   210         foreach (map { /(.*)/ } sort { -M $a <=> -M $b } @files) {
   240         foreach (map { /(.*)/ } sort { -M $a <=> -M $b } @files) {
   211             my ($file, $dir) = fileparse($_);
   241             my ($file, $dir) = fileparse($_);
   212             $dir = substr $dir,
   242             $dir = substr $dir,
   213               length $ONCE_VAR;    # make it relative to $ONCE_VAR
   243               length $ONCE_VAR;    # make it relative to $ONCE_VAR
   214 
   244 
   215             $dir =~ /(\S+)-(\d+)-(.)\/?$/ or next;
   245             # FIXME: use the rx* patterns from above
   216             my ($hash, $expires, $delete) = ($1, $2, $3);
   246             $dir =~ m{/(?<expires>[a-z\d]{6})-(?<hash>[a-z\d]+)-(?<delete>.)}i or next;
   217             if (${expires} <= time and $delete eq 'l') {
   247             my ($hash, $expires, $delete) = @{+}{qw/hash expires delete/};
       
   248             if (${expires} <= time and $delete eq 'e') {
   218                 /(.*)/;
   249                 /(.*)/;
   219                 unlink $_  or die "Can't unlik $_: $!\n";
   250                 unlink $_  or die "Can't unlik $_: $!\n";
   220                 rmdir $dir or die "Can't rmdir $dir: $!\n";
   251                 rmdir $dir or die "Can't rmdir $dir: $!\n";
   221                 next;
   252                 next;
   222             }
   253             }
   223 
   254 
   224             my $d;
   255             my $d;
   225             if ($delete eq 'l') {
   256             if ($delete eq 'e') {
   226                 $d = localtime ${expires};
   257                 $d = localtime ${expires};
   227             }
   258             }
   228             elsif ($delete eq 'd') {
   259             elsif ($delete eq 'd') {
   229                 $d = 'unmittelbar nach Download';
   260                 $d = 'unmittelbar nach Download';
   230             }
   261             }
   240                 td(a({ href => "?delete=$dir" }, 'remove'))
   271                 td(a({ href => "?delete=$dir" }, 'remove'))
   241             );
   272             );
   242         }
   273         }
   243 
   274 
   244         print end_table, hr;
   275         print end_table, hr;
       
   276         return 0;
   245     }
   277     }
   246 
   278 
   247     print start_multipart_form, start_table,
   279     print start_multipart_form, start_table,
   248       Tr(td("Dateiname: "),
   280       Tr(td("Dateiname: "),
   249         td(filefield(-name => "upload", -default => "nothing")),
   281         td(filefield(-name => "upload", -default => "nothing")),
   256       Tr(td(), td(submit(-value => "Hochladen")),),
   288       Tr(td(), td(submit(-value => "Hochladen")),),
   257       end_table,
   289       end_table,
   258       end_multipart_form,
   290       end_multipart_form,
   259       @footer,
   291       @footer,
   260       end_html;
   292       end_html;
       
   293 
       
   294       return 0;
   261 }
   295 }
   262 
   296 
   263 sub deletedir {
   297 sub deletedir {
   264     remove_tree
   298     remove_tree
   265       map  { /^(\/.*)/ }
   299       map  { /^(\/.*)/ }
   301     exit 0;
   335     exit 0;
   302 }
   336 }
   303 
   337 
   304 sub base62 {
   338 sub base62 {
   305     my $n = shift // $_;
   339     my $n = shift // $_;
   306     die 'left integer precision' if $n == $n - 1 or $n == $n + 1;
   340     die 'input is >MAXINT' if $n == $n - 1 or $n == $n + 1;
   307     state $digits = [0..9, 'a'..'z', 'A'..'Z'];
   341     state $digits = [0..9, 'a'..'z', 'A'..'Z'];
   308     state $base = @$digits;
   342     state $base = @$digits;
   309     my @result;
   343     my @result;
   310 
   344 
   311     for (;$n >= $base; $n = int($n/$base)) {
   345     for (;$n >= $base; $n = int($n/$base)) {
   314     }
   348     }
   315     unshift @result, $digits->[$n];
   349     unshift @result, $digits->[$n];
   316     join '', @result;
   350     join '', @result;
   317 }
   351 }
   318 
   352 
       
   353 
   319 sub gen_uuid {
   354 sub gen_uuid {
   320     open my $f, '/dev/random' or croak;
   355     #open my $f, '/dev/urandom' or croak;
   321     read $f, my $_, 64/8;
   356     #read $f, my($_), 128/8;
   322     /^(.*)$/;
   357     #/^(.*)$/;
   323     return join '-', map { base62 $_ } unpack 'Q*', $1;
   358     #die join '-', map { base62 $_ } unpack 'Q*', $1;
   324 }
   359     return base62 int rand(2**64);
       
   360 }