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   715 use Mojo::Base -base;
  1         2  
  1         7  
3 1     1   189 use Digest::SHA qw(hmac_sha256 hmac_sha256_hex sha256_hex);
  1         1  
  1         57  
4 1     1   4 use Mojo::Util qw(url_escape);
  1         2  
  1         37  
5 1     1   4 use Scalar::Util 'blessed';
  1         1  
  1         1969  
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 9 uc pop;
17             }
18              
19             sub canonical_uri {
20 4     4 0 622 my $path = join '/' => map { url_escape $_ } split /\// => pop->path;
  2         261  
21 4         182 $path .= '/';
22 4         14 return $path;
23             }
24              
25             sub canonical_query_string {
26 6     6 0 376 my $url = pop;
27              
28 6         11 my @cqs = ();
29 6         11 my $names = $url->query->names;
30 6         1251 for my $name (@$names) {
31 12         108 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         336 for my $val (sort { $a cmp $b } @$values) {
  5         13  
37 17         88 push @cqs, join '=', url_escape($name) => url_escape($val);
38             }
39             }
40              
41 6         106 return join '&' => @cqs;
42             }
43              
44             sub canonical_headers {
45 3   50 3 0 901 my $headers = Mojo::Headers->new->from_hash(pop // {});
46              
47 3         219 my @headers = ();
48 3         6 my $names = $headers->names;
49 3         48 for my $name (sort { lc($a) cmp lc($b) } @$names) {
  11         19  
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         25  
  12         23  
  12         27  
  12         19  
  12         31  
  12         21  
54              
55 12         35 push @headers, lc($name) . ':' . $value;
56             }
57              
58 3         7 my $response = join "\n" => @headers;
59 3         21 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 4 return join ';' => sort map { lc $_ } @{pop()};
  7         18  
  2         3  
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 181 my $self = shift;
74 2         7 my %args = @_;
75 2         43 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         1033 $self->hashed_payload($args{payload});
81              
82 2         13 return $creq;
83             }
84              
85             sub canonical_request_hash {
86 2     2 0 22 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 23 (my $date = Mojo::Date->new(pop)->to_datetime) =~ s/[^0-9TZ]//g;
95 8         911 return $date;
96             }
97              
98             sub aws_date {
99 5     5 0 6 my $self = shift;
100 5         8 (my $date = $self->aws_datetime(pop)) =~ s/^(\d+)T.*/$1/;
101 5         15 return $date;
102             }
103              
104             sub aws_credentials {
105 3     3 0 23 my $self = shift;
106 3         9 my %args = @_;
107              
108 3         7 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         6 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         24 return $string;
126             }
127              
128             sub signing_key {
129 2     2 0 52 my $self = shift;
130 2         9 my %args = @_;
131              
132 2         4 my $date = $self->aws_date($args{datetime});
133 2         6 my $kDate = hmac_sha256($date, 'AWS4' . $self->secret_key);
134 2         37 my $kRegion = hmac_sha256($self->region, $kDate);
135 2         22 my $kService = hmac_sha256($self->service, $kRegion);
136 2         28 my $kSigning = hmac_sha256('aws4_request', $kService);
137              
138 2         6 return $kSigning;
139             }
140              
141             sub signature {
142 2     2 0 650 my $self = shift;
143 2         6 my %args = @_;
144              
145 2         21 my $digest = hmac_sha256_hex($args{string_to_sign}, $args{signing_key});
146              
147 2         7 return $digest;
148             }
149              
150             sub authorization_header {
151 2     2 0 50 my $self = shift;
152 2         6 my %args = @_;
153              
154 2         4 my $algorithm = $self->aws_algorithm;
155 2         5 my $access_key = $self->access_key;
156 2         7 my $credential = $args{credential_scope};
157 2         3 my $signed_headers = join ';' => map {lc} @{$args{signed_headers}};
  7         14  
  2         4  
158 2         5 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 923 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       3 die "Parameter 'url' required.\n" unless $args{url};
174 1 50 33     16 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         4 = $self->transactor->tx($args{method} => $args{url}, @{$args{payload}})->req->body;
  1         8  
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     1187 %{$args{signed_headers} // {}}
  1         21  
190             },
191             );
192              
193             ## build the authorization header
194 1         69 my $signed_headers = [sort map {lc} @{$headers->names}];
  4         23  
  1         3  
195              
196             my $aws_credentials = $self->aws_credentials(
197             datetime => $args{datetime},
198 1         4 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         10 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         3 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         4 region => $self->region,
222             service => $self->service,
223             hash => $canonical_request_hash,
224             );
225              
226 1         3 my $signature
227             = $self->signature(signing_key => $aws_signing_key, string_to_sign => $string_to_sign);
228              
229 1         4 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         3 $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         67  
240              
241 1         920 return $tx;
242             }
243              
244             1;