| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
# Copyrights 2013-2019 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
|
1
|
|
|
1
|
|
5
|
use vars '$VERSION'; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
41
|
|
|
11
|
|
|
|
|
|
|
$VERSION = '0.29'; |
|
12
|
|
|
|
|
|
|
|
|
13
|
1
|
|
|
1
|
|
4
|
use parent 'Any::Daemon::HTTP::Source'; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
4
|
|
|
14
|
|
|
|
|
|
|
|
|
15
|
1
|
|
|
1
|
|
38
|
use warnings; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
21
|
|
|
16
|
1
|
|
|
1
|
|
4
|
use strict; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
17
|
|
|
17
|
|
|
|
|
|
|
|
|
18
|
1
|
|
|
1
|
|
4
|
use Log::Report 'any-daemon-http'; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
5
|
|
|
19
|
|
|
|
|
|
|
|
|
20
|
1
|
|
|
1
|
|
185
|
use File::Spec (); |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
16
|
|
|
21
|
1
|
|
|
1
|
|
4
|
use File::Basename qw/dirname/; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
58
|
|
|
22
|
1
|
|
|
1
|
|
350
|
use POSIX::1003 qw/strftime :fd :fs/; |
|
|
1
|
|
|
|
|
3807
|
|
|
|
1
|
|
|
|
|
5
|
|
|
23
|
1
|
|
|
1
|
|
10107
|
use HTTP::Status qw/:constants/; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
245
|
|
|
24
|
1
|
|
|
1
|
|
394
|
use HTTP::Response (); |
|
|
1
|
|
|
|
|
1908
|
|
|
|
1
|
|
|
|
|
21
|
|
|
25
|
1
|
|
|
1
|
|
6
|
use Encode qw/encode/; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
41
|
|
|
26
|
1
|
|
|
1
|
|
387
|
use MIME::Types (); |
|
|
1
|
|
|
|
|
3268
|
|
|
|
1
|
|
|
|
|
756
|
|
|
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
|
1
|
|
|
1
|
|
7
|
no warnings 'uninitialized'; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
842
|
|
|
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; |