File Coverage

lib/Amazon/S3.pm
Criterion Covered Total %
statement 122 587 20.7
branch 20 218 9.1
condition 11 184 5.9
subroutine 26 70 37.1
pod 16 24 66.6
total 195 1083 18.0


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