webpush 0.3.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.rubocop.yml +30 -0
- data/.travis.yml +14 -1
- data/CHANGELOG.md +40 -2
- data/README.md +25 -2
- data/Rakefile +2 -2
- data/lib/webpush.rb +23 -12
- data/lib/webpush/encryption.rb +23 -40
- data/lib/webpush/errors.rb +3 -2
- data/lib/webpush/railtie.rb +2 -0
- data/lib/webpush/request.rb +71 -56
- data/lib/webpush/vapid_key.rb +22 -1
- data/lib/webpush/version.rb +3 -1
- data/webpush.gemspec +17 -16
- metadata +19 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5477ba01938fc82903b96c38eb3663259ffb2c85c5c077acb12c39ca0bf4f595
|
|
4
|
+
data.tar.gz: 1574886feed49138a2de64c37ad8997b54715b890a4517d13edbd82197d4e092
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebe9bed5482e052ccef497c0befc0c946079d6ce99db40acfbce09b042cc8123413cd8ac8c01776a40daaea3a8e4d4575d37e611d8d55191df5351adb6a29562
|
|
7
|
+
data.tar.gz: 23e55f3c0d33c1f9b12cde268132e9414c804a1c00ee3dbe44fb2a84ad01852aa88b8426f578f9a77ed31f3d6649feb6bf94f013e781cab2051bd7fb35abee0d
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require: rubocop-performance
|
|
2
|
+
|
|
3
|
+
AllCops:
|
|
4
|
+
Exclude:
|
|
5
|
+
- 'bin/**/*'
|
|
6
|
+
|
|
7
|
+
Metrics/AbcSize:
|
|
8
|
+
Max: 20
|
|
9
|
+
|
|
10
|
+
Metrics/ClassLength:
|
|
11
|
+
Max: 100
|
|
12
|
+
|
|
13
|
+
Metrics/ModuleLength:
|
|
14
|
+
Max: 100
|
|
15
|
+
|
|
16
|
+
Metrics/LineLength:
|
|
17
|
+
Enabled: false
|
|
18
|
+
|
|
19
|
+
Metrics/BlockLength:
|
|
20
|
+
Exclude:
|
|
21
|
+
- spec/**/*_spec.rb
|
|
22
|
+
|
|
23
|
+
Lint/AmbiguousBlockAssociation:
|
|
24
|
+
Enabled: false
|
|
25
|
+
|
|
26
|
+
Style/Documentation:
|
|
27
|
+
Enabled: false
|
|
28
|
+
|
|
29
|
+
Style/IndentHeredoc:
|
|
30
|
+
Enabled: false
|
data/.travis.yml
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
env:
|
|
2
|
+
global:
|
|
3
|
+
- CC_TEST_REPORTER_ID=155202524386dfebe0c3267a5c868b5417ff4cc2cde8ed301fb36b177d46a458
|
|
1
4
|
language: ruby
|
|
2
5
|
rvm:
|
|
3
6
|
- 2.2
|
|
4
7
|
- 2.3
|
|
5
8
|
- 2.4
|
|
6
9
|
- 2.5
|
|
7
|
-
|
|
10
|
+
- 2.6
|
|
11
|
+
- 2.7
|
|
12
|
+
before_install:
|
|
13
|
+
- gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
|
|
14
|
+
- gem install bundler -v 1.17.3
|
|
15
|
+
before_script:
|
|
16
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
|
17
|
+
- chmod +x ./cc-test-reporter
|
|
18
|
+
- ./cc-test-reporter before-build
|
|
8
19
|
script: "bundle exec rake spec"
|
|
20
|
+
after_script:
|
|
21
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
|
9
22
|
addons:
|
|
10
23
|
code_climate:
|
|
11
24
|
repo_token: 155202524386dfebe0c3267a5c868b5417ff4cc2cde8ed301fb36b177d46a458
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,46 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [v1.0.0](https://github.com/zaru/webpush/tree/v1.0.0) (2019-08-15)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A stable version 1.0.0 has been released.
|
|
6
|
+
|
|
7
|
+
Thanks @mohamedhafez, @mplatov and @MedetaiAkaru for everything!
|
|
8
|
+
|
|
9
|
+
[Full Changelog](https://github.com/zaru/webpush/compare/v0.3.8...v1.0.0)
|
|
10
|
+
|
|
11
|
+
**Merged pull requests:**
|
|
12
|
+
|
|
13
|
+
- switch to aes128gcm encoding [\#84](https://github.com/zaru/webpush/pull/84) ([mohamedhafez](https://github.com/mohamedhafez))
|
|
14
|
+
- Fixed fcm spec [\#77](https://github.com/zaru/webpush/pull/77) ([zaru](https://github.com/zaru))
|
|
15
|
+
- add fcm endpoints [\#76](https://github.com/zaru/webpush/pull/76) ([MedetaiAkaru](https://github.com/MedetaiAkaru))
|
|
16
|
+
- Add Rubocop and fix [\#74](https://github.com/zaru/webpush/pull/74) ([zaru](https://github.com/zaru))
|
|
17
|
+
- Fix TravisCI bundler version [\#73](https://github.com/zaru/webpush/pull/73) ([zaru](https://github.com/zaru))
|
|
18
|
+
|
|
19
|
+
## [v0.3.8](https://github.com/zaru/webpush/tree/v0.3.8) (2019-04-16)
|
|
20
|
+
[Full Changelog](https://github.com/zaru/webpush/compare/v0.3.7...v0.3.8)
|
|
21
|
+
|
|
22
|
+
**Merged pull requests:**
|
|
23
|
+
|
|
24
|
+
- Fix authorization header [\#72](https://github.com/zaru/webpush/pull/72) ([xronos-i-am](https://github.com/xronos-i-am))
|
|
25
|
+
|
|
26
|
+
## [v0.3.7](https://github.com/zaru/webpush/tree/v0.3.7) (2019-03-06)
|
|
27
|
+
[Full Changelog](https://github.com/zaru/webpush/compare/v0.3.6...v0.3.7)
|
|
28
|
+
|
|
29
|
+
**Merged pull requests:**
|
|
30
|
+
|
|
31
|
+
- Add PEM support to import / export keys [\#65](https://github.com/zaru/webpush/pull/65) ([collimarco](https://github.com/collimarco))
|
|
32
|
+
|
|
33
|
+
## [v0.3.6](https://github.com/zaru/webpush/tree/v0.3.6) (2019-01-09)
|
|
34
|
+
[Full Changelog](https://github.com/zaru/webpush/compare/v0.3.5...v0.3.6)
|
|
35
|
+
|
|
36
|
+
**Merged pull requests:**
|
|
37
|
+
|
|
38
|
+
- Added a error class to arguments of raise\_error [\#62](https://github.com/zaru/webpush/pull/62) ([zaru](https://github.com/zaru))
|
|
39
|
+
- Fix TravisCI bundler version [\#61](https://github.com/zaru/webpush/pull/61) ([zaru](https://github.com/zaru))
|
|
40
|
+
- Raise Webpush::Unauthorized on HTTP 403 [\#59](https://github.com/zaru/webpush/pull/59) ([collimarco](https://github.com/collimarco))
|
|
41
|
+
|
|
42
|
+
## [v0.3.5](https://github.com/zaru/webpush/tree/v0.3.5) (2019-01-02)
|
|
43
|
+
[Full Changelog](https://github.com/zaru/webpush/compare/v0.3.4...v0.3.5)
|
|
6
44
|
|
|
7
45
|
**Merged pull requests:**
|
|
8
46
|
|
data/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# WebPush
|
|
2
2
|
|
|
3
3
|
[](https://codeclimate.com/github/zaru/webpush)
|
|
4
|
+
[](https://codeclimate.com/github/zaru/webpush/coverage)
|
|
4
5
|
[](https://travis-ci.org/zaru/webpush)
|
|
5
6
|
[](https://badge.fury.io/rb/webpush)
|
|
6
7
|
|
|
7
8
|
This gem makes it possible to send push messages to web browsers from Ruby backends using the [Web Push Protocol](https://tools.ietf.org/html/draft-ietf-webpush-protocol-10). It supports [Message Encryption for Web Push](https://tools.ietf.org/html/draft-ietf-webpush-encryption) to send messages securely from server to user agent.
|
|
8
9
|
|
|
9
|
-
Payload is supported by
|
|
10
|
+
Payload is supported by Chrome 50+, Firefox 48+, Edge 79+.
|
|
11
|
+
|
|
12
|
+
[webpush Demo app here (building by Sinatra app).](https://github.com/zaru/webpush_demo_ruby)
|
|
10
13
|
|
|
11
14
|
## Installation
|
|
12
15
|
|
|
@@ -45,6 +48,9 @@ vapid_key = Webpush.generate_key
|
|
|
45
48
|
# Save these in your application server settings
|
|
46
49
|
vapid_key.public_key
|
|
47
50
|
vapid_key.private_key
|
|
51
|
+
|
|
52
|
+
# Or you can save in PEM format if you prefer
|
|
53
|
+
vapid_key.to_pem
|
|
48
54
|
```
|
|
49
55
|
|
|
50
56
|
### Declaring manifest.json
|
|
@@ -274,13 +280,30 @@ Webpush.payload_send(
|
|
|
274
280
|
p256dh: "BO/aG9nYXNkZmFkc2ZmZHNmYWRzZmFl...",
|
|
275
281
|
auth: "aW1hcmthcmFpa3V6ZQ==",
|
|
276
282
|
vapid: {
|
|
277
|
-
subject: "mailto:sender@example.com"
|
|
283
|
+
subject: "mailto:sender@example.com",
|
|
278
284
|
public_key: ENV['VAPID_PUBLIC_KEY'],
|
|
279
285
|
private_key: ENV['VAPID_PRIVATE_KEY']
|
|
280
286
|
}
|
|
281
287
|
)
|
|
282
288
|
```
|
|
283
289
|
|
|
290
|
+
### With VAPID in PEM format
|
|
291
|
+
|
|
292
|
+
This library also supports the PEM format for the VAPID keys:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
Webpush.payload_send(
|
|
296
|
+
endpoint: "https://fcm.googleapis.com/gcm/send/eah7hak....",
|
|
297
|
+
message: "A message",
|
|
298
|
+
p256dh: "BO/aG9nYXNkZmFkc2ZmZHNmYWRzZmFl...",
|
|
299
|
+
auth: "aW1hcmthcmFpa3V6ZQ==",
|
|
300
|
+
vapid: {
|
|
301
|
+
subject: "mailto:sender@example.com"
|
|
302
|
+
pem: ENV['VAPID_KEYS']
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
```
|
|
306
|
+
|
|
284
307
|
### With GCM api key
|
|
285
308
|
|
|
286
309
|
```ruby
|
data/Rakefile
CHANGED
data/lib/webpush.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'openssl'
|
|
2
4
|
require 'base64'
|
|
3
5
|
require 'hkdf'
|
|
@@ -11,6 +13,10 @@ require 'webpush/encryption'
|
|
|
11
13
|
require 'webpush/request'
|
|
12
14
|
require 'webpush/railtie' if defined?(Rails)
|
|
13
15
|
|
|
16
|
+
# Push API implementation
|
|
17
|
+
#
|
|
18
|
+
# https://tools.ietf.org/html/rfc8030
|
|
19
|
+
# https://www.w3.org/TR/push-api/
|
|
14
20
|
module Webpush
|
|
15
21
|
class << self
|
|
16
22
|
# Deliver the payload to the required endpoint given by the JavaScript
|
|
@@ -28,21 +34,16 @@ module Webpush
|
|
|
28
34
|
# @param options [Hash<Symbol,String>] additional options for the notification
|
|
29
35
|
# @option options [#to_s] :ttl Time-to-live in seconds
|
|
30
36
|
# @option options [#to_s] :urgency Urgency can be very-low, low, normal, high
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
endpoint: endpoint,
|
|
34
|
-
keys: {
|
|
35
|
-
p256dh: p256dh,
|
|
36
|
-
auth: auth
|
|
37
|
-
}
|
|
38
|
-
}
|
|
37
|
+
# rubocop:disable Metrics/ParameterLists
|
|
38
|
+
def payload_send(message: '', endpoint:, p256dh: '', auth: '', vapid: {}, **options)
|
|
39
39
|
Webpush::Request.new(
|
|
40
40
|
message: message,
|
|
41
|
-
subscription: subscription,
|
|
41
|
+
subscription: subscription(endpoint, p256dh, auth),
|
|
42
42
|
vapid: vapid,
|
|
43
43
|
**options
|
|
44
44
|
).perform
|
|
45
45
|
end
|
|
46
|
+
# rubocop:enable Metrics/ParameterLists
|
|
46
47
|
|
|
47
48
|
# Generate a VapidKey instance to obtain base64 encoded public and private keys
|
|
48
49
|
# suitable for VAPID protocol JSON web token signing
|
|
@@ -59,11 +60,21 @@ module Webpush
|
|
|
59
60
|
def decode64(str)
|
|
60
61
|
# For Ruby < 2.3, Base64.urlsafe_decode64 strict decodes and will raise errors if encoded value is not properly padded
|
|
61
62
|
# Implementation: http://ruby-doc.org/stdlib-2.3.0/libdoc/base64/rdoc/Base64.html#method-i-urlsafe_decode64
|
|
62
|
-
if !str.end_with?(
|
|
63
|
-
str = str.ljust((str.length + 3) & ~3, "=")
|
|
64
|
-
end
|
|
63
|
+
str = str.ljust((str.length + 3) & ~3, '=') if !str.end_with?('=') && str.length % 4 != 0
|
|
65
64
|
|
|
66
65
|
Base64.urlsafe_decode64(str)
|
|
67
66
|
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def subscription(endpoint, p256dh, auth)
|
|
71
|
+
{
|
|
72
|
+
endpoint: endpoint,
|
|
73
|
+
keys: {
|
|
74
|
+
p256dh: p256dh,
|
|
75
|
+
auth: auth
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
end
|
|
68
79
|
end
|
|
69
80
|
end
|
data/lib/webpush/encryption.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Webpush
|
|
2
4
|
module Encryption
|
|
3
5
|
extend self
|
|
4
6
|
|
|
7
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
5
8
|
def encrypt(message, p256dh, auth)
|
|
6
9
|
assert_arguments(message, p256dh, auth)
|
|
7
10
|
|
|
8
|
-
group_name =
|
|
11
|
+
group_name = 'prime256v1'
|
|
9
12
|
salt = Random.new.bytes(16)
|
|
10
13
|
|
|
11
14
|
server = OpenSSL::PKey::EC.new(group_name)
|
|
@@ -20,75 +23,55 @@ module Webpush
|
|
|
20
23
|
|
|
21
24
|
client_auth_token = Webpush.decode64(auth)
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
info = "WebPush: info\0" + client_public_key_bn.to_s(2) + server_public_key_bn.to_s(2)
|
|
27
|
+
content_encryption_key_info = "Content-Encoding: aes128gcm\0"
|
|
28
|
+
nonce_info = "Content-Encoding: nonce\0"
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
prk = HKDF.new(shared_secret, salt: client_auth_token, algorithm: 'SHA256', info: info).next_bytes(32)
|
|
26
31
|
|
|
27
|
-
content_encryption_key_info = create_info('aesgcm', context)
|
|
28
32
|
content_encryption_key = HKDF.new(prk, salt: salt, info: content_encryption_key_info).next_bytes(16)
|
|
29
33
|
|
|
30
|
-
nonce_info = create_info('nonce', context)
|
|
31
34
|
nonce = HKDF.new(prk, salt: salt, info: nonce_info).next_bytes(12)
|
|
32
35
|
|
|
33
36
|
ciphertext = encrypt_payload(message, content_encryption_key, nonce)
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
server_public_key_bn: convert16bit(server_public_key_bn),
|
|
39
|
-
server_public_key: server_public_key_bn.to_s(2),
|
|
40
|
-
shared_secret: shared_secret
|
|
41
|
-
}
|
|
42
|
-
end
|
|
38
|
+
serverkey16bn = convert16bit(server_public_key_bn)
|
|
39
|
+
rs = ciphertext.bytesize
|
|
40
|
+
raise ArgumentError, "encrypted payload is too big" if rs > 4096
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
aes128gcmheader = "#{salt}" + [rs].pack('N*') + [serverkey16bn.bytesize].pack('C*') + serverkey16bn
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
c = convert16bit(client_public_key)
|
|
48
|
-
s = convert16bit(server_public_key)
|
|
49
|
-
context = "\0"
|
|
50
|
-
context += [c.bytesize].pack("n*")
|
|
51
|
-
context += c
|
|
52
|
-
context += [s.bytesize].pack("n*")
|
|
53
|
-
context += s
|
|
54
|
-
context
|
|
44
|
+
aes128gcmheader + ciphertext
|
|
55
45
|
end
|
|
46
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
47
|
+
|
|
48
|
+
private
|
|
56
49
|
|
|
57
50
|
def encrypt_payload(plaintext, content_encryption_key, nonce)
|
|
58
51
|
cipher = OpenSSL::Cipher.new('aes-128-gcm')
|
|
59
52
|
cipher.encrypt
|
|
60
53
|
cipher.key = content_encryption_key
|
|
61
54
|
cipher.iv = nonce
|
|
62
|
-
padding = cipher.update("\0\0")
|
|
63
55
|
text = cipher.update(plaintext)
|
|
64
|
-
|
|
65
|
-
e_text =
|
|
56
|
+
padding = cipher.update("\2\0")
|
|
57
|
+
e_text = text + padding + cipher.final
|
|
66
58
|
e_tag = cipher.auth_tag
|
|
67
59
|
|
|
68
60
|
e_text + e_tag
|
|
69
61
|
end
|
|
70
62
|
|
|
71
|
-
def create_info(type, context)
|
|
72
|
-
info = "Content-Encoding: "
|
|
73
|
-
info += type
|
|
74
|
-
info += "\0"
|
|
75
|
-
info += "P-256"
|
|
76
|
-
info += context
|
|
77
|
-
info
|
|
78
|
-
end
|
|
79
|
-
|
|
80
63
|
def convert16bit(key)
|
|
81
|
-
[key.to_s(16)].pack(
|
|
64
|
+
[key.to_s(16)].pack('H*')
|
|
82
65
|
end
|
|
83
66
|
|
|
84
67
|
def assert_arguments(message, p256dh, auth)
|
|
85
|
-
raise ArgumentError,
|
|
86
|
-
raise ArgumentError,
|
|
87
|
-
raise ArgumentError,
|
|
68
|
+
raise ArgumentError, 'message cannot be blank' if blank?(message)
|
|
69
|
+
raise ArgumentError, 'p256dh cannot be blank' if blank?(p256dh)
|
|
70
|
+
raise ArgumentError, 'auth cannot be blank' if blank?(auth)
|
|
88
71
|
end
|
|
89
72
|
|
|
90
73
|
def blank?(value)
|
|
91
74
|
value.nil? || value.empty?
|
|
92
75
|
end
|
|
93
76
|
end
|
|
94
|
-
end
|
|
77
|
+
end
|
data/lib/webpush/errors.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Webpush
|
|
2
4
|
class Error < RuntimeError; end
|
|
3
5
|
|
|
4
6
|
class ConfigurationError < Error; end
|
|
5
7
|
|
|
6
|
-
class ResponseError < Error
|
|
8
|
+
class ResponseError < Error
|
|
7
9
|
attr_reader :response, :host
|
|
8
10
|
|
|
9
11
|
def initialize(response, host)
|
|
@@ -24,5 +26,4 @@ module Webpush
|
|
|
24
26
|
class TooManyRequests < ResponseError; end
|
|
25
27
|
|
|
26
28
|
class PushServiceError < ResponseError; end
|
|
27
|
-
|
|
28
29
|
end
|
data/lib/webpush/railtie.rb
CHANGED
data/lib/webpush/request.rb
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
1
4
|
require 'jwt'
|
|
2
5
|
require 'base64'
|
|
3
6
|
|
|
4
7
|
module Webpush
|
|
5
8
|
# It is temporary URL until supported by the GCM server.
|
|
6
|
-
GCM_URL = 'https://android.googleapis.com/gcm/send'
|
|
7
|
-
TEMP_GCM_URL = 'https://
|
|
9
|
+
GCM_URL = 'https://android.googleapis.com/gcm/send'.freeze
|
|
10
|
+
TEMP_GCM_URL = 'https://fcm.googleapis.com/fcm'.freeze
|
|
8
11
|
|
|
12
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
13
|
class Request
|
|
10
|
-
def initialize(message:
|
|
14
|
+
def initialize(message: '', subscription:, vapid:, **options)
|
|
11
15
|
endpoint = subscription.fetch(:endpoint)
|
|
12
16
|
@endpoint = endpoint.gsub(GCM_URL, TEMP_GCM_URL)
|
|
13
17
|
@payload = build_payload(message, subscription)
|
|
@@ -15,8 +19,9 @@ module Webpush
|
|
|
15
19
|
@options = default_options.merge(options)
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
# rubocop:disable Metrics/AbcSize
|
|
18
23
|
def perform
|
|
19
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
24
|
+
http = Net::HTTP.new(uri.host, uri.port, *proxy_options)
|
|
20
25
|
http.use_ssl = true
|
|
21
26
|
http.ssl_timeout = @options[:ssl_timeout] unless @options[:ssl_timeout].nil?
|
|
22
27
|
http.open_timeout = @options[:open_timeout] unless @options[:open_timeout].nil?
|
|
@@ -24,64 +29,56 @@ module Webpush
|
|
|
24
29
|
|
|
25
30
|
req = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
26
31
|
req.body = body
|
|
27
|
-
resp = http.request(req)
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
elsif resp.is_a?(Net::HTTPNotFound) # 404
|
|
32
|
-
raise InvalidSubscription.new(resp, uri.host)
|
|
33
|
-
elsif resp.is_a?(Net::HTTPUnauthorized) || # 401, Mozilla autopush
|
|
34
|
-
resp.is_a?(Net::HTTPBadRequest) && resp.message == "UnauthorizedRegistration" # 400, Google FCM
|
|
35
|
-
raise Unauthorized.new(resp, uri.host)
|
|
36
|
-
elsif resp.is_a?(Net::HTTPRequestEntityTooLarge) # 413
|
|
37
|
-
raise PayloadTooLarge.new(resp, uri.host)
|
|
38
|
-
elsif resp.is_a?(Net::HTTPTooManyRequests) # 429, try again later!
|
|
39
|
-
raise TooManyRequests.new(resp, uri.host)
|
|
40
|
-
elsif resp.is_a?(Net::HTTPServerError) # 5xx
|
|
41
|
-
raise PushServiceError.new(resp, uri.host)
|
|
42
|
-
elsif !resp.is_a?(Net::HTTPSuccess) # unknown/unhandled response error
|
|
43
|
-
raise ResponseError.new(resp, uri.host)
|
|
44
|
-
end
|
|
33
|
+
resp = http.request(req)
|
|
34
|
+
verify_response(resp)
|
|
45
35
|
|
|
46
36
|
resp
|
|
47
37
|
end
|
|
38
|
+
# rubocop:enable Metrics/AbcSize
|
|
39
|
+
|
|
40
|
+
def proxy_options
|
|
41
|
+
return [] unless @options[:proxy]
|
|
42
|
+
|
|
43
|
+
proxy_uri = URI.parse(@options[:proxy])
|
|
48
44
|
|
|
45
|
+
[proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# rubocop:disable Metrics/MethodLength
|
|
49
49
|
def headers
|
|
50
50
|
headers = {}
|
|
51
|
-
headers[
|
|
52
|
-
headers[
|
|
53
|
-
headers[
|
|
54
|
-
|
|
55
|
-
if @payload
|
|
56
|
-
headers[
|
|
57
|
-
headers["
|
|
58
|
-
headers["Crypto-Key"] = "dh=#{dh_param}"
|
|
51
|
+
headers['Content-Type'] = 'application/octet-stream'
|
|
52
|
+
headers['Ttl'] = ttl
|
|
53
|
+
headers['Urgency'] = urgency
|
|
54
|
+
|
|
55
|
+
if @payload
|
|
56
|
+
headers['Content-Encoding'] = 'aes128gcm'
|
|
57
|
+
headers["Content-Length"] = @payload.length.to_s
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
if api_key?
|
|
62
|
-
headers[
|
|
61
|
+
headers['Authorization'] = "key=#{api_key}"
|
|
63
62
|
elsif vapid?
|
|
64
|
-
|
|
65
|
-
headers["Authorization"] = vapid_headers["Authorization"]
|
|
66
|
-
headers["Crypto-Key"] = [ headers["Crypto-Key"], vapid_headers["Crypto-Key"] ].compact.join(";")
|
|
63
|
+
headers["Authorization"] = build_vapid_header
|
|
67
64
|
end
|
|
68
65
|
|
|
69
66
|
headers
|
|
70
67
|
end
|
|
68
|
+
# rubocop:enable Metrics/MethodLength
|
|
71
69
|
|
|
72
|
-
def
|
|
73
|
-
|
|
70
|
+
def build_vapid_header
|
|
71
|
+
# https://tools.ietf.org/id/draft-ietf-webpush-vapid-03.html
|
|
72
|
+
|
|
73
|
+
vapid_key = vapid_pem ? VapidKey.from_pem(vapid_pem) : VapidKey.from_keys(vapid_public_key, vapid_private_key)
|
|
74
74
|
jwt = JWT.encode(jwt_payload, vapid_key.curve, 'ES256', jwt_header_fields)
|
|
75
75
|
p256ecdsa = vapid_key.public_key_for_push_header
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
'Authorization' => 'WebPush ' + jwt,
|
|
79
|
-
'Crypto-Key' => 'p256ecdsa=' + p256ecdsa,
|
|
80
|
-
}
|
|
77
|
+
"vapid t=#{jwt},k=#{p256ecdsa}"
|
|
81
78
|
end
|
|
82
79
|
|
|
83
80
|
def body
|
|
84
|
-
@payload
|
|
81
|
+
@payload || ''
|
|
85
82
|
end
|
|
86
83
|
|
|
87
84
|
private
|
|
@@ -98,32 +95,24 @@ module Webpush
|
|
|
98
95
|
@options.fetch(:urgency).to_s
|
|
99
96
|
end
|
|
100
97
|
|
|
101
|
-
def dh_param
|
|
102
|
-
trim_encode64(@payload.fetch(:server_public_key))
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def salt_param
|
|
106
|
-
trim_encode64(@payload.fetch(:salt))
|
|
107
|
-
end
|
|
108
|
-
|
|
109
98
|
def jwt_payload
|
|
110
99
|
{
|
|
111
100
|
aud: audience,
|
|
112
101
|
exp: Time.now.to_i + expiration,
|
|
113
|
-
sub: subject
|
|
102
|
+
sub: subject
|
|
114
103
|
}
|
|
115
104
|
end
|
|
116
105
|
|
|
117
106
|
def jwt_header_fields
|
|
118
|
-
{
|
|
107
|
+
{ "typ": "JWT", "alg": "ES256" }
|
|
119
108
|
end
|
|
120
109
|
|
|
121
110
|
def audience
|
|
122
|
-
uri.scheme +
|
|
111
|
+
uri.scheme + '://' + uri.host
|
|
123
112
|
end
|
|
124
113
|
|
|
125
114
|
def expiration
|
|
126
|
-
@vapid_options.fetch(:expiration, 24*60*60)
|
|
115
|
+
@vapid_options.fetch(:expiration, 24 * 60 * 60)
|
|
127
116
|
end
|
|
128
117
|
|
|
129
118
|
def subject
|
|
@@ -138,17 +127,21 @@ module Webpush
|
|
|
138
127
|
@vapid_options.fetch(:private_key, nil)
|
|
139
128
|
end
|
|
140
129
|
|
|
130
|
+
def vapid_pem
|
|
131
|
+
@vapid_options.fetch(:pem, nil)
|
|
132
|
+
end
|
|
133
|
+
|
|
141
134
|
def default_options
|
|
142
135
|
{
|
|
143
|
-
ttl: 60*60*24*7*4, # 4 weeks
|
|
136
|
+
ttl: 60 * 60 * 24 * 7 * 4, # 4 weeks
|
|
144
137
|
urgency: 'normal'
|
|
145
138
|
}
|
|
146
139
|
end
|
|
147
140
|
|
|
148
141
|
def build_payload(message, subscription)
|
|
149
|
-
return
|
|
142
|
+
return nil if message.nil? || message.empty?
|
|
150
143
|
|
|
151
|
-
encrypt_payload(message, subscription.fetch(:keys))
|
|
144
|
+
encrypt_payload(message, **subscription.fetch(:keys))
|
|
152
145
|
end
|
|
153
146
|
|
|
154
147
|
def encrypt_payload(message, p256dh:, auth:)
|
|
@@ -160,7 +153,7 @@ module Webpush
|
|
|
160
153
|
end
|
|
161
154
|
|
|
162
155
|
def api_key?
|
|
163
|
-
!(api_key.nil? || api_key.empty?) && @endpoint =~
|
|
156
|
+
!(api_key.nil? || api_key.empty?) && @endpoint =~ %r{\Ahttps://(android|gcm-http|fcm)\.googleapis\.com}
|
|
164
157
|
end
|
|
165
158
|
|
|
166
159
|
def vapid?
|
|
@@ -170,5 +163,27 @@ module Webpush
|
|
|
170
163
|
def trim_encode64(bin)
|
|
171
164
|
Webpush.encode64(bin).delete('=')
|
|
172
165
|
end
|
|
166
|
+
|
|
167
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Style/GuardClause
|
|
168
|
+
def verify_response(resp)
|
|
169
|
+
if resp.is_a?(Net::HTTPGone) # 410
|
|
170
|
+
raise ExpiredSubscription.new(resp, uri.host)
|
|
171
|
+
elsif resp.is_a?(Net::HTTPNotFound) # 404
|
|
172
|
+
raise InvalidSubscription.new(resp, uri.host)
|
|
173
|
+
elsif resp.is_a?(Net::HTTPUnauthorized) || resp.is_a?(Net::HTTPForbidden) || # 401, 403
|
|
174
|
+
resp.is_a?(Net::HTTPBadRequest) && resp.message == 'UnauthorizedRegistration' # 400, Google FCM
|
|
175
|
+
raise Unauthorized.new(resp, uri.host)
|
|
176
|
+
elsif resp.is_a?(Net::HTTPRequestEntityTooLarge) # 413
|
|
177
|
+
raise PayloadTooLarge.new(resp, uri.host)
|
|
178
|
+
elsif resp.is_a?(Net::HTTPTooManyRequests) # 429, try again later!
|
|
179
|
+
raise TooManyRequests.new(resp, uri.host)
|
|
180
|
+
elsif resp.is_a?(Net::HTTPServerError) # 5xx
|
|
181
|
+
raise PushServiceError.new(resp, uri.host)
|
|
182
|
+
elsif !resp.is_a?(Net::HTTPSuccess) # unknown/unhandled response error
|
|
183
|
+
raise ResponseError.new(resp, uri.host)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Style/GuardClause
|
|
173
187
|
end
|
|
188
|
+
# rubocop:enable Metrics/ClassLength
|
|
174
189
|
end
|
data/lib/webpush/vapid_key.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Webpush
|
|
2
4
|
# Class for abstracting the generation and encoding of elliptic curve public and private keys for use with the VAPID protocol
|
|
3
5
|
#
|
|
@@ -14,6 +16,18 @@ module Webpush
|
|
|
14
16
|
key
|
|
15
17
|
end
|
|
16
18
|
|
|
19
|
+
# Create a VapidKey instance from pem encoded elliptic curve public and private keys
|
|
20
|
+
#
|
|
21
|
+
# @return [Webpush::VapidKey] a VapidKey instance for the given public and private keys
|
|
22
|
+
def self.from_pem(pem)
|
|
23
|
+
key = new
|
|
24
|
+
src = OpenSSL::PKey.read pem
|
|
25
|
+
key.curve.public_key = src.public_key
|
|
26
|
+
key.curve.private_key = src.private_key
|
|
27
|
+
|
|
28
|
+
key
|
|
29
|
+
end
|
|
30
|
+
|
|
17
31
|
attr_reader :curve
|
|
18
32
|
|
|
19
33
|
def initialize
|
|
@@ -63,8 +77,15 @@ module Webpush
|
|
|
63
77
|
end
|
|
64
78
|
alias to_hash to_h
|
|
65
79
|
|
|
80
|
+
def to_pem
|
|
81
|
+
public_key = OpenSSL::PKey::EC.new curve
|
|
82
|
+
public_key.private_key = nil
|
|
83
|
+
|
|
84
|
+
curve.to_pem + public_key.to_pem
|
|
85
|
+
end
|
|
86
|
+
|
|
66
87
|
def inspect
|
|
67
|
-
"#<#{self.class}:#{object_id.to_s(16)} #{to_h.map { |k, v| ":#{k}=#{v}" }.join(
|
|
88
|
+
"#<#{self.class}:#{object_id.to_s(16)} #{to_h.map { |k, v| ":#{k}=#{v}" }.join(' ')}>"
|
|
68
89
|
end
|
|
69
90
|
|
|
70
91
|
private
|
data/lib/webpush/version.rb
CHANGED
data/webpush.gemspec
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
3
|
require 'webpush/version'
|
|
5
4
|
|
|
6
5
|
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name =
|
|
6
|
+
spec.name = 'webpush'
|
|
8
7
|
spec.version = Webpush::VERSION
|
|
9
|
-
spec.authors = [
|
|
10
|
-
spec.email = [
|
|
8
|
+
spec.authors = ['zaru@sakuraba']
|
|
9
|
+
spec.email = ['zarutofu@gmail.com']
|
|
11
10
|
|
|
12
|
-
spec.summary =
|
|
13
|
-
spec.homepage =
|
|
11
|
+
spec.summary = 'Encryption Utilities for Web Push payload. '
|
|
12
|
+
spec.homepage = 'https://github.com/zaru/webpush'
|
|
14
13
|
|
|
15
14
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
16
|
-
spec.bindir =
|
|
15
|
+
spec.bindir = 'exe'
|
|
17
16
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
18
|
-
spec.require_paths = [
|
|
17
|
+
spec.require_paths = ['lib']
|
|
18
|
+
|
|
19
|
+
spec.required_ruby_version = '>= 2.2'
|
|
19
20
|
|
|
20
|
-
spec.add_dependency
|
|
21
|
-
spec.add_dependency
|
|
21
|
+
spec.add_dependency 'hkdf', '~> 0.2'
|
|
22
|
+
spec.add_dependency 'jwt', '~> 2.0'
|
|
22
23
|
|
|
23
|
-
spec.add_development_dependency
|
|
24
|
+
spec.add_development_dependency 'bundler', '>= 1.17.3'
|
|
24
25
|
spec.add_development_dependency 'pry'
|
|
25
|
-
spec.add_development_dependency
|
|
26
|
-
spec.add_development_dependency
|
|
27
|
-
spec.add_development_dependency
|
|
28
|
-
spec.add_development_dependency
|
|
26
|
+
spec.add_development_dependency 'rake', '>= 10.0'
|
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
28
|
+
spec.add_development_dependency 'simplecov'
|
|
29
|
+
spec.add_development_dependency 'webmock', '~> 3.0'
|
|
29
30
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: webpush
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- zaru@sakuraba
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-11-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: hkdf
|
|
@@ -42,16 +42,16 @@ dependencies:
|
|
|
42
42
|
name: bundler
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- - "
|
|
45
|
+
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version:
|
|
47
|
+
version: 1.17.3
|
|
48
48
|
type: :development
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
|
-
- - "
|
|
52
|
+
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version:
|
|
54
|
+
version: 1.17.3
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: pry
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -70,14 +70,14 @@ dependencies:
|
|
|
70
70
|
name: rake
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
|
-
- - "
|
|
73
|
+
- - ">="
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
75
|
version: '10.0'
|
|
76
76
|
type: :development
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
|
-
- - "
|
|
80
|
+
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '10.0'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
@@ -95,33 +95,33 @@ dependencies:
|
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
96
|
version: '3.0'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
98
|
+
name: simplecov
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
100
100
|
requirements:
|
|
101
|
-
- - "
|
|
101
|
+
- - ">="
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '
|
|
103
|
+
version: '0'
|
|
104
104
|
type: :development
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
|
-
- - "
|
|
108
|
+
- - ">="
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '
|
|
110
|
+
version: '0'
|
|
111
111
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name:
|
|
112
|
+
name: webmock
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
114
114
|
requirements:
|
|
115
115
|
- - "~>"
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '0
|
|
117
|
+
version: '3.0'
|
|
118
118
|
type: :development
|
|
119
119
|
prerelease: false
|
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '0
|
|
124
|
+
version: '3.0'
|
|
125
125
|
description:
|
|
126
126
|
email:
|
|
127
127
|
- zarutofu@gmail.com
|
|
@@ -131,6 +131,7 @@ extra_rdoc_files: []
|
|
|
131
131
|
files:
|
|
132
132
|
- ".gitignore"
|
|
133
133
|
- ".rspec"
|
|
134
|
+
- ".rubocop.yml"
|
|
134
135
|
- ".travis.yml"
|
|
135
136
|
- CHANGELOG.md
|
|
136
137
|
- Gemfile
|
|
@@ -161,15 +162,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
161
162
|
requirements:
|
|
162
163
|
- - ">="
|
|
163
164
|
- !ruby/object:Gem::Version
|
|
164
|
-
version: '
|
|
165
|
+
version: '2.2'
|
|
165
166
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
167
|
requirements:
|
|
167
168
|
- - ">="
|
|
168
169
|
- !ruby/object:Gem::Version
|
|
169
170
|
version: '0'
|
|
170
171
|
requirements: []
|
|
171
|
-
|
|
172
|
-
rubygems_version: 2.6.13
|
|
172
|
+
rubygems_version: 3.1.2
|
|
173
173
|
signing_key:
|
|
174
174
|
specification_version: 4
|
|
175
175
|
summary: Encryption Utilities for Web Push payload.
|