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 +4 -4
- data/README.md +26 -7
- data/apnotic.gemspec +1 -1
- data/lib/apnotic.rb +2 -0
- data/lib/apnotic/connection.rb +53 -4
- data/lib/apnotic/instance_cache.rb +28 -0
- data/lib/apnotic/notification.rb +5 -1
- data/lib/apnotic/provider_token.rb +43 -0
- data/lib/apnotic/request.rb +1 -0
- data/lib/apnotic/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9bfb47d774564845182d268e32f8820017d0ba32
|
4
|
+
data.tar.gz: 47334f8ffb95809e4c224570c4d4f5ba1b747eb7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
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/
|
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:
|
data/apnotic.gemspec
CHANGED
@@ -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.
|
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"
|
data/lib/apnotic.rb
CHANGED
data/lib/apnotic/connection.rb
CHANGED
@@ -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 =
|
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
|
-
@
|
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 =
|
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
|
-
@
|
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
|
data/lib/apnotic/notification.rb
CHANGED
@@ -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
|
data/lib/apnotic/request.rb
CHANGED
@@ -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
|
data/lib/apnotic/version.rb
CHANGED
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.
|
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-
|
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.
|
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.
|
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
|