File Coverage

blib/lib/Net/Amazon/Signature/V4.pm
Criterion Covered Total %
statement 109 123 88.6
branch 25 34 73.5
condition 10 15 66.6
subroutine 16 17 94.1
pod 2 2 100.0
total 162 191 84.8


line stmt bran cond sub pod time code
1             package Net::Amazon::Signature::V4;
2              
3 2     2   139592 use strict;
  2         4  
  2         105  
4 2     2   10 use warnings;
  2         8  
  2         107  
5 2     2   926 use sort 'stable';
  2         913  
  2         9  
6              
7 2     2   1063 use Digest::SHA qw/sha256_hex hmac_sha256 hmac_sha256_hex/;
  2         7717  
  2         280  
8 2     2   1164 use Time::Piece ();
  2         29470  
  2         70  
9 2     2   1264 use URI::Escape;
  2         5653  
  2         4376  
10              
11             our $ALGORITHM = 'AWS4-HMAC-SHA256';
12              
13             =head1 NAME
14              
15             Net::Amazon::Signature::V4 - Implements the Amazon Web Services signature version 4, AWS4-HMAC-SHA256
16              
17             =head1 VERSION
18              
19             Version 0.22
20              
21             =cut
22              
23             our $VERSION = '0.22';
24              
25              
26             =head1 SYNOPSIS
27              
28             use Net::Amazon::Signature::V4;
29              
30             my $sig = Net::Amazon::Signature::V4->new( $access_key_id, $secret, $endpoint, $service );
31             my $req = HTTP::Request->parse( $request_string );
32             my $signed_req = $sig->sign( $req );
33             ...
34              
35             =head1 DESCRIPTION
36              
37             This module signs an HTTP::Request to Amazon Web Services by appending an Authorization header. Amazon Web Services signature version 4, AWS4-HMAC-SHA256, is used.
38              
39             The primary purpose of this module is to be used by Net::Amazon::Glacier.
40              
41             =head1 METHODS
42              
43             =head2 new
44              
45             my $sig = Net::Amazon::Signature::V4->new( $access_key_id, $secret, $endpoint, $service );
46             my $sig = Net::Amazon::Signature::V4->new({
47             access_key_id => $access_key_id,
48             secret => $secret,
49             endpoint => $endpoint,
50             service => $service,
51             });
52              
53             Constructs the signature object, which is used to sign requests.
54              
55             Note that the access key ID is an alphanumeric string, not your account ID. The endpoint could be "eu-west-1", and the service could be "glacier".
56              
57             Since version 0.20, parameters can be passed in a hashref. The keys C, C, C, and C are required.
58             C, if passed, will be applied to each signed request as the C header.
59              
60             =cut
61              
62             sub new {
63 1     1 1 348203 my $class = shift;
64 1         13 my $self = {};
65 1 50 33     9 if (@_ == 1 and ref $_[0] eq 'HASH') {
66 0         0 @$self{keys %{$_[0]}} = values %{$_[0]};
  0         0  
  0         0  
67             } else {
68 1         9 @$self{qw(access_key_id secret endpoint service)} = @_;
69             }
70             # The URI should not be double escaped for the S3 service
71 1 50       6 $self->{no_escape_uri} = ( lc($self->{service}) eq 's3' ) ? 1 : 0;
72 1         3 bless $self, $class;
73 1         4 return $self;
74             }
75              
76             =head2 sign
77              
78             my $signed_request = $sig->sign( $request );
79              
80             Signs a request with your credentials by appending the Authorization header. $request should be an HTTP::Request. The signed request is returned.
81              
82             =cut
83              
84             sub sign {
85 0     0 1 0 my ( $self, $request ) = @_;
86 0         0 my $authz = $self->_authorization( $request );
87 0         0 $request->header( Authorization => $authz );
88 0         0 return $request;
89             }
90              
91             # _headers_to_sign:
92             # Return the sorted lower case headers as required by the generation of canonical headers
93              
94             sub _headers_to_sign {
95 124     124   186 my $req = shift;
96              
97 124         342 return sort { $a cmp $b } map { lc } $req->headers->header_field_names;
  616         1274  
  524         6589  
98             }
99              
100             # _canonical_request:
101             # Construct the canonical request string from an HTTP::Request.
102              
103             sub _canonical_request {
104 93     93   65280 my ( $self, $req ) = @_;
105              
106 93         325 my $creq_method = $req->method;
107              
108 93 100       1189 my ( $creq_canonical_uri, $creq_canonical_query_string ) =
109             ( $req->uri =~ m@([^?]*)\?(.*)$@ )
110             ? ( $1, $2 )
111             : ( $req->uri, '' );
112 93         2106 $creq_canonical_uri =~ s@^https?://[^/]*/?@/@;
113 93         478 $creq_canonical_uri = $self->_simplify_uri( $creq_canonical_uri );
114 93         256 $creq_canonical_query_string = _sort_query_string( $creq_canonical_query_string );
115              
116             # Ensure Host header is present as its required
117 93 50       285 if (!$req->header('host')) {
118 0 0       0 my $host = $req->uri->_port ? $req->uri->host_port : $req->uri->host;
119 0         0 $req->header('Host' => $host);
120             }
121 93         5027 my $creq_payload_hash = $req->header('x-amz-content-sha256');
122 93 100       4637 if (!$creq_payload_hash) {
123 31         162 $creq_payload_hash = sha256_hex($req->content);
124             # X-Amz-Content-Sha256 must be specified now
125 31         637 $req->header('X-Amz-Content-Sha256' => $creq_payload_hash);
126             }
127              
128             # There's a bug in AMS4 which causes requests without x-amz-date set to be rejected
129             # so we always add one if its not present.
130 93         1952 my $amz_date = $req->header('x-amz-date');
131 93 100       4318 if (!$amz_date) {
132 31         92 $req->header('X-Amz-Date' => _req_timepiece($req)->strftime('%Y%m%dT%H%M%SZ'));
133             }
134 93 50 33     6215 if (defined $self->{security_token} and !defined $req->header('X-Amz-Security-Token')) {
135 0         0 $req->header('X-Amz-Security-Token' => $self->{security_token});
136             }
137 93         220 my @sorted_headers = _headers_to_sign( $req );
138             my $creq_canonical_headers = join '',
139             map {
140 93         261 sprintf "%s:%s\x0a",
141             lc,
142 393         1211 join ',', sort {$a cmp $b } _trim_whitespace($req->header($_) )
  24         76  
143             }
144             @sorted_headers;
145 93         255 my $creq_signed_headers = join ';', map {lc} @sorted_headers;
  393         924  
146 93         328 my $creq = join "\x0a",
147             $creq_method, $creq_canonical_uri, $creq_canonical_query_string,
148             $creq_canonical_headers, $creq_signed_headers, $creq_payload_hash;
149 93         355 return $creq;
150             }
151              
152             # _string_to_sign
153             # Construct the string to sign.
154              
155             sub _string_to_sign {
156 62     62   44010 my ( $self, $req ) = @_;
157 62         149 my $dt = _req_timepiece( $req );
158 62         2123 my $creq = $self->_canonical_request($req);
159 62         236 my $sts_request_date = $dt->strftime( '%Y%m%dT%H%M%SZ' );
160 62         4588 my $sts_credential_scope = join '/', $dt->strftime('%Y%m%d'), $self->{endpoint}, $self->{service}, 'aws4_request';
161 62         2422 my $sts_creq_hash = sha256_hex( $creq );
162              
163 62         164 my $sts = join "\x0a", $ALGORITHM, $sts_request_date, $sts_credential_scope, $sts_creq_hash;
164 62         237 return $sts;
165             }
166              
167             # _authorization
168             # Construct the authorization string
169              
170             sub _authorization {
171 31     31   31061 my ( $self, $req ) = @_;
172              
173 31         86 my $dt = _req_timepiece( $req );
174 31         1225 my $sts = $self->_string_to_sign( $req );
175 31         135 my $k_date = hmac_sha256( $dt->strftime('%Y%m%d'), 'AWS4' . $self->{secret} );
176 31         2214 my $k_region = hmac_sha256( $self->{endpoint}, $k_date );
177 31         264 my $k_service = hmac_sha256( $self->{service}, $k_region );
178 31         240 my $k_signing = hmac_sha256( 'aws4_request', $k_service );
179              
180 31         322 my $authz_signature = hmac_sha256_hex( $sts, $k_signing );
181 31         90 my $authz_credential = join '/', $self->{access_key_id}, $dt->strftime('%Y%m%d'), $self->{endpoint}, $self->{service}, 'aws4_request';
182 31         810 my $authz_signed_headers = join ';', _headers_to_sign( $req );
183              
184 31         120 my $authz = "$ALGORITHM Credential=$authz_credential,SignedHeaders=$authz_signed_headers,Signature=$authz_signature";
185 31         127 return $authz;
186              
187             }
188              
189             =head1 AUTHOR
190              
191             Tim Nordenfur, C<< >>
192              
193             Maintained by Dan Book, C<< >>
194              
195             =cut
196              
197             sub _simplify_uri {
198 93     93   149 my $self = shift;
199 93         148 my $orig_uri = shift;
200 93         220 my @parts = split /\//, $orig_uri;
201 93         475 my @simple_parts = ();
202 93         248 for my $part ( @parts ) {
203 78 100 100     653 if ( $part eq '' || $part eq '.' ) {
    100          
204             } elsif ( $part eq '..' ) {
205 9         19 pop @simple_parts;
206             } else {
207 33 50       80 if ( $self->{no_escape_uri} ) {
208 0         0 push @simple_parts, $part;
209             }
210             else {
211 33         105 push @simple_parts, uri_escape($part);
212             }
213             }
214             }
215 93         609 my $simple_uri = '/' . join '/', @simple_parts;
216 93 100 100     376 $simple_uri .= '/' if $orig_uri =~ m@/$@ && $simple_uri !~ m@/$@;
217 93         735 return $simple_uri;
218             }
219             sub _sort_query_string {
220 93 100   93   267 return '' unless $_[0];
221 30         48 my @params;
222 30         84 for my $param ( split /&/, $_[0] ) {
223             my ( $key, $value ) =
224 42         111 map { tr/+/ /; uri_escape( uri_unescape( $_ ) ) } # escape all non-unreserved chars
  78         1112  
  78         200  
225             split /=/, $param;
226 42 100       1174 push @params, [$key, (defined $value ? $value : '')];
227             }
228             return join '&',
229 42         178 map { join '=', @$_ }
230 30 50       100 sort { ( $a->[0] cmp $b->[0] ) || ( $a->[1] cmp $b->[1] ) }
  12         57  
231             @params;
232             }
233             sub _trim_whitespace {
234 393     393   17475 return map { my $str = $_; $str =~ s/^\s*//; $str =~ s/\s*$//; $str } @_;
  408         638  
  408         1779  
  408         3431  
  408         2000  
235             }
236             sub _str_to_timepiece {
237 124     124   197 my $date = shift;
238 124 100       651 if ( $date =~ m/^\d{8}T\d{6}Z$/ ) {
239             # assume basic ISO 8601, as demanded by AWS
240 93         554 return Time::Piece->strptime($date, '%Y%m%dT%H%M%SZ');
241             } else {
242             # assume the format given in the AWS4 test suite
243 31         316 $date =~ s/^.{5}//; # remove weekday, as Amazon's test suite contains internally inconsistent dates
244 31         220 return Time::Piece->strptime($date, '%d %b %Y %H:%M:%S %Z');
245             }
246             }
247             sub _req_timepiece {
248 124     124   197 my $req = shift;
249 124         411 my $x_date = $req->header('X-Amz-Date');
250 124   66     6611 my $date = $x_date || $req->header('Date');
251 124 50       1477 if (!$date) {
252             # No date set by the caller so set one up
253 0         0 my $piece = Time::Piece::gmtime;
254 0         0 $req->date($piece->epoch);
255 0         0 return $piece
256             }
257 124         260 return _str_to_timepiece($date);
258             }
259              
260             =head1 BUGS
261              
262             Please report any bugs or feature requests to C, or through
263             the web interface at L. I will be notified, and then you'll
264             automatically be notified of progress on your bug as I make changes.
265              
266              
267              
268              
269             =head1 SUPPORT
270              
271             You can find documentation for this module with the perldoc command.
272              
273             perldoc Net::Amazon::Signature::V4
274              
275              
276             You can also look for information at:
277              
278             =over 4
279              
280             =item * RT: CPAN's request tracker (report bugs here)
281              
282             L
283              
284             =item * Source on GitHub
285              
286             L
287              
288             =item * Search CPAN
289              
290             L
291              
292             =back
293              
294             =head1 LICENSE AND COPYRIGHT
295              
296             This software is copyright (c) 2012 by Tim Nordenfur.
297              
298             This is free software; you can redistribute it and/or modify it under
299             the same terms as the Perl 5 programming language system itself.
300              
301              
302             =cut
303              
304             1; # End of Net::Amazon::Signature::V4