File Coverage

blib/lib/WebService/Amazon/Signature/v4.pm
Criterion Covered Total %
statement 143 166 86.1
branch 23 38 60.5
condition 5 11 45.4
subroutine 31 33 93.9
pod 13 13 100.0
total 215 261 82.3


line stmt bran cond sub pod time code
1             package WebService::Amazon::Signature::v4;
2             $WebService::Amazon::Signature::v4::VERSION = '0.002';
3 1     1   4 use strict;
  1         1  
  1         28  
4 1     1   4 use warnings;
  1         1  
  1         55  
5              
6 1     1   403 use parent qw(WebService::Amazon::Signature);
  1         234  
  1         5  
7              
8             =head1 NAME
9              
10             WebService::Amazon::Signature::v4 - support for v4 of the Amazon signing method
11              
12             =head1 VERSION
13              
14             version 0.002
15              
16             =head1 SYNOPSIS
17              
18             my $req = 'GET / HTTP/1.1 ...';
19             my $amz = WebService::Amazon::Signature::v4->new(
20             scope => '20110909/us-east-1/host/aws4_request',
21             access_key => 'AKIDEXAMPLE',
22             secret_key => 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
23             );
24             $amz->parse_request($req)
25             my $signed_req = $amz->signed_request($req);
26              
27             =head1 DESCRIPTION
28              
29             =cut
30              
31 1     1   463 use Time::Moment;
  1         2963  
  1         26  
32 1     1   462 use POSIX qw(strftime);
  1         5430  
  1         5  
33 1     1   1359 use Digest::SHA qw(sha256 sha256_hex);
  1         22295  
  1         109  
34 1     1   656 use Digest::HMAC qw(hmac hmac_hex);
  1         644  
  1         88  
35 1     1   710 use List::UtilsBy qw(sort_by);
  1         1878  
  1         81  
36 1     1   724 use HTTP::StreamParser::Request;
  1         7743  
  1         44  
37 1     1   834 use URI;
  1         3898  
  1         27  
38 1     1   436 use URI::QueryParam;
  1         522  
  1         26  
39 1     1   5 use URI::Escape qw(uri_escape_utf8 uri_unescape);
  1         2  
  1         60  
40              
41 1     1   3 use constant DEBUG => 0;
  1         2  
  1         59  
42              
43 1     1   448 use Log::Any qw($log);
  1         1375  
  1         4  
44              
45             =head1 METHODS - Constructor
46              
47             =cut
48              
49             =head2 new
50              
51             Instantiate a signing object. Expects the following named parameters:
52              
53             =over 4
54              
55             =item * scope - the scope used for requests, typically something like C<20130112/us-west-2/dynamodb/aws4_request>
56              
57             =item * secret_key - your secret key
58              
59             =item * access_key - your access key
60              
61             =back
62              
63             =cut
64              
65             sub new {
66 31     31 1 64 my $class = shift;
67 31         255 bless {
68             algorithm => 'AWS4-HMAC-SHA256',
69             @_
70             }, $class
71             }
72              
73             =head1 METHODS - Accessors
74              
75             =head2 algorithm
76              
77             Read-only accessor for the algorithm (default is C)
78              
79             =cut
80              
81 152     152 1 467 sub algorithm { shift->{algorithm} }
82              
83             =head2 date
84              
85             Read-only accessor for the date field.
86              
87             =cut
88              
89 91     91 1 218 sub date { shift->{date} }
90              
91             =head2 scope
92              
93             Read-only accessor for scope information - typically something like
94             C<20110909/us-east-1/host/aws4_request>.
95              
96             =cut
97              
98 274     274 1 1058 sub scope { shift->{scope} }
99              
100             =head2 access_key
101              
102             Readonly accessor for the access key used when signing requests.
103              
104             =cut
105              
106 61     61 1 194 sub access_key { shift->{access_key} }
107              
108             =head2 secret_key
109              
110             Readonly accessor for the secret key used when signing requests.
111              
112             =cut
113              
114 122     122 1 352 sub secret_key { shift->{secret_key} }
115              
116             =head2 signed_headers
117              
118             Read-only accessor for the headers used for signing purposes
119             (a string consisting of the lowercase headers separated by ;
120             in lexical order)
121              
122             =cut
123              
124 61     61 1 133 sub signed_headers { shift->{signed_headers} }
125              
126             =head1 METHODS
127              
128             =head2 parse_request
129              
130             Parses a given request. Takes a single parameter - the HTTP request as a string.
131              
132             =cut
133              
134             sub parse_request {
135 31     31 1 22460 my $self = shift;
136 31         54 my $txt = shift;
137 31         340 my $parser = HTTP::StreamParser::Request->new;
138 31         1062 my $method;
139             my $uri;
140 0         0 my %header;
141 0         0 my @headers;
142 31         58 my $payload = '';
143             $parser->subscribe_to_event(
144 31     31   2446 http_method => sub { $method = $_[1] },
145 31     31   2782 http_uri => sub { $uri = $_[1]; },
146             http_header => sub {
147 75 100   75   5832 if(exists $header{lc $_[1]}) {
148 5 100       25 $header{lc $_[1]} = [
149             $header{lc $_[1]}
150             ] unless ref $header{lc $_[1]};
151 5         5 push @{$header{lc $_[1]}}, $_[2]
  5         19  
152             } else {
153 70         326 $header{lc $_[1]} = $_[2]
154             }
155             },
156             http_body_chunk => sub {
157 2     2   123 $payload .= $_[1]
158             }
159 31         492 );
160 31         1530 $parser->parse($txt);
161 31         1742 $self->{headers} = \@headers;
162 31         72 $self->{header} = \%header;
163 31         60 $self->{method} = $method;
164 31         84 $self->{uri} = $uri;
165 31         79 $self->{payload} = $payload;
166 31         456 $self
167             }
168              
169             =head2 from_http_request
170              
171             Parses information from an L instance.
172              
173             =cut
174              
175             sub from_http_request {
176 0     0 1 0 my $self = shift;
177 0         0 my $req = shift;
178 0         0 $self->{method} = $req->method;
179 0         0 $self->{uri} = '' . $req->uri;
180 0         0 $self->{payload} = $req->content;
181 0         0 my %header;
182             $req->scan(sub {
183 0     0   0 my ($k, $v) = @_;
184 0 0       0 if(exists $header{lc $k}) {
185 0 0       0 $header{lc $k} = [
186             $header{lc $k}
187             ] unless ref $header{lc $k};
188 0         0 push @{$header{lc $k}}, $v
  0         0  
189             } else {
190 0         0 $header{lc $k} = $v
191             }
192 0         0 });
193 0         0 $self->{header} = \%header;
194 0         0 $self
195             }
196              
197             =head2 canonical_request
198              
199             Returns the string form of the canonical request, used
200             as an intermediate point in generating the signature.
201              
202             =cut
203              
204             sub canonical_request {
205 121     121 1 12438 my $self = shift;
206              
207 121         133 my %header = %{$self->{header}};
  121         664  
208 121         218 my $method = $self->{method};
209 121         167 my $payload = $self->{payload};
210 121         172 my $uri = $self->{uri};
211              
212             # Strip all leading/trailing whitespace from headers,
213             # convert to canonical comma-separated format
214 121         535 s/^\s+//, s/\s+$// for map @$_, grep ref($_), values %header;
215 121         318 $_ = join ',', sort @$_ for grep ref($_), values %header;
216 121         1115 s/^\s+//, s/\s+$// for values %header;
217              
218 121         262 $uri =~ s{ .*$}{}g;
219 121         165 $uri =~ s{#}{%23}g;
220              
221             # We're not actually connecting to this so a default
222             # value should be safe here. Only used to ensure URI
223             # detects this as an HTTP URI.
224 121 50 33     878 $uri = 'http://localhost' . $uri if !length($uri) or $uri !~ /^https?:/;
225              
226 121 50       788 my $u = (ref $uri ? $uri : URI->new($uri))->clone->canonical;
227 121         26654 $uri = $u->path;
228 121         891 my $path = '';
229 121         325 while(length $uri) {
230 173 50 33     1450 if(substr($uri, 0, 3) eq '../') {
    50          
    100          
    100          
    100          
    50          
    50          
231 0         0 substr $uri, 0, 3, '';
232             } elsif(substr($uri, 0, 2) eq './') {
233 0         0 substr $uri, 0, 2, '';
234             } elsif(substr($uri, 0, 3) eq '/./') {
235 8         21 substr $uri, 0, 3, '/';
236             } elsif(substr($uri, 0, 4) eq '/../') {
237 4         10 substr $uri, 0, 4, '/';
238 4         17 $path =~ s{/?[^/]*$}{};
239             } elsif(substr($uri, 0, 3) eq '/..') {
240 8         15 substr $uri, 0, 3, '/';
241 8         34 $path =~ s{/?[^/]*$}{};
242             } elsif(substr($uri, 0, 2) eq '/.') {
243 0         0 substr $uri, 0, 3, '/';
244             } elsif($uri eq '.' or $uri eq '..') {
245 0         0 $uri = '';
246             } else {
247 153 50       967 $path .= $1 if $uri =~ s{^(/?[^/]*)}{};
248             }
249             }
250 121         414 $path =~ s{/+}{/}g;
251 121         308 $u->path($path);
252 121         3243 my @query;
253 121 100       364 if(length $u->query) {
254 40         416 for my $qp (split /&/, $u->query) {
255 56         480 my ($k, $v) = map uri_unescape($_), split /=/, $qp, 2;
256 56         641 for ($k, $v) {
257 112   100     939 $_ //= '';
258 112         135 s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  0         0  
259 112         239 $_ = Encode::decode('UTF-8' => $_);
260 112         3519 s{\+}{ }g;
261 112         224 $_ = uri_escape_utf8($_);
262             }
263 56         822 push @query, "$k=$_" for $v
264             }
265             }
266 121         1041 my $query = join '&', sort @query;
267              
268             # Now apply the date. This can come from various sources:
269             # * X-Amz-Date:
270             # * Date:
271             # * Current date/time
272 121 50       399 if(exists $header{'x-amz-date'}) {
    50          
273 0         0 $self->{date} = $header{'x-amz-date'};
274             } elsif(exists $header{'date'}) {
275 121         134 eval {
276 121         1336692 require Time::Piece;
277 121         11628 $self->{date} = Time::Piece->strptime($header{date}, '%a, %d %b %Y %H:%M:%S GMT')->strftime('%Y%m%dT%H%M%SZ');
278             };
279             # ignore $@, we'll fall through to default value on failure
280             }
281             # Worst-case scenario: apply a default value.
282 121   33     7752 $self->{date} ||= strftime '%Y%m%dT%H%M%SZ', gmtime;
283              
284 121         314 my $can_req = join "\n",
285             $method,
286             $path,
287             $query;
288 121     271   896 my @can_header = map { lc($_) . ':' . $header{$_} } sort_by { lc } keys %header;
  271         2027  
  271         1645  
289 121         581 $can_req .= "\n" . join "\n", @can_header;
290 121         588 $can_req .= "\n\n" . join ';', sort map lc, keys %header;
291 121         540 $self->{signed_headers} = join ';', sort map lc, keys %header;
292 121         1201 $can_req .= "\n" . sha256_hex($payload);
293 121         601 return $can_req;
294             }
295              
296             =head2 string_to_sign
297              
298             Returns the \n-separated string as the last step before
299             generating the signature itself.
300              
301             =cut
302              
303             sub string_to_sign {
304 91     91 1 16393 my $self = shift;
305 91         190 my $can_req = $self->canonical_request;
306 91         101 $log->debugf("Canonical request:\n%s", $can_req) if DEBUG;
307 91         701 my $hashed = sha256_hex($can_req);
308 91         91 $log->debugf("Hashed [%s]", $hashed) if DEBUG;
309 91         274 my $to_sign = join "\n",
310             $self->algorithm,
311             $self->date,
312             $self->scope,
313             $hashed;
314 91         99 $log->debugf("To sign:\n%s", $to_sign) if DEBUG;
315 91         326 $to_sign
316             }
317              
318             =head2 calculate_signature
319              
320             Calculates the signature for the current request and returns it
321             as a string suitable for the C header.
322              
323             =cut
324              
325             sub calculate_signature {
326 61     61 1 14700 my $self = shift;
327 61 50       155 die "No secret key" unless defined($self->secret_key);
328 61 50       118 die "No scope" unless defined($self->scope);
329 61         116 my $hmac = 'AWS4' . $self->secret_key;
330 61         419 $hmac = hmac($_, $hmac, \&sha256) for split qr{/}, $self->scope;
331 61         3814 my $signature = hmac_hex($self->string_to_sign, $hmac, \&sha256);
332 61         1458 my $headers = $self->signed_headers;
333 61         103 return $self->algorithm . ' Credential=' . $self->access_key . '/' . $self->scope . ', SignedHeaders=' . $headers . ', Signature=' . $signature;
334             }
335              
336             =head2 signed_request
337              
338             Returns a signed version of the request.
339              
340             =cut
341              
342             sub signed_request {
343 31     31 1 14622 my $self = shift;
344 31         55 my $req = shift;
345 31         73 my $signature = $self->calculate_signature;
346 31         273 $req =~ s{\x0D\x0A\x0D\x0A}{\x0D\x0AAuthorization: $signature\x0D\x0A\x0D\x0A};
347 31         176 $req
348             }
349              
350             1;
351              
352             __END__