lorkhan 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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: []