webpush 0.3.5 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Code Climate](https://codeclimate.com/github/zaru/webpush/badges/gpa.svg)](https://codeclimate.com/github/zaru/webpush)
|
4
|
+
[![Test Coverage](https://codeclimate.com/github/zaru/webpush/badges/coverage.svg)](https://codeclimate.com/github/zaru/webpush/coverage)
|
4
5
|
[![Build Status](https://travis-ci.org/zaru/webpush.svg?branch=master)](https://travis-ci.org/zaru/webpush)
|
5
6
|
[![Gem Version](https://badge.fury.io/rb/webpush.svg)](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.
|