File Coverage

blib/lib/Net/Amazon/S3/Signature/V4Implementation.pm
Criterion Covered Total %
statement 137 150 91.3
branch 21 38 55.2
condition 9 19 47.3
subroutine 26 27 96.3
pod 2 3 66.6
total 195 237 82.2


line stmt bran cond sub pod time code
1             # ABSTRACT: Implements the Amazon Web Services signature version 4, AWS4-HMAC-SHA256 (copy of Net::Amazon::Signature::V4)
2             $Net::Amazon::S3::Signature::V4Implementation::VERSION = '0.991';
3              
4             use strict;
5 99     99   691 use warnings;
  99         226  
  99         2712  
6 99     99   512 use sort 'stable';
  99         224  
  99         2399  
7 99     99   42012  
  99         50271  
  99         563  
8             use Digest::SHA qw/sha256_hex hmac_sha256 hmac_sha256_hex/;
9 99     99   4526 use Time::Piece ();
  99         229  
  99         6092  
10 99     99   43281 use URI::Escape;
  99         590649  
  99         2410  
11 99     99   732 use URI;
  99         234  
  99         5110  
12 99     99   646 use URI::QueryParam;
  99         231  
  99         1694  
13 99     99   478  
  99         202  
  99         201108  
14             our $ALGORITHM = 'AWS4-HMAC-SHA256';
15             our $MAX_EXPIRES = 604800; # Max, 7 days
16              
17             our $X_AMZ_ALGORITHM = 'X-Amz-Algorithm';
18             our $X_AMZ_CONTENT_SHA256 = 'X-Amz-Content-Sha256';
19             our $X_AMZ_CREDENTIAL = 'X-Amz-Credential';
20             our $X_AMZ_DATE = 'X-Amz-Date';
21             our $X_AMZ_EXPIRES = 'X-Amz-Expires';
22             our $X_AMZ_SIGNEDHEADERS = 'X-Amz-SignedHeaders';
23             our $X_AMZ_SIGNATURE = 'X-Amz-Signature';
24              
25              
26              
27             my $class = shift;
28             my ( $access_key_id, $secret, $endpoint, $service ) = @_;
29 7     7 0 13 my $self = {
30 7         18 access_key_id => $access_key_id,
31 7         31 secret => $secret,
32             endpoint => $endpoint,
33             service => $service,
34             };
35             bless $self, $class;
36             return $self;
37 7         15 }
38 7         20  
39              
40             my ( $self, $request ) = @_;
41              
42             $request = $self->_augment_request( $request );
43 1     1 1 3  
44             my $authz = $self->_authorization( $request );
45 1         3 $request->header( Authorization => $authz );
46             return $request;
47 1         3 }
48 1         4  
49 1         46  
50             my ( $self, $uri, $expires_in, $for_method ) = @_;
51              
52             my $request = $self->_augment_uri( $uri, $expires_in, $for_method );
53              
54 6     6 1 124 my $signature = $self->_signature( $request );
55              
56 6         17 $uri = $request->uri;
57             my $query = $uri->query;
58 6         21 $uri->query( undef );
59             $uri = $uri . '?' . $self->_sort_query_string( $query );
60 6         28 $uri .= "&$X_AMZ_SIGNATURE=$signature";
61 6         46  
62 6         71 return $uri;
63 6         92 }
64 6         21  
65             # _headers_to_sign:
66 6         75 # Return the sorted lower case headers as required by the generation of canonical headers
67              
68             my $req = shift;
69              
70             my @headers_to_sign = $req->uri->query_param( $X_AMZ_SIGNEDHEADERS )
71             ? $req->uri->query_param( $X_AMZ_SIGNEDHEADERS )
72             : $req->headers->header_field_names
73 15     15   23 ;
74              
75 15 100       41 return sort { $a cmp $b } map { lc } @headers_to_sign
76             }
77              
78             # _augment_request:
79             # Append mandatory header fields
80 15         2961  
  33         47  
  33         75  
81             my ( $self, $request ) = @_;
82              
83             $request->header($X_AMZ_DATE => $self->_format_amz_date( $self->_req_timepiece($request) ))
84             unless $request->header($X_AMZ_DATE);
85              
86             $request->header($X_AMZ_CONTENT_SHA256 => sha256_hex($request->content))
87 1     1   2 unless $request->header($X_AMZ_CONTENT_SHA256);
88              
89 1 50       5 return $request;
90             }
91              
92 1 50       89 # _augment_uri:
93             # Append mandatory uri parameters
94              
95 1         101 my ($self, $uri, $expires_in, $method) = @_;
96              
97             my $request = HTTP::Request->new( $method || GET => $uri );
98              
99             $request->uri->query_param( $X_AMZ_DATE => $self->_format_amz_date( $self->_now ) )
100             unless $request->uri->query_param( $X_AMZ_DATE );
101              
102 6     6   15 $request->uri->query_param( $X_AMZ_ALGORITHM => $ALGORITHM )
103             unless $request->uri->query_param( $X_AMZ_ALGORITHM );
104 6   50     23  
105             $request->uri->query_param( $X_AMZ_CREDENTIAL => $self->_credential( $request ) )
106 6 50       344 unless $request->uri->query_param( $X_AMZ_CREDENTIAL );
107              
108             $request->uri->query_param( $X_AMZ_EXPIRES => $expires_in || $MAX_EXPIRES )
109 6 50       896 unless $request->uri->query_param( $X_AMZ_EXPIRES );
110             $request->uri->query_param( $X_AMZ_EXPIRES => $MAX_EXPIRES )
111             if $request->uri->query_param( $X_AMZ_EXPIRES ) > $MAX_EXPIRES;
112 6 50       1046  
113             $request->uri->query_param( $X_AMZ_SIGNEDHEADERS => 'host' );
114              
115 6 50 33     1148 return $request;
116             }
117 6 50       1989  
118             # _canonical_request:
119             # Construct the canonical request string from an HTTP::Request.
120 6         636  
121             my ( $self, $req ) = @_;
122 6         1451  
123             my $creq_method = $req->method;
124              
125             my ( $creq_canonical_uri, $creq_canonical_query_string ) =
126             ( $req->uri =~ m@([^?]*)\?(.*)$@ )
127             ? ( $1, $2 )
128             : ( $req->uri, '' );
129 7     7   28 $creq_canonical_uri =~ s@^https?://[^/]*/?@/@;
130              
131 7         27 # Documentation says "do not normalize URI paths for requests to Amazon S3"
132             # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
133 7 100       177  
134             #$creq_canonical_uri = _simplify_uri( $creq_canonical_uri );
135              
136             $creq_canonical_query_string = $self->_sort_query_string( $creq_canonical_query_string );
137 7         154  
138             # Ensure Host header is present as its required
139             if (!$req->header('host')) {
140             my $host = $req->uri->_port
141             ? $req->uri->host_port
142             : $req->uri->host
143             ;
144 7         133 $req->header('Host' => $host);
145             }
146             my $creq_payload_hash = $req->header($X_AMZ_CONTENT_SHA256)
147 7 50       24 # Signed uri doesn't have content
148 7 100       295 || 'UNSIGNED-PAYLOAD';
149              
150             # There's a bug in AMS4 which causes requests without x-amz-date set to be rejected
151             # so we always add one if its not present.
152 7         356 my $amz_date = $req->header($X_AMZ_DATE);
153             my @sorted_headers = _headers_to_sign( $req );
154 7   100     299 my $creq_canonical_headers = join '',
155             map {
156             sprintf "%s:%s\x0a",
157             lc,
158             join ',', sort {$a cmp $b } _trim_whitespace($req->header($_) )
159             }
160 7         311 @sorted_headers;
161 7         230 my $creq_signed_headers = $self->_signed_headers( $req );
162             my $creq = join "\x0a",
163             $creq_method, $creq_canonical_uri, $creq_canonical_query_string,
164 7         17 $creq_canonical_headers, $creq_signed_headers, $creq_payload_hash;
165              
166 13         38 return $creq;
  0         0  
167             }
168              
169 7         26 # _string_to_sign
170 7         39 # Construct the string to sign.
171              
172             my ( $self, $req ) = @_;
173             my $dt = $self->_req_timepiece( $req );
174 7         24 my $creq = $self->_canonical_request($req);
175             my $sts_request_date = $self->_format_amz_date( $dt );
176             my $sts_credential_scope = join '/', $dt->strftime('%Y%m%d'), $self->{endpoint}, $self->{service}, 'aws4_request';
177             my $sts_creq_hash = sha256_hex( $creq );
178              
179             my $sts = join "\x0a", $ALGORITHM, $sts_request_date, $sts_credential_scope, $sts_creq_hash;
180             return $sts;
181 7     7   20 }
182 7         12  
183 7         358 # _authorization
184 7         19 # Construct the authorization string
185 7         232  
186 7         203 my ( $self, $req ) = @_;
187              
188 7         21 my $dt = $self->_req_timepiece( $req );
189 7         20 my $sts = $self->_string_to_sign( $req );
190             my $k_date = hmac_sha256( $dt->strftime('%Y%m%d'), 'AWS4' . $self->{secret} );
191             my $k_region = hmac_sha256( $self->{endpoint}, $k_date );
192             my $k_service = hmac_sha256( $self->{service}, $k_region );
193             my $k_signing = hmac_sha256( 'aws4_request', $k_service );
194              
195             my $authz_signature = hmac_sha256_hex( $sts, $k_signing );
196 7     7   45 return $authz_signature;
197             }
198 7         27  
199 7         407 my ( $self, $req ) = @_;
200 7         29  
201 7         229 my $dt = $self->_req_timepiece( $req );
202 7         49  
203 7         46 my $authz_credential = join '/', $self->{access_key_id}, $dt->strftime('%Y%m%d'), $self->{endpoint}, $self->{service}, 'aws4_request';
204             return $authz_credential;
205 7         58 }
206 7         20  
207             my ( $self, $req ) = @_;
208              
209             my $authz_signed_headers = join ';', _headers_to_sign( $req );
210 7     7   410 return $authz_signed_headers;
211             }
212 7         21  
213             my ( $self, $req ) = @_;
214 7         415  
215 7         203 my $authz_signature = $self->_signature( $req );
216             my $authz_credential = $self->_credential( $req );
217             my $authz_signed_headers = $self->_signed_headers( $req );
218              
219 8     8   43 my $authz = "$ALGORITHM Credential=$authz_credential,SignedHeaders=$authz_signed_headers,Signature=$authz_signature";
220             return $authz;
221 8         20  
222 8         28 }
223              
224             my $orig_uri = shift;
225             my @parts = split /\//, $orig_uri;
226 1     1   3 my @simple_parts = ();
227             for my $part ( @parts ) {
228 1         4 if ( ! length $part || $part eq '.' ) {
229 1         3 } elsif ( $part eq '..' ) {
230 1         4 pop @simple_parts;
231             } else {
232 1         5 push @simple_parts, $part;
233 1         2 }
234             }
235             my $simple_uri = '/' . join '/', @simple_parts;
236             $simple_uri .= '/' if $orig_uri =~ m@/$@ && $simple_uri !~ m@/$@;
237             return $simple_uri;
238 0     0   0 }
239 0         0 my $self = shift;
240 0         0 return '' unless $_[0];
241 0         0 my @params;
242 0 0 0     0 for my $param ( split /&/, $_[0] ) {
    0          
243             my ( $key, $value ) =
244 0         0 map { tr/+/ /; uri_escape( uri_unescape( $_ ) ) } # escape all non-unreserved chars
245             split /=/, $param;
246 0         0 push @params, [$key, (defined $value ? $value : '')];
247             #push @params, [$key, $value];
248             }
249 0         0 return join '&',
250 0 0 0     0 map { join '=', grep defined, @$_ }
251 0         0 sort { ( $a->[0] cmp $b->[0] ) || ( $a->[1] cmp $b->[1] ) }
252             @params;
253             }
254 13     13   48 return map { my $str = $_; $str =~ s/^\s*//; $str =~ s/\s*$//; $str } @_;
255 13 100       33 }
256 12         16 my $date = shift;
257 12         42 if ( $date =~ m/^\d{8}T\d{6}Z$/ ) {
258             # assume basic ISO 8601, as demanded by AWS
259 60         122 return Time::Piece->strptime($date, '%Y%m%dT%H%M%SZ');
  120         870  
  120         238  
260             } else {
261 60 50       1102 # assume the format given in the AWS4 test suite
262             $date =~ s/^.{5}//; # remove weekday, as Amazon's test suite contains internally inconsistent dates
263             return Time::Piece->strptime($date, '%d %b %Y %H:%M:%S %Z');
264             }
265 60         195 }
266 12 50       46  
  108         193  
267             my ($self, $dt) = @_;
268              
269             $dt->strftime('%Y%m%dT%H%M%SZ');
270 13     13   470 }
  13         18  
  13         46  
  13         78  
  13         87  
271              
272             return scalar Time::Piece->gmtime;
273 21     21   31 }
274 21 50       81  
275             my ($self, $req) = @_;
276 21         70 my $x_date = $req->header($X_AMZ_DATE) || $req->uri->query_param($X_AMZ_DATE);
277             my $date = $x_date || $req->header('Date');
278             if (!$date) {
279 0         0 # No date set by the caller so set one up
280 0         0 my $piece = $self->_now;
281             $req->date($piece->epoch);
282             return $piece
283             }
284             return _str_to_timepiece($date);
285 14     14   383 }
286              
287 14         43 1;
288              
289              
290             =pod
291 7     7   294  
292             =encoding UTF-8
293              
294             =head1 NAME
295 22     22   101  
296 22   100     82 Net::Amazon::S3::Signature::V4Implementation - Implements the Amazon Web Services signature version 4, AWS4-HMAC-SHA256 (copy of Net::Amazon::Signature::V4)
297 22   66     2898  
298 22 100       80 =head1 VERSION
299              
300 1         3 version 0.991
301 1         83  
302 1         165 =head1 DESCRIPTION
303              
304 21         48 This package clones L<Net::Amazon::Signature::V4> 0.19 adding support for
305             signing URIs (GET request)
306              
307             Until https://github.com/Grinnz/Net-Amazon-Signature-V4/pull/5 will be merged
308             we have to maintain our clone.
309              
310             =head1 Net::Amazon::Signature::S4 AUTHORS
311              
312             Tim Nordenfur, C<< <tim at gurka.se> >>
313              
314             Maintained by Dan Book, C<< <dbook at cpan.org> >>
315              
316             =head2 sign( $request )
317              
318             Signs a request with your credentials by appending the Authorization header. $request should be an HTTP::Request. The signed request is returned.
319              
320             =head2 sign_uri( $uri, $expires_in?, $for_method? )
321              
322             Signs an uri with your credentials by appending the Authorization query parameters.
323              
324             C<< $expires_in >> integer value in range 1..604800 (1 second .. 7 days).
325              
326             C<< $expires_in >> default value is its maximum: 604800
327              
328             C<< $for_method >> HTTP method this uri should be signed for, default C<GET>
329              
330             The signed uri is returned.
331              
332             =head1 AUTHOR
333              
334             Branislav Zahradník <barney@cpan.org>
335              
336             =head1 COPYRIGHT AND LICENSE
337              
338             This software is copyright (c) 2022 by Amazon Digital Services, Leon Brocard, Brad Fitzpatrick, Pedro Figueiredo, Rusty Conover, Branislav Zahradník.
339              
340             This is free software; you can redistribute it and/or modify it under
341             the same terms as the Perl 5 programming language system itself.
342              
343             =cut