File Coverage

blib/lib/Any/Daemon/HTTP/Directory.pm
Criterion Covered Total %
statement 39 155 25.1
branch 0 82 0.0
condition 0 26 0.0
subroutine 13 25 52.0
pod 4 5 80.0
total 56 293 19.1


line stmt bran cond sub pod time code
1             # Copyrights 2013-2020 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of distribution Any-Daemon-HTTP. Meta-POD processed
6             # with OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package Any::Daemon::HTTP::Directory;
10 2     2   11 use vars '$VERSION';
  2         3  
  2         88  
11             $VERSION = '0.30';
12              
13 2     2   8 use parent 'Any::Daemon::HTTP::Source';
  2         4  
  2         7  
14              
15 2     2   85 use warnings;
  2         4  
  2         43  
16 2     2   8 use strict;
  2         3  
  2         45  
17              
18 2     2   19 use Log::Report 'any-daemon-http';
  2         7  
  2         14  
19              
20 2     2   419 use File::Spec ();
  2         3  
  2         30  
21 2     2   9 use File::Basename qw/dirname/;
  2         4  
  2         136  
22 2     2   1071 use POSIX::1003 qw/strftime :fd :fs/;
  2         8165  
  2         11  
23 2     2   21166 use HTTP::Status qw/:constants/;
  2         4  
  2         583  
24 2     2   844 use HTTP::Response ();
  2         16738  
  2         45  
25 2     2   12 use Encode qw/encode/;
  2         6  
  2         80  
26 2     2   812 use MIME::Types ();
  2         6723  
  2         1527  
27              
28             my $mimetypes = MIME::Types->new(only_complete => 1);
29              
30             sub _filename_trans($$);
31              
32              
33             sub init($)
34 0     0 0   { my ($self, $args) = @_;
35 0           $self->SUPER::init($args);
36              
37 0           my $path = $self->path;
38             my $loc = $args->{location}
39 0 0         or error __x"directory definition requires location";
40              
41 0           my $trans;
42 0 0         if(ref $loc eq 'CODE')
43 0           { $trans = $loc;
44 0           undef $loc;
45             }
46             else
47 0           { $loc = File::Spec->rel2abs($loc);
48 0 0         substr($loc, -1) eq '/' or $loc .= '/';
49 0           $trans = _filename_trans $path, $loc;
50              
51 0 0         -d $loc
52             or error __x"directory location {loc} for {path} does not exist"
53             , loc => $loc, path => $path;
54             }
55              
56 0           $self->{ADHD_loc} = $loc;
57 0           $self->{ADHD_fn} = $trans;
58 0   0       $self->{ADHD_dirlist} = $args->{directory_list} || 0;
59 0   0       $self->{ADHD_charset} = $args->{charset} || 'utf-8';
60              
61 0           my $if = $args->{index_file};
62 0 0         my @if = ref $if eq 'ARRAY' ? @$if
    0          
63             : defined $if ? $if
64             : qw/index.html index.htm/;
65 0           $self->{ADHD_indexfns} = \@if;
66 0           $self;
67             }
68              
69             #-----------------
70              
71 0     0 1   sub location() {shift->{ADHD_location}}
72 0     0 1   sub charset() {shift->{ADHD_charset}}
73              
74             #-----------------
75              
76             #-----------------------
77              
78 0     0 1   sub filename($) { $_[0]->{ADHD_fn}->($_[1]) }
79              
80             sub _filename_trans($$)
81 0     0     { my ($path, $loc) = @_;
82 0 0         return $loc if ref $loc eq 'CODE';
83             sub
84 0     0     { my $x = shift;
85 0 0         $x =~ s!^\Q$path!$loc! or panic "path $x not inside $path";
86 0           $x;
87 0           };
88             }
89              
90             sub _collect($$$$)
91 0     0     { my ($self, $vhost, $session, $req, $uri) = @_;
92              
93 0           my $item = $self->filename($uri);
94              
95             # soft-fail when the item does not exists
96 0 0         -e $item or return;
97              
98 0 0         return $self->_file_response($req, $item)
99             if -f _;
100              
101 0 0         return HTTP::Response->new(HTTP_FORBIDDEN)
102             if ! -d _; # neither file nor directory
103              
104 0 0         return HTTP::Response->new(HTTP_TEMPORARY_REDIRECT, undef
105             , [Location => $uri.'/'])
106             if substr($item, -1) ne '/';
107              
108 0           foreach my $if (@{$self->{ADHD_indexfns}})
  0            
109 0 0         { -f $item.$if or next;
110 0           return $self->_file_response($req, $item.$if);
111             }
112              
113             $self->{ADHD_dirlist}
114 0 0         or return HTTP::Response->new(HTTP_FORBIDDEN, "no directory lists");
115              
116 0           $self->_list_response($req, $uri, $item);
117             }
118              
119             sub _file_response($$)
120 0     0     { my ($self, $req, $fn) = @_;
121              
122 0 0         -f $fn
123             or return HTTP::Response->new(HTTP_NOT_FOUND);
124              
125 0 0         open my $fh, '<:raw', $fn
126             or return HTTP::Response->new(HTTP_FORBIDDEN);
127              
128 0           my ($dev, $inode, $mtime) = (stat $fh)[0,1,9];
129 0           my $etag = "$dev-$inode-$mtime";
130              
131 0           my $has_etag = $req->header('If-None-Match');
132 0 0 0       return HTTP::Response->new(HTTP_NOT_MODIFIED, 'match etag')
133             if defined $has_etag && $has_etag eq $etag;
134              
135 0           my $has_mtime = $req->if_modified_since;
136 0 0 0       return HTTP::Response->new(HTTP_NOT_MODIFIED, 'unchanged')
137             if defined $has_mtime && $has_mtime >= $mtime;
138              
139 0           my $head = HTTP::Headers->new;
140              
141 0           my $ct;
142 0 0         if(my $mime = $mimetypes->mimeTypeOf($fn))
143 0           { $ct = $mime->type;
144 0 0         $ct .= '; charset='.$self->charset if $mime->isAscii;
145             }
146             else
147 0           { $ct = 'binary/octet-stream';
148             }
149              
150 0           $head->content_type($ct);
151 0           $head->last_modified($mtime);
152 0           $head->header(ETag => $etag);
153              
154 0           local $/;
155 0           HTTP::Response->new(HTTP_OK, undef, $head, <$fh>);
156             }
157              
158             sub _list_response($$$)
159 0     0     { my ($self, $req, $uri, $dir) = @_;
160              
161 2     2   15 no warnings 'uninitialized';
  2         4  
  2         1716  
162              
163 0           my $list = $self->list($dir);
164              
165 0           my $now = localtime;
166 0           my @rows;
167 0 0         push @rows, <<__UP if $dir ne '/';
168            
 (up)
169             __UP
170              
171 0           foreach my $item (sort keys %$list)
172 0           { my $d = $list->{$item};
173 0 0         my $symdest = $d->{is_symlink} ? "→ $d->{symlink_dest}" : "";
174 0           push @rows, <<__ROW;
175            
$d->{flags}
176             $d->{user}
177             $d->{group}
178             $d->{size_nice}
179             $d->{mtime_nice}
180             $d->{name}$symdest
181             __ROW
182             }
183              
184 0           local $" = "\n";
185 0           my $content = encode 'utf8', <<__PAGE;
186             $dir
187            
188            
189            

Directory $dir

190            
191             @rows
192            
193            

Generated $now

194            
195             __PAGE
196              
197 0           HTTP::Response->new(HTTP_OK, undef
198             , ['Content-Type' => 'text/html; charset='.$self->charset]
199             , $content
200             );
201             }
202              
203              
204             my %filetype =
205             ( &S_IFSOCK => 's', &S_IFLNK => 'l', &S_IFREG => '-', &S_IFBLK => 'b'
206             , &S_IFDIR => 'd', &S_IFCHR => 'c', &S_IFIFO => 'p');
207              
208             my @flags = ('---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx');
209            
210             my @stat_fields =
211             qw/dev ino mode nlink uid gid rdev size atime mtime ctime blksize blocks/;
212              
213             sub list($@)
214 0     0 1   { my ($self, $dirname, %opts) = @_;
215              
216 0 0         opendir my $from_dir, $dirname
217             or return;
218              
219 0   0       my $names = $opts{names} || qr/^[^.]/;
220             my $prefilter
221 0     0     = ref $names eq 'Regexp' ? sub { $_[0] =~ $names }
222 0 0         : ref $names eq 'CODE' ? $names
    0          
223             : panic "::Directory::list(names) must be regexp or code, not $names";
224              
225 0   0 0     my $postfilter = $opts{filter} || sub {1};
  0            
226 0 0         ref $postfilter eq 'CODE'
227             or panic "::Directory::list(filter) must be code, not $postfilter";
228              
229 0           my $hide_symlinks = $opts{hide_symlinks};
230              
231 0           my (%dirlist, %users, %groups);
232 0           foreach my $name (grep $prefilter->($_), readdir $from_dir)
233 0           { my $path = $dirname.$name;
234 0           my %d = (name => $name, path => $path);
235 0 0         @d{@stat_fields}
236             = $hide_symlinks ? stat($path) : lstat($path);
237              
238 0 0 0       if(!$hide_symlinks && -l _)
    0          
    0          
239 0           { @d{qw/kind is_symlink /} = ('SYMLINK', 1)}
240 0           elsif(-d _) { @d{qw/kind is_directory/} = ('DIRECTORY',1)}
241 0           elsif(-f _) { @d{qw/kind is_file /} = ('FILE', 1)}
242 0           else { @d{qw/kind is_other /} = ('OTHER', 1)}
243              
244 0 0         $postfilter->(\%d)
245             or next;
246              
247 0 0         if($d{is_symlink})
    0          
    0          
248 0           { my $sl = $d{symlink_dest} = readlink $path;
249 0           $d{symlink_dest_exists} = -e $sl;
250             }
251             elsif($d{is_file})
252 0           { my ($s, $l) = ($d{size}, ' ');
253 0 0         ($s,$l) = ($s/1024, 'kB') if $s > 1024;
254 0 0         ($s,$l) = ($s/1024, 'MB') if $s > 1024;
255 0 0         ($s,$l) = ($s/1024, 'GB') if $s > 1024;
256 0 0         $d{size_nice} = sprintf +($s>=100?"%.0f%s":"%.1f%s"), $s,$l;
257             }
258             elsif($d{is_directory})
259 0           { $d{name} .= '/';
260             }
261              
262 0   0       $d{user} = $users{$d{uid}} ||= getpwuid $d{uid};
263 0   0       $d{group} = $users{$d{gid}} ||= getgrgid $d{gid};
264              
265 0           my $mode = $d{mode};
266 0   0       my $b = $filetype{$mode & S_IFMT} || '?';
267 0           $b .= $flags[ ($mode & S_IRWXU) >> 6 ];
268 0 0         substr($b, -1, -1) = 's' if $mode & S_ISUID;
269 0           $b .= $flags[ ($mode & S_IRWXG) >> 3 ];
270 0 0         substr($b, -1, -1) = 's' if $mode & S_ISGID;
271 0           $b .= $flags[ $mode & S_IRWXO ];
272 0 0         substr($b, -1, -1) = 't' if $mode & S_ISVTX;
273 0           $d{flags} = $b;
274 0           $d{mtime_nice} = strftime "%F %T", localtime $d{mtime};
275              
276 0           $dirlist{$name} = \%d;
277             }
278 0           \%dirlist;
279             }
280              
281             #-----------------------
282              
283             1;