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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e6d555b555c5277dc1abbe12a9808292b62b241a
4
- data.tar.gz: 3f12232353b917a3135ce12f33a38f1a2a7f5388
2
+ SHA256:
3
+ metadata.gz: 5477ba01938fc82903b96c38eb3663259ffb2c85c5c077acb12c39ca0bf4f595
4
+ data.tar.gz: 1574886feed49138a2de64c37ad8997b54715b890a4517d13edbd82197d4e092
5
5
  SHA512:
6
- metadata.gz: b7429002edc35b7d8e92cb15ff6d329f52257d0f36ab46f316440c31f3141869d5617973e4c3250d79abd05efa4d60f0918194ab3160b42866cb400ba6220a20
7
- data.tar.gz: 1a22cb446da2ff49013a3a5474bb970bbfa5789b3d24c693be4aadca2eca64e94db242cbe4fc36185a77dee25b45c3259ef8edb834de273f136db4efbfa98746
6
+ metadata.gz: ebe9bed5482e052ccef497c0befc0c946079d6ce99db40acfbce09b042cc8123413cd8ac8c01776a40daaea3a8e4d4575d37e611d8d55191df5351adb6a29562
7
+ data.tar.gz: 23e55f3c0d33c1f9b12cde268132e9414c804a1c00ee3dbe44fb2a84ad01852aa88b8426f578f9a77ed31f3d6649feb6bf94f013e781cab2051bd7fb35abee0d
@@ -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
@@ -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
- before_install: gem install bundler
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
@@ -1,8 +1,46 @@
1
1
  # Change Log
2
2
 
3
- ## [Unreleased](https://github.com/zaru/webpush/tree/HEAD)
3
+ ## [v1.0.0](https://github.com/zaru/webpush/tree/v1.0.0) (2019-08-15)
4
4
 
5
- [Full Changelog](https://github.com/zaru/webpush/compare/v0.3.4...HEAD)
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 Chrome50+, Firefox48+.
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
@@ -1,5 +1,5 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
@@ -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
- def payload_send(message: "", endpoint:, p256dh: "", auth: "", vapid: {}, **options)
32
- subscription = {
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?("=") && str.length % 4 != 0
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
@@ -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 = "prime256v1"
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
- prk = HKDF.new(shared_secret, salt: client_auth_token, algorithm: 'SHA256', info: "Content-Encoding: auth\0").next_bytes(32)
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
- context = create_context(client_public_key_bn, server_public_key_bn)
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
- ciphertext: ciphertext,
37
- salt: salt,
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
- private
42
+ aes128gcmheader = "#{salt}" + [rs].pack('N*') + [serverkey16bn.bytesize].pack('C*') + serverkey16bn
45
43
 
46
- def create_context(client_public_key, server_public_key)
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 = padding + text + cipher.final
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("H*")
64
+ [key.to_s(16)].pack('H*')
82
65
  end
83
66
 
84
67
  def assert_arguments(message, p256dh, auth)
85
- raise ArgumentError, "message cannot be blank" if blank?(message)
86
- raise ArgumentError, "p256dh cannot be blank" if blank?(p256dh)
87
- raise ArgumentError, "auth cannot be blank" if blank?(auth)
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
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Webpush
2
4
  class Railtie < Rails::Railtie
3
5
  rake_tasks do
@@ -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://gcm-http.googleapis.com/gcm'
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: "", subscription:, vapid:, **options)
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
- if resp.is_a?(Net::HTTPGone) # 410
30
- raise ExpiredSubscription.new(resp, uri.host)
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["Content-Type"] = "application/octet-stream"
52
- headers["Ttl"] = ttl
53
- headers["Urgency"] = urgency
54
-
55
- if @payload.has_key?(:server_public_key)
56
- headers["Content-Encoding"] = "aesgcm"
57
- headers["Encryption"] = "salt=#{salt_param}"
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["Authorization"] = api_key
61
+ headers['Authorization'] = "key=#{api_key}"
63
62
  elsif vapid?
64
- vapid_headers = build_vapid_headers
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 build_vapid_headers
73
- vapid_key = VapidKey.from_keys(vapid_public_key, vapid_private_key)
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.fetch(:ciphertext, "")
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
- { 'typ' => 'JWT' }
107
+ { "typ": "JWT", "alg": "ES256" }
119
108
  end
120
109
 
121
110
  def audience
122
- uri.scheme + "://" + uri.host
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 {} if message.nil? || message.empty?
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 =~ /\Ahttps:\/\/(android|gcm-http)\.googleapis\.com/
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
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Webpush
2
- VERSION = "0.3.5"
4
+ VERSION = '1.1.0'.freeze
3
5
  end
@@ -1,29 +1,30 @@
1
- # coding: utf-8
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 = "webpush"
6
+ spec.name = 'webpush'
8
7
  spec.version = Webpush::VERSION
9
- spec.authors = ["zaru@sakuraba"]
10
- spec.email = ["zarutofu@gmail.com"]
8
+ spec.authors = ['zaru@sakuraba']
9
+ spec.email = ['zarutofu@gmail.com']
11
10
 
12
- spec.summary = %q{Encryption Utilities for Web Push payload. }
13
- spec.homepage = "https://github.com/zaru/webpush"
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 = "exe"
15
+ spec.bindir = 'exe'
17
16
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
- spec.require_paths = ["lib"]
17
+ spec.require_paths = ['lib']
18
+
19
+ spec.required_ruby_version = '>= 2.2'
19
20
 
20
- spec.add_dependency "hkdf", "~> 0.2"
21
- spec.add_dependency "jwt", "~> 2.0"
21
+ spec.add_dependency 'hkdf', '~> 0.2'
22
+ spec.add_dependency 'jwt', '~> 2.0'
22
23
 
23
- spec.add_development_dependency "bundler", "~> 1.11"
24
+ spec.add_development_dependency 'bundler', '>= 1.17.3'
24
25
  spec.add_development_dependency 'pry'
25
- spec.add_development_dependency "rake", "~> 10.0"
26
- spec.add_development_dependency "rspec", "~> 3.0"
27
- spec.add_development_dependency "webmock", "~> 1.24"
28
- spec.add_development_dependency "ece", "~> 0.2"
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: 0.3.5
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: 2019-01-02 00:00:00.000000000 Z
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: '1.11'
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: '1.11'
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: webmock
98
+ name: simplecov
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: '1.24'
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: '1.24'
110
+ version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: ece
112
+ name: webmock
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0.2'
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.2'
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: '0'
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
- rubyforge_project:
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.