File Coverage

blib/lib/Net/Amazon/Signature/V4.pm
Criterion Covered Total %
statement 104 112 92.8
branch 21 24 87.5
condition 8 9 88.8
subroutine 16 17 94.1
pod 2 2 100.0
total 151 164 92.0


line stmt bran cond sub pod time code
1             package Net::Amazon::Signature::V4;
2              
3 2     2   76155 use strict;
  2         9  
  2         59  
4 2     2   8 use warnings;
  2         3  
  2         54  
5 2     2   998 use sort 'stable';
  2         1581  
  2         8  
6              
7 2     2   1004 use Digest::SHA qw/sha256_hex hmac_sha256 hmac_sha256_hex/;
  2         5385  
  2         156  
8 2     2   981 use Time::Piece ();
  2         20225  
  2         47  
9 2     2   878 use URI::Escape;
  2         4067  
  2         2883  
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.19
20              
21             =cut
22              
23             our $VERSION = '0.19';
24              
25              
26             =head1 SYNOPSIS
27              
28             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.
29              
30             use Net::Amazon::Signature::V4;
31              
32             my $sig = Net::Amazon::Signature::V4->new( $access_key_id, $secret, $endpoint, $service );
33             my $req = HTTP::Request->parse( $request_string );
34             my $signed_req = $sig->sign( $req );
35             ...
36              
37             The primary purpose of this module is to be used by Net::Amazon::Glacier.
38              
39             =head1 METHODS
40              
41             =head2 new( $access_key_id, $secret, $endpoint, $service )
42              
43             Constructs the signature object, which is used to sign requests.
44              
45             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".
46              
47             =cut
48              
49             sub new {
50 1     1 1 735 my $class = shift;
51 1         3 my ( $access_key_id, $secret, $endpoint, $service ) = @_;
52 1         6 my $self = {
53             access_key_id => $access_key_id,
54             secret => $secret,
55             endpoint => $endpoint,
56             service => $service,
57             };
58 1         2 bless $self, $class;
59 1         3 return $self;
60             }
61              
62             =head2 sign( $request )
63              
64             Signs a request with your credentials by appending the Authorization header. $request should be an HTTP::Request. The signed request is returned.
65              
66             =cut
67              
68             sub sign {
69 0     0 1 0 my ( $self, $request ) = @_;
70 0         0 my $authz = $self->_authorization( $request );
71 0         0 $request->header( Authorization => $authz );
72 0         0 return $request;
73             }
74              
75             # _headers_to_sign:
76             # Return the sorted lower case headers as required by the generation of canonical headers
77              
78             sub _headers_to_sign {
79 124     124   180 my $req = shift;
80              
81 124         229 return sort { $a cmp $b } map { lc } $req->headers->header_field_names;
  616         928  
  524         4483  
82             }
83              
84             # _canonical_request:
85             # Construct the canonical request string from an HTTP::Request.
86              
87             sub _canonical_request {
88 93     93   38959 my ( $self, $req ) = @_;
89              
90 93         210 my $creq_method = $req->method;
91              
92 93 100       868 my ( $creq_canonical_uri, $creq_canonical_query_string ) =
93             ( $req->uri =~ m@([^?]*)\?(.*)$@ )
94             ? ( $1, $2 )
95             : ( $req->uri, '' );
96 93         1528 $creq_canonical_uri =~ s@^https?://[^/]*/?@/@;
97 93         329 $creq_canonical_uri = _simplify_uri( $creq_canonical_uri );
98 93         171 $creq_canonical_query_string = _sort_query_string( $creq_canonical_query_string );
99              
100             # Ensure Host header is present as its required
101 93 50       220 if (!$req->header('host')) {
102 0         0 $req->header('Host' => $req->uri->host);
103             }
104 93         3486 my $creq_payload_hash = $req->header('x-amz-content-sha256');
105 93 100       3347 if (!$creq_payload_hash) {
106 31         74 $creq_payload_hash = sha256_hex($req->content);
107             # X-Amz-Content-Sha256 must be specified now
108 31         465 $req->header('X-Amz-Content-Sha256' => $creq_payload_hash);
109             }
110              
111             # There's a bug in AMS4 which causes requests without x-amz-date set to be rejected
112             # so we always add one if its not present.
113 93         1370 my $amz_date = $req->header('x-amz-date');
114 93 100       3148 if (!$amz_date) {
115 31         70 $req->header('X-Amz-Date' => _req_timepiece($req)->strftime('%Y%m%dT%H%M%SZ'));
116             }
117 93         4793 my @sorted_headers = _headers_to_sign( $req );
118             my $creq_canonical_headers = join '',
119             map {
120 93         209 sprintf "%s:%s\x0a",
121             lc,
122 393         935 join ',', sort {$a cmp $b } _trim_whitespace($req->header($_) )
  24         62  
123             }
124             @sorted_headers;
125 93         177 my $creq_signed_headers = join ';', map {lc} @sorted_headers;
  393         632  
126 93         257 my $creq = join "\x0a",
127             $creq_method, $creq_canonical_uri, $creq_canonical_query_string,
128             $creq_canonical_headers, $creq_signed_headers, $creq_payload_hash;
129 93         238 return $creq;
130             }
131              
132             # _string_to_sign
133             # Construct the string to sign.
134              
135             sub _string_to_sign {
136 62     62   17712 my ( $self, $req ) = @_;
137 62         107 my $dt = _req_timepiece( $req );
138 62         2733 my $creq = $self->_canonical_request($req);
139 62         166 my $sts_request_date = $dt->strftime( '%Y%m%dT%H%M%SZ' );
140 62         2038 my $sts_credential_scope = join '/', $dt->strftime('%Y%m%d'), $self->{endpoint}, $self->{service}, 'aws4_request';
141 62         1686 my $sts_creq_hash = sha256_hex( $creq );
142              
143 62         149 my $sts = join "\x0a", $ALGORITHM, $sts_request_date, $sts_credential_scope, $sts_creq_hash;
144 62         159 return $sts;
145             }
146              
147             # _authorization
148             # Construct the authorization string
149              
150             sub _authorization {
151 31     31   17444 my ( $self, $req ) = @_;
152              
153 31         61 my $dt = _req_timepiece( $req );
154 31         1462 my $sts = $self->_string_to_sign( $req );
155 31         67 my $k_date = hmac_sha256( $dt->strftime('%Y%m%d'), 'AWS4' . $self->{secret} );
156 31         991 my $k_region = hmac_sha256( $self->{endpoint}, $k_date );
157 31         214 my $k_service = hmac_sha256( $self->{service}, $k_region );
158 31         174 my $k_signing = hmac_sha256( 'aws4_request', $k_service );
159              
160 31         226 my $authz_signature = hmac_sha256_hex( $sts, $k_signing );
161 31         73 my $authz_credential = join '/', $self->{access_key_id}, $dt->strftime('%Y%m%d'), $self->{endpoint}, $self->{service}, 'aws4_request';
162 31         653 my $authz_signed_headers = join ';', _headers_to_sign( $req );
163              
164 31         96 my $authz = "$ALGORITHM Credential=$authz_credential,SignedHeaders=$authz_signed_headers,Signature=$authz_signature";
165 31         89 return $authz;
166              
167             }
168              
169             =head1 AUTHOR
170              
171             Tim Nordenfur, C<< >>
172              
173             Maintained by Dan Book, C<< >>
174              
175             =cut
176              
177             sub _simplify_uri {
178 93     93   125 my $orig_uri = shift;
179 93         176 my @parts = split /\//, $orig_uri;
180 93         648 my @simple_parts = ();
181 93         168 for my $part ( @parts ) {
182 78 100 100     420 if ( $part eq '' || $part eq '.' ) {
    100          
183             } elsif ( $part eq '..' ) {
184 9         15 pop @simple_parts;
185             } else {
186 33         71 push @simple_parts, uri_escape($part);
187             }
188             }
189 93         391 my $simple_uri = '/' . join '/', @simple_parts;
190 93 100 100     263 $simple_uri .= '/' if $orig_uri =~ m@/$@ && $simple_uri !~ m@/$@;
191 93         563 return $simple_uri;
192             }
193             sub _sort_query_string {
194 93 100   93   179 return '' unless $_[0];
195 30         41 my @params;
196 30         77 for my $param ( split /&/, $_[0] ) {
197             my ( $key, $value ) =
198 42         87 map { tr/+/ /; uri_escape( uri_unescape( $_ ) ) } # escape all non-unreserved chars
  78         659  
  78         146  
199             split /=/, $param;
200 42 100       816 push @params, [$key, (defined $value ? $value : '')];
201             }
202             return join '&',
203 42         127 map { join '=', @$_ }
204 30 50       72 sort { ( $a->[0] cmp $b->[0] ) || ( $a->[1] cmp $b->[1] ) }
  12         55  
205             @params;
206             }
207             sub _trim_whitespace {
208 393     393   12439 return map { my $str = $_; $str =~ s/^\s*//; $str =~ s/\s*$//; $str } @_;
  408         504  
  408         1211  
  408         1953  
  408         1980  
209             }
210             sub _str_to_timepiece {
211 124     124   151 my $date = shift;
212 124 100       475 if ( $date =~ m/^\d{8}T\d{6}Z$/ ) {
213             # assume basic ISO 8601, as demanded by AWS
214 93         355 return Time::Piece->strptime($date, '%Y%m%dT%H%M%SZ');
215             } else {
216             # assume the format given in the AWS4 test suite
217 31         115 $date =~ s/^.{5}//; # remove weekday, as Amazon's test suite contains internally inconsistent dates
218 31         137 return Time::Piece->strptime($date, '%d %b %Y %H:%M:%S %Z');
219             }
220             }
221             sub _req_timepiece {
222 124     124   168 my $req = shift;
223 124         281 my $x_date = $req->header('X-Amz-Date');
224 124   66     4513 my $date = $x_date || $req->header('Date');
225 124 50       1167 if (!$date) {
226             # No date set by the caller so set one up
227 0         0 my $piece = Time::Piece::gmtime;
228 0         0 $req->date($piece->epoch);
229 0         0 return $piece
230             }
231 124         190 return _str_to_timepiece($date);
232             }
233              
234             =head1 BUGS
235              
236             Please report any bugs or feature requests to C, or through
237             the web interface at L. I will be notified, and then you'll
238             automatically be notified of progress on your bug as I make changes.
239              
240              
241              
242              
243             =head1 SUPPORT
244              
245             You can find documentation for this module with the perldoc command.
246              
247             perldoc Net::Amazon::Signature::V4
248              
249              
250             You can also look for information at:
251              
252             =over 4
253              
254             =item * RT: CPAN's request tracker (report bugs here)
255              
256             L
257              
258             =item * AnnoCPAN: Annotated CPAN documentation
259              
260             L
261              
262             =item * CPAN Ratings
263              
264             L
265              
266             =item * Search CPAN
267              
268             L
269              
270             =back
271              
272             =head1 LICENSE AND COPYRIGHT
273              
274             Copyright 2012 Tim Nordenfur.
275              
276             This program is free software; you can redistribute it and/or modify it
277             under the terms of either: the GNU General Public License as published
278             by the Free Software Foundation; or the Artistic License.
279              
280             See http://dev.perl.org/licenses/ for more information.
281              
282              
283             =cut
284              
285             1; # End of Net::Amazon::Signature::V4