push_kit-apns 1.0.0.pre.beta1
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 +7 -0
- data/.gitignore +46 -0
- data/.rspec +2 -0
- data/.rubocop.yml +21 -0
- data/Gemfile +5 -0
- data/Guardfile +17 -0
- data/README.md +118 -0
- data/Rakefile +8 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/push_kit/apns.rb +65 -0
- data/lib/push_kit/apns/constants.rb +9 -0
- data/lib/push_kit/apns/http_client.rb +277 -0
- data/lib/push_kit/apns/notification.rb +257 -0
- data/lib/push_kit/apns/notification/localization.rb +61 -0
- data/lib/push_kit/apns/push_client.rb +207 -0
- data/lib/push_kit/apns/token_generator.rb +97 -0
- data/push_kit.gemspec +33 -0
- data/spec/push_kit/apns/constants_spec.rb +9 -0
- data/spec/push_kit/apns/notification/localization_spec.rb +89 -0
- data/spec/push_kit/apns/notification_spec.rb +264 -0
- data/spec/spec_helper.rb +85 -0
- data/spec/support/have_accessor.rb +19 -0
- metadata +189 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 81a37cc16b4ed2fa5d99091530b2352cfce87b8329b7573c2a57cd66c8f951c6
|
4
|
+
data.tar.gz: d7d6b80348f17dec42e2ba44cc1c2b8e9348a4cd5979b63e4194d0aa728f7d5a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c3bf44f4cd9d0935ed1ed3ea76f5ae5d7407ec9723946f8086e262a5184f8e92f62ba69268585c706c66bf21e5a3fab6529858e9af2c57f03139a859a0772747
|
7
|
+
data.tar.gz: 39b9bf33159a6a53669515700f169d9865c8442aa8d05d625c34032bfc9b46b9c14571cc2fd3bd190992c5eade594dbd029fc9c7f638aa75c0429c5fd039a37e
|
data/.gitignore
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# Packages #
|
2
|
+
############
|
3
|
+
*.7z
|
4
|
+
*.dmg
|
5
|
+
*.gz
|
6
|
+
*.iso
|
7
|
+
*.jar
|
8
|
+
*.rar
|
9
|
+
*.tar
|
10
|
+
*.zip
|
11
|
+
|
12
|
+
# Logs #
|
13
|
+
########
|
14
|
+
*.log
|
15
|
+
|
16
|
+
# Databases #
|
17
|
+
#############
|
18
|
+
*.sql
|
19
|
+
*.sqlite
|
20
|
+
|
21
|
+
# OS Files #
|
22
|
+
############
|
23
|
+
.DS_Store
|
24
|
+
.Trashes
|
25
|
+
ehthumbs.db
|
26
|
+
Icon?
|
27
|
+
Thumbs.db
|
28
|
+
|
29
|
+
# Vagrant #
|
30
|
+
###########
|
31
|
+
.vagrant
|
32
|
+
|
33
|
+
# Ruby Files #
|
34
|
+
##############
|
35
|
+
/.bundle/
|
36
|
+
/.yardoc
|
37
|
+
/Gemfile.lock
|
38
|
+
/_yardoc/
|
39
|
+
/coverage/
|
40
|
+
/doc/
|
41
|
+
/pkg/
|
42
|
+
/spec/reports/
|
43
|
+
/spec/examples.txt
|
44
|
+
/tmp/
|
45
|
+
/vendor/bundle/
|
46
|
+
*.gem
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
AllCops:
|
2
|
+
Exclude:
|
3
|
+
- 'bin/**/*'
|
4
|
+
|
5
|
+
Metrics/AbcSize:
|
6
|
+
Max: 30
|
7
|
+
|
8
|
+
Metrics/LineLength:
|
9
|
+
Max: 120
|
10
|
+
|
11
|
+
Metrics/BlockLength:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Metrics/ClassLength:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Metrics/MethodLength:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Style/Documentation:
|
21
|
+
Enabled: false
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
4
|
+
require 'guard/rspec/dsl'
|
5
|
+
|
6
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
7
|
+
|
8
|
+
# RSpec files
|
9
|
+
rspec = dsl.rspec
|
10
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
11
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
12
|
+
watch(rspec.spec_files)
|
13
|
+
|
14
|
+
# Ruby files
|
15
|
+
ruby = dsl.ruby
|
16
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
17
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# PushKit
|
2
|
+
|
3
|
+
PushKit makes it easy to create and deliver push notifications.
|
4
|
+
|
5
|
+
## PushKit::APNS
|
6
|
+
|
7
|
+
PushKit::APNS provides an easy-to-use API for creating and then delivering notifications via the Apple Push Notification service.
|
8
|
+
It uses the new HTTP/2 API and tokens so no more yearly certificate renewals!
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
You can install **PushKit::APNS** using the following command:
|
13
|
+
|
14
|
+
$ gem install push_kit-apns
|
15
|
+
|
16
|
+
### Usage
|
17
|
+
|
18
|
+
You'll need to grab a few things in order to send push notifications:
|
19
|
+
|
20
|
+
- A key from the Apple Developer portal, one which has the push notification option enabled.
|
21
|
+
- The ID of the key you're going to use.
|
22
|
+
- The ID of the team that generated the key.
|
23
|
+
- The bundle identifier of your application, which is used as the APNS topic.
|
24
|
+
|
25
|
+
Once you've obtained all of the above, you can create a new client like this:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
client = PushKit::APNS.client(
|
29
|
+
key: PushKit::APNS.load_key('/path/to/APNsAuthKey_XXXXXXXXXX.p8'),
|
30
|
+
key_id: 'XXXXXXXXXX',
|
31
|
+
team_id: 'XXXXXXXXXX',
|
32
|
+
topic: 'com.example.app'
|
33
|
+
)
|
34
|
+
```
|
35
|
+
|
36
|
+
Ideally, you should create the client when your app boots and keep it until your app is rebooted.
|
37
|
+
This allows us to maintain a connection to the Apple Push Notification service and quickly deliver notifications without the overhead of establishing a connection.
|
38
|
+
|
39
|
+
See [Communicating with APNs - Best Practices for Managing Connections](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW8) for more info on this.
|
40
|
+
|
41
|
+
### Additional Options
|
42
|
+
|
43
|
+
There are a few additional options you can specify when creating a client.
|
44
|
+
|
45
|
+
##### :host
|
46
|
+
|
47
|
+
You can provide the full hostname (or IP address) as a String. For example: 'api.push.apple.com'
|
48
|
+
|
49
|
+
Because this option's value is rarely set to anything other than the standard hostnames, we've added
|
50
|
+
a couple of convenience Symbols. These are *:development* and *:production* which sets the hostname
|
51
|
+
to the appropriate hostname for that environment.
|
52
|
+
|
53
|
+
The default value of this option is *:development*.
|
54
|
+
|
55
|
+
##### :port
|
56
|
+
|
57
|
+
You can also provide the port number as an Integer. For example: 443
|
58
|
+
This also has a couple of convenience Symbols, *:default* and *:alternative*.
|
59
|
+
|
60
|
+
The default value of this option is *:default*.
|
61
|
+
|
62
|
+
---
|
63
|
+
|
64
|
+
Creating a notification is easy with PushKit and there are quite a few different options you can set.
|
65
|
+
For a full list, checkout the documentation for `PushKit::APNS::Notification`.
|
66
|
+
|
67
|
+
For now, let's just create a simple notification:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
notification = PushKit::APNS::Notification.new
|
71
|
+
notification.title = 'Hello, World!'
|
72
|
+
notification.body = 'How are you today?'
|
73
|
+
notification.sound = :default
|
74
|
+
notification.device_token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
75
|
+
```
|
76
|
+
|
77
|
+
Then using our `PushKit::APNS` instance we created earlier, deliver the notification:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
apns.deliver(notification)
|
81
|
+
```
|
82
|
+
|
83
|
+
EXPLAIN_ASYNC_NEEDS_WAIT
|
84
|
+
|
85
|
+
---
|
86
|
+
|
87
|
+
The `PushKit::APNS::Notification` instance has a *:device_token* attribute which needs to be set to the token of the device you're sending the notification to.
|
88
|
+
|
89
|
+
It's common to want to send the same notification to a bunch of devices. When you want to do this, leave the initial notification's *:device_token* attribute blank and instead use the helper method *:for_tokens*:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
notification = PushKit::APNS::Notification.new
|
93
|
+
notification.title = 'Hello, World!'
|
94
|
+
notification.body = 'How are you today?'
|
95
|
+
notification.sound = :default
|
96
|
+
|
97
|
+
tokens = %w[
|
98
|
+
1111111111111111111111111111111111111111111111111111111111111111
|
99
|
+
2222222222222222222222222222222222222222222222222222222222222222
|
100
|
+
3333333333333333333333333333333333333333333333333333333333333333
|
101
|
+
]
|
102
|
+
|
103
|
+
notifications = notification.for_tokens(*tokens)
|
104
|
+
```
|
105
|
+
|
106
|
+
When you're ready, you can just deliver the array of notifications:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
client.deliver(notifications) do |notification, result|
|
110
|
+
# ...
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
## Development
|
115
|
+
|
116
|
+
After checking out the repo, run `bundle exec rake spec` to run the tests.
|
117
|
+
|
118
|
+
To install this gem onto your machine, run `bundle exec rake install`.
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent'
|
4
|
+
require 'http/2'
|
5
|
+
require 'json'
|
6
|
+
require 'jwt'
|
7
|
+
require 'openssl'
|
8
|
+
require 'securerandom'
|
9
|
+
require 'uri'
|
10
|
+
|
11
|
+
require 'push_kit/apns/constants'
|
12
|
+
require 'push_kit/apns/notification/localization'
|
13
|
+
require 'push_kit/apns/notification'
|
14
|
+
require 'push_kit/apns/http_client'
|
15
|
+
require 'push_kit/apns/token_generator'
|
16
|
+
require 'push_kit/apns/push_client'
|
17
|
+
|
18
|
+
module PushKit
|
19
|
+
# PushKit::APNS provides an easy-to-use API for creating and then delivering notifications via the
|
20
|
+
# Apple Push Notification service.
|
21
|
+
#
|
22
|
+
module APNS
|
23
|
+
# Read the key from the file at the specified path.
|
24
|
+
#
|
25
|
+
# @param path [String] Path to a key file.
|
26
|
+
# @return [OpenSSL::PKey::EC] The loaded key.
|
27
|
+
#
|
28
|
+
def self.load_key(path)
|
29
|
+
raise ArgumentError, "The key file does not exist: #{path}" unless File.file?(path)
|
30
|
+
|
31
|
+
OpenSSL::PKey::EC.new(File.read(path))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Create a new PushClient instance.
|
35
|
+
#
|
36
|
+
# @param options [Hash] The APNS options:
|
37
|
+
# host [String|Symbol] The host (can also be :production or :development).
|
38
|
+
# port [Integer|Symbol] The port number (can also be :default or :alternative).
|
39
|
+
# topic [String] The APNS topic (matches the app's bundle identifier).
|
40
|
+
# key [OpenSSL::PKey::EC] The elliptic curve key to use for authentication.
|
41
|
+
# key_id [String] The identifier for the elliptic curve key.
|
42
|
+
# team_id [String] The identifier for the Apple Developer team.
|
43
|
+
# topic [String] The topic for your application (usually the bundle identifier).
|
44
|
+
#
|
45
|
+
def self.client(options = {})
|
46
|
+
options = {
|
47
|
+
host: :development,
|
48
|
+
port: :default
|
49
|
+
}.merge(options)
|
50
|
+
|
51
|
+
token_generator = TokenGenerator.new(
|
52
|
+
key: options[:key],
|
53
|
+
key_id: options[:key_id],
|
54
|
+
team_id: options[:team_id]
|
55
|
+
)
|
56
|
+
|
57
|
+
PushClient.new(
|
58
|
+
host: options[:host],
|
59
|
+
port: options[:port],
|
60
|
+
topic: options[:topic],
|
61
|
+
token_generator: token_generator
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PushKit
|
4
|
+
module APNS
|
5
|
+
# The HTTPClient class provides a minimal HTTP/2 client.
|
6
|
+
#
|
7
|
+
class HTTPClient
|
8
|
+
# @return [String] The ALPN protocol required by HTTP/2.
|
9
|
+
#
|
10
|
+
ALPN_PROTOCOL = 'h2'
|
11
|
+
|
12
|
+
# @return [URI] The URI which indicates the scheme, host and port of the remote server.
|
13
|
+
#
|
14
|
+
attr_reader :uri
|
15
|
+
|
16
|
+
# Creates a new HTTPClient.
|
17
|
+
#
|
18
|
+
# @param uri [String|URI] A URI which indicates the scheme, host and port of the remote server.
|
19
|
+
#
|
20
|
+
def initialize(uri)
|
21
|
+
case uri
|
22
|
+
when String then @uri = URI(uri)
|
23
|
+
when URI then @uri = uri
|
24
|
+
else raise 'Expected the :uri attribute to be a String or a URI.'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Perform a HTTP request.
|
29
|
+
#
|
30
|
+
# @param method [String|Symbol] The request method (:get, :post, etc).
|
31
|
+
# @param path [String] The request path.
|
32
|
+
# @param headers [Hash] The request headers.
|
33
|
+
# @param body [String] The request body.
|
34
|
+
# @param block [Proc] A block to call once the request has completed.
|
35
|
+
#
|
36
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
37
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
38
|
+
#
|
39
|
+
def request(method: nil, path: nil, headers: nil, body: nil, &block)
|
40
|
+
raise 'The :method should be a String or a Symbol' unless method.is_a?(String) || method.is_a?(Symbol)
|
41
|
+
raise 'The :path should be a String' unless path.is_a?(String)
|
42
|
+
raise 'The :headers should be a Hash' unless headers.nil? || headers.is_a?(Hash)
|
43
|
+
raise 'The :body should be a String' unless body.nil? || body.is_a?(String)
|
44
|
+
raise 'The completion block must be provided.' unless block.is_a?(Proc)
|
45
|
+
|
46
|
+
requests.push(method: method, path: path, headers: headers, body: body, completion: block)
|
47
|
+
|
48
|
+
perform_requests
|
49
|
+
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
53
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# @return [Mutex] The semaphore used to ensure only one #perform_requests method call executes at a time.
|
58
|
+
#
|
59
|
+
def perform_requests_mutex
|
60
|
+
@perform_requests_mutex ||= Mutex.new
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Queue] The queue containing requests that need to be processed.
|
64
|
+
#
|
65
|
+
def requests
|
66
|
+
@requests ||= Queue.new
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [OpenSSL::SSL::SSLSocket|TCPSocket] An SSL or TCP socket.
|
70
|
+
#
|
71
|
+
def socket
|
72
|
+
ssl_socket || tcp_socket
|
73
|
+
end
|
74
|
+
|
75
|
+
# Reset the client and sockets if the connection is broken.
|
76
|
+
#
|
77
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
78
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
79
|
+
#
|
80
|
+
def reset_if_required!
|
81
|
+
socket = @ssl_socket || @tcp_socket
|
82
|
+
|
83
|
+
return unless socket.nil? || socket.closed? || @client.nil? || @client.closed?
|
84
|
+
|
85
|
+
@ssl_socket.close unless @ssl_socket.nil? || @ssl_socket.closed?
|
86
|
+
@tcp_socket.close unless @tcp_socket.nil? || @tcp_socket.closed?
|
87
|
+
|
88
|
+
@ssl_socket = nil
|
89
|
+
@tcp_socket = nil
|
90
|
+
@client = nil
|
91
|
+
end
|
92
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
93
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
94
|
+
|
95
|
+
# @return [HTTP2::Client] An HTTP/2 client.
|
96
|
+
#
|
97
|
+
def client
|
98
|
+
reset_if_required!
|
99
|
+
|
100
|
+
return @client unless @client.nil? || @client.closed?
|
101
|
+
|
102
|
+
@client = HTTP2::Client.new
|
103
|
+
|
104
|
+
@client.on(:frame) do |bytes|
|
105
|
+
next if socket.closed?
|
106
|
+
|
107
|
+
socket.print(bytes)
|
108
|
+
socket.flush
|
109
|
+
end
|
110
|
+
|
111
|
+
@client.send_connection_preface
|
112
|
+
|
113
|
+
# The @client instance variable must exist before calling this method!
|
114
|
+
spawn_input_thread
|
115
|
+
|
116
|
+
@client
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [TCPSocket] The TCP socket.
|
120
|
+
#
|
121
|
+
def tcp_socket
|
122
|
+
return @tcp_socket unless @tcp_socket.nil? || @tcp_socket.closed?
|
123
|
+
|
124
|
+
@tcp_socket = TCPSocket.new(uri.host, uri.port)
|
125
|
+
end
|
126
|
+
|
127
|
+
# @return [OpenSSL::SSL::SSLSocket] An SSL socket.
|
128
|
+
#
|
129
|
+
def ssl_socket
|
130
|
+
return @ssl_socket unless @ssl_socket.nil? || @ssl_socket.closed?
|
131
|
+
|
132
|
+
# Only create the SSLSocket if the URI requires a secure connection.
|
133
|
+
return nil unless uri.scheme == 'https'
|
134
|
+
|
135
|
+
socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
|
136
|
+
socket.sync_close = true
|
137
|
+
socket.hostname = uri.host
|
138
|
+
socket.connect
|
139
|
+
|
140
|
+
if socket.alpn_protocol != ALPN_PROTOCOL
|
141
|
+
raise "Expected ALPN protocol '#{ALPN_PROTOCOL}' but received '#{socket.alpn_protocol}'."
|
142
|
+
end
|
143
|
+
|
144
|
+
@ssl_socket = socket
|
145
|
+
end
|
146
|
+
|
147
|
+
# @return [OpenSSL::SSL::SSLContext] An SSL context.
|
148
|
+
#
|
149
|
+
def ssl_context
|
150
|
+
context = OpenSSL::SSL::SSLContext.new
|
151
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
152
|
+
context.alpn_protocols = [ALPN_PROTOCOL]
|
153
|
+
context.alpn_select_cb = lambda do |protocols|
|
154
|
+
ALPN_PROTOCOL if protocols.include?(ALPN_PROTOCOL)
|
155
|
+
end
|
156
|
+
|
157
|
+
context
|
158
|
+
end
|
159
|
+
|
160
|
+
# Loop over the requests in the queue, performing them until we've emptied the queue or we've hit
|
161
|
+
# the concurrent stream limit.
|
162
|
+
#
|
163
|
+
def perform_requests
|
164
|
+
return unless perform_requests_mutex.try_lock
|
165
|
+
|
166
|
+
loop do
|
167
|
+
begin
|
168
|
+
request = requests.pop(true)
|
169
|
+
perform_request(request)
|
170
|
+
rescue HTTP2::Error::StreamLimitExceeded
|
171
|
+
requests.push(request)
|
172
|
+
break
|
173
|
+
rescue ThreadError
|
174
|
+
break
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
perform_requests_mutex.unlock
|
179
|
+
end
|
180
|
+
|
181
|
+
# @param request [Hash] The request.
|
182
|
+
#
|
183
|
+
def perform_request(request)
|
184
|
+
stream = client.new_stream
|
185
|
+
|
186
|
+
if (block = request[:completion])
|
187
|
+
handle_response(stream, &block)
|
188
|
+
end
|
189
|
+
|
190
|
+
headers = headers(request)
|
191
|
+
body = request[:body]
|
192
|
+
|
193
|
+
if body.is_a?(String)
|
194
|
+
stream.headers(headers, end_stream: false)
|
195
|
+
stream.data(body, end_stream: true)
|
196
|
+
else
|
197
|
+
stream.headers(headers, end_stream: true)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# @param stream [HTTP2::Stream] The unique stream for the request.
|
202
|
+
# @param block [Proc] A block to call once the request has completed.
|
203
|
+
#
|
204
|
+
def handle_response(stream, &block)
|
205
|
+
return unless block.is_a?(Proc)
|
206
|
+
|
207
|
+
response_headers = {}
|
208
|
+
response_body = String.new
|
209
|
+
|
210
|
+
stream.on(:headers) do |headers|
|
211
|
+
response_headers.merge!(Hash[headers])
|
212
|
+
end
|
213
|
+
|
214
|
+
stream.on(:data) do |data|
|
215
|
+
response_body.concat(data)
|
216
|
+
end
|
217
|
+
|
218
|
+
stream.on(:close) do
|
219
|
+
perform_requests
|
220
|
+
|
221
|
+
status = response_headers[':status']
|
222
|
+
status = status.to_i if status.is_a?(String)
|
223
|
+
status = nil unless status.is_a?(Integer)
|
224
|
+
|
225
|
+
block.call(status, response_headers, response_body)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# @param request [Hash] The request.
|
230
|
+
# @return [Hash] The request headers.
|
231
|
+
#
|
232
|
+
def headers(request)
|
233
|
+
return nil unless request.is_a?(Hash)
|
234
|
+
return nil unless (method = request[:method])
|
235
|
+
return nil unless (path = request[:path])
|
236
|
+
|
237
|
+
headers = {
|
238
|
+
':scheme' => uri.scheme,
|
239
|
+
':authority' => [uri.host, uri.port].join(':'),
|
240
|
+
':method' => method.to_s.upcase,
|
241
|
+
':path' => path.to_s
|
242
|
+
}
|
243
|
+
|
244
|
+
body = request[:body]
|
245
|
+
headers['content-length'] = body.length.to_s if body.is_a?(String)
|
246
|
+
|
247
|
+
headers.merge!(request[:headers]) if request[:headers].is_a?(Hash)
|
248
|
+
|
249
|
+
headers
|
250
|
+
end
|
251
|
+
|
252
|
+
# Spawn a thread to wrap the #input_loop method.
|
253
|
+
#
|
254
|
+
def spawn_input_thread
|
255
|
+
return if @input_thread&.alive?
|
256
|
+
|
257
|
+
@input_thread = Thread.new { input_loop }
|
258
|
+
end
|
259
|
+
|
260
|
+
# Start a continuous loop, reading bytes from the socket and passing them to the HTTP/2 client.
|
261
|
+
#
|
262
|
+
def input_loop
|
263
|
+
loop do
|
264
|
+
break if @client.nil? || @client.closed? || socket.closed? || socket.eof?
|
265
|
+
|
266
|
+
begin
|
267
|
+
@client << socket.read_nonblock(1024)
|
268
|
+
rescue StandardError
|
269
|
+
socket.close
|
270
|
+
|
271
|
+
raise
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|