lorkhan 0.0.4 → 1.0.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 +4 -4
- data/.rubocop.yml +6 -0
- data/CHANGELOG.md +0 -4
- data/Gemfile.lock +51 -0
- data/README.md +27 -2
- data/lib/lorkhan/connection.rb +27 -5
- data/lib/lorkhan/errors/apple_errors.rb +56 -0
- data/lib/lorkhan/notification.rb +50 -3
- data/lib/lorkhan/provider_token.rb +13 -0
- data/lib/lorkhan/response.rb +13 -0
- data/lib/lorkhan/version.rb +1 -1
- data/lorkhan.gemspec +2 -2
- metadata +6 -6
- data/.gitignore +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9d9e98e45358c0cd329ab15535946be86a84a2b
|
4
|
+
data.tar.gz: ad11e57d08eb593634f3f741a0d532a57673ab86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff895526c5d9d028eedb0a6bc0c14bd3e864ed6bed44b11ae25ec92328c7e7896710ab670dccbac6b393e9e471d57ee492622d2b1a715cd03ebbb6d50634b0c1
|
7
|
+
data.tar.gz: e06ac2239cb27cbd11966f1566e721a1340d5b3521484432e4dfeb1593e2363de2cbeb2fc4efc93c1f5fabd33fe6b091bede2dc0d8deaab7cabb028dd9d1c0d9
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
lorkhan (1.0.0)
|
5
|
+
connection_pool (>= 2.0)
|
6
|
+
jwt (>= 1.5.6)
|
7
|
+
net-http2 (>= 0.14.1, < 2)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
coderay (1.1.1)
|
13
|
+
connection_pool (2.2.1)
|
14
|
+
diff-lcs (1.3)
|
15
|
+
http-2 (0.8.3)
|
16
|
+
jwt (1.5.6)
|
17
|
+
method_source (0.8.2)
|
18
|
+
net-http2 (0.14.1)
|
19
|
+
http-2 (~> 0.8.2)
|
20
|
+
pry (0.10.4)
|
21
|
+
coderay (~> 1.1.0)
|
22
|
+
method_source (~> 0.8.1)
|
23
|
+
slop (~> 3.4)
|
24
|
+
rake (10.5.0)
|
25
|
+
rspec (3.5.0)
|
26
|
+
rspec-core (~> 3.5.0)
|
27
|
+
rspec-expectations (~> 3.5.0)
|
28
|
+
rspec-mocks (~> 3.5.0)
|
29
|
+
rspec-core (3.5.4)
|
30
|
+
rspec-support (~> 3.5.0)
|
31
|
+
rspec-expectations (3.5.0)
|
32
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
+
rspec-support (~> 3.5.0)
|
34
|
+
rspec-mocks (3.5.0)
|
35
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
36
|
+
rspec-support (~> 3.5.0)
|
37
|
+
rspec-support (3.5.0)
|
38
|
+
slop (3.6.0)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
bundler (~> 1.3)
|
45
|
+
lorkhan!
|
46
|
+
pry
|
47
|
+
rake (~> 10.0)
|
48
|
+
rspec (~> 3.5)
|
49
|
+
|
50
|
+
BUNDLED WITH
|
51
|
+
1.13.6
|
data/README.md
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
-
|
1
|
+
## Lorkhan
|
2
2
|
|
3
|
-
|
3
|
+
Apple Push Notification Services client using the HTTP/2 API and Provider Authentication Tokens
|
4
|
+
|
5
|
+
### Getting Provider Authentication Tokens
|
6
|
+
|
7
|
+
See: "Generate a universal provider token signing key" under "Configure push notifications." in [Xcode Help](http://help.apple.com/xcode)
|
8
|
+
|
9
|
+
### Sending a Push Notification
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
token = Lorkhan::ProviderToken.new(key_id: '<token key id>', team_id: '<developer team id>', secret: '<PAT secret>')
|
13
|
+
connection = Lorkhan::Connection.new(production: true, token: token)
|
14
|
+
|
15
|
+
notification = Lorkhan::Notification.new('<device token>')
|
16
|
+
notification.topic = '<your app ID>'
|
17
|
+
notification.alert = 'Hello from Lorkhan'
|
18
|
+
|
19
|
+
connection.push(notification)
|
20
|
+
```
|
21
|
+
|
22
|
+
### Other Gotcha's
|
23
|
+
|
24
|
+
- Your OpenSSL implementation __MUST__ support the `ES256` elliptical curve.
|
25
|
+
|
26
|
+
### Extra reading
|
27
|
+
|
28
|
+
##### [Local and Remote Notification Programming Guide](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1)
|
data/lib/lorkhan/connection.rb
CHANGED
@@ -4,31 +4,53 @@ require 'openssl'
|
|
4
4
|
module Lorkhan
|
5
5
|
APNS_PRODUCTION_HOST = 'api.push.apple.com'.freeze
|
6
6
|
APNS_DEVELOPMENT_HOST = 'api.development.push.apple.com'.freeze
|
7
|
-
TOKEN_EXPIRE_TIME_SEC = 3300 # 55 minutes (Apple timeout
|
7
|
+
TOKEN_EXPIRE_TIME_SEC = 3300 # 55 minutes (Apple timeout is 60)
|
8
8
|
|
9
|
+
##
|
10
|
+
# The Connection class manages the HTTP/2 connection to Apple's servers
|
11
|
+
#
|
12
|
+
# token: (Lorkhan::ProviderToken) Used to authenticate with Apple's servers
|
13
|
+
# production: (Boolean) Used to determine which endpoint to establish the connection
|
14
|
+
# timeout: (Int) Number of seconds to wait before timing out opening the connection
|
15
|
+
##
|
9
16
|
class Connection
|
10
17
|
attr_reader :host, :token
|
11
18
|
|
12
19
|
def initialize(token:, production: true, timeout: 30)
|
20
|
+
raise ArgumentError, 'Token must be a Lorkhan::ProviderToken' unless token.is_a?(Lorkhan::ProviderToken)
|
21
|
+
|
13
22
|
@host = production ? APNS_PRODUCTION_HOST : APNS_DEVELOPMENT_HOST
|
14
23
|
@timeout = timeout
|
15
24
|
@token = token
|
16
25
|
@client = NetHttp2::Client.new(url, connect_timeout: @connect_timeout)
|
17
26
|
end
|
18
27
|
|
28
|
+
##
|
29
|
+
# Closes the connection to apple.
|
30
|
+
##
|
19
31
|
def close
|
20
32
|
@client.close
|
21
33
|
end
|
22
34
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
35
|
+
##
|
36
|
+
# Closes the connection and resets the authentication token.
|
37
|
+
#
|
38
|
+
# This should happen automatically 55 minutes after the original token was generated.
|
39
|
+
##
|
27
40
|
def refresh_token
|
28
41
|
@auth_token = nil
|
29
42
|
close
|
30
43
|
end
|
31
44
|
|
45
|
+
##
|
46
|
+
# Deliver a Lorkhan::Notification to Apple's servers.
|
47
|
+
#
|
48
|
+
# If the connection is not open, this will attempt to establish the connection.
|
49
|
+
#
|
50
|
+
# notification: (Lorkhan::Notification) The notification to deliver.
|
51
|
+
#
|
52
|
+
# return: The HTTP response from Apple
|
53
|
+
##
|
32
54
|
def push(notification)
|
33
55
|
check_token_should_refresh
|
34
56
|
request = Request.new(notification)
|
@@ -4,31 +4,87 @@ require 'lorkhan/errors/http_error'
|
|
4
4
|
module Lorkhan
|
5
5
|
module Errors
|
6
6
|
module Apple
|
7
|
+
# The collapse identifier exceeds the maximum allowed size
|
7
8
|
class BadCollapseId < Errors::HTTPError; end
|
9
|
+
|
10
|
+
# The specified device token was bad.
|
11
|
+
# Verify that the request contains a valid token and that the token matches the environment.
|
8
12
|
class BadDeviceToken < Errors::HTTPError; end
|
13
|
+
|
14
|
+
# The apns-expiration value is bad.
|
9
15
|
class BadExpirationDate < Errors::HTTPError; end
|
16
|
+
|
17
|
+
# The apns-id value is bad.
|
10
18
|
class BadMessageId < Errors::HTTPError; end
|
19
|
+
|
20
|
+
# The apns-priority value is bad.
|
11
21
|
class BadPriority < Errors::HTTPError; end
|
22
|
+
|
23
|
+
# The apns-topic was invalid.
|
12
24
|
class BadTopic < Errors::HTTPError; end
|
25
|
+
|
26
|
+
# The device token does not match the specified topic.
|
13
27
|
class DeviceTokenNotForTopic < Errors::HTTPError; end
|
28
|
+
|
29
|
+
# One or more headers were repeated.
|
14
30
|
class DuplicateHeaders < Errors::HTTPError; end
|
31
|
+
|
32
|
+
# Idle time out.
|
15
33
|
class IdleTimeout < Errors::HTTPError; end
|
34
|
+
|
35
|
+
# The device token is not specified in the request :path. Verify that the :path header contains the device token.
|
16
36
|
class MissingDeviceToken < Errors::HTTPError; end
|
37
|
+
|
38
|
+
# The apns-topic header of the request was not specified and was required.
|
39
|
+
# The apns-topic header is mandatory when the client is
|
40
|
+
# connected using a certificate that supports multiple topics.
|
17
41
|
class MissingTopic < Errors::HTTPError; end
|
42
|
+
|
43
|
+
# The message payload was empty.
|
18
44
|
class PayloadEmpty < Errors::HTTPError; end
|
45
|
+
|
46
|
+
# Pushing to this topic is not allowed.
|
19
47
|
class TopicDisallowed < Errors::HTTPError; end
|
48
|
+
|
49
|
+
# The provider token is stale and a new token should be generated.
|
20
50
|
class ExpiredProviderToken < Errors::HTTPError; end
|
51
|
+
|
52
|
+
# The specified action is not allowed.
|
21
53
|
class Forbidden < Errors::HTTPError; end
|
54
|
+
|
55
|
+
# The provider token is not valid or the token signature could not be verified
|
22
56
|
class InvalidProviderToken < Errors::HTTPError; end
|
57
|
+
|
58
|
+
# No provider certificate was used to connect to APNs and Authorization
|
59
|
+
# header was missing or no provider token was specified.
|
60
|
+
# The specified :method was not POST.
|
23
61
|
class MissingProviderToken < Errors::HTTPError; end
|
62
|
+
|
63
|
+
# The request contained a bad :path value.
|
24
64
|
class BadPath < Errors::HTTPError; end
|
65
|
+
|
66
|
+
# The specified :method was not POST.
|
25
67
|
class MethodNotAllowed < Errors::HTTPError; end
|
68
|
+
|
69
|
+
# The device token is inactive for the specified topic.
|
26
70
|
class Unregistered < Errors::HTTPError; end
|
71
|
+
|
72
|
+
# The message payload was too large.
|
27
73
|
class PayloadTooLarge < Errors::HTTPError; end
|
74
|
+
|
75
|
+
# The provider token is being updated too often.
|
28
76
|
class TooManyProviderTokenUpdates < Errors::HTTPError; end
|
77
|
+
|
78
|
+
# Too many requests were made consecutively to the same device token.
|
29
79
|
class TooManyRequests < Errors::HTTPError; end
|
80
|
+
|
81
|
+
# An internal server error occurred.
|
30
82
|
class InternalServerError < Errors::HTTPError; end
|
83
|
+
|
84
|
+
# The service is unavailable.
|
31
85
|
class ServiceUnavailable < Errors::HTTPError; end
|
86
|
+
|
87
|
+
# The server is shutting down.
|
32
88
|
class Shutdown < Errors::HTTPError; end
|
33
89
|
|
34
90
|
MAPPINGS = {
|
data/lib/lorkhan/notification.rb
CHANGED
@@ -2,18 +2,65 @@ require 'json'
|
|
2
2
|
require 'securerandom'
|
3
3
|
|
4
4
|
module Lorkhan
|
5
|
+
##
|
6
|
+
# Describes a push notification
|
7
|
+
#
|
8
|
+
# Required attributes
|
9
|
+
#
|
10
|
+
# - token: The device token to deliver the notification to
|
11
|
+
# - apns_id: The APNS id for the notification.
|
12
|
+
# This *must* be a UUID and is created when the instance is initialized.
|
13
|
+
# - priority: The priority of the notification. Apple will use this to determine how to deliver the notification.
|
14
|
+
# See `PRIORITY_DELIVER_IMMEDIATELY` & `PRIORITY_DELIVER_BACKGROUND` for more info.
|
15
|
+
# - expiration: A UNIX UTC epoch expressed in seconds for how long Apple will retain the notification,
|
16
|
+
# and attempt redelivery. A 0 time will attempt a single delivery then disguard the message.
|
17
|
+
# - topic: The topic of the remote notification, which is typically the bundle ID for your app.
|
18
|
+
# The certificate you create in your developer account must include the capability for this topic.
|
19
|
+
#
|
20
|
+
# Optional attributes
|
21
|
+
#
|
22
|
+
# - alert: The message displayed to the user.
|
23
|
+
# - badge: The badge number displayed on the app icon.
|
24
|
+
# - category: The category for the notification. Used to provide direct notification actions.
|
25
|
+
# - collapse_id: Used to group multiple notifications on the screen.
|
26
|
+
# - content_available: A boolean if this should be a content available push.
|
27
|
+
# This will deliver a silent notification to the device.
|
28
|
+
# Your app's entitlements must be enabled.
|
29
|
+
# If the value for this is truthy, the alert, badge, sound will be ignored.
|
30
|
+
# - custom_payload: A custom hash of data to send. Must be JSON encodable.
|
31
|
+
# - mutable_content: A `1` if the notificaiton is mutable.
|
32
|
+
# See `UNNotificationServiceExtension` (https://developer.apple.com/reference/usernotifications/unnotificationserviceextension)
|
33
|
+
# - sound: The name of a bundled sound file to play.
|
34
|
+
# - url_args: Used in conjunction with the `urlFormatString` in
|
35
|
+
# the `website.json` file for sending web notifications.
|
36
|
+
##
|
5
37
|
class Notification
|
6
|
-
|
38
|
+
##
|
39
|
+
# Send the push message immediately.
|
40
|
+
# Notifications with this priority must trigger an alert, sound, or badge on the target device.
|
41
|
+
##
|
42
|
+
PRIORITY_DELIVER_IMMEDIATELY = 10
|
43
|
+
|
44
|
+
##
|
45
|
+
# Send the push message at a time that takes into account power considerations for the device.
|
46
|
+
# Notifications with this priority might be grouped and delivered in bursts.
|
47
|
+
##
|
48
|
+
PRIORITY_DELIVER_BACKGROUND = 5
|
7
49
|
|
8
50
|
attr_reader :token, :apns_id
|
9
51
|
attr_accessor :custom_payload, :alert, :badge, :sound, :category, :content_available, :url_args, :mutable_content
|
10
52
|
attr_accessor :expiration, :priority, :topic, :collapse_id
|
11
53
|
|
54
|
+
##
|
55
|
+
# Create a new notification
|
56
|
+
#
|
57
|
+
# token: The Device token that the notification will be delivered to
|
58
|
+
##
|
12
59
|
def initialize(token)
|
13
60
|
@token = token
|
14
61
|
@apns_id = SecureRandom.uuid
|
15
62
|
@expiration = 0
|
16
|
-
@priority =
|
63
|
+
@priority = PRIORITY_DELIVER_IMMEDIATELY
|
17
64
|
end
|
18
65
|
|
19
66
|
def body
|
@@ -32,7 +79,7 @@ module Lorkhan
|
|
32
79
|
end
|
33
80
|
aps[:category] = category if category
|
34
81
|
aps['url-args'] = url_args if url_args
|
35
|
-
aps['mutable-content'] =
|
82
|
+
aps['mutable-content'] = 1 if mutable_content
|
36
83
|
end
|
37
84
|
root.merge!(custom_payload) if custom_payload
|
38
85
|
end
|
@@ -2,9 +2,22 @@ require 'jwt'
|
|
2
2
|
require 'openssl'
|
3
3
|
|
4
4
|
module Lorkhan
|
5
|
+
##
|
6
|
+
# Wrapper for creating the authentication token for communicating with Apple's servers.
|
7
|
+
#
|
8
|
+
# See the "Provider Authentication Tokens" from Apple's "Local and Remote Notification Programming Guide"
|
9
|
+
# https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW1
|
10
|
+
##
|
5
11
|
class ProviderToken
|
6
12
|
ALGORITHM = 'ES256'.freeze
|
7
13
|
|
14
|
+
##
|
15
|
+
# Create a new token
|
16
|
+
#
|
17
|
+
# key_id: An alphanumeric string obtained from the developer portal.
|
18
|
+
# team_id: The team id for your developer account. Obtained from the developer portal.
|
19
|
+
# secret: The content of the Authentication Token key file obtained from the developer portal.
|
20
|
+
##
|
8
21
|
def initialize(key_id:, team_id:, secret:)
|
9
22
|
@key_id = key_id
|
10
23
|
@team_id = team_id
|
data/lib/lorkhan/response.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
require 'json'
|
2
2
|
|
3
3
|
module Lorkhan
|
4
|
+
##
|
5
|
+
# A wrapper around the HTTP/2 response from Apple
|
6
|
+
##
|
4
7
|
class Response
|
5
8
|
attr_reader :headers, :body
|
6
9
|
|
10
|
+
# This class should never be instantiated directly.
|
7
11
|
def initialize(raw)
|
8
12
|
@headers = raw.headers
|
9
13
|
@body = JSON.parse(raw.body)
|
@@ -11,14 +15,23 @@ module Lorkhan
|
|
11
15
|
@body = nil
|
12
16
|
end
|
13
17
|
|
18
|
+
##
|
19
|
+
# Convenience method to check if the status is 200
|
20
|
+
##
|
14
21
|
def ok?
|
15
22
|
status == 200
|
16
23
|
end
|
17
24
|
|
25
|
+
##
|
26
|
+
# Get the HTTP status
|
27
|
+
##
|
18
28
|
def status
|
19
29
|
headers[':status'].to_i
|
20
30
|
end
|
21
31
|
|
32
|
+
##
|
33
|
+
# Get the APNS id for the notification
|
34
|
+
##
|
22
35
|
def apns_id
|
23
36
|
headers['apns-id']
|
24
37
|
end
|
data/lib/lorkhan/version.rb
CHANGED
data/lorkhan.gemspec
CHANGED
@@ -8,9 +8,9 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = Lorkhan::VERSION
|
9
9
|
spec.licenses = ['MIT']
|
10
10
|
spec.authors = ['Skylar Schipper']
|
11
|
-
spec.email = ['
|
11
|
+
spec.email = ['me@skylarsch.com']
|
12
12
|
spec.summary = 'APNS HTTP/2 Client'
|
13
|
-
spec.homepage = 'http://github.com/
|
13
|
+
spec.homepage = 'http://github.com/skylarsch/lorkhan'
|
14
14
|
spec.required_ruby_version = '>=2.3.0'
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lorkhan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Skylar Schipper
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-03-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: net-http2
|
@@ -116,16 +116,16 @@ dependencies:
|
|
116
116
|
version: '0'
|
117
117
|
description:
|
118
118
|
email:
|
119
|
-
-
|
119
|
+
- me@skylarsch.com
|
120
120
|
executables: []
|
121
121
|
extensions: []
|
122
122
|
extra_rdoc_files: []
|
123
123
|
files:
|
124
|
-
- ".gitignore"
|
125
124
|
- ".rspec"
|
126
125
|
- ".rubocop.yml"
|
127
126
|
- CHANGELOG.md
|
128
127
|
- Gemfile
|
128
|
+
- Gemfile.lock
|
129
129
|
- README.md
|
130
130
|
- Rakefile
|
131
131
|
- bin/console
|
@@ -144,7 +144,7 @@ files:
|
|
144
144
|
- lib/lorkhan/response.rb
|
145
145
|
- lib/lorkhan/version.rb
|
146
146
|
- lorkhan.gemspec
|
147
|
-
homepage: http://github.com/
|
147
|
+
homepage: http://github.com/skylarsch/lorkhan
|
148
148
|
licenses:
|
149
149
|
- MIT
|
150
150
|
metadata: {}
|
@@ -164,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
164
|
version: '0'
|
165
165
|
requirements: []
|
166
166
|
rubyforge_project:
|
167
|
-
rubygems_version: 2.
|
167
|
+
rubygems_version: 2.5.2
|
168
168
|
signing_key:
|
169
169
|
specification_version: 4
|
170
170
|
summary: APNS HTTP/2 Client
|
data/.gitignore
DELETED