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
|
|
|
|
|
|
|
|
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; |