File Coverage

lib/Mojo/AWS.pm
Criterion Covered Total %
statement 101 102 99.0
branch 1 2 50.0
condition 1 2 50.0
subroutine 20 20 100.0
pod 0 17 0.0
total 123 143 86.0


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