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   69895 use strict;
  2         10  
  2         58  
4 2     2   15 use warnings;
  2         5  
  2         61  
5 2     2   1159 use sort 'stable';
  2         1129  
  2         19  
6              
7 2     2   1092 use Digest::SHA qw/sha256_hex hmac_sha256 hmac_sha256_hex/;
  2         6313  
  2         152  
8 2     2   1074 use Time::Piece ();
  2         24266  
  2         54  
9 2     2   2666 use URI::Escape;
  2         2851  
  2         3763  
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.21
20              
21             =cut
22              
23             our $VERSION = '0.21';
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 941 my $class = shift;
64 1         2 my $self = {};
65 1 50 33     7 if (@_ == 1 and ref $_[0] eq 'HASH') {
66 0         0 @$self{keys %{$_[0]}} = values %{$_[0]};
  0         0  
  0         0  
67             } else {
68 1         6 @$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         2 bless $self, $class;
73 1         3 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   183 my $req = shift;
96              
97 124         284 return sort { $a cmp $b } map { lc } $req->headers->header_field_names;
  616         1102  
  524         5481  
98             }
99              
100             # _canonical_request:
101             # Construct the canonical request string from an HTTP::Request.
102              
103             sub _canonical_request {
104 93     93   46777 my ( $self, $req ) = @_;
105              
106 93         218 my $creq_method = $req->method;
107              
108 93 100       1040 my ( $creq_canonical_uri, $creq_canonical_query_string ) =
109             ( $req->uri =~ m@([^?]*)\?(.*)$@ )
110             ? ( $1, $2 )
111             : ( $req->uri, '' );
112 93         1856 $creq_canonical_uri =~ s@^https?://[^/]*/?@/@;
113 93         450 $creq_canonical_uri = $self->_simplify_uri( $creq_canonical_uri );
114 93         200 $creq_canonical_query_string = _sort_query_string( $creq_canonical_query_string );
115              
116             # Ensure Host header is present as its required
117 93 50       241 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         4205 my $creq_payload_hash = $req->header('x-amz-content-sha256');
122 93 100       4061 if (!$creq_payload_hash) {
123 31         87 $creq_payload_hash = sha256_hex($req->content);
124             # X-Amz-Content-Sha256 must be specified now
125 31         532 $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         1624 my $amz_date = $req->header('x-amz-date');
131 93 100       3874 if (!$amz_date) {
132 31         69 $req->header('X-Amz-Date' => _req_timepiece($req)->strftime('%Y%m%dT%H%M%SZ'));
133             }
134 93 50 33     6273 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         194 my @sorted_headers = _headers_to_sign( $req );
138             my $creq_canonical_headers = join '',
139             map {
140 93         242 sprintf "%s:%s\x0a",
141             lc,
142 393         1064 join ',', sort {$a cmp $b } _trim_whitespace($req->header($_) )
  24         65  
143             }
144             @sorted_headers;
145 93         217 my $creq_signed_headers = join ';', map {lc} @sorted_headers;
  393         781  
146 93         330 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         284 return $creq;
150             }
151              
152             # _string_to_sign
153             # Construct the string to sign.
154              
155             sub _string_to_sign {
156 62     62   21242 my ( $self, $req ) = @_;
157 62         126 my $dt = _req_timepiece( $req );
158 62         3485 my $creq = $self->_canonical_request($req);
159 62         204 my $sts_request_date = $dt->strftime( '%Y%m%dT%H%M%SZ' );
160 62         2499 my $sts_credential_scope = join '/', $dt->strftime('%Y%m%d'), $self->{endpoint}, $self->{service}, 'aws4_request';
161 62         2024 my $sts_creq_hash = sha256_hex( $creq );
162              
163 62         181 my $sts = join "\x0a", $ALGORITHM, $sts_request_date, $sts_credential_scope, $sts_creq_hash;
164 62         191 return $sts;
165             }
166              
167             # _authorization
168             # Construct the authorization string
169              
170             sub _authorization {
171 31     31   20916 my ( $self, $req ) = @_;
172              
173 31         77 my $dt = _req_timepiece( $req );
174 31         1839 my $sts = $self->_string_to_sign( $req );
175 31         73 my $k_date = hmac_sha256( $dt->strftime('%Y%m%d'), 'AWS4' . $self->{secret} );
176 31         1131 my $k_region = hmac_sha256( $self->{endpoint}, $k_date );
177 31         227 my $k_service = hmac_sha256( $self->{service}, $k_region );
178 31         215 my $k_signing = hmac_sha256( 'aws4_request', $k_service );
179              
180 31         269 my $authz_signature = hmac_sha256_hex( $sts, $k_signing );
181 31         82 my $authz_credential = join '/', $self->{access_key_id}, $dt->strftime('%Y%m%d'), $self->{endpoint}, $self->{service}, 'aws4_request';
182 31         813 my $authz_signed_headers = join ';', _headers_to_sign( $req );
183              
184 31         124 my $authz = "$ALGORITHM Credential=$authz_credential,SignedHeaders=$authz_signed_headers,Signature=$authz_signature";
185 31         111 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   145 my $self = shift;
199 93         154 my $orig_uri = shift;
200 93         208 my @parts = split /\//, $orig_uri;
201 93         427 my @simple_parts = ();
202 93         216 for my $part ( @parts ) {
203 78 100 100     489 if ( $part eq '' || $part eq '.' ) {
    100          
204             } elsif ( $part eq '..' ) {
205 9         18 pop @simple_parts;
206             } else {
207 33 50       87 if ( $self->{no_escape_uri} ) {
208 0         0 push @simple_parts, $part;
209             }
210             else {
211 33         83 push @simple_parts, uri_escape($part);
212             }
213             }
214             }
215 93         487 my $simple_uri = '/' . join '/', @simple_parts;
216 93 100 100     362 $simple_uri .= '/' if $orig_uri =~ m@/$@ && $simple_uri !~ m@/$@;
217 93         684 return $simple_uri;
218             }
219             sub _sort_query_string {
220 93 100   93   227 return '' unless $_[0];
221 30         46 my @params;
222 30         78 for my $param ( split /&/, $_[0] ) {
223             my ( $key, $value ) =
224 42         101 map { tr/+/ /; uri_escape( uri_unescape( $_ ) ) } # escape all non-unreserved chars
  78         781  
  78         184  
225             split /=/, $param;
226 42 100       1015 push @params, [$key, (defined $value ? $value : '')];
227             }
228             return join '&',
229 42         153 map { join '=', @$_ }
230 30 50       95 sort { ( $a->[0] cmp $b->[0] ) || ( $a->[1] cmp $b->[1] ) }
  12         48  
231             @params;
232             }
233             sub _trim_whitespace {
234 393     393   15181 return map { my $str = $_; $str =~ s/^\s*//; $str =~ s/\s*$//; $str } @_;
  408         637  
  408         1660  
  408         2438  
  408         2381  
235             }
236             sub _str_to_timepiece {
237 124     124   190 my $date = shift;
238 124 100       560 if ( $date =~ m/^\d{8}T\d{6}Z$/ ) {
239             # assume basic ISO 8601, as demanded by AWS
240 93         394 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         152 $date =~ s/^.{5}//; # remove weekday, as Amazon's test suite contains internally inconsistent dates
244 31         156 return Time::Piece->strptime($date, '%d %b %Y %H:%M:%S %Z');
245             }
246             }
247             sub _req_timepiece {
248 124     124   202 my $req = shift;
249 124         310 my $x_date = $req->header('X-Amz-Date');
250 124   66     5788 my $date = $x_date || $req->header('Date');
251 124 50       1438 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         230 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