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