File Coverage

lib/Mojo/AWS.pm
Criterion Covered Total %
statement 112 113 99.1
branch 4 8 50.0
condition 3 7 42.8
subroutine 21 21 100.0
pod 0 17 0.0
total 140 166 84.3


line stmt bran cond sub pod time code
1             package Mojo::AWS;
2 1     1   732 use Mojo::Base -base;
  1         2  
  1         11  
3 1     1   204 use Digest::SHA qw(hmac_sha256 hmac_sha256_hex sha256_hex);
  1         1  
  1         61  
4 1     1   5 use Mojo::Util qw(url_escape);
  1         2  
  1         50  
5 1     1   5 use Scalar::Util 'blessed';
  1         2  
  1         1988  
6              
7             our $VERSION = '0.10';
8              
9             has service => '';
10             has region => '';
11             has access_key => '';
12             has secret_key => '';
13             has transactor => '';
14              
15             sub request_method {
16 2     2 0 10 uc pop;
17             }
18              
19             sub canonical_uri {
20 4     4 0 661 my $path = join '/' => map { url_escape $_ } split /\// => pop->path;
  2         277  
21 4         197 $path .= '/';
22 4         15 return $path;
23             }
24              
25             sub canonical_query_string {
26 6     6 0 422 my $url = pop;
27              
28 6         9 my @cqs = ();
29 6         11 my $names = $url->query->names;
30 6         1335 for my $name (@$names) {
31 12         112 my $values = $url->query->every_param($name);
32              
33             ## FIXME: we assume a lexicographical sort. I don't know how
34             ## FIXME: AWS prefers to sort numerical values and couldn't
35             ## FIXME: find any guidance on that
36 12         349 for my $val (sort { $a cmp $b } @$values) {
  5         11  
37 17         96 push @cqs, join '=', url_escape($name) => url_escape($val);
38             }
39             }
40              
41 6         111 return join '&' => @cqs;
42             }
43              
44             sub canonical_headers {
45 3   50 3 0 972 my $headers = Mojo::Headers->new->from_hash(pop // {});
46              
47 3         230 my @headers = ();
48 3         7 my $names = $headers->names;
49 3         50 for my $name (sort { lc($a) cmp lc($b) } @$names) {
  15         26  
50 12         24 my $values = $headers->every_header($name);
51              
52             my $value
53 12         48 = join ',' => map { s/ +/ /g; $_ } map { s/\s+$//; $_ } map { s/^\s*//; $_ } @$values;
  12         26  
  12         23  
  12         26  
  12         21  
  12         36  
  12         22  
54              
55 12         38 push @headers, lc($name) . ':' . $value;
56             }
57              
58 3         18 my $response = join "\n" => @headers;
59 3         26 return $response . "\n";
60             }
61              
62             sub signed_headers {
63             ## FIXME: ensure 'host' (http/1.1) or ':authority' (http/2) header is present
64             ## FIXME: ensure date or 'x-amz-date' is present and in iso 8601 format
65 2     2 0 3 return join ';' => sort map { lc $_ } @{pop()};
  7         21  
  2         5  
66             }
67              
68             sub hashed_payload {
69 3     3 0 49 return lc sha256_hex(pop);
70             }
71              
72             sub canonical_request {
73 2     2 0 196 my $self = shift;
74 2         8 my %args = @_;
75 2         8 my $url = Mojo::URL->new($args{url});
76              
77             my $creq = join "\n" => $self->request_method($args{method}),
78             $self->canonical_uri($url), $self->canonical_query_string($url),
79             $self->canonical_headers($args{headers}), $self->signed_headers($args{signed_headers}),
80 2         1101 $self->hashed_payload($args{payload});
81              
82 2         15 return $creq;
83             }
84              
85             sub canonical_request_hash {
86 2     2 0 25 return lc sha256_hex(pop);
87             }
88              
89             sub aws_algorithm {
90 4     4 0 8 return 'AWS4-HMAC-SHA256';
91             }
92              
93             sub aws_datetime {
94 8     8 0 28 (my $date = Mojo::Date->new(pop)->to_datetime) =~ s/[^0-9TZ]//g;
95 8         959 return $date;
96             }
97              
98             sub aws_date {
99 5     5 0 7 my $self = shift;
100 5         10 (my $date = $self->aws_datetime(pop)) =~ s/^(\d+)T.*/$1/;
101 5         14 return $date;
102             }
103              
104             sub aws_credentials {
105 3     3 0 25 my $self = shift;
106 3         9 my %args = @_;
107              
108 3         10 return join '/' => $self->aws_date($args{datetime}),
109             $self->region, $self->service, 'aws4_request';
110             }
111              
112             sub string_to_sign {
113 2     2 0 10 my $self = shift;
114 2         7 my %args = @_;
115              
116             my $string = join "\n" => $self->aws_algorithm,
117             $self->aws_datetime($args{datetime}),
118             $self->aws_credentials(
119             datetime => $args{datetime},
120             region => $self->region,
121             service => $self->service
122             ),
123 2         5 $args{hash};
124              
125 2         26 return $string;
126             }
127              
128             sub signing_key {
129 2     2 0 55 my $self = shift;
130 2         7 my %args = @_;
131              
132 2         5 my $date = $self->aws_date($args{datetime});
133 2         7 my $kDate = hmac_sha256($date, 'AWS4' . $self->secret_key);
134 2         29 my $kRegion = hmac_sha256($self->region, $kDate);
135 2         21 my $kService = hmac_sha256($self->service, $kRegion);
136 2         30 my $kSigning = hmac_sha256('aws4_request', $kService);
137              
138 2         7 return $kSigning;
139             }
140              
141             sub signature {
142 2     2 0 617 my $self = shift;
143 2         6 my %args = @_;
144              
145 2         24 my $digest = hmac_sha256_hex($args{string_to_sign}, $args{signing_key});
146              
147 2         8 return $digest;
148             }
149              
150             sub authorization_header {
151 2     2 0 51 my $self = shift;
152 2         6 my %args = @_;
153              
154 2         5 my $algorithm = $self->aws_algorithm;
155 2         5 my $access_key = $self->access_key;
156 2         9 my $credential = $args{credential_scope};
157 2         2 my $signed_headers = join ';' => map {lc} @{$args{signed_headers}};
  7         16  
  2         5  
158 2         4 my $signature = $args{signature};
159 2         5 my $headers
160             = Mojo::Headers->new->authorization(
161             "$algorithm Credential=$access_key/$credential, SignedHeaders=$signed_headers, Signature=$signature"
162             );
163              
164 2         36 return $headers;
165             }
166              
167             sub signed_request {
168 1     1 0 966 my $self = shift;
169 1         4 my %args = @_;
170              
171             ## FIXME: more guards
172 1 50       3 die "Parameter 'method' required.\n" unless $args{method};
173 1 50       5 die "Parameter 'url' required.\n" unless $args{url};
174 1 50 33     15 die "Parameter 'url' must be a Mojo::URL object\n" if !blessed $args{url} || blessed $args{url} ne 'Mojo::URL';
175              
176             ## FIXME: Is it possible that x-amz-date and Date differ if they are
177             ## FIXME: created across a second?
178              
179             ## FIXME (disposable build_tx): is there a better way to build a request body?
180             my $payload
181 1         3 = $self->transactor->tx($args{method} => $args{url}, @{$args{payload}})->req->body;
  1         10  
182              
183             ## build a normal transaction
184             my $headers = Mojo::Headers->new->from_hash(
185             {
186             'Host' => $args{url}->host,
187             'x-amz-content-sha256' => $self->hashed_payload($payload),
188             'x-amz-date' => $self->aws_datetime($args{datetime}),
189 1   50     1326 %{$args{signed_headers} // {}}
  1         9  
190             },
191             );
192              
193             ## build the authorization header
194 1         72 my $signed_headers = [sort map {lc} @{$headers->names}];
  4         24  
  1         3  
195              
196             my $aws_credentials = $self->aws_credentials(
197             datetime => $args{datetime},
198 1         6 region => $self->region,
199             service => $self->service
200             );
201              
202             my $aws_signing_key = $self->signing_key(
203             secret => $self->secret_key,
204             datetime => $args{datetime},
205 1         11 region => $self->region,
206             service => $self->service
207             );
208              
209             my $canonical_request = $self->canonical_request(
210             url => $args{url},
211             method => $args{method},
212 1         4 headers => $headers->to_hash,
213             signed_headers => $signed_headers,
214             payload => $payload,
215             );
216              
217 1         4 my $canonical_request_hash = $self->canonical_request_hash($canonical_request);
218              
219             my $string_to_sign = $self->string_to_sign(
220             datetime => $args{datetime},
221 1         5 region => $self->region,
222             service => $self->service,
223             hash => $canonical_request_hash,
224             );
225              
226 1         4 my $signature
227             = $self->signature(signing_key => $aws_signing_key, string_to_sign => $string_to_sign);
228              
229 1         3 my $auth_header = $self->authorization_header(
230             access_key => $self->access_key,
231             credential_scope => $aws_credentials,
232             signed_headers => $signed_headers,
233             signature => $signature,
234             );
235              
236 1 50       4 $headers->add(%{$args{headers}}) if $args{headers};
  0         0  
237 1         6 $headers->add(Authorization => $auth_header->authorization);
238             my $tx
239 1         23 = $self->transactor->tx($args{method}, $args{url}, $headers->to_hash, @{$args{payload}});
  1         72  
240              
241 1         996 return $tx;
242             }
243              
244             1;