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 21 24 87.5
pod 1 2 50.0
total 147 172 85.4


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::WebPush;
2 4     4   18012 use Mojo::Base 'Mojolicious::Plugin';
  4         13  
  4         26  
3 4     4   773 use Mojo::JSON qw(decode_json encode_json);
  4         8  
  4         290  
4 4     4   27 use Mojo::URL;
  4         9  
  4         44  
5 4     4   126 use Crypt::PK::ECC;
  4         10  
  4         188  
6 4     4   23 use MIME::Base64 qw(encode_base64url decode_base64url);
  4         8  
  4         221  
7 4     4   2700 use Crypt::JWT qw(encode_jwt decode_jwt);
  4         132065  
  4         329  
8 4     4   1670 use Crypt::RFC8188 qw(ece_encrypt_aes128gcm);
  4         160407  
  4         8916  
9              
10             our $VERSION = '0.05';
11              
12             my @MANDATORY_CONF = qw(
13             subs_session2user_p
14             save_endpoint
15             subs_create_p
16             subs_read_p
17             subs_delete_p
18             );
19             my @AUTH_CONF = qw(claim_sub ecc_private_key);
20             my $DEFAULT_PUSH_HANDLER = <<'EOF';
21             event => {
22             var msg = event.data.json();
23             var title = msg.title;
24             delete msg.title;
25             event.waitUntil(self.registration.showNotification(title, msg));
26             }
27             EOF
28              
29             sub _decode {
30 2     2   56 my ($bytes) = @_;
31 2         5 my $body = eval { decode_json($bytes) };
  2         8  
32             # conceal error info like versions from attackers
33 2 50       280 return (0, "Malformed request") if $@;
34 2         7 (1, $body);
35             }
36              
37             sub _error {
38 1     1   11 my ($c, $error) = @_;
39 1         8 $c->render(status => 500, json => { errors => [ { message => $error } ] });
40             }
41              
42             sub _make_route_handler {
43 4     4   24 my ($subs_session2user_p, $subs_create_p) = @_;
44             sub {
45 2     2   16915 my ($c) = @_;
46 2         7 my ($decode_ok, $body) = _decode($c->req->body);
47 2 50       21 return _error($c, $body) if !$decode_ok;
48 2         5 eval { validate_subs_info($body) };
  2         6  
49 2 100       522 return _error($c, $@) if $@;
50             return $subs_session2user_p->($c, $c->session)->then(
51 1         5241 sub { $subs_create_p->($c, $_[0], $body) },
52             )->then(
53 1         441 sub { $c->render(json => { data => { success => \1 } }) },
54 0         0 sub { _error($c, @_) },
55 1         6 );
56 4         71 };
57             }
58              
59             sub _make_auth_helper {
60 4     4   11 my ($app, $conf) = @_;
61 4   50     50 my $exp_offset = $conf->{claim_exp_offset} || 86400;
62 4         40 my $key = Crypt::PK::ECC->new($conf->{ecc_private_key});
63 4         24440 my $claims_start = { sub => $conf->{claim_sub} };
64 4         112 my $pkey = encode_base64url $key->export_key_raw('public');
65 4     0   118 $app->helper('webpush.public_key' => sub { $pkey });
  0         0  
66             sub {
67 3     3   1455 my ($c, $subs_info) = @_;
68 3         35 my $aud = Mojo::URL->new($subs_info->{endpoint})->path(Mojo::Path->new->trailing_slash(0)).'';
69 3         1750 my $claims = { aud => $aud, exp => time + $exp_offset, %$claims_start };
70 3         18 my $token = encode_jwt key => $key, alg => 'ES256', payload => $claims;
71 3         9484 "vapid t=$token,k=$pkey";
72 4         1638 };
73             }
74              
75             sub _verify_helper {
76 3     3   9601 my ($app, $auth_header_value) = @_;
77 3         13 (my $schema, $auth_header_value) = split ' ', $auth_header_value;
78 3 50       14 return if $schema ne 'vapid';
79 3         23 my %k2v = map split('=', $_), split ',', $auth_header_value;
80 3         8 eval {
81 3         14 my $key = Crypt::PK::ECC->new;
82 3         229 $key->import_key_raw(decode_base64url($k2v{k}), 'P-256');
83 3         8815 decode_jwt token => $k2v{t}, key => $key, alg => 'ES256', verify_exp => 0;
84             };
85             }
86              
87             sub _encrypt_helper {
88 3     3   7840 my ($c, $plaintext, $receiver_key, $auth_key) = @_;
89 3 50 33     28 die "Invalid p256dh key specified\n"
90             if length($receiver_key) != 65 or $receiver_key !~ /^\x04/;
91 3         19 my $onetime_key = Crypt::PK::ECC->new->generate_key('prime256v1');
92 3         9104 ece_encrypt_aes128gcm(
93             $plaintext, (undef) x 2, $onetime_key, $receiver_key, $auth_key,
94             );
95             }
96              
97             sub _send_helper {
98 2     2   2999 my ($c, $message, $user_id, $ttl, $urgency) = @_;
99 2   50     7 $ttl ||= 30;
100 2   50     11 $urgency ||= 'normal';
101             $c->webpush->read_p($user_id)->then(sub {
102 2     2   1075 my ($subs_info) = @_;
103             my $body = $c->webpush->encrypt(
104             encode_json($message),
105 2         10 map decode_base64url($_), @{$subs_info->{keys}}{qw(p256dh auth)}
  2         180  
106             );
107 2         15743 my $headers = {
108             Authorization => $c->webpush->authorization($subs_info),
109             'Content-Length' => length($body),
110             'Content-Encoding' => 'aes128gcm',
111             TTL => $ttl,
112             Urgency => $urgency,
113             };
114 2         13 $c->app->ua->post_p($subs_info->{endpoint}, $headers, $body);
115             })->then(sub {
116 2     2   85171 my ($tx) = @_;
117             return $c->webpush->delete_p($user_id)->then(sub {
118 1         329 { data => { success => \1 } }
119 2 100 66     9 }) if $tx->res->code == 404 or $tx->res->code == 410;
120 1 50       31 return { errors => [ { message => $tx->res->body } ] }
121             if $tx->res->code > 399;
122 1         12 { data => { success => \1 } };
123             }, sub {
124 0     0   0 { errors => [ { message => $_[0] } ] }
125 2         20 });
126             }
127              
128             sub register {
129 4     4 1 283 my ($self, $app, $conf) = @_;
130 4         26 my @config_errors = grep !exists $conf->{$_}, @MANDATORY_CONF;
131 4 50       20 die "Missing config keys @config_errors\n" if @config_errors;
132             $app->helper('webpush.create_p' => sub {
133 4     4   30878 eval { validate_subs_info($_[2]) };
  4         22  
134 4 50       16 return Mojo::Promise->reject($@) if $@;
135 4         10 goto &{ $conf->{subs_create_p} };
  4         25  
136 4         54 });
137 4         2046 $app->helper('webpush.read_p' => $conf->{subs_read_p});
138 4         1282 $app->helper('webpush.delete_p' => $conf->{subs_delete_p});
139             $app->helper('webpush.authorization' => (grep !$conf->{$_}, @AUTH_CONF)
140 0     0   0 ? sub { die "Must provide @AUTH_CONF\n" }
141 4 50       1416 : _make_auth_helper($app, $conf)
142             );
143 4         1635 $app->helper('webpush.verify_token' => \&_verify_helper);
144 4         1711 $app->helper('webpush.encrypt' => \&_encrypt_helper);
145 4         1824 $app->helper('webpush.send_p' => \&_send_helper);
146 4         1980 my $r = $app->routes;
147             $r->post($conf->{save_endpoint} => _make_route_handler(
148 4         36 @$conf{qw(subs_session2user_p subs_create_p)},
149             ), 'webpush.save');
150 4         2064 push @{ $app->renderer->classes }, __PACKAGE__;
  4         17  
151             $app->serviceworker->add_event_listener(
152 4   33     74 push => $conf->{push_handler} || $DEFAULT_PUSH_HANDLER
153             );
154 4         1049 $self;
155             }
156              
157             sub validate_subs_info {
158 11     11 0 30971 my ($info) = @_;
159 11 50       48 die "Expected object\n" if ref $info ne 'HASH';
160 11         55 my @errors = map "no $_", grep !exists $info->{$_}, qw(keys endpoint);
161 11         46 push @errors, map "no $_", grep !exists $info->{keys}{$_}, qw(auth p256dh);
162 11 100       90 die "Errors found in subscription info: " . join(", ", @errors) . "\n"
163             if @errors;
164             }
165              
166             1;
167              
168             =encoding utf8
169              
170             =head1 NAME
171              
172             Mojolicious::Plugin::WebPush - plugin to aid real-time web push
173              
174             =head1 SYNOPSIS
175              
176             # Mojolicious::Lite
177             my $sw = plugin 'ServiceWorker' => { debug => 1 };
178             my $webpush = plugin 'WebPush' => {
179             save_endpoint => '/api/savesubs',
180             subs_session2user_p => \&subs_session2user_p,
181             subs_create_p => \&subs_create_p,
182             subs_read_p => \&subs_read_p,
183             subs_delete_p => \&subs_delete_p,
184             ecc_private_key => 'vapid_private_key.pem',
185             claim_sub => "mailto:admin@example.com",
186             };
187              
188             sub subs_session2user_p {
189             my ($c, $session) = @_;
190             return Mojo::Promise->reject("Session not logged in") if !$session->{user_id};
191             Mojo::Promise->resolve($session->{user_id});
192             }
193              
194             sub subs_create_p {
195             my ($c, $session, $subs_info) = @_;
196             app->db->save_subs_p($session->{user_id}, $subs_info);
197             }
198              
199             sub subs_read_p {
200             my ($c, $user_id) = @_;
201             app->db->lookup_subs_p($user_id);
202             }
203              
204             sub subs_delete_p {
205             my ($c, $user_id) = @_;
206             app->db->delete_subs_p($user_id);
207             }
208              
209             =head1 DESCRIPTION
210              
211             L is a L plugin. In
212             order to function, your app needs to have first installed
213             L as shown in the synopsis above.
214              
215             =head1 METHODS
216              
217             L inherits all methods from
218             L and implements the following new ones.
219              
220             =head2 register
221              
222             my $p = $plugin->register(Mojolicious->new, \%conf);
223              
224             Register plugin in L application, returning the plugin
225             object. Takes a hash-ref as configuration, see L for keys.
226              
227             =head1 OPTIONS
228              
229             =head2 save_endpoint
230              
231             Required. The route to be added to the app for the service worker to
232             register users for push notification. The handler for that will call
233             the L. If success is indicated, it will return JSON:
234              
235             { "data": { "success": true } }
236              
237             If failure:
238              
239             { "errors": [ { "message": "The exception reason" } ] }
240              
241             This will be handled by the provided service worker. In case it is
242             required by the app itself, the added route is named C.
243              
244             =head2 subs_session2user_p
245              
246             Required. The code to be called to look up the user currently identified
247             by this session, which returns a promise of the user ID. Must reject
248             if no user logged in and that matters. It will be passed parameters:
249              
250             =over
251              
252             =item *
253              
254             The L object, to correctly identify
255             the user.
256              
257             =back
258              
259             =head2 subs_create_p
260              
261             Required. The code to be called to store users registered for push
262             notifications, which must return a promise of a true value if the
263             operation succeeds, or reject with a reason. It will be passed parameters:
264              
265             =over
266              
267             =item *
268              
269             The ID to correctly identify the user. Please note that you ought to
270             allow one person to have several devices with web-push enabled, and to
271             design accordingly.
272              
273             =item *
274              
275             The C hash-ref, needed to push actual messages.
276              
277             =back
278              
279             =head2 subs_read_p
280              
281             Required. The code to be called to look up a user registered for push
282             notifications. It will be passed parameters:
283              
284             =over
285              
286             =item *
287              
288             The opaque information your app uses to identify the user.
289              
290             =back
291              
292             Returns a promise of the C hash-ref. Must reject if
293             not found.
294              
295             =head2 subs_delete_p
296              
297             Required. The code to be called to delete up a user registered for push
298             notifications. It will be passed parameters:
299              
300             =over
301              
302             =item *
303              
304             The opaque information your app uses to identify the user.
305              
306             =back
307              
308             Returns a promise of the deletion result. Must reject if not found.
309              
310             =head2 ecc_private_key
311              
312             A value to be passed to L: a simple scalar is a
313             filename, a scalar-ref is the actual key. If not provided,
314             L will (obviously) not be able to function.
315              
316             =head2 claim_sub
317              
318             A value to be used as the C claim by the L,
319             which needs it. Must be either an HTTPS or C URL.
320              
321             =head2 claim_exp_offset
322              
323             A value to be added to current time, in seconds, in the C claim
324             for L. Defaults to 86400 (24 hours). The maximum
325             valid value in RFC 8292 is 86400.
326              
327             =head2 push_handler
328              
329             Override the default push-event handler supplied to
330             L. The default
331             will interpret the message as a JSON object. The key C will be </td> </tr> <tr> <td class="h" > <a name="332">332</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="333">333</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="334">334</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="335">335</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="336">336</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="337">337</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="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"> =head1 HELPERS </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">   </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"> =head2 webpush.create_p </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"> $c->webpush->create_p($user_id, $subs_info)->then(sub { </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"> $c->render(json => { data => { success => \1 } }); </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"> }); </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"> =head2 webpush.read_p </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">   </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"> $c->webpush->read_p($user_id)->then(sub { </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"> $c->render(text => 'Info: ' . to_json(shift)); </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"> }); </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"> =head2 webpush.delete_p </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">   </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"> $c->webpush->delete_p($user_id)->then(sub { </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"> $c->render(json => { data => { success => \1 } }); </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"> }); </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"> =head2 webpush.authorization </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">   </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"> my $header_value = $c->webpush->authorization($subs_info); </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"> Won't function without L</claim_sub> and L</ecc_private_key>, or </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"> C<$subs_info> having a valid URL to get the base of as the C<aud> </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"> claim. Returns a suitable C<Authorization> header value to send to </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"> a push service. Valid for a period defined by L</claim_exp_offset>. </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"> but could become so to avoid unnecessary computation. </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">   </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"> =head2 webpush.public_key </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">   </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"> my $pkey = $c->webpush->public_key; </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">   </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"> Gives the app's public VAPID key, calculated from the private key. </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">   </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"> =head2 webpush.verify_token </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">   </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"> my $bool = $c->webpush->verify_token($authorization_header_value); </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"> Cryptographically verifies a JSON Web Token (JWT), such as generated </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"> by L</webpush.authorization>. </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">   </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"> =head2 webpush.encrypt </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">   </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"> use MIME::Base64 qw(decode_base64url); </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"> my $ciphertext = $c->webpush->encrypt($data_bytes, </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"> map decode_base64url($_), @{$subscription_info->{keys}}{qw(p256dh auth)} </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"> ); </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"> Returns the data encrypted according to RFC 8188, for the relevant </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"> subscriber. </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.send_p </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"> my $result_p = $c->webpush->send_p($jsonable_data, $user_id, $ttl, $urgency); </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">   </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"> JSON-encodes the given value, encrypts it according to the given user's </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"> subscription data, adds a VAPID C<Authorization> header, then sends it </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"> to the relevant web-push endpoint. </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">   </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"> Returns a promise of the result, which will be a hash-ref with either a </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"> C<data> key indicating success, or an C<errors> key for an array-ref of </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"> hash-refs with a C<message> giving reasons. </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"> If the sending gets a status code of 404 or 410, this indicates the </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"> subscriber has unsubscribed, and L</webpush.delete_p> will be used to </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"> remove the registration. This is considered success. </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">   </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"> The C<urgency> must be one of C<very-low>, C<low>, C<normal> (the default) </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"> or C<high>. The C<ttl> defaults to 30 seconds. </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">   </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"> =head1 TEMPLATES </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">   </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"> Various templates are available for including in the app's templates: </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">   </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"> =head2 webpush-askPermission.html.ep </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">   </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"> JavaScript functions, also for putting inside a C<script> element: </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">   </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"> =over </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"> =item * </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"> askPermission </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"> =item * </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"> subscribeUserToPush </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"> =item * </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"> sendSubscriptionToBackEnd </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"> =back </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"> These each return a promise, and should be chained together: </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"> <button onclick=" </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"> askPermission().then(subscribeUserToPush).then(sendSubscriptionToBackEnd) </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"> "> </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"> Ask permission </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"> </button> </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"> <script> </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"> %= include 'serviceworker-install' </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"> %= include 'webpush-askPermission' </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"> </script> </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"> Each application must decide when to ask such permission, bearing in </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"> mind that once permission is refused, it is very difficult for the user </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"> to change such a refusal. </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">   </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"> When it is granted, the JavaScript code will communicate with the </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"> application, registering the needed information needed to web-push. </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">   </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"> =head1 SEE ALSO </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">   </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"> L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>. </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">   </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"> L<Mojolicious::Command::webpush> - command-line control of web-push. </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">   </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"> RFC 8292 - Voluntary Application Server Identification (for web push). </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">   </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"> L<Crypt::RFC8188> - Encrypted Content-Encoding for HTTP (using C<aes128gcm>). </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"> L<https://developers.google.com/web/fundamentals/push-notifications> </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"> =head1 ACKNOWLEDGEMENTS </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"> Part of this code is ported from </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"> L<https://github.com/web-push-libs/pywebpush>. </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">   </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"> =cut </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">   </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"> __DATA__ </td> </tr> </table> </body> </html>