lorkhan 0.0.4

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b8c3fb041b617f47b7301f48c86f41b06c0db14a
4
+ data.tar.gz: 56a83e2fff46bbf4499ec092bc8f367d2ec3f602
5
+ SHA512:
6
+ metadata.gz: 869f5ce31fb9c8a52ff2b7cfbe851073072a04bf32aa3b02723d4a4779624bfdc97742a93b51e281d195ab824897f8497b3468f1c19826bce266444c5c4a4733
7
+ data.tar.gz: 214eecef306334378480d276d184b9e896072b356f9cc8e58c86d02bf8d93ffe8c2903ccb4d39edb04e0d225be86ab2b1ec03af2fb8bc75d6af29f78e705dfcc
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,22 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.4
3
+ Exclude:
4
+ - spec/**/*
5
+
6
+ Metrics/AbcSize:
7
+ Max: 30
8
+
9
+ Metrics/LineLength:
10
+ Max: 120
11
+
12
+ Metrics/MethodLength:
13
+ Max: 16
14
+
15
+ Style/Documentation:
16
+ Enabled: false
17
+
18
+ Style/ClassAndModuleChildren:
19
+ Enabled: false
20
+
21
+ Style/FrozenStringLiteralComment:
22
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ ### 0.0.4
2
+
3
+ - Added `Lorkhan::Notification::DEFAULT_SOUND_NAME`
4
+
5
+ ### 0.0.3
6
+
7
+ - Sending a `content_available` push, now excludes the `alert`, `badge`, `sound` keys.
8
+
9
+ ### 0.0.2
10
+
11
+ - Calling `Lorkan::Connection#push` will automatically check when the auth token was generated and refresh the token if needed.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # THIS IS DEPRECATED
2
+
3
+ `gem 'lorkhan', '~> 1.0'`
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'lorkhan'
5
+ require 'pry'
6
+
7
+ DEVICE_TOKEN = ENV['LORKHAN_TEST_DEVICE_TOKEN']
8
+ AUTH_TOKEN_PATH = File.expand_path(ENV['LORKHAN_TEST_AUTH_KEY_PATH'])
9
+ TEAM_ID = ENV['LORKHAN_TEST_TEAM_ID']
10
+ KEY_ID = ENV['LORKHAN_TEST_AUTH_KEY_ID']
11
+
12
+ raise "Missing device token ENV['LORKHAN_TEST_DEVICE_TOKEN']" if DEVICE_TOKEN.nil?
13
+ raise "Missing device token ENV['LORKHAN_TEST_AUTH_KEY_PATH']" if AUTH_TOKEN_PATH.nil?
14
+ raise 'Missing auth token file' unless File.exist?(AUTH_TOKEN_PATH)
15
+ raise "Missing device token ENV['LORKHAN_TEST_TEAM_ID']" if TEAM_ID.nil?
16
+ raise "Missing device token ENV['LORKHAN_TEST_AUTH_KEY_ID']" if KEY_ID.nil?
17
+
18
+ Pry.start
@@ -0,0 +1,73 @@
1
+ require 'net-http2'
2
+ require 'openssl'
3
+
4
+ module Lorkhan
5
+ APNS_PRODUCTION_HOST = 'api.push.apple.com'.freeze
6
+ APNS_DEVELOPMENT_HOST = 'api.development.push.apple.com'.freeze
7
+ TOKEN_EXPIRE_TIME_SEC = 3300 # 55 minutes (Apple timeout in 60)
8
+
9
+ class Connection
10
+ attr_reader :host, :token
11
+
12
+ def initialize(token:, production: true, timeout: 30)
13
+ @host = production ? APNS_PRODUCTION_HOST : APNS_DEVELOPMENT_HOST
14
+ @timeout = timeout
15
+ @token = token
16
+ @client = NetHttp2::Client.new(url, connect_timeout: @connect_timeout)
17
+ end
18
+
19
+ def close
20
+ @client.close
21
+ end
22
+
23
+ def join
24
+ @client.join
25
+ end
26
+
27
+ def refresh_token
28
+ @auth_token = nil
29
+ close
30
+ end
31
+
32
+ def push(notification)
33
+ check_token_should_refresh
34
+ request = Request.new(notification)
35
+ request.validate!
36
+ headers = request.headers
37
+ headers['authorization'] = "bearer #{auth_token}"
38
+ raw_response = @client.call(:post, request.path, body: request.body, headers: headers, timeout: 5)
39
+ raise Errors::TimeoutError if raw_response.nil?
40
+ response = Response.new(raw_response)
41
+ handle_http_error(response) unless response.ok?
42
+ response
43
+ end
44
+
45
+ private
46
+
47
+ def auth_token
48
+ @auth_token ||= begin
49
+ @refresh_token_at = Time.now.to_i + TOKEN_EXPIRE_TIME_SEC
50
+ token.encode
51
+ end
52
+ end
53
+
54
+ def url
55
+ "https://#{host}:443"
56
+ end
57
+
58
+ def handle_http_error(response)
59
+ if response.body
60
+ if (reason = response.body['reason'])
61
+ klass = Lorkhan::Errors::Apple::MAPPINGS[reason]
62
+ raise klass, response if klass
63
+ end
64
+ end
65
+ raise Errors::HTTPError, response
66
+ end
67
+
68
+ def check_token_should_refresh
69
+ return if @refresh_token_at.nil?
70
+ refresh_token if Time.now.to_i >= @refresh_token_at
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,6 @@
1
+ require 'connection_pool'
2
+
3
+ module Lorkhan
4
+ class ConnectionPool < ::ConnectionPool
5
+ end
6
+ end
@@ -0,0 +1,64 @@
1
+ require 'lorkhan/errors/base'
2
+ require 'lorkhan/errors/http_error'
3
+
4
+ module Lorkhan
5
+ module Errors
6
+ module Apple
7
+ class BadCollapseId < Errors::HTTPError; end
8
+ class BadDeviceToken < Errors::HTTPError; end
9
+ class BadExpirationDate < Errors::HTTPError; end
10
+ class BadMessageId < Errors::HTTPError; end
11
+ class BadPriority < Errors::HTTPError; end
12
+ class BadTopic < Errors::HTTPError; end
13
+ class DeviceTokenNotForTopic < Errors::HTTPError; end
14
+ class DuplicateHeaders < Errors::HTTPError; end
15
+ class IdleTimeout < Errors::HTTPError; end
16
+ class MissingDeviceToken < Errors::HTTPError; end
17
+ class MissingTopic < Errors::HTTPError; end
18
+ class PayloadEmpty < Errors::HTTPError; end
19
+ class TopicDisallowed < Errors::HTTPError; end
20
+ class ExpiredProviderToken < Errors::HTTPError; end
21
+ class Forbidden < Errors::HTTPError; end
22
+ class InvalidProviderToken < Errors::HTTPError; end
23
+ class MissingProviderToken < Errors::HTTPError; end
24
+ class BadPath < Errors::HTTPError; end
25
+ class MethodNotAllowed < Errors::HTTPError; end
26
+ class Unregistered < Errors::HTTPError; end
27
+ class PayloadTooLarge < Errors::HTTPError; end
28
+ class TooManyProviderTokenUpdates < Errors::HTTPError; end
29
+ class TooManyRequests < Errors::HTTPError; end
30
+ class InternalServerError < Errors::HTTPError; end
31
+ class ServiceUnavailable < Errors::HTTPError; end
32
+ class Shutdown < Errors::HTTPError; end
33
+
34
+ MAPPINGS = {
35
+ 'BadCollapseId' => BadCollapseId,
36
+ 'BadDeviceToken' => BadDeviceToken,
37
+ 'BadExpirationDate' => BadExpirationDate,
38
+ 'BadMessageId' => BadMessageId,
39
+ 'BadPriority' => BadPriority,
40
+ 'BadTopic' => BadTopic,
41
+ 'DeviceTokenNotForTopic' => DeviceTokenNotForTopic,
42
+ 'DuplicateHeaders' => DuplicateHeaders,
43
+ 'IdleTimeout' => IdleTimeout,
44
+ 'MissingDeviceToken' => MissingDeviceToken,
45
+ 'MissingTopic' => MissingTopic,
46
+ 'PayloadEmpty' => PayloadEmpty,
47
+ 'TopicDisallowed' => TopicDisallowed,
48
+ 'ExpiredProviderToken' => ExpiredProviderToken,
49
+ 'Forbidden' => Forbidden,
50
+ 'InvalidProviderToken' => InvalidProviderToken,
51
+ 'MissingProviderToken' => MissingProviderToken,
52
+ 'BadPath' => BadPath,
53
+ 'MethodNotAllowed' => MethodNotAllowed,
54
+ 'Unregistered' => Unregistered,
55
+ 'PayloadTooLarge' => PayloadTooLarge,
56
+ 'TooManyProviderTokenUpdates' => TooManyProviderTokenUpdates,
57
+ 'TooManyRequests' => TooManyRequests,
58
+ 'InternalServerError' => InternalServerError,
59
+ 'ServiceUnavailable' => ServiceUnavailable,
60
+ 'Shutdown' => Shutdown
61
+ }.freeze
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,6 @@
1
+ module Lorkhan
2
+ module Errors
3
+ class Base < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ module Lorkhan
2
+ module Errors
3
+ class HTTPError < Base
4
+ attr_reader :status, :response
5
+
6
+ def initialize(response)
7
+ super("http_error/#{response.status}")
8
+ @response = response
9
+ @status = response.status
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module Lorkhan
2
+ module Errors
3
+ class MissingTopicError < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Lorkhan
2
+ module Errors
3
+ class TimeoutError < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ require 'lorkhan/errors/apple_errors'
2
+ require 'lorkhan/errors/base'
3
+ require 'lorkhan/errors/http_error'
4
+ require 'lorkhan/errors/missing_topic_error'
5
+ require 'lorkhan/errors/timeout_error'
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+ require 'securerandom'
3
+
4
+ module Lorkhan
5
+ class Notification
6
+ DEFAULT_SOUND_NAME = 'default'.freeze
7
+
8
+ attr_reader :token, :apns_id
9
+ attr_accessor :custom_payload, :alert, :badge, :sound, :category, :content_available, :url_args, :mutable_content
10
+ attr_accessor :expiration, :priority, :topic, :collapse_id
11
+
12
+ def initialize(token)
13
+ @token = token
14
+ @apns_id = SecureRandom.uuid
15
+ @expiration = 0
16
+ @priority = 10
17
+ end
18
+
19
+ def body
20
+ JSON.dump(to_h).force_encoding(Encoding::BINARY)
21
+ end
22
+
23
+ def to_h
24
+ {}.tap do |root|
25
+ root[:aps] = {}.tap do |aps|
26
+ if content_available
27
+ aps['content-available'] = 1
28
+ else
29
+ aps[:alert] = alert if alert
30
+ aps[:badge] = badge if badge
31
+ aps[:sound] = sound if sound
32
+ end
33
+ aps[:category] = category if category
34
+ aps['url-args'] = url_args if url_args
35
+ aps['mutable-content'] = mutable_content if mutable_content
36
+ end
37
+ root.merge!(custom_payload) if custom_payload
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ require 'jwt'
2
+ require 'openssl'
3
+
4
+ module Lorkhan
5
+ class ProviderToken
6
+ ALGORITHM = 'ES256'.freeze
7
+
8
+ def initialize(key_id:, team_id:, secret:)
9
+ @key_id = key_id
10
+ @team_id = team_id
11
+ @secret = secret
12
+ end
13
+
14
+ def encode
15
+ JWT.encode(payload, p_key, ALGORITHM, headers)
16
+ end
17
+
18
+ private
19
+
20
+ def p_key
21
+ @p_key ||= begin
22
+ sec = OpenSSL::PKey::EC.new(@secret)
23
+ sec.check_key
24
+ sec
25
+ end
26
+ end
27
+
28
+ def payload
29
+ {
30
+ iss: @team_id,
31
+ iat: Time.now.utc.to_i
32
+ }
33
+ end
34
+
35
+ def headers
36
+ {
37
+ kid: @key_id
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ module Lorkhan
2
+ class Request
3
+ attr_reader :path, :headers, :body
4
+
5
+ def initialize(notification)
6
+ @path = "/3/device/#{notification.token}"
7
+ @body = notification.body
8
+ @headers = build_headers(notification)
9
+ end
10
+
11
+ def validate!
12
+ raise Errors::MissingTopicError if headers['apns-topic'].nil?
13
+ end
14
+
15
+ private
16
+
17
+ def build_headers(notif)
18
+ {}.tap do |head|
19
+ head['apns-id'] = notif.apns_id if notif.apns_id
20
+ head['apns-expiration'] = notif.expiration.to_s if notif.expiration
21
+ head['apns-priority'] = notif.priority.to_s if notif.priority
22
+ head['apns-topic'] = notif.topic if notif.topic
23
+ head['apns-collapse-id'] = notif.collapse_id if notif.collapse_id
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ require 'json'
2
+
3
+ module Lorkhan
4
+ class Response
5
+ attr_reader :headers, :body
6
+
7
+ def initialize(raw)
8
+ @headers = raw.headers
9
+ @body = JSON.parse(raw.body)
10
+ rescue JSON::ParserError
11
+ @body = nil
12
+ end
13
+
14
+ def ok?
15
+ status == 200
16
+ end
17
+
18
+ def status
19
+ headers[':status'].to_i
20
+ end
21
+
22
+ def apns_id
23
+ headers['apns-id']
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module Lorkhan
2
+ VERSION = '0.0.4'.freeze
3
+ end
data/lib/lorkhan.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'lorkhan/connection_pool'
2
+ require 'lorkhan/connection'
3
+ require 'lorkhan/errors'
4
+ require 'lorkhan/notification'
5
+ require 'lorkhan/provider_token'
6
+ require 'lorkhan/request'
7
+ require 'lorkhan/response'
8
+ require 'lorkhan/version'
9
+
10
+ module Lorkhan
11
+ end
data/lorkhan.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'lorkhan/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'lorkhan'
8
+ spec.version = Lorkhan::VERSION
9
+ spec.licenses = ['MIT']
10
+ spec.authors = ['Skylar Schipper']
11
+ spec.email = ['skylar@pco.bz']
12
+ spec.summary = 'APNS HTTP/2 Client'
13
+ spec.homepage = 'http://github.com/ministrycentered/lorkhan'
14
+ spec.required_ruby_version = '>=2.3.0'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = %w(lib)
20
+
21
+ spec.add_dependency 'net-http2', '>= 0.14.1', '< 2'
22
+ spec.add_dependency 'connection_pool', '>= 2.0'
23
+ spec.add_dependency 'jwt', '>= 1.5.6'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.3'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.5'
28
+ spec.add_development_dependency 'pry'
29
+ end
metadata ADDED
@@ -0,0 +1,171 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lorkhan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Skylar Schipper
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-http2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.14.1
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.14.1
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2'
33
+ - !ruby/object:Gem::Dependency
34
+ name: connection_pool
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: jwt
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.5.6
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.5.6
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.3'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.3'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '10.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '10.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rspec
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.5'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.5'
103
+ - !ruby/object:Gem::Dependency
104
+ name: pry
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ description:
118
+ email:
119
+ - skylar@pco.bz
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".gitignore"
125
+ - ".rspec"
126
+ - ".rubocop.yml"
127
+ - CHANGELOG.md
128
+ - Gemfile
129
+ - README.md
130
+ - Rakefile
131
+ - bin/console
132
+ - lib/lorkhan.rb
133
+ - lib/lorkhan/connection.rb
134
+ - lib/lorkhan/connection_pool.rb
135
+ - lib/lorkhan/errors.rb
136
+ - lib/lorkhan/errors/apple_errors.rb
137
+ - lib/lorkhan/errors/base.rb
138
+ - lib/lorkhan/errors/http_error.rb
139
+ - lib/lorkhan/errors/missing_topic_error.rb
140
+ - lib/lorkhan/errors/timeout_error.rb
141
+ - lib/lorkhan/notification.rb
142
+ - lib/lorkhan/provider_token.rb
143
+ - lib/lorkhan/request.rb
144
+ - lib/lorkhan/response.rb
145
+ - lib/lorkhan/version.rb
146
+ - lorkhan.gemspec
147
+ homepage: http://github.com/ministrycentered/lorkhan
148
+ licenses:
149
+ - MIT
150
+ metadata: {}
151
+ post_install_message:
152
+ rdoc_options: []
153
+ require_paths:
154
+ - lib
155
+ required_ruby_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: 2.3.0
160
+ required_rubygems_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ requirements: []
166
+ rubyforge_project:
167
+ rubygems_version: 2.6.13
168
+ signing_key:
169
+ specification_version: 4
170
+ summary: APNS HTTP/2 Client
171
+ test_files: []