File Coverage

blib/lib/WebService/Amazon/Signature/v4.pm
Criterion Covered Total %
statement 128 150 85.3
branch 17 26 65.3
condition 5 9 55.5
subroutine 30 32 93.7
pod 14 14 100.0
total 194 231 83.9


line stmt bran cond sub pod time code
1             package WebService::Amazon::Signature::v4;
2             {
3             $WebService::Amazon::Signature::v4::VERSION = '0.001';
4             }
5 1     1   6 use strict;
  1         2  
  1         33  
6 1     1   6 use warnings;
  1         2  
  1         29  
7 1     1   897 use parent qw(WebService::Amazon::Signature);
  1         357  
  1         6  
8              
9             =head1 NAME
10              
11             WebService::Amazon::Signature::v4 - support for v4 of the Amazon signing method
12              
13             =head1 VERSION
14              
15             version 0.001
16              
17             =head1 SYNOPSIS
18              
19             my $req = 'GET / HTTP/1.1 ...';
20             my $amz = WebService::Amazon::Signature::v4->new(
21             scope => '20110909/us-east-1/host/aws4_request',
22             access_key => 'AKIDEXAMPLE',
23             secret_key => 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY',
24             host_port => 'dynamodb.us-west-2.amazonaws.com',
25             );
26             $amz->parse_request($req)
27             my $signed_req = $amz->signed_request($req);
28              
29             =head1 DESCRIPTION
30              
31             =cut
32              
33 1     1   929 use POSIX qw(strftime);
  1         9171  
  1         7  
34 1     1   2081 use POSIX::2008;
  1         2597  
  1         98  
35 1     1   1219 use Digest::SHA qw(sha256 sha256_hex);
  1         4436  
  1         151  
36 1     1   1004 use Digest::HMAC qw(hmac hmac_hex);
  1         607  
  1         75  
37 1     1   963 use List::UtilsBy qw(sort_by);
  1         1295  
  1         57  
38 1     1   699 use HTTP::StreamParser::Request;
  1         5651  
  1         25  
39 1     1   843 use URI;
  1         4630  
  1         36  
40 1     1   744 use URI::QueryParam;
  1         587  
  1         60  
41 1     1   5 use URI::Escape qw(uri_escape_utf8 uri_unescape);
  1         2  
  1         2029  
42              
43             =head1 METHODS - Constructor
44              
45             =cut
46              
47             =head2 new
48              
49             Instantiate a signing object. Expects the following named parameters:
50              
51             =over 4
52              
53             =item * scope - the scope used for requests, typically something like C<20130112/us-west-2/dynamodb/aws4_request>
54              
55             =item * secret_key - your secret key
56              
57             =item * access_key - your access key
58              
59             =item * host_port - the host and optional port info, will be something like C
60              
61             =back
62              
63             =cut
64              
65             sub new {
66 31     31 1 45 my $class = shift;
67 31         279 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 482 sub algorithm { shift->{algorithm} }
82              
83             =head2 host_port
84              
85             Read-only accessor for the host and optional port information,
86             as a colon-separated string (e.g. C).
87              
88             =cut
89              
90 121     121 1 590 sub host_port { shift->{host_port} }
91              
92             =head2 date
93              
94             Read-only accessor for the date field.
95              
96             =cut
97              
98 91     91 1 294 sub date { shift->{date} }
99              
100             =head2 scope
101              
102             Read-only accessor for scope information - typically something like
103             C<20110909/us-east-1/host/aws4_request>.
104              
105             =cut
106              
107 213     213 1 1281 sub scope { shift->{scope} }
108              
109             =head2 access_key
110              
111             Readonly accessor for the access key used when signing requests.
112              
113             =cut
114              
115 61     61 1 159 sub access_key { shift->{access_key} }
116              
117             =head2 secret_key
118              
119             Readonly accessor for the secret key used when signing requests.
120              
121             =cut
122              
123 61     61 1 169 sub secret_key { shift->{secret_key} }
124              
125             =head2 signed_headers
126              
127             Read-only accessor for the headers used for signing purposes
128             (a string consisting of the lowercase headers separated by ;
129             in lexical order)
130              
131             =cut
132              
133 61     61 1 131 sub signed_headers { shift->{signed_headers} }
134              
135             =head1 METHODS
136              
137             =head2 parse_request
138              
139             Parses a given request. Takes a single parameter - the HTTP request as a string.
140              
141             =cut
142              
143             sub parse_request {
144 31     31 1 16220 my $self = shift;
145 31         44 my $txt = shift;
146 31         342 my $parser = HTTP::StreamParser::Request->new;
147 31         850 my $method;
148             my $uri;
149 0         0 my %header;
150 0         0 my @headers;
151 31         54 my $payload = '';
152             $parser->subscribe_to_event(
153 31     31   2265 http_method => sub { $method = $_[1] },
154 31     31   2866 http_uri => sub { $uri = $_[1]; },
155             http_header => sub {
156 75 100   75   6619 if(exists $header{lc $_[1]}) {
157 5 100       25 $header{lc $_[1]} = [
158             $header{lc $_[1]}
159             ] unless ref $header{lc $_[1]};
160 5         6 push @{$header{lc $_[1]}}, $_[2]
  5         25  
161             } else {
162 70         403 $header{lc $_[1]} = $_[2]
163             }
164             },
165             http_body_chunk => sub {
166 2     2   146 $payload .= $_[1]
167             }
168 31         416 );
169 31         1584 $parser->parse($txt);
170 31         1849 $self->{headers} = \@headers;
171 31         73 $self->{header} = \%header;
172 31         67 $self->{method} = $method;
173 31         120 $self->{uri} = $uri;
174 31         57 $self->{payload} = $payload;
175 31         475 $self
176             }
177              
178             =head2 from_http_request
179              
180             Parses information from an L instance.
181              
182             =cut
183              
184             sub from_http_request {
185 0     0 1 0 my $self = shift;
186 0         0 my $req = shift;
187 0         0 $self->{method} = $req->method;
188 0         0 $self->{uri} = '' . $req->uri;
189 0         0 $self->{payload} = $req->content;
190 0         0 my %header;
191             $req->scan(sub {
192 0     0   0 my ($k, $v) = @_;
193 0 0       0 if(exists $header{lc $k}) {
194 0 0       0 $header{lc $k} = [
195             $header{lc $k}
196             ] unless ref $header{lc $k};
197 0         0 push @{$header{lc $k}}, $v
  0         0  
198             } else {
199 0         0 $header{lc $k} = $v
200             }
201 0         0 });
202 0         0 $self->{header} = \%header;
203 0         0 $self
204             }
205              
206             =head2 canonical_request
207              
208             Returns the string form of the canonical request, used
209             as an intermediate point in generating the signature.
210              
211             =cut
212              
213             sub canonical_request {
214 121     121 1 13985 my $self = shift;
215              
216 121         141 my %header = %{$self->{header}};
  121         831  
217 121         259 my $method = $self->{method};
218 121         174 my $payload = $self->{payload};
219 121         182 my $uri = $self->{uri};
220              
221             # Strip all leading/trailing whitespace from headers,
222             # convert to canonical comma-separated format
223 121         551 s/^\s+//, s/\s+$// for map @$_, grep ref($_), values %header;
224 121         430 $_ = join ',', sort @$_ for grep ref($_), values %header;
225 121         1150 s/^\s+//, s/\s+$// for values %header;
226              
227 121         266 $uri =~ s{ .*$}{}g;
228 121         189 $uri =~ s{#}{%23}g;
229              
230             # We're not actually connecting to this so a default
231             # value should be safe here.
232 121   50     272 my $host_port = $self->host_port || 'localhost:8000';
233 121         652 my $u = URI->new('http://' . $host_port . $uri)->canonical;
234 121         30128 $uri = $u->path;
235 121         1066 my $path = '';
236 121         296 while(length $uri) {
237 173 50 33     1581 if(substr($uri, 0, 3) eq '../') {
    50          
    100          
    100          
    100          
    50          
    50          
238 0         0 substr $uri, 0, 3, '';
239             } elsif(substr($uri, 0, 2) eq './') {
240 0         0 substr $uri, 0, 2, '';
241             } elsif(substr($uri, 0, 3) eq '/./') {
242 8         22 substr $uri, 0, 3, '/';
243             } elsif(substr($uri, 0, 4) eq '/../') {
244 4         7 substr $uri, 0, 4, '/';
245 4         28 $path =~ s{/?[^/]*$}{};
246             } elsif(substr($uri, 0, 3) eq '/..') {
247 8         15 substr $uri, 0, 3, '/';
248 8         43 $path =~ s{/?[^/]*$}{};
249             } elsif(substr($uri, 0, 2) eq '/.') {
250 0         0 substr $uri, 0, 3, '/';
251             } elsif($uri eq '.' or $uri eq '..') {
252 0         0 $uri = '';
253             } else {
254 153 50       1001 $path .= $1 if $uri =~ s{^(/?[^/]*)}{};
255             }
256             }
257 121         406 $path =~ s{/+}{/}g;
258 121         346 $u->path($path);
259 121         2815 my @query;
260 121 100       388 if(length $u->query) {
261 40         459 for my $qp (split /&/, $u->query) {
262 56         546 my ($k, $v) = map uri_unescape($_), split /=/, $qp, 2;
263 56         767 for ($k, $v) {
264 112   100     1038 $_ //= '';
265 112         145 s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  0         0  
266 112         287 $_ = Encode::decode('UTF-8' => $_);
267 112         4054 s{\+}{ }g;
268 112         258 $_ = uri_escape_utf8($_);
269             }
270 56         959 push @query, "$k=$_" for $v
271             }
272             }
273 121         1037 my $query = join '&', sort @query;
274 121   50     375 $self->{date} = ($header{date} && eval {
275             my @parts = map $_ // 0, POSIX::2008::strptime $header{date}, '%a, %d %b %Y %H:%M:%S GMT';
276             @parts ? strftime '%Y%m%dT%H%M%SZ', @parts
277             : ();
278             }) || '20110909T23:36:00GMT';
279              
280              
281 121         393 my $can_req = join "\n",
282             $method,
283             $path,
284             $query;
285 121     271   966 my @can_header = map { lc($_) . ':' . $header{$_} } sort_by { lc } keys %header;
  271         2197  
  271         1837  
286 121         643 $can_req .= "\n" . join "\n", @can_header;
287 121         711 $can_req .= "\n\n" . join ';', sort map lc, keys %header;
288 121         662 $self->{signed_headers} = join ';', sort map lc, keys %header;
289 121         1181 $can_req .= "\n" . sha256_hex($payload);
290 121         635 return $can_req;
291             }
292              
293             =head2 string_to_sign
294              
295             Returns the \n-separated string as the last step before
296             generating the signature itself.
297              
298             =cut
299              
300             sub string_to_sign {
301 91     91 1 15477 my $self = shift;
302 91         197 my $can_req = $self->canonical_request;
303 91         741 my $hashed = sha256_hex($can_req);
304 91         244 my $to_sign = join "\n",
305             $self->algorithm,
306             $self->date,
307             $self->scope,
308             $hashed;
309             }
310              
311             =head2 calculate_signature
312              
313             Calculates the signature for the current request and returns it
314             as a string suitable for the C header.
315              
316             =cut
317              
318             sub calculate_signature {
319 61     61 1 14740 my $self = shift;
320 61         148 my $hmac = 'AWS4' . $self->secret_key;
321 61         411 $hmac = hmac($_, $hmac, \&sha256) for split qr{/}, $self->scope;
322 61         4271 my $signature = hmac_hex($self->string_to_sign, $hmac, \&sha256);
323 61         1451 my $headers = $self->signed_headers;
324 61         104 return $self->algorithm . ' Credential=' . $self->access_key . '/' . $self->scope . ', SignedHeaders=' . $headers . ', Signature=' . $signature;
325             }
326              
327             =head2 signed_request
328              
329             Returns a signed version of the request.
330              
331             =cut
332              
333             sub signed_request {
334 31     31 1 14689 my $self = shift;
335 31         49 my $req = shift;
336 31         74 my $signature = $self->calculate_signature;
337 31         263 $req =~ s{\x0D\x0A\x0D\x0A}{\x0D\x0AAuthorization: $signature\x0D\x0A\x0D\x0A};
338 31         177 $req
339             }
340              
341             1;
342              
343             __END__