line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package HTTP::Request::Webpush;
|
2
|
|
|
|
|
|
|
#===========================================================================
|
3
|
|
|
|
|
|
|
# Copyright 2021 Erich Strelow
|
4
|
|
|
|
|
|
|
#
|
5
|
|
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
|
|
|
|
|
|
# you may not use this file except in compliance with the License.
|
7
|
|
|
|
|
|
|
# You may obtain a copy of the License at
|
8
|
|
|
|
|
|
|
#
|
9
|
|
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
|
|
|
|
|
|
#
|
11
|
|
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
12
|
|
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
|
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
|
|
|
|
|
|
# See the License for the specific language governing permissions and
|
15
|
|
|
|
|
|
|
# limitations under the License.
|
16
|
|
|
|
|
|
|
#============================================================================
|
17
|
3
|
|
|
3
|
|
375751
|
use strict 'vars';
|
|
3
|
|
|
|
|
30
|
|
|
3
|
|
|
|
|
121
|
|
18
|
3
|
|
|
3
|
|
19
|
use warnings;
|
|
3
|
|
|
|
|
6
|
|
|
3
|
|
|
|
|
143
|
|
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
our $VERSION='0.14';
|
21
|
|
|
|
|
|
|
|
22
|
3
|
|
|
3
|
|
1581
|
use parent 'HTTP::Request';
|
|
3
|
|
|
|
|
973
|
|
|
3
|
|
|
|
|
18
|
|
23
|
|
|
|
|
|
|
|
24
|
3
|
|
|
3
|
|
53818
|
use JSON;
|
|
3
|
|
|
|
|
18301
|
|
|
3
|
|
|
|
|
27
|
|
25
|
3
|
|
|
3
|
|
1895
|
use Crypt::JWT qw(encode_jwt);
|
|
3
|
|
|
|
|
60177
|
|
|
3
|
|
|
|
|
258
|
|
26
|
3
|
|
|
3
|
|
33
|
use MIME::Base64 qw( encode_base64url decode_base64url);
|
|
3
|
|
|
|
|
6
|
|
|
3
|
|
|
|
|
159
|
|
27
|
3
|
|
|
3
|
|
20
|
use Crypt::PRNG qw(random_bytes);
|
|
3
|
|
|
|
|
7
|
|
|
3
|
|
|
|
|
130
|
|
28
|
3
|
|
|
3
|
|
20
|
use Crypt::AuthEnc::GCM 'gcm_encrypt_authenticate';
|
|
3
|
|
|
|
|
7
|
|
|
3
|
|
|
|
|
125
|
|
29
|
3
|
|
|
3
|
|
19
|
use Crypt::PK::ECC 'ecc_shared_secret';
|
|
3
|
|
|
|
|
7
|
|
|
3
|
|
|
|
|
123
|
|
30
|
3
|
|
|
3
|
|
27
|
use Digest::SHA 'hmac_sha256';
|
|
3
|
|
|
|
|
6
|
|
|
3
|
|
|
|
|
136
|
|
31
|
3
|
|
|
3
|
|
16
|
use Carp;
|
|
3
|
|
|
|
|
18
|
|
|
3
|
|
|
|
|
204
|
|
32
|
3
|
|
|
3
|
|
21
|
use URI;
|
|
3
|
|
|
|
|
5
|
|
|
3
|
|
|
|
|
3856
|
|
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
#================================================================
|
35
|
|
|
|
|
|
|
# hkdf()
|
36
|
|
|
|
|
|
|
#
|
37
|
|
|
|
|
|
|
# Calculates a key derivation using HMAC
|
38
|
|
|
|
|
|
|
# This is a simplified version based on Mat Scales jscript code
|
39
|
|
|
|
|
|
|
# see https://developers.google.com/web/updates/2016/03/web-push-encryption
|
40
|
|
|
|
|
|
|
#
|
41
|
|
|
|
|
|
|
# Notes: all args are expected to be binary strings, as the result
|
42
|
|
|
|
|
|
|
#================================================================
|
43
|
|
|
|
|
|
|
sub _hkdf($$$$$) {
|
44
|
9
|
|
|
9
|
|
21
|
my $self=shift();
|
45
|
9
|
|
|
|
|
14
|
my $salt=shift();
|
46
|
9
|
|
|
|
|
18
|
my $ikm=shift();
|
47
|
9
|
|
|
|
|
14
|
my $info=shift();
|
48
|
9
|
|
|
|
|
17
|
my $len=shift();
|
49
|
|
|
|
|
|
|
|
50
|
9
|
|
|
|
|
93
|
my $key=hmac_sha256($ikm,$salt);
|
51
|
9
|
|
|
|
|
77
|
my $infoHmac= hmac_sha256($info,chr(1),$key);
|
52
|
|
|
|
|
|
|
|
53
|
9
|
|
|
|
|
46
|
return substr($infoHmac,0,$len);
|
54
|
|
|
|
|
|
|
}
|
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
sub subscription($$) {
|
58
|
|
|
|
|
|
|
|
59
|
3
|
|
|
3
|
1
|
2354
|
my $self=shift();
|
60
|
3
|
|
|
|
|
10
|
my $subscription=shift();
|
61
|
|
|
|
|
|
|
|
62
|
3
|
|
|
|
|
6
|
my $agent;
|
63
|
|
|
|
|
|
|
|
64
|
3
|
50
|
|
|
|
16
|
if (ref $subscription eq 'HASH') {
|
65
|
3
|
|
|
|
|
8
|
$agent=$subscription;
|
66
|
|
|
|
|
|
|
} else {
|
67
|
0
|
|
|
|
|
0
|
eval {$agent=from_json($subscription); };
|
|
0
|
|
|
|
|
0
|
|
68
|
|
|
|
|
|
|
}
|
69
|
|
|
|
|
|
|
|
70
|
3
|
50
|
|
|
|
13
|
croak "Can't process subscription object" unless ($agent);
|
71
|
3
|
50
|
|
|
|
62
|
croak "Subscription must include endpoint" unless (exists $agent->{endpoint});
|
72
|
|
|
|
|
|
|
|
73
|
3
|
|
|
|
|
39
|
$self->uri($agent->{endpoint});
|
74
|
3
|
|
|
|
|
26130
|
$self->{subscription}=$agent;
|
75
|
3
|
|
|
|
|
25
|
return $agent;
|
76
|
|
|
|
|
|
|
}
|
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
sub auth($@) {
|
79
|
|
|
|
|
|
|
|
80
|
3
|
|
|
3
|
1
|
8
|
my $self=shift();
|
81
|
|
|
|
|
|
|
|
82
|
3
|
50
|
0
|
|
|
16
|
if (scalar @_ == 2) {
|
|
|
0
|
|
|
|
|
|
83
|
3
|
|
|
|
|
9
|
$self->{'app-pub'}=shift();
|
84
|
3
|
|
|
|
|
10
|
$self->{'app-key'}=shift();
|
85
|
|
|
|
|
|
|
} elsif (scalar (@_) == 1 && ref $_ eq 'Crypt::PK::ECC') {
|
86
|
0
|
|
|
|
|
0
|
$self->{'app-pub'}=$_->export_key_raw('public');
|
87
|
0
|
|
|
|
|
0
|
$self->{'app-key'}=$_->export_key_raw('private');
|
88
|
|
|
|
|
|
|
}
|
89
|
|
|
|
|
|
|
|
90
|
3
|
|
|
|
|
14
|
return 1;
|
91
|
|
|
|
|
|
|
}
|
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
sub authbase64($$$) {
|
94
|
|
|
|
|
|
|
|
95
|
3
|
|
|
3
|
1
|
8
|
my $self=shift();
|
96
|
3
|
|
|
|
|
22
|
my $pub=decode_base64url(shift());
|
97
|
3
|
|
|
|
|
61
|
my $key=decode_base64url(shift());
|
98
|
3
|
|
|
|
|
38
|
return $self->auth($pub,$key);
|
99
|
|
|
|
|
|
|
}
|
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
sub reuseecc($$) {
|
102
|
|
|
|
|
|
|
|
103
|
1
|
|
|
1
|
1
|
3054
|
my $self=shift();
|
104
|
1
|
|
|
|
|
17
|
return $self->{'ecc'}=shift();
|
105
|
|
|
|
|
|
|
}
|
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
sub subject($$) {
|
108
|
0
|
|
|
0
|
1
|
0
|
my $self=shift();
|
109
|
0
|
|
|
|
|
0
|
return $self->{'subject'}=shift();
|
110
|
|
|
|
|
|
|
}
|
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
sub encode($$) {
|
113
|
|
|
|
|
|
|
|
114
|
3
|
|
|
3
|
1
|
8
|
my $self=shift();
|
115
|
3
|
|
50
|
|
|
24
|
my $enc= shift() || 'aes128gcm';
|
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
|
118
|
|
|
|
|
|
|
#This method is inherited from HTTP::Message, but here only aes128gcm applies
|
119
|
3
|
50
|
|
|
|
15
|
croak 'Only aes128gcm encoding available' unless ($enc eq 'aes128gcm');
|
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
#Check prerequisites
|
122
|
3
|
50
|
|
|
|
26
|
croak 'Endpoint must be present for message encoding' unless ($self->url());
|
123
|
3
|
50
|
|
|
|
286
|
croak 'Authentication keys must be present for message encoding' unless ($self->{'app-key'});
|
124
|
3
|
50
|
33
|
|
|
31
|
croak 'UA auth params must be present for message encoding' unless ($self->{subscription}->{'keys'}->{'p256dh'} && $self->{subscription}->{'keys'}->{'auth'});
|
125
|
3
|
50
|
|
|
|
12
|
croak 'Message payload must be 3992 bytes or less' unless (length($self->content) <= 3992);
|
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
#This is the JWT part
|
128
|
3
|
|
|
|
|
57
|
my $origin=URI->new($self->{subscription}->{endpoint});
|
129
|
3
|
|
|
|
|
340
|
$origin->path_query('');
|
130
|
3
|
|
|
|
|
192
|
my $data={
|
131
|
|
|
|
|
|
|
'aud' => "$origin",
|
132
|
|
|
|
|
|
|
'exp'=> time() + 86400
|
133
|
|
|
|
|
|
|
};
|
134
|
3
|
50
|
|
|
|
51
|
$data->{'sub'}=$self->{'subject'} if ($self->{'subject'});
|
135
|
|
|
|
|
|
|
|
136
|
3
|
|
|
|
|
34
|
my $appk = Crypt::PK::ECC->new();
|
137
|
3
|
|
|
|
|
17655
|
$appk->import_key_raw($self->{'app-key'},'secp256r1');
|
138
|
3
|
|
|
|
|
69
|
my $token = encode_jwt(payload => $data, key => $appk , alg=>'ES256');
|
139
|
3
|
|
|
|
|
9768
|
$self->header( 'Authorization' => "WebPush $token" );
|
140
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
#Now the encription step
|
142
|
3
|
|
|
|
|
462
|
my $salt=random_bytes(16);
|
143
|
|
|
|
|
|
|
|
144
|
3
|
|
|
|
|
58
|
my $pk; #This will be the session key for encryption
|
145
|
3
|
100
|
|
|
|
17
|
if ($self->{'ecc'}) {
|
146
|
1
|
|
|
|
|
4
|
$pk=$self->{'ecc'};
|
147
|
|
|
|
|
|
|
} else {
|
148
|
2
|
|
|
|
|
14
|
$pk=Crypt::PK::ECC->new();
|
149
|
2
|
|
|
|
|
6042
|
$pk->generate_key('prime256v1');
|
150
|
|
|
|
|
|
|
}
|
151
|
|
|
|
|
|
|
|
152
|
3
|
|
|
|
|
65
|
my $pub_signkey=$pk->export_key_raw('public');
|
153
|
3
|
|
|
|
|
22
|
my $sec_signkey=$pk->export_key_raw('private');
|
154
|
|
|
|
|
|
|
|
155
|
3
|
|
|
|
|
26
|
my $sk=Crypt::PK::ECC->new(); #This will be the UA endpoint key, which we know from the subscription object
|
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
#The p256dh key is given to us in X9.62 format. Crypt::PK::ECC should be able
|
158
|
|
|
|
|
|
|
#to read it as a "raw" format. But it's important to apply the base64url variant
|
159
|
3
|
|
|
|
|
393
|
my $ua_public=decode_base64url($self->{subscription}->{'keys'}->{'p256dh'});
|
160
|
3
|
|
|
|
|
8751
|
$sk->import_key_raw($ua_public, 'secp256r1');
|
161
|
|
|
|
|
|
|
|
162
|
3
|
|
|
|
|
8718
|
my $ecdh_secret=$pk->shared_secret($sk);
|
163
|
3
|
|
|
|
|
61
|
my $auth_secret= decode_base64url($self->{subscription}->{'keys'}->{'auth'});
|
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
#An earlier draft established this header string as Content-Encoding: auth
|
166
|
3
|
|
|
|
|
82
|
my $key_info='WebPush: info'.chr(0).$ua_public.$pub_signkey;
|
167
|
3
|
|
|
|
|
23
|
my $prk=$self->_hkdf($auth_secret, $ecdh_secret, $key_info,32);
|
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
#Again, an earlier RFC8291 draft used a different cek_info and nonce_info
|
170
|
3
|
|
|
|
|
10
|
my $cek_info='Content-Encoding: aes128gcm'.chr(0);
|
171
|
3
|
|
|
|
|
8
|
my $nonce_info='Content-Encoding: nonce'.chr(0);
|
172
|
|
|
|
|
|
|
|
173
|
3
|
|
|
|
|
9
|
my $cek=$self->_hkdf($salt,$prk,$cek_info,16);
|
174
|
3
|
|
|
|
|
10
|
my $nonce= $self->_hkdf($salt, $prk,$nonce_info,12);
|
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
#Now we have all the ingredients, it's time for encryption
|
177
|
3
|
|
|
|
|
45
|
my ($body, $tag) = gcm_encrypt_authenticate('AES', $cek, $nonce, '', $self->content."\x02\x00");
|
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
#Some final composition
|
180
|
|
|
|
|
|
|
#0x00001000" is the coding unit of AES-GCM, and x41 is the key length
|
181
|
3
|
|
|
|
|
1119
|
$body = $salt."\x00\x00\x10\x00\x41".$pub_signkey.$body.$tag;
|
182
|
|
|
|
|
|
|
|
183
|
3
|
|
|
|
|
18
|
$self->content($body);
|
184
|
3
|
|
|
|
|
160
|
$self->remove_header('Content-Length', 'Content-MD5','Content-Encoding','Content-Type','Encryption','Crypto-Key');
|
185
|
3
|
|
|
|
|
311
|
$self->header('Crypto-Key' => "p256ecdsa=". encode_base64url($self->{'app-pub'}) );
|
186
|
3
|
|
|
|
|
390
|
$self->header('Content-Length' => length($body));
|
187
|
3
|
|
|
|
|
153
|
$self->header('Content-Type' => 'application/octet-stream');
|
188
|
3
|
|
|
|
|
141
|
$self->header('Content-Encoding' => 'aes128gcm');
|
189
|
3
|
|
|
|
|
329
|
return 1;
|
190
|
|
|
|
|
|
|
}
|
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
sub decoded_content
|
193
|
|
|
|
|
|
|
{
|
194
|
0
|
|
|
0
|
1
|
0
|
my($self, %opt) = @_;
|
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
#This is included so will always fail. decoded_content is inherited from HTTP::Message
|
197
|
0
|
|
|
|
|
0
|
croak 'No decoding available';
|
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
}
|
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
sub decode {
|
202
|
0
|
|
|
0
|
1
|
0
|
my($self, %opt) = @_;
|
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
#This is included so will always fail. decode is inherited from HTTP::Message
|
205
|
0
|
|
|
|
|
0
|
croak 'No decoding available';
|
206
|
|
|
|
|
|
|
}
|
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
sub new($%) {
|
209
|
|
|
|
|
|
|
|
210
|
3
|
|
|
3
|
1
|
462
|
my ($class, %opts)=@_;
|
211
|
|
|
|
|
|
|
|
212
|
3
|
|
|
|
|
24
|
my $self= HTTP::Request->new();
|
213
|
3
|
|
|
|
|
279
|
$self->method('POST');
|
214
|
|
|
|
|
|
|
|
215
|
3
|
|
|
|
|
33
|
bless $self, $class;
|
216
|
|
|
|
|
|
|
|
217
|
3
|
|
|
|
|
12
|
my @Options= ('auth','subscription','authbase64','reuseecc','subject','content');
|
218
|
3
|
|
|
|
|
11
|
for (@Options) {
|
219
|
18
|
50
|
|
|
|
40
|
&$_($self,$opts{$_}) if (exists $opts{$_});
|
220
|
|
|
|
|
|
|
}
|
221
|
|
|
|
|
|
|
|
222
|
3
|
|
|
|
|
13
|
return $self;
|
223
|
|
|
|
|
|
|
}
|
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
1;
|
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
=pod
|
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
=encoding UTF-8
|
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
=head1 NAME
|
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
HTTP::Request::Webpush - HTTP Request for web push notifications
|
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
=head1 VERSION
|
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
version 0.14
|
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
=head1 SYNOPSIS
|
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
use HTTP::Request::Webpush;
|
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
#This should be the application-wide VAPID key pair
|
245
|
|
|
|
|
|
|
#The APP_PUB part must be the same used by the user UA when requesting the subscription
|
246
|
|
|
|
|
|
|
use constant APP_PUB => 'BCAI...RA8';
|
247
|
|
|
|
|
|
|
use constant APP_KEY => 'M6x...UQTow';
|
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
#This should be previously collected from an already subscribed user UA
|
250
|
|
|
|
|
|
|
my $subscription='{"endpoint":"https://foo/fooer","expirationTime":null,"keys":{"p256dh":"BCNS...","auth":"dZ..."}}';
|
251
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
my $message=HTTP::Request::Webpush->new();
|
253
|
|
|
|
|
|
|
$message->auth(APP_PUB, APP_KEY);
|
254
|
|
|
|
|
|
|
$message->subscription($subscription);
|
255
|
|
|
|
|
|
|
$message->subject('mailto:bobsbeverage@some.com');
|
256
|
|
|
|
|
|
|
$message->content('Hello world');
|
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
#Additional headers can be applied with inherited HTTP::Response methods
|
259
|
|
|
|
|
|
|
$message->header('TTL' => '90');
|
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
#To send a single push message
|
262
|
|
|
|
|
|
|
$message->encode();
|
263
|
|
|
|
|
|
|
my $ua = LWP::UserAgent->new;
|
264
|
|
|
|
|
|
|
my $response = $ua->request($message);
|
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
#To send a batch of messages using the same application's end encryption key
|
267
|
|
|
|
|
|
|
my $ecc = Crypt::PK::ECC->new();
|
268
|
|
|
|
|
|
|
$ecc->generate_key('prime256v1');
|
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
for (@cleverly_stored_subscriptions) {
|
271
|
|
|
|
|
|
|
my $message=HTTP::Request::Webpush->new(reuseecc => $ecc);
|
272
|
|
|
|
|
|
|
$message->subscription($_);
|
273
|
|
|
|
|
|
|
$message->subject('mailto:bobsbeverage@some.com');
|
274
|
|
|
|
|
|
|
$message->content('Come taste our new pale ale brand');
|
275
|
|
|
|
|
|
|
$message->encode();
|
276
|
|
|
|
|
|
|
my $response = $ua->request($message);
|
277
|
|
|
|
|
|
|
}
|
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
C produces an HTTP::Request for Application-side Webpush
|
282
|
|
|
|
|
|
|
notifications as described on L.
|
283
|
|
|
|
|
|
|
Such requests can then be submitted to the push message channel so they will
|
284
|
|
|
|
|
|
|
pop-up in the corresponding end user host.
|
285
|
|
|
|
|
|
|
In this scheme, an Application is a
|
286
|
|
|
|
|
|
|
server-side component that sends push notification to previously subscribed
|
287
|
|
|
|
|
|
|
browser worker(s). This class only covers the Application role. A lot must
|
288
|
|
|
|
|
|
|
be done on the browser side to setup a full working push notification system.
|
289
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
In practical terms, this class is a glue for all the encryption steps involved
|
291
|
|
|
|
|
|
|
in setting up a RFC8291 message, along with the L VAPID scheme.
|
292
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
=over 4
|
294
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
=item C<$r=HTTP::Request::Webpush-Enew()>
|
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
=item C<$r=HTTP::Request::Webpush-Enew(auth =E $my_key, subscription =E $my_subs, content='New lager batch arrived')>
|
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
The following options can be supplied in the constructor: subscription, auth, reuseecc, subject, content.
|
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
=item C<$r-Esubscription($hash_reference)>
|
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
=item C<$r-Esubscription('{"endpoint":"https://foo/fooer","expirationTime":null,"keys":{"p256dh":"BCNS...","auth":"dZ..."}}');>
|
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
This sets the subscription object related to this notification service. This should be the same object
|
306
|
|
|
|
|
|
|
returned inside the browser environment using the browser's Push API C method. The argument can
|
307
|
|
|
|
|
|
|
be either a JSON string or a previously setup hash reference. The HTTP::Request uri is taken verbatim from the endpoint
|
308
|
|
|
|
|
|
|
of the subscription object.
|
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
=item C<$r-Eauth($pk) #pk being a Crypt::PK::ECC ref>
|
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
=item C<$r-Eauth($pub_bin, $priv_bin)>
|
313
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
=item C<$r-Eauthbase64('BCAI...jARA8','M6...Tow')>
|
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
This sets the authentication key for the VAPID authentication scheme related to this push service.
|
317
|
|
|
|
|
|
|
This can either be a (public, private) pair or an already setup L object. The public part
|
318
|
|
|
|
|
|
|
must be the same used earlier in the browser environment in the C option.
|
319
|
|
|
|
|
|
|
The key pair can be passed as URL safe base64 strings using the C variant.
|
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
=item C<$r-Ereuseecc($ecc) #ecc being a Crypt::PK::ECC ref>
|
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
By default, HTTP::Request::Webpush creates a new P-256 key pair for the encryption
|
324
|
|
|
|
|
|
|
step each time. In large push batches this can be time consuming. You can
|
325
|
|
|
|
|
|
|
reuse the same previously setup key pair in repeated messages using this method.
|
326
|
|
|
|
|
|
|
|
327
|
|
|
|
|
|
|
=item C<$r-Esubject('mailto:jdoe@some.com')>
|
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
This establish the contact information related to the origin of the push service. This method
|
330
|
|
|
|
|
|
|
isn't enforced since RFC8292 mentions this as a SHOULD practice. But, if a valid contact information
|
331
|
|
|
|
|
|
|
is not included, the browser push service is likely to bounce the message. The URI passed is
|
332
|
|
|
|
|
|
|
used as the 'sub' claim in the authentication JWT.
|
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
=item C<$r-Econtent('Try our new draft beer')>
|
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
This sets the unencripted message content, or the payload in terms of RFC8291.
|
337
|
|
|
|
|
|
|
This is actually inherited from L,
|
338
|
|
|
|
|
|
|
as well as other methods that can be used to set the message content.
|
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
=item C<$r-Eencode('aes128gcm')>
|
341
|
|
|
|
|
|
|
|
342
|
|
|
|
|
|
|
This does the encryption process, as well as setting the headers expected by the push service.
|
343
|
|
|
|
|
|
|
aes128gcm is the only acceptable argument. Before calling this, the subscription and auth must
|
344
|
|
|
|
|
|
|
be supplied. B, otherwise the encryption
|
345
|
|
|
|
|
|
|
process won't happen.
|
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
Please note that B and B are inherited from L.
|
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
=back
|
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
=head1 REMARKS
|
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
No backward decryption is provided by this class, so the decoded_content() and decode() methods will fail.
|
354
|
|
|
|
|
|
|
|
355
|
|
|
|
|
|
|
After you encode(), the content can still be accessed through HTTP::Message standard methods and it
|
356
|
|
|
|
|
|
|
will be the binary body of the encrypted message.
|
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
This class sets the following headers: I, I, I, I, I.
|
359
|
|
|
|
|
|
|
Additional headers might be added using the C method. Please note that the browser push
|
360
|
|
|
|
|
|
|
service will likely bounce the message if I is missing. The standard also states that an I header might apply.
|
361
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
=head1 REFERENCES
|
364
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
This class relies on L for the HKDF derivation,
|
366
|
|
|
|
|
|
|
L for the encryption itself, L for key management and
|
367
|
|
|
|
|
|
|
L for the salt.
|
368
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
RFC8291 establish the encription steps: L
|
370
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
RFC8292 establish the VAPID scheme: L
|
372
|
|
|
|
|
|
|
|
373
|
|
|
|
|
|
|
RFC8030 covers the whole HTTP push life cycle L
|
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
The following code samples and tutorials were very useful:
|
376
|
|
|
|
|
|
|
|
377
|
|
|
|
|
|
|
L
|
378
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
L
|
380
|
|
|
|
|
|
|
|
381
|
|
|
|
|
|
|
L
|
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
=head1 AUTHOR
|
384
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
Erich Strelow
|
386
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
388
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
Copyright 2021 Erich Strelow
|
390
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
392
|
|
|
|
|
|
|
you may not use this file except in compliance with the License.
|
393
|
|
|
|
|
|
|
You may obtain a copy of the License at
|
394
|
|
|
|
|
|
|
|
395
|
|
|
|
|
|
|
L
|
396
|
|
|
|
|
|
|
|
397
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
398
|
|
|
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
399
|
|
|
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
400
|
|
|
|
|
|
|
See the License for the specific language governing permissions and
|
401
|
|
|
|
|
|
|
limitations under the License.
|
402
|
|
|
|
|
|
|
|
403
|
|
|
|
|
|
|
=cut
|
404
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
__END__
|