File Coverage

blib/lib/Mojolicious/Plugin/WebPush.pm
Criterion Covered Total %
statement 103 107 96.2
branch 15 24 62.5
condition 7 15 46.6
subroutine 23 26 88.4
pod 1 2 50.0
total 149 174 85.6


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::WebPush;
2 4     4   17366 use Mojo::Base 'Mojolicious::Plugin';
  4         9  
  4         25  
3 4     4   672 use Mojo::JSON qw(decode_json encode_json);
  4         8  
  4         201  
4 4     4   26 use Crypt::PK::ECC;
  4         13  
  4         242  
5 4     4   29 use MIME::Base64 qw(encode_base64url decode_base64url);
  4         9  
  4         326  
6 4     4   2541 use Crypt::JWT qw(encode_jwt decode_jwt);
  4         126708  
  4         296  
7 4     4   1440 use Crypt::RFC8188 qw(ece_encrypt_aes128gcm);
  4         155292  
  4         8845  
8              
9             our $VERSION = '0.03';
10              
11             my @MANDATORY_CONF = qw(
12             subs_session2user_p
13             save_endpoint
14             subs_create_p
15             subs_read_p
16             subs_delete_p
17             );
18             my @AUTH_CONF = qw(claim_sub ecc_private_key);
19             my $DEFAULT_PUSH_HANDLER = <<'EOF';
20             event => {
21             var msg = event.data.json();
22             var title = msg.title;
23             delete msg.title;
24             event.waitUntil(self.registration.showNotification(title, msg));
25             }
26             EOF
27              
28             sub _decode {
29 2     2   54 my ($bytes) = @_;
30 2         5 my $body = eval { decode_json($bytes) };
  2         8  
31             # conceal error info like versions from attackers
32 2 50       281 return (0, "Malformed request") if $@;
33 2         7 (1, $body);
34             }
35              
36             sub _error {
37 1     1   10 my ($c, $error) = @_;
38 1         8 $c->render(status => 500, json => { errors => [ { message => $error } ] });
39             }
40              
41             sub _make_route_handler {
42 4     4   21 my ($subs_session2user_p, $subs_create_p) = @_;
43             sub {
44 2     2   17001 my ($c) = @_;
45 2         7 my ($decode_ok, $body) = _decode($c->req->body);
46 2 50       9 return _error($c, $body) if !$decode_ok;
47 2         3 eval { validate_subs_info($body) };
  2         6  
48 2 100       510 return _error($c, $@) if $@;
49             return $subs_session2user_p->($c->session)->then(
50 1         5116 sub { $subs_create_p->($_[0], $body) },
51             )->then(
52 1         444 sub { $c->render(json => { data => { success => \1 } }) },
53 0         0 sub { _error($c, @_) },
54 1         5 );
55 4         68 };
56             }
57              
58             sub _make_auth_helper {
59 4     4   12 my ($app, $conf) = @_;
60 4   50     30 my $exp_offset = $conf->{claim_exp_offset} || 86400;
61 4         37 my $key = Crypt::PK::ECC->new($conf->{ecc_private_key});
62 4         23978 my $aud = $app->webpush->aud;
63 4         18881 my $claims_start = { aud => $aud, sub => $conf->{claim_sub} };
64 4         115 my $pkey = encode_base64url $key->export_key_raw('public');
65 4     0   105 $app->helper('webpush.public_key' => sub { $pkey });
  0         0  
66             sub {
67 3     3   1460 my ($c) = @_;
68 3         27 my $claims = { exp => time + $exp_offset, %$claims_start };
69 3         21 my $token = encode_jwt key => $key, alg => 'ES256', payload => $claims;
70 3         9303 "vapid t=$token,k=$pkey";
71 4         1732 };
72             }
73              
74             sub _aud_helper {
75 5     5   1731 $_[0]->ua->server->url->path(Mojo::Path->new->trailing_slash(0)).'';
76             }
77              
78             sub _verify_helper {
79 3     3   9347 my ($app, $auth_header_value) = @_;
80 3         16 (my $schema, $auth_header_value) = split ' ', $auth_header_value;
81 3 50       12 return if $schema ne 'vapid';
82 3         26 my %k2v = map split('=', $_), split ',', $auth_header_value;
83 3         11 eval {
84 3         17 my $key = Crypt::PK::ECC->new;
85 3         229 $key->import_key_raw(decode_base64url($k2v{k}), 'P-256');
86 3         8570 decode_jwt token => $k2v{t}, key => $key, alg => 'ES256', verify_exp => 0;
87             };
88             }
89              
90             sub _encrypt_helper {
91 3     3   7513 my ($c, $plaintext, $receiver_key, $auth_key) = @_;
92 3 50 33     28 die "Invalid p256dh key specified\n"
93             if length($receiver_key) != 65 or $receiver_key !~ /^\x04/;
94 3         21 my $onetime_key = Crypt::PK::ECC->new->generate_key('prime256v1');
95 3         8795 ece_encrypt_aes128gcm(
96             $plaintext, (undef) x 2, $onetime_key, $receiver_key, $auth_key,
97             );
98             }
99              
100             sub _send_helper {
101 2     2   3035 my ($c, $message, $user_id, $ttl, $urgency) = @_;
102 2   50     8 $ttl ||= 30;
103 2   50     6 $urgency ||= 'normal';
104             $c->webpush->read_p($user_id)->then(sub {
105 2     2   994 my ($subs_info) = @_;
106             my $body = $c->webpush->encrypt(
107             encode_json($message),
108 2         9 map decode_base64url($_), @{$subs_info->{keys}}{qw(p256dh auth)}
  2         172  
109             );
110 2         15347 my $headers = {
111             Authorization => $c->webpush->authorization,
112             'Content-Length' => length($body),
113             'Content-Encoding' => 'aes128gcm',
114             TTL => $ttl,
115             Urgency => $urgency,
116             };
117 2         16 $c->app->ua->post_p($subs_info->{endpoint}, $headers, $body);
118             })->then(sub {
119 2     2   88153 my ($tx) = @_;
120             return $c->webpush->delete_p($user_id)->then(sub {
121 1         309 { data => { success => \1 } }
122 2 100 66     8 }) if $tx->res->code == 404 or $tx->res->code == 410;
123 1 50       30 return { errors => [ { message => $tx->res->body } ] }
124             if $tx->res->code > 399;
125 1         11 { data => { success => \1 } };
126             }, sub {
127 0     0   0 { errors => [ { message => $_[0] } ] }
128 2         14 });
129             }
130              
131             sub register {
132 4     4 1 288 my ($self, $app, $conf) = @_;
133 4         26 my @config_errors = grep !exists $conf->{$_}, @MANDATORY_CONF;
134 4 50       21 die "Missing config keys @config_errors\n" if @config_errors;
135             $app->helper('webpush.create_p' => sub {
136 4     4   30760 eval { validate_subs_info($_[2]) };
  4         17  
137 4 50       18 return Mojo::Promise->reject($@) if $@;
138 4         30 $conf->{subs_create_p}->(@_[1,2]);
139 4         44 });
140 4     6   1851 $app->helper('webpush.read_p' => sub { $conf->{subs_read_p}->($_[1]) });
  6         5722  
141 4     3   1300 $app->helper('webpush.delete_p' => sub { $conf->{subs_delete_p}->($_[1]) });
  3         3413  
142 4         1309 $app->helper('webpush.aud' => \&_aud_helper);
143             $app->helper('webpush.authorization' => (grep !$conf->{$_}, @AUTH_CONF)
144 0     0   0 ? sub { die "Must provide @AUTH_CONF\n" }
145 4 50       1453 : _make_auth_helper($app, $conf)
146             );
147 4         1682 $app->helper('webpush.verify_token' => \&_verify_helper);
148 4         1926 $app->helper('webpush.encrypt' => \&_encrypt_helper);
149 4         2066 $app->helper('webpush.send_p' => \&_send_helper);
150 4         2100 my $r = $app->routes;
151             $r->post($conf->{save_endpoint} => _make_route_handler(
152 4         36 @$conf{qw(subs_session2user_p subs_create_p)},
153             ), 'webpush.save');
154 4         1687 push @{ $app->renderer->classes }, __PACKAGE__;
  4         28  
155             $app->serviceworker->add_event_listener(
156 4   33     73 push => $conf->{push_handler} || $DEFAULT_PUSH_HANDLER
157             );
158 4         546 $self;
159             }
160              
161             sub validate_subs_info {
162 11     11 0 29315 my ($info) = @_;
163 11 50       43 die "Expected object\n" if ref $info ne 'HASH';
164 11         54 my @errors = map "no $_", grep !exists $info->{$_}, qw(keys endpoint);
165 11         60 push @errors, map "no $_", grep !exists $info->{keys}{$_}, qw(auth p256dh);
166 11 100       74 die "Errors found in subscription info: " . join(", ", @errors) . "\n"
167             if @errors;
168             }
169              
170             1;
171              
172             =encoding utf8
173              
174             =head1 NAME
175              
176             Mojolicious::Plugin::WebPush - plugin to aid real-time web push
177              
178             =head1 SYNOPSIS
179              
180             # Mojolicious::Lite
181             my $sw = plugin 'ServiceWorker' => { debug => 1 };
182             my $webpush = plugin 'WebPush' => {
183             save_endpoint => '/api/savesubs',
184             subs_session2user_p => \&subs_session2user_p,
185             subs_create_p => \&subs_create_p,
186             subs_read_p => \&subs_read_p,
187             subs_delete_p => \&subs_delete_p,
188             ecc_private_key => 'vapid_private_key.pem',
189             claim_sub => "mailto:admin@example.com",
190             };
191              
192             sub subs_session2user_p {
193             my ($session) = @_;
194             return Mojo::Promise->reject("Session not logged in") if !$session->{user_id};
195             Mojo::Promise->resolve($session->{user_id});
196             }
197              
198             sub subs_create_p {
199             my ($session, $subs_info) = @_;
200             app->db->save_subs_p($session->{user_id}, $subs_info);
201             }
202              
203             sub subs_read_p {
204             my ($user_id) = @_;
205             app->db->lookup_subs_p($user_id);
206             }
207              
208             sub subs_delete_p {
209             my ($user_id) = @_;
210             app->db->delete_subs_p($user_id);
211             }
212              
213             =head1 DESCRIPTION
214              
215             L is a L plugin. In
216             order to function, your app needs to have first installed
217             L as shown in the synopsis above.
218              
219             =head1 METHODS
220              
221             L inherits all methods from
222             L and implements the following new ones.
223              
224             =head2 register
225              
226             my $p = $plugin->register(Mojolicious->new, \%conf);
227              
228             Register plugin in L application, returning the plugin
229             object. Takes a hash-ref as configuration, see L for keys.
230              
231             =head1 OPTIONS
232              
233             =head2 save_endpoint
234              
235             Required. The route to be added to the app for the service worker to
236             register users for push notification. The handler for that will call
237             the L. If success is indicated, it will return JSON:
238              
239             { "data": { "success": true } }
240              
241             If failure:
242              
243             { "errors": [ { "message": "The exception reason" } ] }
244              
245             This will be handled by the provided service worker. In case it is
246             required by the app itself, the added route is named C.
247              
248             =head2 subs_session2user_p
249              
250             Required. The code to be called to look up the user currently identified
251             by this session, which returns a promise of the user ID. Must reject
252             if no user logged in and that matters. It will be passed parameters:
253              
254             =over
255              
256             =item *
257              
258             The L object, to correctly identify
259             the user.
260              
261             =back
262              
263             =head2 subs_create_p
264              
265             Required. The code to be called to store users registered for push
266             notifications, which must return a promise of a true value if the
267             operation succeeds, or reject with a reason. It will be passed parameters:
268              
269             =over
270              
271             =item *
272              
273             The ID to correctly identify the user. Please note that you ought to
274             allow one person to have several devices with web-push enabled, and to
275             design accordingly.
276              
277             =item *
278              
279             The C hash-ref, needed to push actual messages.
280              
281             =back
282              
283             =head2 subs_read_p
284              
285             Required. The code to be called to look up a user registered for push
286             notifications. It will be passed parameters:
287              
288             =over
289              
290             =item *
291              
292             The opaque information your app uses to identify the user.
293              
294             =back
295              
296             Returns a promise of the C hash-ref. Must reject if
297             not found.
298              
299             =head2 subs_delete_p
300              
301             Required. The code to be called to delete up a user registered for push
302             notifications. It will be passed parameters:
303              
304             =over
305              
306             =item *
307              
308             The opaque information your app uses to identify the user.
309              
310             =back
311              
312             Returns a promise of the deletion result. Must reject if not found.
313              
314             =head2 ecc_private_key
315              
316             A value to be passed to L: a simple scalar is a
317             filename, a scalar-ref is the actual key. If not provided,
318             L will (obviously) not be able to function.
319              
320             =head2 claim_sub
321              
322             A value to be used as the C claim by the L,
323             which needs it. Must be either an HTTPS or C URL.
324              
325             =head2 claim_exp_offset
326              
327             A value to be added to current time, in seconds, in the C claim
328             for L. Defaults to 86400 (24 hours). The maximum
329             valid value in RFC 8292 is 86400.
330              
331             =head2 push_handler
332              
333             Override the default push-event handler supplied to
334             L. The default
335             will interpret the message as a JSON object. The key C will be </td> </tr> <tr> <td class="h" > <a name="336">336</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the notification title, deleted from that object, then the object will be </td> </tr> <tr> <td class="h" > <a name="337">337</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the options passed to C<< <ServiceWorkerRegistration>.showNotification >>. </td> </tr> <tr> <td class="h" > <a name="338">338</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="339">339</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> See </td> </tr> <tr> <td class="h" > <a name="340">340</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<https://developers.google.com/web/fundamentals/push-notifications/handling-messages> </td> </tr> <tr> <td class="h" > <a name="341">341</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> for possibilities. </td> </tr> <tr> <td class="h" > <a name="342">342</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="343">343</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 HELPERS </td> </tr> <tr> <td class="h" > <a name="344">344</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="345">345</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 webpush.create_p </td> </tr> <tr> <td class="h" > <a name="346">346</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="347">347</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> $c->webpush->create_p($user_id, $subs_info)->then(sub { </td> </tr> <tr> <td class="h" > <a name="348">348</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> $c->render(json => { data => { success => \1 } }); </td> </tr> <tr> <td class="h" > <a name="349">349</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> }); </td> </tr> <tr> <td class="h" > <a name="350">350</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="351">351</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 webpush.read_p </td> </tr> <tr> <td class="h" > <a name="352">352</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="353">353</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> $c->webpush->read_p($user_id)->then(sub { </td> </tr> <tr> <td class="h" > <a name="354">354</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> $c->render(text => 'Info: ' . to_json(shift)); </td> </tr> <tr> <td class="h" > <a name="355">355</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> }); </td> </tr> <tr> <td class="h" > <a name="356">356</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="357">357</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 webpush.delete_p </td> </tr> <tr> <td class="h" > <a name="358">358</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="359">359</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> $c->webpush->delete_p($user_id)->then(sub { </td> </tr> <tr> <td class="h" > <a name="360">360</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> $c->render(json => { data => { success => \1 } }); </td> </tr> <tr> <td class="h" > <a name="361">361</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> }); </td> </tr> <tr> <td class="h" > <a name="362">362</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="363">363</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 webpush.authorization </td> </tr> <tr> <td class="h" > <a name="364">364</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="365">365</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $header_value = $c->webpush->authorization; </td> </tr> <tr> <td class="h" > <a name="366">366</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="367">367</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Won't function without L</claim_sub> and L</ecc_private_key>. Returns </td> </tr> <tr> <td class="h" > <a name="368">368</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> a suitable C<Authorization> header value to send to a push service. </td> </tr> <tr> <td class="h" > <a name="369">369</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Valid for a period defined by L</claim_exp_offset>. Not currently cached, </td> </tr> <tr> <td class="h" > <a name="370">370</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> but could become so to avoid unnecessary computation. </td> </tr> <tr> <td class="h" > <a name="371">371</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="372">372</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 webpush.aud </td> </tr> <tr> <td class="h" > <a name="373">373</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="374">374</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $aud = $c->webpush->aud; </td> </tr> <tr> <td class="h" > <a name="375">375</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="376">376</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Gives the app's value it will use for the C<aud> JWT claim, useful mostly </td> </tr> <tr> <td class="h" > <a name="377">377</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> for testing. </td> </tr> <tr> <td class="h" > <a name="378">378</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="379">379</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 webpush.public_key </td> </tr> <tr> <td class="h" > <a name="380">380</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="381">381</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $pkey = $c->webpush->public_key; </td> </tr> <tr> <td class="h" > <a name="382">382</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="383">383</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Gives the app's public VAPID key, calculated from the private key. </td> </tr> <tr> <td class="h" > <a name="384">384</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="385">385</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 webpush.verify_token </td> </tr> <tr> <td class="h" > <a name="386">386</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="387">387</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $bool = $c->webpush->verify_token($authorization_header_value); </td> </tr> <tr> <td class="h" > <a name="388">388</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="389">389</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Cryptographically verifies a JSON Web Token (JWT), such as generated </td> </tr> <tr> <td class="h" > <a name="390">390</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> by L</webpush.authorization>. </td> </tr> <tr> <td class="h" > <a name="391">391</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="392">392</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 webpush.encrypt </td> </tr> <tr> <td class="h" > <a name="393">393</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="394">394</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> use MIME::Base64 qw(decode_base64url); </td> </tr> <tr> <td class="h" > <a name="395">395</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $ciphertext = $c->webpush->encrypt($data_bytes, </td> </tr> <tr> <td class="h" > <a name="396">396</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> map decode_base64url($_), @{$subscription_info->{keys}}{qw(p256dh auth)} </td> </tr> <tr> <td class="h" > <a name="397">397</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> ); </td> </tr> <tr> <td class="h" > <a name="398">398</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="399">399</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Returns the data encrypted according to RFC 8188, for the relevant </td> </tr> <tr> <td class="h" > <a name="400">400</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> subscriber. </td> </tr> <tr> <td class="h" > <a name="401">401</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="402">402</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 webpush.send_p </td> </tr> <tr> <td class="h" > <a name="403">403</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="404">404</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> my $result_p = $c->webpush->send_p($jsonable_data, $user_id, $ttl, $urgency); </td> </tr> <tr> <td class="h" > <a name="405">405</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="406">406</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> JSON-encodes the given value, encrypts it according to the given user's </td> </tr> <tr> <td class="h" > <a name="407">407</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> subscription data, adds a VAPID C<Authorization> header, then sends it </td> </tr> <tr> <td class="h" > <a name="408">408</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> to the relevant web-push endpoint. </td> </tr> <tr> <td class="h" > <a name="409">409</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="410">410</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Returns a promise of the result, which will be a hash-ref with either a </td> </tr> <tr> <td class="h" > <a name="411">411</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> C<data> key indicating success, or an C<errors> key for an array-ref of </td> </tr> <tr> <td class="h" > <a name="412">412</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> hash-refs with a C<message> giving reasons. </td> </tr> <tr> <td class="h" > <a name="413">413</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="414">414</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> If the sending gets a status code of 404 or 410, this indicates the </td> </tr> <tr> <td class="h" > <a name="415">415</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> subscriber has unsubscribed, and L</webpush.delete_p> will be used to </td> </tr> <tr> <td class="h" > <a name="416">416</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> remove the registration. This is considered success. </td> </tr> <tr> <td class="h" > <a name="417">417</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="418">418</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> The C<urgency> must be one of C<very-low>, C<low>, C<normal> (the default) </td> </tr> <tr> <td class="h" > <a name="419">419</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> or C<high>. The C<ttl> defaults to 30 seconds. </td> </tr> <tr> <td class="h" > <a name="420">420</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="421">421</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 TEMPLATES </td> </tr> <tr> <td class="h" > <a name="422">422</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="423">423</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Various templates are available for including in the app's templates: </td> </tr> <tr> <td class="h" > <a name="424">424</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="425">425</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head2 webpush-askPermission.html.ep </td> </tr> <tr> <td class="h" > <a name="426">426</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="427">427</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> JavaScript functions, also for putting inside a C<script> element: </td> </tr> <tr> <td class="h" > <a name="428">428</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="429">429</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =over </td> </tr> <tr> <td class="h" > <a name="430">430</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="431">431</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item * </td> </tr> <tr> <td class="h" > <a name="432">432</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="433">433</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> askPermission </td> </tr> <tr> <td class="h" > <a name="434">434</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="435">435</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item * </td> </tr> <tr> <td class="h" > <a name="436">436</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="437">437</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> subscribeUserToPush </td> </tr> <tr> <td class="h" > <a name="438">438</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="439">439</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item * </td> </tr> <tr> <td class="h" > <a name="440">440</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="441">441</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> sendSubscriptionToBackEnd </td> </tr> <tr> <td class="h" > <a name="442">442</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="443">443</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="444">444</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="445">445</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> These each return a promise, and should be chained together: </td> </tr> <tr> <td class="h" > <a name="446">446</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="447">447</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> <button onclick=" </td> </tr> <tr> <td class="h" > <a name="448">448</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> askPermission().then(subscribeUserToPush).then(sendSubscriptionToBackEnd) </td> </tr> <tr> <td class="h" > <a name="449">449</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> "> </td> </tr> <tr> <td class="h" > <a name="450">450</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Ask permission </td> </tr> <tr> <td class="h" > <a name="451">451</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> </button> </td> </tr> <tr> <td class="h" > <a name="452">452</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> <script> </td> </tr> <tr> <td class="h" > <a name="453">453</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> %= include 'serviceworker-install' </td> </tr> <tr> <td class="h" > <a name="454">454</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> %= include 'webpush-askPermission' </td> </tr> <tr> <td class="h" > <a name="455">455</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> </script> </td> </tr> <tr> <td class="h" > <a name="456">456</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="457">457</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Each application must decide when to ask such permission, bearing in </td> </tr> <tr> <td class="h" > <a name="458">458</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> mind that once permission is refused, it is very difficult for the user </td> </tr> <tr> <td class="h" > <a name="459">459</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> to change such a refusal. </td> </tr> <tr> <td class="h" > <a name="460">460</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="461">461</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> When it is granted, the JavaScript code will communicate with the </td> </tr> <tr> <td class="h" > <a name="462">462</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> application, registering the needed information needed to web-push. </td> </tr> <tr> <td class="h" > <a name="463">463</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="464">464</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 SEE ALSO </td> </tr> <tr> <td class="h" > <a name="465">465</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="466">466</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>. </td> </tr> <tr> <td class="h" > <a name="467">467</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="468">468</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<Mojolicious::Command::webpush> - command-line control of web-push. </td> </tr> <tr> <td class="h" > <a name="469">469</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="470">470</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> RFC 8292 - Voluntary Application Server Identification (for web push). </td> </tr> <tr> <td class="h" > <a name="471">471</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="472">472</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<Crypt::RFC8188> - Encrypted Content-Encoding for HTTP (using C<aes128gcm>). </td> </tr> <tr> <td class="h" > <a name="473">473</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="474">474</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<https://developers.google.com/web/fundamentals/push-notifications> </td> </tr> <tr> <td class="h" > <a name="475">475</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="476">476</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 ACKNOWLEDGEMENTS </td> </tr> <tr> <td class="h" > <a name="477">477</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="478">478</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Part of this code is ported from </td> </tr> <tr> <td class="h" > <a name="479">479</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<https://github.com/web-push-libs/pywebpush>. </td> </tr> <tr> <td class="h" > <a name="480">480</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="481">481</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =cut </td> </tr> <tr> <td class="h" > <a name="482">482</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="483">483</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> __DATA__ </td> </tr> </table> </body> </html>