File Coverage

blib/lib/Mojolicious/Plugin/AWS/SNS.pm
Criterion Covered Total %
statement 127 134 94.7
branch n/a
condition 1 8 12.5
subroutine 22 23 95.6
pod 1 1 100.0
total 151 166 90.9


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::AWS::SNS;
2 1     1   1228 use Mojo::Base 'Mojolicious::Plugin';
  1         2  
  1         8  
3 1     1   258 use Digest::SHA qw(hmac_sha256 hmac_sha256_hex sha256_hex);
  1         2  
  1         72  
4 1     1   6 use Mojo::Util qw(url_escape);
  1         2  
  1         43  
5 1     1   5 use Mojo::JSON 'encode_json';
  1         2  
  1         2182  
6              
7             our $VERSION = '0.03';
8              
9             sub register {
10 1     1 1 45 my ($self, $app) = @_;
11              
12             $app->helper(
13             request_method => sub {
14 2     2   37 uc pop;
15             }
16 1         14 );
17              
18             $app->helper(
19             canonical_uri => sub {
20 4     4   840 my $path = join '/' => map { url_escape $_ } split /\// => pop->path;
  2         363  
21 4         192 $path .= '/';
22 4         26 return $path;
23             }
24 1         190 );
25              
26             $app->helper(
27             canonical_query_string => sub {
28 6     6   815 my $url = pop;
29              
30 6         12 my @cqs = ();
31 6         16 my $names = $url->query->names;
32 6         1360 for my $name (@$names) {
33 12         125 my $values = $url->query->every_param($name);
34              
35             ## FIXME: we assume a lexicographical sort. I don't know how
36             ## FIXME: AWS prefers to sort numerical values and couldn't
37             ## FIXME: find any guidance on that
38 12         358 for my $val (sort { $a cmp $b } @$values) {
  5         21  
39 17         99 push @cqs, join '=', url_escape($name) => url_escape($val);
40             }
41             }
42              
43 6         113 return join '&' => @cqs;
44             }
45 1         89 );
46              
47             $app->helper(
48             canonical_headers => sub {
49 3     3   424 my $c = shift;
50 3   50     10 my $headers = Mojo::Headers->new->from_hash(shift // {});
51              
52 3         225 my @headers = ();
53 3         8 my $names = $headers->names;
54 3         51 for my $name (sort { lc($a) cmp lc($b) } @$names) {
  15         31  
55 12         34 my $values = $headers->every_header($name);
56              
57 12         34 my $value = join ',' => map { s/ +/ /g; $_ }
  12         30  
58 12         50 map { s/\s+$//; $_ } map { s/^\s*//; $_ } @$values;
  12         32  
  12         23  
  12         36  
  12         29  
59              
60 12         41 push @headers, lc($name) . ':' . $value;
61             }
62              
63 3         8 my $response = join "\n" => @headers;
64 3         31 return $response . "\n";
65             }
66 1         86 );
67              
68             $app->helper(
69             signed_headers => sub {
70 2     2   17 my $c = shift;
71             ## FIXME: ensure 'host' (http/1.1) or ':authority' (http/2) header is present
72             ## FIXME: ensure date or 'x-amz-date' is present and in iso 8601 format
73 2         4 return join ';' => sort map { lc $_ } @{shift()};
  7         27  
  2         3  
74             }
75 1         84 );
76              
77             $app->helper(
78             hashed_payload => sub {
79 2     2   14 my $c = shift;
80 2         3 my $payload = shift;
81              
82 2         34 return lc sha256_hex($payload);
83             }
84 1         95 );
85              
86             $app->helper(
87             canonical_request => sub {
88 2     2   1915 my $c = shift;
89 2         13 my %args = @_;
90 2         7 my $url = Mojo::URL->new($args{url});
91              
92             my $creq = join "\n" => $c->request_method($args{method}),
93             $c->canonical_uri($url), $c->canonical_query_string($url),
94             $c->canonical_headers($args{headers}), $c->signed_headers($args{signed_headers}),
95 2         1113 $c->hashed_payload($args{payload});
96              
97 2         16 return $creq;
98             }
99 1         98 );
100              
101             $app->helper(
102             canonical_request_hash => sub {
103 2     2   156 my $c = shift;
104 2         6 my $request = shift;
105              
106 2         39 return lc sha256_hex($request);
107             }
108 1         84 );
109              
110             ##
111              
112             $app->helper(
113             aws_algorithm => sub {
114 4     4   34 return 'AWS4-HMAC-SHA256';
115             }
116 1         97 );
117              
118             $app->helper(
119             aws_datetime => sub {
120 7     7   67 (my $date = Mojo::Date->new(pop)->to_datetime) =~ s/[^0-9TZ]//g;
121 7         871 return $date;
122             }
123 1         81 );
124              
125             $app->helper(
126             aws_date => sub {
127 5     5   33 my $c = shift;
128 5         47 (my $date = $c->aws_datetime(pop)) =~ s/^(\d+)T.*/$1/;
129 5         26 return $date;
130             }
131 1         80 );
132              
133             $app->helper(
134             aws_credentials => sub {
135 3     3   27 my $c = shift;
136 3         13 my %args = @_;
137              
138             return join '/' => $c->aws_date($args{datetime}),
139 3         11 $args{region}, $args{service}, 'aws4_request';
140             }
141 1         148 );
142              
143             $app->helper(
144             string_to_sign => sub {
145 2     2   116 my $c = shift;
146 2         8 my %args = @_;
147              
148             my $string = join "\n" => $c->aws_algorithm,
149             $c->aws_datetime($args{datetime}),
150             $c->aws_credentials(
151             datetime => $args{datetime},
152             region => $args{region},
153             service => $args{service}
154             ),
155 2         8 $args{hash};
156              
157 2         11 return $string;
158             }
159 1         107 );
160              
161             ##
162              
163             $app->helper(
164             signing_key => sub {
165 2     2   120 my $c = shift;
166 2         8 my %args = @_;
167              
168 2         7 my $date = $c->aws_date($args{datetime});
169 2         29 my $kDate = hmac_sha256($date, 'AWS4' . $args{secret});
170 2         19 my $kRegion = hmac_sha256($args{region}, $kDate);
171 2         16 my $kService = hmac_sha256($args{service}, $kRegion);
172 2         15 my $kSigning = hmac_sha256('aws4_request', $kService);
173              
174 2         8 return $kSigning;
175             }
176 1         106 );
177              
178             $app->helper(
179             signature => sub {
180 2     2   760 my $c = shift;
181 2         9 my %args = @_;
182              
183 2         23 my $digest = hmac_sha256_hex($args{string_to_sign}, $args{signing_key});
184              
185 2         10 return $digest;
186             }
187 1         95 );
188              
189             $app->helper(
190             authorization_header => sub {
191 2     2   120 my $c = shift;
192 2         16 my %args = @_;
193              
194 2         8 my $algorithm = $c->aws_algorithm;
195 2         5 my $access_key = $args{access_key};
196 2         4 my $credential = $args{credential_scope};
197 2         14 my $signed_headers = join ';' => map {lc} @{$args{signed_headers}};
  7         18  
  2         9  
198 2         5 my $signature = $args{signature};
199 2         14 my $headers
200             = Mojo::Headers->new->authorization(
201             "$algorithm Credential=$access_key/$credential, SignedHeaders=$signed_headers, Signature=$signature"
202             );
203              
204 2         43 return $headers;
205             }
206 1         124 );
207              
208             $app->helper(
209             signed_request => sub {
210 1     1   1190 my $c = shift;
211 1         17 my %args = @_;
212              
213             ## build a normal transaction
214             my $headers = Mojo::Headers->new->from_hash(
215             {
216             'X-Amz-Date' => $args{datetime},
217             'Host' => $args{url}->host,
218 1         4 'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8',
219             'Accept' => 'application/json',
220             }
221             );
222              
223             ## build the authorization header
224 1         118 my $signed_headers = [qw/content-type host x-amz-date accept/];
225             my $aws_credentials = $c->aws_credentials(
226             datetime => $args{datetime},
227             region => $args{region},
228             service => $args{service}
229 1         5 );
230             my $aws_signing_key = $c->signing_key(
231             secret => $args{secret_key},
232             datetime => $args{datetime},
233             region => $args{region},
234             service => $args{service}
235 1         8 );
236             ## FIXME (disposable build_tx): is there a better way to build a request body?
237             my $canonical_request = $c->canonical_request(
238             url => $args{url},
239             method => 'POST',
240             headers => $headers->to_hash,
241             signed_headers => $signed_headers,
242             payload =>
243             $c->ua->build_tx(POST => $args{url}, $headers->to_hash, form => $args{form})
244 1         10 ->req->body
245             );
246 1         34 my $canonical_request_hash = $c->canonical_request_hash($canonical_request);
247             my $string_to_sign = $c->string_to_sign(
248             datetime => $args{datetime},
249             region => $args{region},
250             service => $args{service},
251 1         7 hash => $canonical_request_hash,
252             );
253 1         26 my $signature = $c->signature(
254             signing_key => $aws_signing_key,
255             string_to_sign => $string_to_sign,
256             );
257              
258             my $auth_header = $c->authorization_header(
259             access_key => $args{access_key},
260 1         9 credential_scope => $aws_credentials,
261             signed_headers => $signed_headers,
262             signature => $signature,
263             );
264              
265 1         3 $headers->add(Authorization => $auth_header->authorization);
266             my $tx
267 1         26 = $c->ua->build_tx(POST => $args{url}, $headers->to_hash, form => $args{form});
268              
269 1         984 return $tx;
270             }
271 1         85 );
272              
273             $app->helper(
274             sns_publish => sub {
275 0     0     my $c = shift;
276 0           my %args = @_;
277              
278 0           my $region = $args{region};
279              
280 0   0       $args{datetime} ||= Mojo::Date->new(time)->to_datetime;
281 0   0       $args{url} ||= Mojo::URL->new("https://sns.$region.amazonaws.com");
282              
283             my $tx = $c->signed_request(
284             datetime => $args{datetime},
285             url => $args{url},
286             service => 'sns',
287             region => $region,
288             access_key => $args{access_key},
289             secret_key => $args{secret_key},
290             form => {
291             Action => 'Publish',
292             TopicArn => $args{topic},
293             Subject => $args{subject},
294             MessageStructure => 'json',
295 0           Message => encode_json($args{message}),
296             Version => '2010-03-31',
297             }
298             );
299              
300 0           $c->ua->start_p($tx);
301             }
302 1         98 );
303             }
304              
305             1;
306             __END__