File Coverage

lib/Amazon/S3.pm
Criterion Covered Total %
statement 125 673 18.5
branch 21 246 8.5
condition 11 206 5.3
subroutine 27 90 30.0
pod 19 31 61.2
total 203 1246 16.2


line stmt bran cond sub pod time code
1             package Amazon::S3;
2              
3 2     2   567578 use strict;
  2         4  
  2         62  
4 2     2   7 use warnings;
  2         2  
  2         97  
5              
6 2     2   976 use Amazon::S3::Bucket;
  2         7  
  2         107  
7 2     2   1070 use Amazon::S3::BucketV2;
  2         6  
  2         123  
8 2     2   12 use Amazon::S3::Constants qw(:all);
  2         2  
  2         745  
9              
10 2         130 use Amazon::S3::Util qw(
11             set_md5_header
12             urlencode
13             get_parameters
14             create_xml_request
15             create_api_uri
16             create_query_string
17 2     2   12 );
  2         3  
18              
19 2     2   947 use Amazon::S3::Logger;
  2         7  
  2         84  
20 2     2   1399 use Amazon::S3::Signature::V4;
  2         9  
  2         90  
21              
22 2     2   23 use Carp;
  2         3  
  2         155  
23 2     2   13 use Data::Dumper;
  2         3  
  2         157  
24 2     2   1335 use Digest::HMAC_SHA1;
  2         3630  
  2         119  
25 2     2   18 use Digest::MD5 qw(md5_hex);
  2         5  
  2         175  
26 2     2   67 use English qw(-no_match_vars);
  2         3  
  2         19  
27 2     2   1860 use HTTP::Date;
  2         4  
  2         180  
28 2     2   1353 use LWP::UserAgent::Determined;
  2         1614  
  2         120  
29 2     2   15 use List::Util qw( any pairs none );
  2         4  
  2         198  
30 2     2   16 use MIME::Base64 qw(encode_base64 decode_base64);
  2         4  
  2         146  
31 2     2   16 use Scalar::Util qw( reftype blessed );
  2         5  
  2         148  
32 2     2   13 use URI;
  2         4  
  2         43  
33 2     2   8 use XML::Simple;
  2         4  
  2         16  
34              
35 2     2   173 use parent qw(Class::Accessor::Fast Exporter);
  2         4  
  2         36  
36              
37             __PACKAGE__->mk_accessors(
38             qw(
39             aws_access_key_id
40             aws_secret_access_key
41             token
42             buffer_size
43             cache_signer
44             credentials
45             dns_bucket_names
46             digest
47             err
48             errstr
49             error
50             express
51             host
52             last_request
53             last_response
54             logger
55             log_level
56             retry
57             _region
58             secure
59             _signer
60             timeout
61             ua
62             ),
63             );
64              
65             our $VERSION = '2.0.2'; ## no critic (RequireInterpolation)
66              
67             our @EXPORT_OK = qw(is_domain_bucket);
68              
69             ########################################################################
70             sub new {
71             ########################################################################
72 3     3 1 2369 my ( $class, @args ) = @_;
73              
74 3 50       14 my %options = ref $args[0] ? %{ $args[0] } : @args;
  3         13  
75              
76 3   33     33 $options{timeout} //= $DEFAULT_TIMEOUT;
77 3   33     63 $options{secure} //= $TRUE;
78 3   33     23 $options{host} //= $DEFAULT_HOST;
79 3   33     69 $options{dns_bucket_names} //= $TRUE;
80 3   33     23 $options{cache_signer} //= $FALSE;
81 3   33     22 $options{retry} //= $FALSE;
82 3   33     48 $options{express} //= $FALSE;
83              
84 3         17 $options{_region} = delete $options{region};
85 3         9 $options{_signer} = delete $options{signer};
86              
87             # convenience for level => 'debug' & for consistency with
88             # Amazon::Credentials only do this if we are using internal logger,
89             # call should NOT use debug flag but rather use their own logger's
90             # level to turn on higher levels of logging...
91              
92 3 50       9 if ( !$options{logger} ) {
93 3 50       11 if ( delete $options{debug} ) {
94 0         0 $options{level} = 'debug';
95             }
96              
97 3         9 $options{log_level} = delete $options{level};
98 3   33     18 $options{log_level} //= $DEFAULT_LOG_LEVEL;
99              
100             $options{logger}
101 3         39 = Amazon::S3::Logger->new( log_level => $options{log_level} );
102             }
103              
104 3         22 my $self = $class->SUPER::new( \%options );
105              
106             # setup logger internal logging
107              
108             $self->get_logger->debug(
109             sub {
110 0     0   0 my %safe_options = %options;
111              
112 0 0       0 if ( $safe_options{aws_secret_access_key} ) {
113 0         0 $safe_options{aws_secret_access_key} = '****';
114 0         0 $safe_options{aws_access_key_id} = '****';
115             }
116              
117 0         0 return Dumper( [ options => \%safe_options ] );
118             },
119 3         43 );
120              
121 3 50       135 if ( !$self->credentials ) {
122              
123 3 50       64 croak 'No aws_access_key_id'
124             if !$self->aws_access_key_id;
125              
126 3 50       59 croak 'No aws_secret_access_key'
127             if !$self->aws_secret_access_key;
128              
129             # encrypt credentials
130 3         54 $self->aws_access_key_id( _encrypt( $self->aws_access_key_id ) );
131 3         72 $self->aws_secret_access_key( _encrypt( $self->aws_secret_access_key ) );
132 3         79 $self->token( _encrypt( $self->token ) );
133             }
134              
135 3         28 my $ua;
136              
137 3 50       69 if ( $self->retry ) {
138 0         0 $ua = LWP::UserAgent::Determined->new(
139             keep_alive => $KEEP_ALIVE_CACHESIZE,
140             requests_redirectable => [qw(GET HEAD DELETE)],
141             );
142              
143 0         0 $ua->timing( join $COMMA, map { 2**$_ } 0 .. $MAX_RETRIES );
  0         0  
144             }
145             else {
146 3         45 $ua = LWP::UserAgent->new(
147             keep_alive => $KEEP_ALIVE_CACHESIZE,
148             requests_redirectable => [qw(GET HEAD DELETE)],
149             );
150             }
151              
152 3         8600 $ua->timeout( $self->timeout );
153 3         81 $ua->env_proxy;
154 3         10983 $self->ua($ua);
155              
156 3   66     138 $self->region( $self->_region // $DEFAULT_REGION );
157              
158 3 50 33     57 if ( !$self->_signer && $self->cache_signer ) {
159 0         0 $self->_signer( $self->signer );
160             }
161              
162 3 50       129 if ( $self->express ) {
163 0         0 $self->use_express_one_zone();
164             }
165              
166 3         21 $self->turn_on_special_retry();
167              
168 3         71 return $self;
169             }
170              
171             ########################################################################
172             sub use_express_one_zone {
173             ########################################################################
174 0     0 0 0 my ($self) = @_;
175              
176 0         0 my $express = $self->express;
177              
178 0         0 $self->express($TRUE);
179              
180 0         0 $self->host( sprintf 's3express-control.%s.amazonaws.com', $self->region );
181 0         0 $self->dns_bucket_names($FALSE);
182              
183 0         0 return $express;
184             }
185              
186             ########################################################################
187             {
188             my $encryption_key;
189              
190             ########################################################################
191             sub _encrypt {
192             ########################################################################
193 9     9   48 my ($text) = @_;
194              
195 9 100       84 return $text if !$text;
196              
197 6 50       13 if ( !defined $encryption_key ) {
198 6         9 $encryption_key = eval {
199 6 50       16 if ( !defined $encryption_key ) {
200 6         888 require Crypt::Blowfish;
201 0         0 require Crypt::CBC;
202              
203 0         0 return md5_hex( rand $PID );
204             }
205             };
206              
207 6 50       186 return $text if $EVAL_ERROR;
208             }
209              
210 0 0       0 return $text if !$encryption_key;
211              
212 0         0 my $cipher = Crypt::CBC->new(
213             -pass => $encryption_key,
214             -key => $encryption_key,
215             -cipher => 'Crypt::Blowfish',
216             -nodeprecate => $TRUE,
217             );
218              
219 0         0 return $cipher->encrypt($text);
220             }
221              
222             ########################################################################
223             sub _decrypt {
224             ########################################################################
225 0     0   0 my ($secret) = @_;
226              
227 0 0 0     0 return $secret
228             if !$secret || !$encryption_key;
229              
230 0         0 my $cipher = Crypt::CBC->new(
231             -pass => $encryption_key,
232             -key => $encryption_key,
233             -cipher => 'Crypt::Blowfish',
234             );
235              
236 0         0 return $cipher->decrypt($secret);
237             }
238              
239             }
240              
241             ########################################################################
242             sub get_bucket_location {
243             ########################################################################
244 0     0 1 0 my ( $self, $bucket ) = @_;
245              
246 0         0 my $region;
247              
248 0 0 0     0 if ( !ref $bucket || ref $bucket !~ /Amazon::S3::Bucket/xsm ) {
249 0         0 $bucket = Amazon::S3::Bucket->new( bucket => $bucket, account => $self );
250             }
251              
252 0   0     0 return $bucket->get_location_constraint // $DEFAULT_REGION;
253             }
254              
255             ########################################################################
256             sub get_default_region {
257             ########################################################################
258 0     0 0 0 my ($self) = @_;
259              
260 0   0     0 my $region = $ENV{AWS_REGION} || $ENV{AWS_DEFAULT_REGION};
261              
262 0 0       0 return $region
263             if $region;
264              
265 0         0 my $url = $AWS_METADATA_BASE_URL . 'placement/availability-zone';
266              
267 0         0 my $request = HTTP::Request->new( 'GET', $url );
268              
269 0         0 my $ua = LWP::UserAgent->new;
270 0         0 $ua->timeout(0);
271              
272 0         0 my $response = eval { return $ua->request($request); };
  0         0  
273              
274 0 0 0     0 if ( $response && $response->is_success ) {
275 0 0       0 if ( $response->content =~ /\A([[:lower:]]+[-][[:lower:]]+[-]\d+)/xsm ) {
276 0         0 $region = $1;
277             }
278             }
279              
280 0   0     0 return $region || $DEFAULT_REGION;
281             }
282              
283             # Amazon::Credentials compatibility methods
284             ########################################################################
285             sub get_aws_access_key_id {
286             ########################################################################
287 0     0 0 0 my ($self) = @_;
288              
289 0         0 return _decrypt( $self->aws_access_key_id );
290             }
291              
292             ########################################################################
293             sub get_aws_secret_access_key {
294             ########################################################################
295 0     0 0 0 my ($self) = @_;
296              
297 0         0 return _decrypt( $self->aws_secret_access_key );
298             }
299              
300             ########################################################################
301             sub get_token {
302             ########################################################################
303 0     0 0 0 my ($self) = @_;
304              
305 0         0 return _decrypt( $self->token );
306             }
307              
308             ########################################################################
309             sub turn_on_special_retry {
310             ########################################################################
311 3     3 1 6 my ($self) = @_;
312              
313             return
314 3 50       205 if !$self->retry;
315              
316             # In the field we are seeing issue of Amazon returning with a 400
317             # code in the case of timeout. From AWS S3 logs: REST.PUT.PART
318             # Backups/2017-05-04/.tar.gz "PUT
319             # /Backups?partNumber=27&uploadId= - HTTP/1.1" 400
320             # RequestTimeout 360 20971520 20478 - "-" "libwww-perl/6.15"
321 0         0 my $http_codes_hr = $self->ua->codes_to_determinate();
322 0         0 $http_codes_hr->{$HTTP_BAD_REQUEST} = $TRUE;
323              
324 0         0 return;
325             }
326              
327             ########################################################################
328             sub turn_off_special_retry {
329             ########################################################################
330 0     0 1 0 my ($self) = @_;
331              
332             return
333 0 0       0 if !$self->retry;
334              
335             # In the field we are seeing issue with Amazon returning a 400
336             # code in the case of timeout. From AWS S3 logs: REST.PUT.PART
337             # Backups/2017-05-04/.tar.gz "PUT
338             # /Backups?partNumber=27&uploadId= - HTTP/1.1" 400
339             # RequestTimeout 360 20971520 20478 - "-" "libwww-perl/6.15"
340 0         0 my $http_codes_hr = $self->ua->codes_to_determinate();
341 0         0 delete $http_codes_hr->{$HTTP_BAD_REQUEST};
342              
343 0         0 return;
344             }
345              
346             ########################################################################
347             sub region {
348             ########################################################################
349 7     7 1 1793 my ( $self, @args ) = @_;
350              
351 7 100       29 if (@args) {
352 4         92 $self->_region( $args[0] );
353             }
354              
355             $self->get_logger->debug(
356 7   0 0   38 sub { return 'region: ' . ( $self->_region // $EMPTY ) } );
  0         0  
357              
358 7 50       256 if ( $self->_region ) {
359 7         134 my $host = $self->host;
360 7     0   35 $self->get_logger->debug( sub { return 'host: ' . $self->host } );
  0         0  
361              
362 7 50       156 if ( $host =~ /\As3[.](.*)?amazonaws/xsm ) {
363 7         125 $self->host( sprintf 's3.%s.amazonaws.com', $self->_region );
364             }
365             }
366              
367 7         255 return $self->_region;
368             }
369              
370             ########################################################################
371             sub buckets {
372             ########################################################################
373 0     0 1 0 my ( $self, $verify_region ) = @_;
374              
375             # The "default" region for Amazon is us-east-1
376             # This is the region to set it to for listing buckets
377             # You may need to reset the signer's endpoint to 'us-east-1'
378              
379             # temporarily cache signer
380 0         0 my $region = $self->_region;
381 0         0 my $bucket_list;
382              
383 0         0 $self->reset_signer_region($DEFAULT_REGION); # default region for buckets op
384              
385 0         0 my $r = $self->_send_request(
386             { method => 'GET',
387             path => $EMPTY,
388             headers => {},
389             region => $DEFAULT_REGION,
390             },
391             );
392              
393 0 0 0     0 return $bucket_list
394             if !$r || $self->errstr;
395              
396 0         0 my $owner_id = $r->{Owner}{ID};
397 0         0 my $owner_displayname = $r->{Owner}{DisplayName};
398              
399 0         0 my @buckets;
400              
401 0 0       0 if ( ref $r->{Buckets} ) {
402 0         0 my $buckets = $r->{Buckets}{Bucket};
403              
404 0 0 0     0 if ( !ref $buckets || reftype($buckets) ne 'ARRAY' ) {
405 0         0 $buckets = [$buckets];
406             }
407              
408 0         0 foreach my $node ( @{$buckets} ) {
  0         0  
409             push @buckets,
410             Amazon::S3::Bucket->new(
411             { bucket => $node->{Name},
412             creation_date => $node->{CreationDate},
413 0   0     0 account => $self,
414             buffer_size => $self->buffer_size,
415             verify_region => $verify_region // $FALSE,
416             },
417             );
418              
419             }
420             }
421              
422 0         0 $self->reset_signer_region($region); # restore original region
423              
424 0         0 $bucket_list = {
425             owner_id => $owner_id,
426             owner_displayname => $owner_displayname,
427             buckets => \@buckets,
428             };
429              
430 0         0 return $bucket_list;
431             }
432              
433             ########################################################################
434             sub reset_signer_region {
435             ########################################################################
436 0     0 0 0 my ( $self, $region ) = @_;
437              
438             # reset signer's region, if the region wasn't us-east-1...note this
439             # is probably not needed anymore since bucket operations now send
440             # the region of the bucket to the signer
441 0 0       0 if ( $self->cache_signer ) {
442 0 0 0     0 if ( $self->region && $self->region ne $DEFAULT_REGION ) {
443 0 0       0 if ( $self->signer->can('region') ) {
444 0         0 $self->signer->region($region);
445             }
446             }
447             }
448             else {
449 0         0 $self->region($region);
450             }
451              
452 0         0 return $self->region;
453             }
454              
455             ########################################################################
456             sub add_bucket {
457             ########################################################################
458 0     0 1 0 my ( $self, $conf ) = @_;
459              
460 0         0 my $bucket = $conf->{bucket};
461              
462 0 0       0 croak 'must specify bucket'
463             if !$bucket;
464              
465 0   0     0 my $headers = $conf->{headers} // {};
466              
467 0 0       0 if ( $conf->{acl_short} ) {
468 0         0 $self->_validate_acl_short( $conf->{acl_short} );
469              
470 0   0     0 $headers->{'x-amz-acl'} //= $conf->{acl_short};
471 0   0     0 $headers->{'x-amz-object-ownership'} //= 'ObjectWriter';
472             }
473              
474 0   0     0 my $region = $conf->{location_constraint} // $conf->{region};
475              
476 0   0     0 $region //= $self->region;
477              
478 0 0 0     0 if ( $region && $region eq $DEFAULT_REGION ) {
479 0         0 undef $region;
480             }
481              
482             return $self->_add_bucket(
483             { headers => $headers,
484             bucket => $conf->{bucket},
485             region => $region,
486             availability_zone => $conf->{availability_zone},
487             }
488 0         0 );
489             }
490              
491             ########################################################################
492             sub _add_bucket {
493             ########################################################################
494 0     0   0 my ( $self, @args ) = @_;
495              
496 0         0 my $parameters = get_parameters(@args);
497              
498             my ( $bucket, $headers, $region, $availability_zone )
499 0         0 = @{$parameters}{qw(bucket headers region availability_zone)};
  0         0  
500              
501 0   0     0 $region //= $EMPTY;
502 0   0     0 $headers //= {};
503              
504 0         0 my $request
505             = { CreateBucketConfiguration => { LocationConstraint => $region, } };
506              
507 0 0       0 if ($availability_zone) {
508             $request->{CreateBucketConfiguration}->{Location} = {
509 0         0 Name => $availability_zone,
510             Type => 'AvailabilityZone',
511             };
512              
513             $request->{CreateBucketConfiguration}->{Bucket} = {
514 0         0 DataRedundancy => 'SingleAvailabilityZone',
515             Type => 'Directory',
516             };
517              
518 0         0 delete $request->{CreateBucketConfiguration}->{LocationConstraint};
519             }
520              
521 0         0 $self->dns_bucket_names(0);
522              
523 0 0 0     0 my $data
524             = ( $region || $availability_zone )
525             ? create_xml_request($request)
526             : $EMPTY;
527              
528 0         0 $headers->{'Content-Length'} = length $data;
529              
530 0         0 my $retval = $self->_send_request_expect_nothing(
531             { method => 'PUT',
532             path => "$bucket/",
533             headers => $headers,
534             data => $data,
535             region => $region,
536             },
537             );
538              
539 0 0       0 my $bucket_obj = $retval ? $self->bucket($bucket) : undef;
540              
541 0         0 return $bucket_obj;
542             }
543              
544             ########################################################################
545             sub bucket {
546             ########################################################################
547 0     0 1 0 my ( $self, @args ) = @_;
548              
549 0         0 my ( $bucketname, $region, $verify_region );
550              
551 0 0 0     0 if ( ref $args[0] && reftype( $args[0] ) eq 'HASH' ) {
552             ( $bucketname, $region, $verify_region )
553 0         0 = @{ $args[0] }{qw(bucket region verify_region)};
  0         0  
554             }
555             else {
556 0         0 ( $bucketname, $region ) = @args;
557             }
558              
559             # only set to default region if a region wasn't passed or region
560             # verification not requested
561 0 0 0     0 if ( !$region && !$verify_region ) {
562 0         0 $region = $self->region;
563             }
564              
565 0         0 return Amazon::S3::Bucket->new(
566             { bucket => $bucketname,
567             account => $self,
568             region => $region,
569             verify_region => $verify_region,
570             },
571             );
572             }
573              
574             ########################################################################
575             sub delete_bucket {
576             ########################################################################
577 0     0 1 0 my ( $self, $conf ) = @_;
578              
579 0         0 my $bucket;
580             my $region;
581 0         0 my $headers;
582              
583 0 0       0 if ( eval { return $conf->isa('Amazon::S3::Bucket'); } ) {
  0         0  
584 0         0 $bucket = $conf->bucket;
585 0         0 $region = $conf->region;
586             }
587             else {
588 0         0 $bucket = $conf->{bucket};
589 0   0     0 $region = $conf->{region} || $self->get_bucket_location($bucket);
590 0         0 $headers = $conf->{headers};
591             }
592              
593 0 0       0 croak 'must specify bucket'
594             if !$bucket;
595              
596 0   0     0 return $self->_send_request_expect_nothing(
597             { method => 'DELETE',
598             path => $bucket . $SLASH,
599             headers => $headers // {},
600             region => $region,
601             },
602             );
603             }
604              
605             ########################################################################
606             sub list_directory_buckets {
607             ########################################################################
608 0     0 1 0 my ( $self, @args ) = @_;
609              
610 0         0 my $parameters = get_parameters(@args);
611              
612 0         0 my $express = $self->use_express_one_zone;
613              
614             my $result = $self->_send_request(
615             { method => 'GET',
616             headers => {},
617             path => $SLASH,
618             uri_params => $parameters->{uri_params} // {},
619 0   0     0 region => $self->region,
620             }
621             );
622              
623 0         0 $self->express($express);
624              
625 0         0 return $result;
626             }
627              
628             ########################################################################
629             sub list_bucket_v2 {
630             ########################################################################
631 0     0 1 0 my ( $self, $conf ) = @_;
632              
633 0         0 $conf->{'list-type'} = '2';
634              
635 0         0 goto &list_bucket;
636             }
637              
638             ########################################################################
639             sub list_bucket {
640             ########################################################################
641 0     0 1 0 my ( $self, $conf ) = @_;
642              
643 0         0 my $bucket = delete $conf->{bucket};
644              
645 0 0       0 croak 'must specify bucket'
646             if !$bucket;
647              
648 0   0     0 $conf //= {};
649              
650 0         0 my $bucket_list; # return this
651 0         0 my $path = $bucket . $SLASH;
652              
653 0         0 my $headers = delete $conf->{headers};
654              
655 0   0     0 my $list_type = $conf->{'list-type'} // '1';
656              
657             my ( $marker, $next_marker, $query_next )
658 0         0 = @{ $LIST_OBJECT_MARKERS{$list_type} };
  0         0  
659              
660 0 0       0 if ( $conf->{marker} ) {
661 0         0 $conf->{$query_next} = delete $conf->{marker};
662             }
663              
664 0 0       0 if ( %{$conf} ) {
  0         0  
665              
666 0         0 my @vars = keys %{$conf};
  0         0  
667              
668             # remove undefined elements
669 0         0 foreach (@vars) {
670 0 0       0 next if defined $conf->{$_};
671              
672 0         0 delete $conf->{$_};
673             }
674              
675             my $query_string = $QUESTION_MARK . join $AMPERSAND,
676 0         0 map { $_ . $EQUAL_SIGN . urlencode( $conf->{$_} ) }
677 0         0 keys %{$conf};
  0         0  
678              
679 0         0 $path .= $query_string;
680             }
681              
682 0         0 $self->get_logger->debug( sprintf 'PATH: %s', $path );
683              
684 0   0     0 my $r = $self->_send_request(
685             { method => 'GET',
686             path => $path,
687             headers => $headers // {}, # { 'Content-Length' => 0 },
688             region => $self->region,
689             },
690             );
691              
692 0         0 $self->get_logger->trace(
693             Dumper(
694             [ r => $r,
695             errstr => $self->errstr,
696             ]
697             )
698             );
699              
700 0 0 0     0 return $bucket_list
701             if !$r || $self->errstr;
702              
703             $self->get_logger->trace(
704             sub {
705 0     0   0 return Dumper(
706             [ marker => $marker,
707             next_marker => $next_marker,
708             response => $r,
709             ],
710             );
711             },
712 0         0 );
713              
714             $bucket_list = {
715             bucket => $r->{Name},
716             prefix => $r->{Prefix} // $EMPTY,
717             marker => $r->{$marker} // $EMPTY,
718             next_marker => $r->{$next_marker} // $EMPTY,
719             max_keys => $r->{MaxKeys},
720             is_truncated => (
721 0 0 0     0 ( defined $r->{IsTruncated} && scalar $r->{IsTruncated} eq 'true' )
      0        
      0        
      0        
722             ? $TRUE
723             : $FALSE
724             ),
725             };
726              
727 0         0 my @keys;
728              
729 0         0 foreach my $node ( @{ $r->{Contents} } ) {
  0         0  
730 0         0 my $etag = $node->{ETag};
731              
732 0 0       0 if ( defined $etag ) {
733 0         0 $etag =~ s{(^"|"$)}{}gxsm;
734             }
735              
736             push @keys,
737             {
738             key => $node->{Key},
739             last_modified => $node->{LastModified},
740             etag => $etag,
741             size => $node->{Size},
742             storage_class => $node->{StorageClass},
743             owner_id => $node->{Owner}{ID},
744             owner_displayname => $node->{Owner}{DisplayName},
745 0         0 };
746             }
747              
748 0         0 $bucket_list->{keys} = \@keys;
749              
750 0 0       0 if ( $conf->{delimiter} ) {
751 0         0 my @common_prefixes;
752 0         0 my $strip_delim = qr/$conf->{delimiter}$/xsm;
753              
754 0         0 foreach my $node ( $r->{CommonPrefixes} ) {
755 0 0       0 if ( ref $node ne 'ARRAY' ) {
756 0         0 $node = [$node];
757             }
758              
759 0         0 foreach my $n ( @{$node} ) {
  0         0  
760 0 0       0 next if !exists $n->{Prefix};
761 0         0 my $prefix = $n->{Prefix};
762              
763             # strip delimiter from end of prefix
764 0 0       0 if ($prefix) {
765 0         0 $prefix =~ s/$strip_delim//xsm;
766             }
767              
768 0         0 push @common_prefixes, $prefix;
769             }
770             }
771              
772 0         0 $bucket_list->{common_prefixes} = \@common_prefixes;
773             }
774              
775 0         0 $self->get_logger->trace( Dumper( [ bucket_list => $bucket_list ] ) );
776              
777 0         0 return $bucket_list;
778             }
779             ########################################################################
780             sub list_bucket_all_v2 {
781             ########################################################################
782 0     0 1 0 my ( $self, $conf ) = @_;
783 0   0     0 $conf ||= {};
784              
785 0         0 $conf->{'list-type'} = '2';
786              
787 0         0 return $self->list_bucket_all($conf);
788             }
789              
790             ########################################################################
791             sub list_bucket_all {
792             ########################################################################
793 0     0 1 0 my ( $self, $conf ) = @_;
794 0   0     0 $conf ||= {};
795              
796 0         0 my $bucket = $conf->{bucket};
797              
798 0 0       0 croak 'must specify bucket'
799             if !$bucket;
800              
801 0         0 my $response = $self->list_bucket($conf);
802              
803 0 0       0 croak $EVAL_ERROR
804             if !$response;
805              
806             return $response
807 0 0       0 if !$response->{is_truncated};
808              
809 0         0 my $all = $response;
810              
811 0         0 while ($TRUE) {
812             my $next_marker = $response->{next_marker}
813 0   0     0 || $response->{keys}->[-1]->{key};
814              
815 0         0 $conf->{marker} = $next_marker;
816 0         0 $conf->{bucket} = $bucket;
817              
818 0         0 $response = $self->list_bucket($conf);
819              
820 0 0       0 croak $EVAL_ERROR
821             if !$response;
822              
823 0         0 push @{ $all->{keys} }, @{ $response->{keys} };
  0         0  
  0         0  
824              
825 0 0       0 last if !$response->{is_truncated};
826             }
827              
828 0         0 delete $all->{is_truncated};
829 0         0 delete $all->{next_marker};
830              
831 0         0 return $all;
832             }
833              
834             ########################################################################
835             # API: ListObjectVersions
836             #########################################################################
837             # Documentation:
838             # https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectVersions.html
839             #
840             # Request:
841             # GET /?versions
842             # HOST: Bucket.s3.amazonaws.com
843             # x-amz-expected-bucket-owner: ExpectedBucketOwner
844             # x-amz-request-payer: RequestPayer
845             # x-amz-optional-object-attributes: OptionalObjectAtttributes
846             #
847             # Parameters:
848             # delimiter => Delimiter
849             # encoding-type => EncodingType
850             # key-marker => KeyMarker
851             # max-keys => MaxKeys
852             # prefix => Prefix
853             # version-id-marker => VersionIdMarker
854             #
855             # Response
856              
857             ########################################################################
858             sub list_object_versions {
859             ########################################################################
860 0     0 1 0 my ( $self, $conf ) = @_;
861              
862 0         0 my $bucket = delete $conf->{bucket};
863              
864 0 0       0 die 'no bucket'
865             if !$bucket;
866              
867 0         0 my $headers = delete $conf->{headers};
868              
869 0 0       0 croak 'must specify bucket'
870             if !$bucket;
871              
872 0   0     0 $conf ||= {};
873              
874             my ( $marker, $next_marker, $query_next )
875 0         0 = @{ $LIST_OBJECT_MARKERS{'3'} };
  0         0  
876              
877 0 0       0 if ( $conf->{'key-marker'} ) {
878 0         0 $conf->{$query_next} = delete $conf->{'key-marker'};
879             }
880              
881 0 0       0 if ( %{$conf} ) {
  0         0  
882              
883             # remove undefined elements
884 0         0 foreach ( keys %{$conf} ) {
  0         0  
885 0 0       0 next if defined $conf->{$_};
886              
887 0         0 delete $conf->{$_};
888             }
889             }
890              
891             my $path
892 0         0 = create_api_uri( path => "$bucket/", api => 'versions', %{$conf} );
  0         0  
893              
894 0   0     0 my $r = $self->_send_request(
895             { method => 'GET',
896             path => $path,
897             headers => $headers // {},
898             region => $self->region,
899             },
900             );
901              
902             return
903 0 0 0     0 if !$r || $self->errstr;
904              
905             $self->get_logger->debug(
906             sub {
907 0     0   0 return Dumper(
908             [ marker => $marker,
909             next_marker => $next_marker,
910             response => $r,
911             ],
912             );
913             },
914 0         0 );
915              
916 0         0 return $r;
917             }
918              
919             ########################################################################
920             sub get_credentials {
921             ########################################################################
922 0     0 0 0 my ($self) = @_;
923              
924 0         0 my $aws_access_key_id;
925             my $aws_secret_access_key;
926 0         0 my $token;
927              
928 0 0       0 if ( $self->credentials ) {
929 0         0 $aws_access_key_id = $self->credentials->get_aws_access_key_id;
930 0         0 $aws_secret_access_key = $self->credentials->get_aws_secret_access_key;
931 0         0 $token = $self->credentials->get_token;
932             }
933             else {
934 0         0 $aws_access_key_id = $self->aws_access_key_id;
935 0         0 $aws_secret_access_key = $self->aws_secret_access_key;
936 0         0 $token = $self->token;
937             }
938              
939 0         0 return ( $aws_access_key_id, $aws_secret_access_key, $token );
940             }
941              
942             # Log::Log4perl compatibility routines
943             ########################################################################
944             sub get_logger {
945             ########################################################################
946 106     106 1 525 my ($self) = @_;
947              
948 106         1842 return $self->logger;
949             }
950              
951             ########################################################################
952             sub level {
953             ########################################################################
954 17     17 1 16713 my ( $self, @args ) = @_;
955              
956 17 100       40 if (@args) {
957 6         229 $self->log_level( $args[0] );
958              
959 6         47 $self->get_logger->level( uc $args[0] );
960             }
961              
962 17         32 return $self->get_logger->level;
963             }
964              
965             ########################################################################
966             sub signer {
967             ########################################################################
968 0     0 1   my ($self) = @_;
969              
970 0 0         return $self->_signer
971             if $self->_signer;
972              
973 0 0         my $creds = $self->credentials ? $self->credentials : $self;
974 0           my $express = $self->express;
975              
976 0 0 0       my $signer = Amazon::S3::Signature::V4->new(
977             { access_key_id => $creds->get_aws_access_key_id,
978             secret => $creds->get_aws_secret_access_key,
979             region => $self->region || $self->get_default_region,
980             service => $express ? 's3express' : 's3',
981             security_token => $creds->get_token,
982             },
983             );
984              
985 0 0         if ( $self->cache_signer ) {
986 0           $self->_signer($signer);
987             }
988              
989 0           return $signer;
990             }
991              
992             ########################################################################
993             sub _validate_acl_short {
994             ########################################################################
995 0     0     my ( $self, $policy_name ) = @_;
996              
997             croak sprintf '%s is not a supported canned access policy', $policy_name
998 0     0     if none { $policy_name eq $_ }
999 0 0         qw(private public-read public-read-write authenticated-read);
1000              
1001 0           return;
1002             }
1003              
1004             ########################################################################
1005             # Determine if a bucket can used as subdomain for the host
1006             # Specifying the bucket in the URL path is being deprecated
1007             # So, if the bucket name is suitable, we need to use it
1008             # as a subdomain in the host name instead.
1009             #
1010             # Currently buckets with periods in their names cannot be handled in
1011             # that manner due to SSL certificate issues, they will have to remain
1012             # in the url path instead.
1013             #
1014             ########################################################################
1015 0     0 0   sub is_domain_bucket { goto &_can_bucket_be_subdomain; }
1016             ########################################################################
1017              
1018             ########################################################################
1019             sub _can_bucket_be_subdomain {
1020             ########################################################################
1021 0     0     my ($bucketname) = @_;
1022              
1023 0 0         return $FALSE
1024             if length $bucketname > $MAX_BUCKET_NAME_LENGTH - 1;
1025              
1026 0 0         return $FALSE
1027             if length $bucketname < $MIN_BUCKET_NAME_LENGTH;
1028              
1029 0 0         return $FALSE
1030             if $bucketname !~ m{\A[[:lower:]][[:lower:]\d-]*\z}xsm;
1031              
1032 0 0         return $FALSE
1033             if $bucketname !~ m{[[:lower:]\d]\z}xsm;
1034              
1035 0           return $TRUE;
1036             }
1037              
1038             ########################################################################
1039             sub _make_request {
1040             ########################################################################
1041 0     0     my ( $self, @args ) = @_;
1042              
1043 0           my $parameters = get_parameters(@args);
1044              
1045             my ( $method, $path, $headers, $data, $metadata, $region )
1046 0           = @{$parameters}{qw(method path headers data metadata region)};
  0            
1047              
1048             # reset region on every call...every bucket can have it's own region
1049 0   0       $self->region( $region // $self->_region );
1050              
1051 0 0         croak 'must specify method'
1052             if !$method;
1053              
1054 0 0         croak 'must specify path'
1055             if !defined $path;
1056              
1057 0   0       $headers //= {};
1058              
1059 0   0       $metadata //= {};
1060              
1061 0   0       $data //= $EMPTY;
1062 0   0       $headers->{'Content-Length'} //= length $data;
1063              
1064 0           my $http_headers = $self->_merge_meta( $headers, $metadata );
1065              
1066 0 0         my $protocol = $self->secure ? 'https' : 'http';
1067              
1068 0           my $host = $self->host;
1069              
1070 0           $path =~ s/\A\///xsm;
1071 0           my $url = sprintf '%s://%s/%s', $protocol, $host, $path;
1072              
1073             # if ( $path =~ m{\A([^/?]+)([^?]+)(.*)}xsm
1074 0 0 0       if ( $path =~ /\A([^\/?]+)([^?]+)(.*)/xsm
      0        
1075             && $self->dns_bucket_names
1076             && is_domain_bucket($1) ) {
1077              
1078 0           my $bucket = $1;
1079 0           $path = $2;
1080              
1081 0           my $query_string = $3;
1082              
1083             $self->logger->debug(
1084             sub {
1085 0     0     return Dumper(
1086             [ bucket => $bucket,
1087             path => $path,
1088             query_string => $query_string,
1089             ]
1090             );
1091             }
1092 0           );
1093              
1094 0 0         if ( $host =~ /([^:]+):([^:]\d+)$/xsm ) {
1095              
1096 0           my $port;
1097              
1098 0           $url = eval {
1099 0           $port = $2;
1100 0           $host = $1;
1101              
1102 0           my $uri = URI->new;
1103              
1104 0           $uri->scheme('http');
1105 0           $uri->host("$bucket.$host");
1106 0           $uri->port($port);
1107 0           $uri->path($path);
1108              
1109 0           return $uri . $query_string;
1110             };
1111              
1112 0 0 0       die sprintf
1113             "error creating uri for bucket: [%s], host: [%s], path: [%s], port: [%s]\n%s",
1114             $bucket, $host, $path, $port, $EVAL_ERROR
1115              
1116             if !$url || $EVAL_ERROR;
1117             }
1118             else {
1119 0           $url = sprintf '%s://%s.%s%s%s', $protocol, $bucket, $host, $path,
1120             $query_string;
1121             }
1122             }
1123              
1124 0           my $request = HTTP::Request->new( $method, $url, $http_headers );
1125              
1126 0           $self->last_request($request);
1127              
1128 0 0         if ($data) {
1129 0           $request->content($data);
1130             }
1131              
1132 0           $self->signer->region($region); # always set regional endpoint for signing
1133              
1134 0           $self->signer->sign($request);
1135              
1136 0           return $request;
1137             }
1138              
1139             # $self->_send_request($HTTP::Request)
1140             # $self->_send_request(@params_to_make_request)
1141             # $self->_send_request($params_to_make_request)
1142             ########################################################################
1143             sub _send_request {
1144             ########################################################################
1145 0     0     my ( $self, @args ) = @_;
1146              
1147 0           my $logger = $self->get_logger;
1148              
1149             $logger->trace(
1150             sub {
1151 0     0     return Dumper( [ args => \@args ] );
1152             },
1153 0           );
1154              
1155 0           my $keep_root = $FALSE;
1156              
1157 0           my $request = eval {
1158 0 0         return $args[0]
1159             if ref( $args[0] ) =~ /HTTP::Request/xsm;
1160              
1161 0 0 0       return {@args}
1162             if @args > 1 && !@args % 2;
1163              
1164 0 0         return $args[0]
1165             if ref $args[0];
1166              
1167 0           croak 'invalid argument to _send_request';
1168             };
1169              
1170 0 0         if ( ref($request) !~ /HTTP::Request/xsm ) {
1171 0           $keep_root = delete $request->{keep_root};
1172              
1173 0           $request = $self->_make_request($request);
1174             }
1175              
1176 0           my $response = $self->_do_http($request);
1177              
1178 0           $self->last_response($response);
1179              
1180             $logger->debug(
1181             sub {
1182 0     0     return Dumper( [ response => $response ] );
1183             }
1184 0           );
1185              
1186 0           return $self->_decode_response( $response, $keep_root );
1187             }
1188              
1189             ########################################################################
1190             sub _decode_response {
1191             ########################################################################
1192 0     0     my ( $self, $response, $keep_root ) = @_;
1193              
1194 0           my $content;
1195              
1196 0 0         if ( $response->code !~ /\A2\d{2}\z/xsm ) {
    0          
1197 0           $self->_remember_errors( $response->content, 1 );
1198 0           $content = undef;
1199             }
1200             elsif ( is_xml_response($response) ) {
1201 0           $content = $self->_xpc_of_content( $response->content, $keep_root );
1202             }
1203              
1204 0           return $content;
1205             }
1206              
1207             ########################################################################
1208             sub is_xml_response {
1209             ########################################################################
1210 0     0 0   my ($rsp) = @_;
1211              
1212 0 0         return $FALSE
1213             if !$rsp->content;
1214              
1215 0 0         return $TRUE
1216             if $rsp->content_type eq 'application/xml';
1217              
1218 0 0         return $TRUE
1219             if $rsp->content =~ /\A\s*<[?]xml/xsm;
1220              
1221 0           return $FALSE;
1222             }
1223              
1224             #
1225             # This is the necessary to find the region for a specific bucket
1226             # and set the signer object to use that region when signing requests
1227             ########################################################################
1228             sub adjust_region {
1229             ########################################################################
1230 0     0 0   my ( $self, $bucket, $called_from_redirect ) = @_;
1231              
1232 0           my $url = sprintf 'https://%s.%s', $bucket, $self->host;
1233              
1234 0           my $request = HTTP::Request->new( GET => $url );
1235              
1236 0           $self->{'signer'}->sign($request);
1237              
1238             # We have to turn off our special retry since this will deliberately
1239             # trigger that code
1240 0           $self->turn_off_special_retry();
1241              
1242             # If the bucket name has a period in it, the certificate validation
1243             # will fail since it will expect a certificate for a subdomain.
1244             # Setting it to verify against the expected host guards against
1245             # that while still being secure since we will have verified
1246             # the response as coming from the expected server.
1247 0           $self->ua->ssl_opts( SSL_verifycn_name => $self->host );
1248              
1249 0           my $response = $self->_do_http($request);
1250              
1251             # Turn this off, since all other requests have the bucket after
1252             # the host in the URL, and the host may change depending on the region
1253 0           $self->ua->ssl_opts( SSL_verifycn_name => undef );
1254              
1255 0           $self->turn_on_special_retry();
1256              
1257             # If No error, then nothing to do
1258 0 0         return $TRUE
1259             if $response->is_success();
1260              
1261             # If the error is due to the wrong region, then we will get
1262             # back a block of XML with the details
1263 0 0         return $FALSE
1264             if !is_xml_response($response);
1265              
1266 0           my $error_hash = $self->_xpc_of_content( $response->content );
1267              
1268             my ( $endpoint, $code, $region, $message )
1269 0           = @{$error_hash}{qw(Endpoint Code Region Message)};
  0            
1270              
1271 0           my $condition = eval {
1272 0 0 0       return 'PermanentRedirect'
1273             if $code eq 'PermanentRedirect' && $endpoint;
1274              
1275 0 0 0       return 'AuthorizationHeaderMalformed'
1276             if $code eq 'AuthorizationHeaderMalformed' && $region;
1277              
1278 0 0         return 'IllegalLocationConstraintException'
1279             if $code eq 'IllegalLocationConstraintException';
1280              
1281 0           return 'Other';
1282             };
1283              
1284             my %error_handlers = (
1285             PermanentRedirect => sub {
1286             # Don't recurse through multiple redirects
1287 0 0   0     return $FALSE
1288             if $called_from_redirect;
1289              
1290             # With a permanent redirect error, they are telling us the explicit
1291             # host to use. The endpoint will be in the form of bucket.host
1292 0           my $host = $endpoint;
1293              
1294             # Remove the bucket name from the front of the host name
1295             # All the requests will need to be of the form https://host/bucket
1296 0           $host =~ s/\A$bucket[.]//xsm;
1297 0           $self->host($host);
1298              
1299             # We will need to call ourselves again in order to trigger the
1300             # AuthorizationHeaderMalformed error in order to get the region
1301 0           return $self->adjust_region( $bucket, $TRUE );
1302             },
1303             AuthorizationHeaderMalformed => sub {
1304             # Set the signer to use the correct reader evermore
1305 0     0     $self->{signer}->{endpoint} = $region;
1306              
1307             # Only change the host if we haven't been called as a redirect
1308             # where an exact host has been given
1309 0 0         if ( !$called_from_redirect ) {
1310 0           $self->host( sprintf 's3-%s-amazonaws.com', $region );
1311             }
1312              
1313 0           return $TRUE;
1314             },
1315             IllegalLocationConstraintException => sub {
1316             # This is hackish; but in this case the region name only appears in the message
1317 0 0   0     if ( $message =~ /The (\S+) location/xsm ) {
1318 0           my $new_region = $1;
1319              
1320             # Correct the region for the signer
1321 0           $self->{signer}->{endpoint} = $new_region;
1322              
1323             # Set the proper host for the region
1324 0           $self->host( sprintf 's3.%s.amazonaws.com', $new_region );
1325              
1326 0           return $TRUE;
1327             }
1328             },
1329             'Other' => sub {
1330             # Some other error
1331 0     0     $self->_remember_errors( $response->content, 1 );
1332 0           return $FALSE;
1333             },
1334 0           );
1335              
1336 0           return $error_handlers{$condition}->();
1337             }
1338              
1339             ########################################################################
1340             sub reset_errors {
1341             ########################################################################
1342 0     0 0   my ($self) = @_;
1343              
1344 0           $self->err(undef);
1345 0           $self->errstr(undef);
1346 0           $self->error(undef);
1347              
1348 0           return $self;
1349             }
1350              
1351             ########################################################################
1352             sub _do_http {
1353             ########################################################################
1354 0     0     my ( $self, $request, $filename ) = @_;
1355              
1356             # convenient time to reset any error conditions
1357 0           $self->reset_errors;
1358              
1359 0           my $response = $self->ua->request( $request, $filename );
1360              
1361             # For new buckets at non-standard locations, amazon will sometimes
1362             # respond with a temporary redirect. In this case it is necessary
1363             # to try again with the new URL
1364 0           my $location = $response->header('Location');
1365              
1366 0 0 0       if ( $response->code =~ /\A3/xsm and defined $location ) {
1367              
1368             $self->get_logger->debug(
1369             sub {
1370 0     0     return { sprintf 'Redirecting to: %s', $location };
1371             }
1372 0           );
1373              
1374 0           $request->uri($location);
1375 0           $response = $self->ua->request( $request, $filename );
1376             }
1377              
1378 0     0     $self->get_logger->debug( sub { return Dumper( [$response] ) } );
  0            
1379              
1380 0           $self->last_response($response);
1381              
1382 0           return $response;
1383             }
1384              
1385             # Call this if handling any temporary redirect issues
1386             # (Like needing to probe with a HEAD request when file handle are involved)
1387              
1388             ########################################################################
1389             sub _do_http_no_redirect {
1390             ########################################################################
1391 0     0     my ( $self, $request, $filename ) = @_;
1392              
1393             # convenient time to reset any error conditions
1394 0           $self->reset_errors;
1395              
1396 0           my $response = $self->ua->request( $request, $filename );
1397              
1398 0     0     $self->get_logger->debug( sub { return Dumper( [$response] ) } );
  0            
1399              
1400 0           $self->last_response($response);
1401              
1402 0           return $response;
1403             }
1404              
1405             ########################################################################
1406             sub _send_request_expect_nothing {
1407             ########################################################################
1408 0     0     my ( $self, @args ) = @_;
1409              
1410 0           my $request = $self->_make_request(@args);
1411              
1412 0           my $response = $self->_do_http($request);
1413              
1414 0           my $content = $response->content;
1415              
1416 0 0         return $TRUE
1417             if $response->code =~ /^2\d\d$/xsm;
1418              
1419             # anything else is a failure, and we save the parsed result
1420 0           $self->_remember_errors( $response->content, $TRUE );
1421              
1422 0           return $FALSE;
1423             }
1424              
1425             # Send a HEAD request first, to find out if we'll be hit with a 307 redirect.
1426             # Since currently LWP does not have true support for 100 Continue, it simply
1427             # slams the PUT body into the socket without waiting for any possible redirect.
1428             # Thus when we're reading from a filehandle, when LWP goes to reissue the request
1429             # having followed the redirect, the filehandle's already been closed from the
1430             # first time we used it. Thus, we need to probe first to find out what's going on,
1431             # before we start sending any actual data.
1432             ########################################################################
1433             sub _send_request_expect_nothing_probed {
1434             ########################################################################
1435 0     0     my ( $self, @args ) = @_;
1436              
1437 0           my $parameters = get_parameters(@args);
1438              
1439             my ( $method, $path, $conf, $value, $region )
1440 0           = @{$parameters}{qw(method path headers data region)};
  0            
1441              
1442 0   0       $region = $region // $self->region;
1443              
1444 0           my $request = $self->_make_request(
1445             { method => 'HEAD',
1446             path => $path,
1447             region => $region,
1448             },
1449             );
1450              
1451 0           my $override_uri;
1452              
1453 0           my $old_redirectable = $self->ua->requests_redirectable;
1454 0           $self->ua->requests_redirectable( [] );
1455              
1456 0           my $response = $self->_do_http_no_redirect($request);
1457              
1458 0 0         if ( $response->code =~ /^3/xsm ) {
1459 0 0         if ( defined $response->header('Location') ) {
1460 0           $override_uri = $response->header('Location');
1461             }
1462             else {
1463 0           $self->_croak_if_response_error($response);
1464             }
1465              
1466             $self->get_logger->debug(
1467             sub {
1468 0     0     return sprintf 'setting override URI: [%s]', $override_uri;
1469             }
1470 0           );
1471             }
1472              
1473 0           $request = $self->_make_request(
1474             { method => $method,
1475             path => $path,
1476             headers => $conf,
1477             data => $value,
1478             region => $region,
1479             },
1480             );
1481              
1482 0 0         if ( defined $override_uri ) {
1483 0           $request->uri($override_uri);
1484             }
1485              
1486 0           $response = $self->_do_http_no_redirect($request);
1487              
1488 0           $self->ua->requests_redirectable($old_redirectable);
1489              
1490 0           my $content = $response->content;
1491              
1492 0 0         return $TRUE
1493             if $response->code =~ /^2\d\d$/xsm;
1494              
1495             # anything else is a failure, and we save the parsed result
1496 0           $self->_remember_errors( $response->content, $TRUE );
1497              
1498 0           return $FALSE;
1499             }
1500              
1501             ########################################################################
1502             sub _croak_if_response_error {
1503             ########################################################################
1504 0     0     my ( $self, $response ) = @_;
1505              
1506 0 0         if ( $response->code !~ /^2\d{2}$/xsm ) {
1507 0           $self->err('network_error');
1508              
1509 0           $self->errstr( $response->status_line );
1510              
1511 0           croak $response->status_line;
1512             }
1513              
1514 0           return;
1515             }
1516              
1517             ########################################################################
1518             sub _xpc_of_content {
1519             ########################################################################
1520 0     0     my ( $self, $src, $keep_root ) = @_;
1521              
1522 0           my $xml_hr = eval {
1523 0           XMLin(
1524             $src,
1525             SuppressEmpty => $EMPTY,
1526             ForceArray => ['Contents'],
1527             KeepRoot => $keep_root,
1528             NoAttr => $TRUE,
1529             );
1530             };
1531              
1532 0 0 0       if ( !$xml_hr && $EVAL_ERROR ) {
1533 0           confess "Error parsing $src: $EVAL_ERROR";
1534             }
1535              
1536 0           return $xml_hr;
1537             }
1538              
1539             # returns 1 if errors were found
1540             ########################################################################
1541             sub _remember_errors {
1542             ########################################################################
1543 0     0     my ( $self, $src, $keep_root ) = @_;
1544              
1545             return
1546 0 0         if !$src;
1547              
1548 0 0 0       if ( !ref $src && $src !~ /^[[:space:]]*
1549 0           ( my $code = $src ) =~ s/^[[:space:]]*[(][\d]*[)].*$/$1/xsm;
1550              
1551 0           $self->err($code);
1552 0           $self->errstr($src);
1553              
1554 0           return $TRUE;
1555             }
1556              
1557 0 0         my $r = ref $src ? $src : $self->_xpc_of_content( $src, $keep_root );
1558              
1559 0           $self->error($r);
1560              
1561             # apparently buckets() does not keep_root
1562 0 0         if ( $r->{Error} ) {
1563 0           $r = $r->{Error};
1564             }
1565              
1566 0           my ( $code, $message ) = @{$r}{qw(Code Message)};
  0            
1567              
1568 0 0         return $FALSE
1569             if !$code;
1570              
1571 0           $self->err($code);
1572 0           $self->errstr($message);
1573              
1574 0           return $TRUE;
1575             }
1576              
1577             # Deprecated - this adds a header for the old V2 auth signatures
1578             ########################################################################
1579             sub _add_auth_header { ## no critic (ProhibitUnusedPrivateSubroutines)
1580             ########################################################################
1581 0     0     my ( $self, $headers, $method, $path ) = @_;
1582              
1583 0           my ( $aws_access_key_id, $aws_secret_access_key, $token )
1584             = $self->get_credentials;
1585              
1586 0 0         if ( not $headers->header('Date') ) {
1587 0           $headers->header( Date => time2str(time) );
1588             }
1589              
1590 0 0         if ($token) {
1591 0           $headers->header( $AMAZON_HEADER_PREFIX . 'security-token' => $token );
1592             }
1593              
1594 0           my $canonical_string = $self->_canonical_string( $method, $path, $headers );
1595              
1596             $self->get_logger->trace(
1597             sub {
1598 0     0     return Dumper(
1599             [ headers => $headers,
1600             canonincal_sring => $canonical_string,
1601             ]
1602             );
1603             }
1604 0           );
1605              
1606 0           my $encoded_canonical
1607             = $self->_encode( $aws_secret_access_key, $canonical_string );
1608              
1609 0           $headers->header(
1610             Authorization => sprintf 'AWS %s:%s',
1611             $aws_access_key_id, $encoded_canonical
1612             );
1613              
1614 0           return;
1615             }
1616              
1617             # generates an HTTP::Headers objects given one hash that represents http
1618             # headers to set and another hash that represents an object's metadata.
1619             ########################################################################
1620             sub _merge_meta {
1621             ########################################################################
1622 0     0     my ( $self, $headers, $metadata ) = @_;
1623              
1624 0   0       $headers //= {};
1625 0   0       $metadata //= {};
1626              
1627 0           my $http_header = HTTP::Headers->new;
1628              
1629 0           foreach my $p ( pairs %{$headers} ) {
  0            
1630 0           my ( $k, $v ) = @{$p};
  0            
1631 0           $http_header->header( $k => $v );
1632             }
1633              
1634 0           foreach my $p ( pairs %{$metadata} ) {
  0            
1635 0           my ( $k, $v ) = @{$p};
  0            
1636 0           $http_header->header( "$METADATA_PREFIX$k" => $v );
1637             }
1638              
1639 0           return $http_header;
1640             }
1641              
1642             # generate a canonical string for the given parameters. expires is optional and is
1643             # only used by query string authentication.
1644             ########################################################################
1645             sub _canonical_string {
1646             ########################################################################
1647 0     0     my ( $self, $method, $path, $headers, $expires ) = @_;
1648              
1649             # initial / meant to force host/bucket-name instead of DNS based name
1650 0           $path =~ s/^\///xsm;
1651              
1652 0           my %interesting_headers = ();
1653              
1654 0           foreach my $p ( pairs %{$headers} ) {
  0            
1655 0           my ( $key, $value ) = @{$p};
  0            
1656 0           my $lk = lc $key;
1657              
1658 0 0 0       if ( $lk eq 'content-md5'
      0        
      0        
1659             or $lk eq 'content-type'
1660             or $lk eq 'date'
1661             or $lk =~ /^$AMAZON_HEADER_PREFIX/xsm ) {
1662 0           $interesting_headers{$lk} = $self->_trim($value);
1663             }
1664             }
1665              
1666             # these keys get empty strings if they don't exist
1667 0   0       $interesting_headers{'content-type'} ||= $EMPTY;
1668 0   0       $interesting_headers{'content-md5'} ||= $EMPTY;
1669              
1670             # just in case someone used this. it's not necessary in this lib.
1671 0 0         if ( $interesting_headers{'x-amz-date'} ) {
1672 0           $interesting_headers{'date'} = $EMPTY;
1673             }
1674              
1675             # if you're using expires for query string auth, then it trumps date
1676             # (and x-amz-date)
1677 0 0         if ($expires) {
1678 0           $interesting_headers{'date'} = $expires;
1679             }
1680              
1681 0           my $buf = "$method\n";
1682              
1683 0           foreach my $key ( sort keys %interesting_headers ) {
1684 0 0         if ( $key =~ /^$AMAZON_HEADER_PREFIX/xsm ) {
1685 0           $buf .= "$key:$interesting_headers{$key}\n";
1686             }
1687             else {
1688 0           $buf .= "$interesting_headers{$key}\n";
1689             }
1690             }
1691              
1692             # don't include anything after the first ? in the resource...
1693             # $path =~ /^([^?]*)/xsm;
1694             # $buf .= "/$1";
1695 0           $path =~ /\A([^?]*)/xsm;
1696 0           $buf .= "/$1";
1697              
1698             # ...unless there any parameters we're interested in...
1699 0 0         if ( $path =~ /[&?](acl|torrent|location|uploads|delete)([=&]|$)/xsm ) {
    0          
1700             # if ( $path =~ /[&?](acl|torrent|location|uploads|delete)([=&])?/xsm ) {
1701 0           $buf .= "?$1";
1702             }
1703             elsif ( my %query_params = URI->new($path)->query_form ) {
1704             # see if the remaining parsed query string provides us with any
1705             # query string or upload id
1706              
1707 0 0 0       if ( $query_params{partNumber} && $query_params{uploadId} ) {
    0          
1708             # re-evaluate query string, the order of the params is important
1709             # for request signing, so we can't depend on URI to do the right
1710             # thing
1711             $buf .= sprintf '?partNumber=%s&uploadId=%s',
1712             $query_params{partNumber},
1713 0           $query_params{uploadId};
1714             }
1715             elsif ( $query_params{uploadId} ) {
1716 0           $buf .= sprintf '?uploadId=%s', $query_params{uploadId};
1717             }
1718             }
1719              
1720 0           return $buf;
1721             }
1722              
1723             ########################################################################
1724             sub _trim {
1725             ########################################################################
1726 0     0     my ( $self, $value ) = @_;
1727              
1728 0           $value =~ s/^\s+//xsm;
1729 0           $value =~ s/\s+$//xsm;
1730              
1731 0           return $value;
1732             }
1733              
1734             # finds the hmac-sha1 hash of the canonical string and the aws secret access key and then
1735             # base64 encodes the result (optionally urlencoding after that).
1736             ########################################################################
1737             sub _encode {
1738             ########################################################################
1739 0     0     my ( $self, $aws_secret_access_key, $str, $urlencode ) = @_;
1740              
1741 0           my $hmac = Digest::HMAC_SHA1->new($aws_secret_access_key);
1742 0           $hmac->add($str);
1743              
1744 0           my $b64 = encode_base64( $hmac->digest, $EMPTY );
1745              
1746 0 0         return $urlencode ? urlencode($b64) : return $b64;
1747             }
1748              
1749             ########################################################################
1750             sub bucketv2 {
1751             ########################################################################
1752 0     0 0   my ( $self, @args ) = @_;
1753              
1754 0           my $parameters = get_parameters(@args);
1755              
1756             my ( $bucketname, $region, $verify_region )
1757 0           = @{$parameters}{qw(bucket region verify_region)};
  0            
1758              
1759             # only set to default region if a region wasn't passed or region
1760             # verification not requested
1761 0 0 0       if ( !$region && !$verify_region ) {
1762 0           $region = $self->region;
1763             }
1764              
1765 0           return Amazon::S3::BucketV2->new(
1766             { bucket => $bucketname,
1767             account => $self,
1768             region => $region,
1769             verify_region => $verify_region,
1770             },
1771             );
1772             }
1773              
1774             ########################################################################
1775             sub delete_public_access_block {
1776             ########################################################################
1777 0     0 1   my ( $self, $bucket ) = @_;
1778              
1779 0           my $bucketv2 = bless $bucket, 'Amazon::S3::BucketV2';
1780              
1781 0           return $bucketv2->DeletePublicAccessBlock;
1782             }
1783              
1784             1;
1785              
1786             __END__