apnotic 1.1.0 → 1.2.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
2
  SHA1:
3
- metadata.gz: 97653d26a662ae9a7faa8d9d4c2914de5f17b83e
4
- data.tar.gz: a069da2ff88b6de95d2ad3f118a7eb4f22fb240b
3
+ metadata.gz: 9bfb47d774564845182d268e32f8820017d0ba32
4
+ data.tar.gz: 47334f8ffb95809e4c224570c4d4f5ba1b747eb7
5
5
  SHA512:
6
- metadata.gz: e86c4f893bcdee198dad0f6276d4f5d4c05b4999bf4ae5cdeccd91450f99bbecd05a9fd8a67abfa60e14f8c498bbfd9d14872b47a534c244809e7db8908275b4
7
- data.tar.gz: 2e7820e5123299223fdf0e027c8a6bdb5500768d68a1a68afe59a3c88f5bc878c20a9fa8071e75fec6899b404e04cc7164fa4cd163b72de5011ded48fdb6a3c4
6
+ metadata.gz: 84259061d8e09e4ba609b0ae4e99980236984570c64207eed01f0cd3102aa361ec276829609891bc84c237f99df344dac232a69dabeb6672e67150b67c8cde03
7
+ data.tar.gz: fdb1ba71863a3520f3005817980dcc0e06073791b4958488517053b458a52bec3a22fa32704da7588cfe25b7b7cab0e4e9422671d0354be4b8fe107e1dc8830a
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # Apnotic
6
6
 
7
- Apnotic is a gem for sending Apple Push Notifications using the [HTTP-2 specifics](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW9).
7
+ Apnotic is a gem for sending Apple Push Notifications using the [HTTP-2 specifics](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html).
8
8
 
9
9
 
10
10
  ## Why "Yet Another APN" gem?
@@ -92,6 +92,25 @@ connection.join
92
92
  connection.close
93
93
  ```
94
94
 
95
+ #### Token-based authentication
96
+ Token-based authentication is supported. There are several advantages with token-based auth:
97
+
98
+ - There is no need to renew push certificates annually.
99
+ - A single key can be used for every app in your developer account.
100
+
101
+ First, you will need a [token signing key](http://help.apple.com/xcode/mac/current/#/dev54d690a66?sub=dev1eb5dfe65) from your Apple developer account.
102
+
103
+ Then configure your connection for `:token` authentication:
104
+
105
+ ```ruby
106
+ require 'apnotic'
107
+ connection = Apnotic::Connection.new(
108
+ auth_method: :token,
109
+ cert_path: "key.p8",
110
+ key_id: "p8_key_id",
111
+ team_id: "apple_team_id"
112
+ )
113
+ ```
95
114
 
96
115
  ### With Sidekiq / Rescue / ...
97
116
  > In case that errors are encountered, Apnotic will raise the error and repair the underlying connection, but it will not retry the requests that have failed. This is by design, so that the job manager (Sidekiq, Resque,...) can retry the job that failed. For this reason, it is recommended to use a queue engine that will retry unsuccessful pushes.
@@ -135,7 +154,7 @@ class MyWorker
135
154
  end
136
155
  ```
137
156
 
138
- > The official [APNs Provider API documentation](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html) explains how to interpret the responses given by the APNS.
157
+ > The official [APNs Provider API documentation](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html) explains how to interpret the responses given by the APNS.
139
158
 
140
159
  You may also consider using async pushes instead in a Sidekiq / Rescue worker.
141
160
 
@@ -184,7 +203,7 @@ Allows to set a callback for the connection. The only available event is `:error
184
203
  connection.on(:error) { |exception| puts "Exception has been raised: #{exception}" }
185
204
  ```
186
205
 
187
- > If the `:error` callback is not set, the underlying socket thread may raise an error in the main thread at unexpected execution times.
206
+ > If the `:error` callback is not set, the underlying socket thread may raise an error in the main thread at unexpected execution times.
188
207
 
189
208
  * **url** → **`URL`**
190
209
 
@@ -291,11 +310,11 @@ The response to a call to `connection.push`.
291
310
  * **body** → **`hash` or `string`**
292
311
 
293
312
  Returns the body of the response in Hash format if a valid JSON was returned, otherwise just the RAW body.
294
-
313
+
295
314
  * **headers** → **`hash`**
296
315
 
297
316
  Returns a Hash containing the Headers of the response.
298
-
317
+
299
318
  * **ok?** → **`boolean`**
300
319
 
301
320
  Returns if the push was successful.
@@ -311,9 +330,9 @@ The push object to be sent in an async call.
311
330
  #### Methods
312
331
 
313
332
  * **http2_request** → **`NetHttp2::Request`**
314
-
333
+
315
334
  Returns the HTTP/2 request of the push.
316
-
335
+
317
336
  * **on(event, &block)**
318
337
 
319
338
  Allows to set a callback for the request. Available events are:
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "net-http2", ">= 0.15", "< 2"
21
+ spec.add_dependency "net-http2", ">= 0.16", "< 2"
22
22
  spec.add_dependency "connection_pool", "~> 2.0"
23
23
 
24
24
  spec.add_development_dependency "bundler", "~> 1.3"
@@ -1,3 +1,5 @@
1
+ require 'apnotic/instance_cache'
2
+ require 'apnotic/provider_token'
1
3
  require 'apnotic/connection'
2
4
  require 'apnotic/connection_pool'
3
5
  require 'apnotic/notification'
@@ -21,6 +21,10 @@ module Apnotic
21
21
  @cert_path = options[:cert_path]
22
22
  @cert_pass = options[:cert_pass]
23
23
  @connect_timeout = options[:connect_timeout] || 30
24
+ @auth_method = options[:auth_method] || :cert
25
+ @team_id = options[:team_id]
26
+ @key_id = options[:key_id]
27
+ @first_push = true
24
28
 
25
29
  raise "Cert file not found: #{@cert_path}" unless @cert_path && (@cert_path.respond_to?(:read) || File.exist?(@cert_path))
26
30
 
@@ -28,7 +32,7 @@ module Apnotic
28
32
  end
29
33
 
30
34
  def push(notification, options={})
31
- request = Apnotic::Request.new(notification)
35
+ request = prepare_request(notification)
32
36
  response = @client.call(:post, request.path,
33
37
  body: request.body,
34
38
  headers: request.headers,
@@ -38,11 +42,16 @@ module Apnotic
38
42
  end
39
43
 
40
44
  def push_async(push)
41
- @client.call_async(push.http2_request)
45
+ if @first_push
46
+ @first_push = false
47
+ @client.call_async(push.http2_request)
48
+ else
49
+ delayed_push_async(push)
50
+ end
42
51
  end
43
52
 
44
53
  def prepare_push(notification)
45
- request = Apnotic::Request.new(notification)
54
+ request = prepare_request(notification)
46
55
  http2_request = @client.prepare_request(:post, request.path,
47
56
  body: request.body,
48
57
  headers: request.headers
@@ -64,8 +73,39 @@ module Apnotic
64
73
 
65
74
  private
66
75
 
76
+ def prepare_request(notification)
77
+ notification.authorization = provider_token if @auth_method == :token
78
+ Apnotic::Request.new(notification)
79
+ end
80
+
81
+ def delayed_push_async(push)
82
+ if streams_available?
83
+ @client.call_async(push.http2_request)
84
+ else
85
+ sleep 0.001
86
+ delayed_push_async(push)
87
+ end
88
+ end
89
+
90
+ def streams_available?
91
+ remote_max_concurrent_streams - @client.stream_count > 0
92
+ end
93
+
94
+ def remote_max_concurrent_streams
95
+ # 0x7fffffff is the default value from http-2 gem (2^31)
96
+ if @client.remote_settings[:settings_max_concurrent_streams] == 0x7fffffff
97
+ 0
98
+ else
99
+ @client.remote_settings[:settings_max_concurrent_streams]
100
+ end
101
+ end
102
+
67
103
  def ssl_context
68
- @ssl_context ||= begin
104
+ @auth_method == :cert ? build_ssl_context : nil
105
+ end
106
+
107
+ def build_ssl_context
108
+ @build_ssl_context ||= begin
69
109
  ctx = OpenSSL::SSL::SSLContext.new
70
110
  begin
71
111
  p12 = OpenSSL::PKCS12.new(certificate, @cert_pass)
@@ -90,5 +130,14 @@ module Apnotic
90
130
  cert
91
131
  end
92
132
  end
133
+
134
+ def provider_token
135
+ @provider_token_cache ||= begin
136
+ instance = ProviderToken.new(certificate, @team_id, @key_id)
137
+ InstanceCache.new(instance, :token, 30 * 60)
138
+ end
139
+ @provider_token_cache.call
140
+ end
141
+
93
142
  end
94
143
  end
@@ -0,0 +1,28 @@
1
+ module Apnotic
2
+ class InstanceCache
3
+ def initialize(instance, method, ttl)
4
+ @instance = instance
5
+ @method = method
6
+ @ttl = ttl
7
+ end
8
+
9
+ def call
10
+ if @cached_value && !expired?
11
+ @cached_value
12
+ else
13
+ new_value
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def expired?
20
+ Time.now - @cached_at >= @ttl
21
+ end
22
+
23
+ def new_value
24
+ @cached_at = Time.now
25
+ @cached_value = @instance.send(@method)
26
+ end
27
+ end
28
+ end
@@ -6,7 +6,7 @@ module Apnotic
6
6
  class Notification
7
7
  attr_reader :token
8
8
  attr_accessor :alert, :badge, :sound, :content_available, :category, :custom_payload, :url_args, :mutable_content
9
- attr_accessor :apns_id, :expiration, :priority, :topic, :apns_collapse_id
9
+ attr_accessor :apns_id, :expiration, :priority, :topic, :apns_collapse_id, :authorization
10
10
 
11
11
  def initialize(token)
12
12
  @token = token
@@ -17,6 +17,10 @@ module Apnotic
17
17
  JSON.dump(to_hash).force_encoding(Encoding::BINARY)
18
18
  end
19
19
 
20
+ def authorization_header
21
+ authorization ? "bearer #{authorization}" : nil
22
+ end
23
+
20
24
  private
21
25
 
22
26
  def to_hash
@@ -0,0 +1,43 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+ require 'json'
4
+
5
+ module Apnotic
6
+ class ProviderToken
7
+ def initialize(key, team_id, key_id)
8
+ @key = OpenSSL::PKey::EC.new(key)
9
+ @team_id = team_id
10
+ @key_id = key_id
11
+ end
12
+
13
+ def token
14
+ [encode(header), encode(payload), encode(signature)].join(".")
15
+ end
16
+
17
+ private
18
+
19
+ def header
20
+ JSON.generate({
21
+ alg: "ES256",
22
+ kid: @key_id
23
+ })
24
+ end
25
+
26
+ def payload
27
+ JSON.generate({
28
+ iss: @team_id,
29
+ iat: Time.now.to_i
30
+ })
31
+ end
32
+
33
+ def signature
34
+ data = [encode(header), encode(payload)].join(".")
35
+ digest = OpenSSL::Digest::SHA256.new().digest(data)
36
+ @key.dsa_sign_asn1(digest)
37
+ end
38
+
39
+ def encode(data)
40
+ Base64.encode64(data).tr('+/', '-_').gsub(/[\n=]/, '')
41
+ end
42
+ end
43
+ end
@@ -18,6 +18,7 @@ module Apnotic
18
18
  h.merge!('apns-priority' => notification.priority) if notification.priority
19
19
  h.merge!('apns-topic' => notification.topic) if notification.topic
20
20
  h.merge!('apns-collapse-id' => notification.apns_collapse_id) if notification.apns_collapse_id
21
+ h.merge!('authorization' => notification.authorization_header) if notification.authorization_header
21
22
  h
22
23
  end
23
24
  end
@@ -1,3 +1,3 @@
1
1
  module Apnotic
2
- VERSION = '1.1.0'.freeze
2
+ VERSION = '1.2.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apnotic
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roberto Ostinelli
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-01 00:00:00.000000000 Z
11
+ date: 2017-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-http2
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0.15'
19
+ version: '0.16'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '2'
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '0.15'
29
+ version: '0.16'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '2'
@@ -108,7 +108,9 @@ files:
108
108
  - lib/apnotic.rb
109
109
  - lib/apnotic/connection.rb
110
110
  - lib/apnotic/connection_pool.rb
111
+ - lib/apnotic/instance_cache.rb
111
112
  - lib/apnotic/notification.rb
113
+ - lib/apnotic/provider_token.rb
112
114
  - lib/apnotic/push.rb
113
115
  - lib/apnotic/request.rb
114
116
  - lib/apnotic/response.rb