lorkhan 0.0.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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