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 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.