push_kit-apns 1.0.0.pre.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|