File Coverage

blib/lib/Apache2/S3.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Apache2::S3;
2              
3 1     1   18575 use strict;
  1         2  
  1         41  
4 1     1   6 use warnings;
  1         1  
  1         44  
5              
6 1     1   2095 use Apache2::Const -compile => qw(OK DECLINED PROXYREQ_REVERSE);
  0            
  0            
7             use Apache2::RequestRec;
8             use Apache2::Filter;
9             use Apache2::FilterRec;
10             use APR::Table;
11             use APR::String;
12             use MIME::Base64;
13             use Digest::SHA1;
14             use Digest::HMAC;
15             use URI::Escape;
16             use HTML::Entities;
17             use XML::Parser;
18             use Time::Local;
19             use POSIX;
20             use CGI;
21              
22             our $VERSION = '0.05';
23              
24             our $ESCAPE = quotemeta " #%<>[\]^`{|}?\\";
25              
26             use constant TEXT => '0';
27              
28             sub _signature
29             {
30             my ($id, $key, $data) = @_;
31             return "AWS $id:".MIME::Base64::encode_base64(Digest::HMAC::hmac($data, $key, \&Digest::SHA1::sha1), "");
32             }
33              
34             sub handler
35             {
36             my $r = shift;
37              
38             return Apache2::Const::DECLINED
39             if $r->proxyreq;
40              
41             return Apache2::Const::DECLINED
42             unless $r->method eq 'GET' or $r->dir_config('S3ReadWrite');
43              
44             my $h = $r->headers_in;
45             my $uri = $r->uri;
46              
47             my %map = split /\s*(?:,|=>)\s*/, $r->dir_config("S3Map");
48              
49             # most specific (longest) match first
50             foreach my $base (sort { length $b <=> length $a } keys %map)
51             {
52             $uri =~ s|^($base/*)|| or next;
53             my $stripped = $1;
54              
55             my ($bucket, $keyId, $keySecret) = split m|/|, $map{$base};
56             $keyId ||= $r->dir_config("S3Key");
57             $keySecret ||= $r->dir_config("S3Secret");
58              
59             my $is_dir = $uri =~ m,(^|/)$,;
60             my $path = "/$bucket/".($is_dir ? "" : $uri);
61              
62             my $args = $r->args || "";
63             my $sub = $args =~ s/^(acl|logging|torrent)(?:&|$)// ? $1 : "";
64             local $CGI::USE_PARAM_SEMICOLONS = 0;
65             $args = CGI->new($r, $args);
66              
67             if ($is_dir)
68             {
69             $args->param('delimiter', $args->param('delimiter') || '/');
70             $args->param('prefix', $uri) if $uri;
71             }
72              
73             my %note = (
74             'id' => $keyId,
75             'secret' => $keySecret,
76             'path' => $path,
77             'sub' => $sub,
78             'stripped' => $stripped,
79             ($is_dir ? ('prefix' => $uri) : ()),
80             (($args->param('raw') or not $is_dir or $sub) ? ('raw' => 1) : ()),
81             (($args->param('nocache') or $is_dir or $sub) ? ('nocache' => 1) : ()),
82             );
83              
84             $r->notes->add(__PACKAGE__."::s3_$_" => $note{$_})
85             foreach keys %note;
86              
87             $r->proxyreq(Apache2::Const::PROXYREQ_REVERSE);
88             $r->uri("http://s3.amazonaws.com$path");
89             $r->args(($sub ? "$sub&" : "").$args->query_string);
90             $r->filename("proxy:http://s3.amazonaws.com$path");
91             $r->handler('proxy-server');
92              
93             # we delay adding the authorization header to give
94             # mod_auth* a chance to authenticate the users request
95             # which would use the same header
96             $r->set_handlers('PerlFixupHandler' => \&s3_auth_handler);
97              
98             # we set up an output filter to translate XML responses
99             # for directory requests into "pretty" HTML
100             $r->add_output_filter(\&output_filter);
101              
102             return Apache2::Const::OK;
103             }
104              
105             return Apache2::Const::DECLINED;
106             }
107              
108             sub s3_auth_handler
109             {
110             my $r = shift;
111             my $h = $r->headers_in;
112              
113             my ($keyId, $keySecret, $path, $sub) =
114             map $r->notes->get(__PACKAGE__."::s3_$_"), qw(id secret path sub);
115              
116             $h->{'Date'} = POSIX::strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime);
117             $h->{'Authorization'} = _signature $keyId, $keySecret, join "\n",
118             $r->method,
119             $h->{'Content-MD5'} || "",
120             $h->{'Content-Type'} || "",
121             $h->{'Date'},
122             uri_escape($path, $ESCAPE).($sub ? "?$sub" : "");
123              
124             return Apache2::Const::OK;
125             }
126              
127             sub _xml_get_tags
128             {
129             my ($tree, $tag, @tags) = @_;
130             my @ret;
131             for (my $i = @$tree % 2; $i < @$tree; $i += 2)
132             {
133             next unless $tree->[$i] eq $tag;
134             push @ret, $tree->[$i+1];
135             last unless wantarray;
136             }
137             return unless @ret;
138             return _xml_get_tags($ret[0], @tags) if @tags;
139             return wantarray ? @ret : $ret[0];
140             }
141              
142             sub _reformat_directory
143             {
144             my ($f, $ctx) = @_;
145              
146             my $stripped = $f->r->notes->get(__PACKAGE__.'::s3_stripped');
147             my $prefix = $f->r->notes->get(__PACKAGE__.'::s3_prefix');
148              
149             my $tree = eval {
150             XML::Parser->new(Style => 'Tree')->parse($ctx->{text});
151             };
152              
153             my $list = _xml_get_tags($tree, 'ListBucketResult')
154             or die $ctx->{text};
155              
156             my $is_truncated = _xml_get_tags($list, 'IsTruncated', TEXT) =~ /^(?:false|)$/i ? 0 : 1;
157             my $next_marker = _xml_get_tags($list, 'NextMarker', TEXT);
158              
159             my @dirs = map +{
160             Name => _xml_get_tags($_, 'Prefix', TEXT),
161             }, _xml_get_tags($list, 'CommonPrefixes');
162              
163             my @files = map +{
164             Name => _xml_get_tags($_, 'Key', TEXT),
165             Size => _xml_get_tags($_, 'Size', TEXT),
166             LastModified => _xml_get_tags($_, 'LastModified', TEXT) =~
167             /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.\d+)?Z$/
168             ? timegm($6, $5, $4, $3, $2-1, $1) : 0,
169             }, _xml_get_tags($list, 'Contents');
170              
171             my $ret = "";
172              
173             $ret .= qq|
|; 
174              
175             $ret .= qq|Parent Directory\n|;
176              
177             $ret .= qq|Next Page\n|
178             if $is_truncated and $next_marker;
179              
180             $ret .= sprintf(qq|%s%s %-18s %s\n|,
181             $stripped.uri_escape($_->{Name}, $ESCAPE),
182             HTML::Entities::encode($_->{DisplayName}),
183             " "x(87 - length $_->{DisplayName}),
184             $_->{LastModified} ? strftime("%d-%b-%Y %H:%M", localtime($_->{LastModified})) : "-",
185             $_->{Size} ? APR::String::format_size($_->{Size}) : "")
186             foreach map {
187             $_->{DisplayName} = $_->{Name} =~ m|([^/]+)/?$| ? $1 : $_->{Name};
188             $_;
189             } @dirs, @files;
190              
191             $ret .= qq||;
192              
193             $ret;
194             }
195              
196             sub output_filter
197             {
198             my $f = shift;
199              
200             my $ctx;
201              
202             unless ($ctx = $f->ctx)
203             {
204             # disable caching layer if requested
205             if ($f->r->notes->get(__PACKAGE__.'::s3_nocache'))
206             {
207             my $next = $f;
208              
209             while ($next)
210             {
211             $next->remove if $next->frec->name =~ /^cache_\w+$/i;
212             $next = $next->next;
213             }
214             }
215             else
216             {
217             # mark as public to allow mod_cache to save it even though it includes an Authorization header
218             $f->r->headers_out->{'Cache-Control'} = join(",", grep defined && length,
219             split(/\s*,\s*/, $f->r->headers_out->{'Cache-Control'} || ""), "public");
220             }
221              
222             # don't process this output if requested
223             if ($f->r->notes->get(__PACKAGE__.'::s3_raw') or lc $f->r->content_type ne 'application/xml')
224             {
225             $f->remove;
226              
227             unless ($f->r->content_type eq 'application/xml')
228             {
229             # S3 supports byte-range requests, but doesn't advertise it.
230             $f->r->headers_out->{'Accept-Ranges'} = 'bytes';
231             }
232              
233             return Apache2::Const::DECLINED
234             }
235              
236             $f->r->content_type('text/html');
237             $f->r->headers_out->unset('Content-Length');
238             $f->ctx($ctx = { text => "" })
239             }
240              
241             $ctx->{text} .= $_
242             while $f->read($_);
243              
244             return Apache2::Const::OK
245             unless $f->seen_eos;
246              
247             my $ret = _reformat_directory($f, $ctx);
248              
249             $f->r->headers_out->{'Content-Length'} = length $ret;
250             $f->print($ret);
251             $f->ctx(undef);
252              
253             return Apache2::Const::OK;
254             }
255              
256             1;
257             __END__