async-http 0.27.13 → 0.28.0
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 +4 -4
- data/README.md +9 -1
- data/Rakefile +26 -0
- data/async-http.gemspec +4 -3
- data/lib/async/http/protocol/http2.rb +17 -328
- data/lib/async/http/protocol/http2/client.rb +57 -0
- data/lib/async/http/protocol/http2/connection.rb +94 -0
- data/lib/async/http/protocol/http2/request.rb +103 -0
- data/lib/async/http/protocol/http2/response.rb +111 -0
- data/lib/async/http/protocol/http2/server.rb +68 -0
- data/lib/async/http/protocol/http2/stream.rb +121 -0
- data/lib/async/http/reference.rb +2 -2
- data/lib/async/http/version.rb +1 -1
- metadata +13 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b0902e29085dc6e9908afb45613d5a8d923f60767fb111b2a1ef5d8a549b75dd
|
4
|
+
data.tar.gz: ff5f7aaf1141e192d95b53a197ef12b5ad4404fad761d09201367d0f029decf5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a70f1bb712b485d3aeae3cc374f4c430f628ac3e8db194757f4b83233e1fc91937d8b13ad04d7d22e0ee3ff8cb95d9515063211e98ff0d796beced993c52a282
|
7
|
+
data.tar.gz: '04821bd0c538d1df175152e3591cef5603a4ab593f57313378bb87a088a98784f40e0f5fc05ebeb988f3fd710329b0859fdec560c7b0048ab17b8efd3115f649'
|
data/README.md
CHANGED
@@ -147,11 +147,19 @@ According to these results, the cost of handling connections is quite high, whil
|
|
147
147
|
4. Push to the branch (`git push origin my-new-feature`)
|
148
148
|
5. Create new Pull Request
|
149
149
|
|
150
|
+
## See Also
|
151
|
+
|
152
|
+
- [benchmark-http](https://github.com/socketry/benchmark-http) — A benchmarking tool to report on web server concurrency.
|
153
|
+
- [falcon](https://github.com/socketry/falcon) — A rack compatible server built on top of `async-http`.
|
154
|
+
- [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets.
|
155
|
+
- [async-rest](https://github.com/socketry/async-rest) — A RESTful resource layer built on top of `async-http`.
|
156
|
+
- [async-http-faraday](https://github.com/socketry/async-http-faraday) — A faraday adapter to use `async-http`.
|
157
|
+
|
150
158
|
## License
|
151
159
|
|
152
160
|
Released under the MIT license.
|
153
161
|
|
154
|
-
Copyright,
|
162
|
+
Copyright, 2018, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
|
155
163
|
|
156
164
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
157
165
|
of this software and associated documentation files (the "Software"), to deal
|
data/Rakefile
CHANGED
@@ -6,6 +6,7 @@ RSpec::Core::RakeTask.new(:test)
|
|
6
6
|
task :default => :test
|
7
7
|
|
8
8
|
require 'async/http/protocol'
|
9
|
+
require 'async/http/url_endpoint'
|
9
10
|
require 'async/io/host_endpoint'
|
10
11
|
|
11
12
|
PROTOCOL = Async::HTTP::Protocol::HTTP1
|
@@ -16,6 +17,31 @@ task :debug do
|
|
16
17
|
Async.logger.level = Logger::DEBUG
|
17
18
|
end
|
18
19
|
|
20
|
+
task :google do
|
21
|
+
require 'async'
|
22
|
+
require 'pry'
|
23
|
+
|
24
|
+
Async.run do
|
25
|
+
endpoint = Async::HTTP::URLEndpoint.parse("https://www.google.com")
|
26
|
+
peer = endpoint.connect
|
27
|
+
stream = Async::IO::Stream.new(peer)
|
28
|
+
|
29
|
+
framer = ::HTTP::Protocol::HTTP2::Framer.new(stream)
|
30
|
+
client = ::HTTP::Protocol::HTTP2::Client.new(framer)
|
31
|
+
|
32
|
+
client.send_connection_preface([])
|
33
|
+
|
34
|
+
stream = ::HTTP::Protocol::HTTP2::Stream.new(client)
|
35
|
+
|
36
|
+
client.read_frame
|
37
|
+
client.read_frame
|
38
|
+
|
39
|
+
stream.send_headers(nil, [[':method', 'GET'], [':authority', 'www.google.com'], [':path', '/']], ::HTTP::Protocol::HTTP2::END_STREAM)
|
40
|
+
|
41
|
+
binding.pry
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
19
45
|
task :server do
|
20
46
|
require 'async/reactor'
|
21
47
|
require 'async/container/forked'
|
data/async-http.gemspec
CHANGED
@@ -13,13 +13,14 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
14
14
|
f.match(%r{^(test|spec|features)/})
|
15
15
|
end
|
16
|
-
spec.executables = spec.files.grep(%r{^bin/}) {
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) {|f| File.basename(f)}
|
17
17
|
spec.require_paths = ["lib"]
|
18
18
|
|
19
19
|
spec.add_dependency("async", "~> 1.6")
|
20
|
-
spec.add_dependency("async-io", "~> 1.
|
20
|
+
spec.add_dependency("async-io", "~> 1.14")
|
21
|
+
|
22
|
+
spec.add_dependency("http-protocol", "~> 0.1.0")
|
21
23
|
|
22
|
-
spec.add_dependency("http-2", "~> 0.9.0")
|
23
24
|
# spec.add_dependency("openssl")
|
24
25
|
|
25
26
|
spec.add_development_dependency "async-rspec", "~> 1.10"
|
@@ -18,345 +18,34 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
require_relative '
|
22
|
-
require_relative '
|
23
|
-
|
24
|
-
require_relative 'http11'
|
25
|
-
|
26
|
-
require 'async/notification'
|
27
|
-
|
28
|
-
require 'http/2'
|
21
|
+
require_relative 'http2/client'
|
22
|
+
require_relative 'http2/server'
|
29
23
|
|
30
24
|
module Async
|
31
25
|
module HTTP
|
32
26
|
module Protocol
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
def self.server(stream)
|
40
|
-
self.new(::HTTP2::Server.new, stream)
|
41
|
-
end
|
42
|
-
|
43
|
-
HTTPS = 'https'.freeze
|
44
|
-
SCHEME = ':scheme'.freeze
|
45
|
-
METHOD = ':method'.freeze
|
46
|
-
PATH = ':path'.freeze
|
47
|
-
AUTHORITY = ':authority'.freeze
|
48
|
-
REASON = ':reason'.freeze
|
49
|
-
STATUS = ':status'.freeze
|
50
|
-
VERSION = 'HTTP/2.0'.freeze
|
51
|
-
|
52
|
-
def initialize(controller, stream)
|
53
|
-
@controller = controller
|
54
|
-
@stream = stream
|
55
|
-
|
56
|
-
@controller.on(:frame) do |data|
|
57
|
-
@stream.write(data)
|
58
|
-
@stream.flush
|
59
|
-
end
|
60
|
-
|
61
|
-
@controller.on(:frame_sent) do |frame|
|
62
|
-
Async.logger.debug(self) {"Sent frame: #{frame.inspect}"}
|
63
|
-
end
|
64
|
-
|
65
|
-
@controller.on(:frame_received) do |frame|
|
66
|
-
Async.logger.debug(self) {"Received frame: #{frame.inspect}"}
|
67
|
-
end
|
68
|
-
|
69
|
-
@goaway = false
|
70
|
-
|
71
|
-
@controller.on(:goaway) do |payload|
|
72
|
-
Async.logger.error(self) {"goaway: #{payload.inspect}"}
|
73
|
-
|
74
|
-
@goaway = true
|
75
|
-
end
|
76
|
-
|
77
|
-
@count = 0
|
78
|
-
end
|
79
|
-
|
80
|
-
def peer
|
81
|
-
@stream.io
|
82
|
-
end
|
83
|
-
|
84
|
-
attr :count
|
85
|
-
|
86
|
-
# Multiple requests can be processed at the same time.
|
87
|
-
def multiplex
|
88
|
-
@controller.remote_settings[:settings_max_concurrent_streams]
|
89
|
-
end
|
90
|
-
|
91
|
-
# Can we use this connection to make requests?
|
92
|
-
def good?
|
93
|
-
@stream.connected?
|
94
|
-
end
|
95
|
-
|
96
|
-
def reusable?
|
97
|
-
!@goaway || !@stream.closed?
|
98
|
-
end
|
99
|
-
|
100
|
-
def version
|
101
|
-
VERSION
|
102
|
-
end
|
103
|
-
|
104
|
-
def start_connection
|
105
|
-
@reader ||= read_in_background
|
106
|
-
end
|
107
|
-
|
108
|
-
def read_in_background(task: Task.current)
|
109
|
-
task.async do |nested_task|
|
110
|
-
nested_task.annotate("#{version} reading data")
|
111
|
-
|
112
|
-
while buffer = @stream.read_partial
|
113
|
-
@controller << buffer
|
114
|
-
end
|
115
|
-
|
116
|
-
Async.logger.debug(self) {"Connection reset by peer!"}
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
def close
|
121
|
-
Async.logger.debug(self) {"Closing connection"}
|
122
|
-
|
123
|
-
@reader.stop if @reader
|
124
|
-
@stream.close
|
125
|
-
end
|
126
|
-
|
127
|
-
class Request < Protocol::Request
|
128
|
-
def initialize(protocol, stream)
|
129
|
-
super(nil, nil, nil, VERSION, Headers.new, Body::Writable.new)
|
130
|
-
|
131
|
-
@protocol = protocol
|
132
|
-
@stream = stream
|
133
|
-
end
|
134
|
-
|
135
|
-
def hijack?
|
136
|
-
false
|
137
|
-
end
|
138
|
-
|
139
|
-
attr :stream
|
140
|
-
|
141
|
-
def assign_headers(headers)
|
142
|
-
headers.each do |key, value|
|
143
|
-
if key == METHOD
|
144
|
-
raise BadRequest, "Request method already specified" if @method
|
145
|
-
|
146
|
-
@method = value
|
147
|
-
elsif key == PATH
|
148
|
-
raise BadRequest, "Request path already specified" if @path
|
149
|
-
|
150
|
-
@path = value
|
151
|
-
elsif key == AUTHORITY
|
152
|
-
raise BadRequest, "Request authority already specified" if @authority
|
153
|
-
|
154
|
-
@authority = value
|
155
|
-
else
|
156
|
-
@headers[key] = value
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def receive_requests(task: Task.current, &block)
|
163
|
-
# emits new streams opened by the client
|
164
|
-
@controller.on(:stream) do |stream|
|
165
|
-
@count += 1
|
166
|
-
|
167
|
-
request = Request.new(self, stream)
|
168
|
-
body = request.body
|
169
|
-
|
170
|
-
stream.on(:headers) do |headers|
|
171
|
-
begin
|
172
|
-
request.assign_headers(headers)
|
173
|
-
rescue
|
174
|
-
Async.logger.error(self) {$!}
|
175
|
-
|
176
|
-
stream.headers({
|
177
|
-
STATUS => "400"
|
178
|
-
}, end_stream: true)
|
179
|
-
else
|
180
|
-
task.async do
|
181
|
-
generate_response(request, stream, &block)
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
stream.on(:data) do |chunk|
|
187
|
-
body.write(chunk.to_s) unless chunk.empty?
|
188
|
-
end
|
189
|
-
|
190
|
-
stream.on(:half_close) do
|
191
|
-
# We are no longer receiving any more data frames:
|
192
|
-
body.finish
|
193
|
-
end
|
194
|
-
|
195
|
-
stream.on(:close) do |error|
|
196
|
-
if error
|
197
|
-
body.stop(EOFError.new(error))
|
198
|
-
else
|
199
|
-
# In theory, we should have received half_close, so there is no need to:
|
200
|
-
# body.finish
|
201
|
-
end
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
start_connection
|
206
|
-
@reader.wait
|
207
|
-
end
|
27
|
+
module HTTP2
|
28
|
+
DEFAULT_SETTINGS = {
|
29
|
+
::HTTP::Protocol::HTTP2::Settings::ENABLE_PUSH => 0,
|
30
|
+
::HTTP::Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS => 256
|
31
|
+
}
|
208
32
|
|
209
|
-
|
210
|
-
|
211
|
-
# We need to close the stream if the user code blows up while generating a response:
|
212
|
-
response = begin
|
213
|
-
yield(request, stream)
|
214
|
-
rescue
|
215
|
-
stream.close(:internal_error)
|
216
|
-
|
217
|
-
raise
|
218
|
-
end
|
219
|
-
|
220
|
-
if response
|
221
|
-
headers = Headers::Merged.new({
|
222
|
-
STATUS => response.status,
|
223
|
-
}, response.headers)
|
224
|
-
|
225
|
-
if response.body.nil? or response.body.empty?
|
226
|
-
stream.headers(headers, end_stream: true)
|
227
|
-
response.body.read if response.body
|
228
|
-
else
|
229
|
-
stream.headers(headers, end_stream: false)
|
230
|
-
|
231
|
-
response.body.each do |chunk|
|
232
|
-
stream.data(chunk, end_stream: false)
|
233
|
-
end
|
234
|
-
|
235
|
-
stream.data("", end_stream: true)
|
236
|
-
end
|
237
|
-
else
|
238
|
-
stream.headers({':status' => '500'}, end_stream: true)
|
239
|
-
end
|
240
|
-
rescue
|
241
|
-
Async.logger.error(request) {$!}
|
242
|
-
end
|
243
|
-
|
244
|
-
class Response < Protocol::Response
|
245
|
-
def initialize(protocol, stream)
|
246
|
-
super(protocol.version, nil, nil, Headers.new, Body::Writable.new)
|
247
|
-
|
248
|
-
@protocol = protocol
|
249
|
-
@stream = stream
|
250
|
-
end
|
251
|
-
|
252
|
-
def assign_headers(headers)
|
253
|
-
headers.each do |key, value|
|
254
|
-
if key == STATUS
|
255
|
-
@status = value.to_i
|
256
|
-
elsif key == REASON
|
257
|
-
@reason = value
|
258
|
-
else
|
259
|
-
@headers[key] = value
|
260
|
-
end
|
261
|
-
end
|
262
|
-
end
|
263
|
-
end
|
264
|
-
|
265
|
-
# Used by the client to send requests to the remote server.
|
266
|
-
def call(request)
|
267
|
-
@count += 1
|
268
|
-
|
269
|
-
stream = @controller.new_stream
|
270
|
-
response = Response.new(self, stream)
|
271
|
-
body = response.body
|
272
|
-
|
273
|
-
exception = nil
|
274
|
-
finished = Async::Notification.new
|
275
|
-
waiting = true
|
276
|
-
|
277
|
-
stream.on(:close) do |error|
|
278
|
-
if waiting
|
279
|
-
if error
|
280
|
-
# If the stream was closed due to an error, we will raise it rather than returning normally.
|
281
|
-
exception = EOFError.new(error)
|
282
|
-
end
|
283
|
-
|
284
|
-
waiting = false
|
285
|
-
finished.signal
|
286
|
-
else
|
287
|
-
# At this point, we are now expecting two events: data and close.
|
288
|
-
# If we receive close after this point, it's not a request error, but a failure we need to signal to the body.
|
289
|
-
if error
|
290
|
-
body.stop(EOFError.new(error))
|
291
|
-
else
|
292
|
-
body.finish
|
293
|
-
end
|
294
|
-
end
|
295
|
-
end
|
296
|
-
|
297
|
-
stream.on(:headers) do |headers|
|
298
|
-
response.assign_headers(headers)
|
299
|
-
|
300
|
-
# Once we receive the headers, we can return. The body will be read in the background.
|
301
|
-
waiting = false
|
302
|
-
finished.signal
|
303
|
-
end
|
304
|
-
|
305
|
-
# This is a little bit tricky due to the event handlers.
|
306
|
-
# 1/ Caller invokes `response.stop` which causes `body.write` below to fail.
|
307
|
-
# 2/ We invoke `stream.close(:internal_error)` which eventually triggers `on(:close)` above.
|
308
|
-
# 3/ Error is set to :internal_error which causes us to call `body.stop` a 2nd time.
|
309
|
-
# So, we guard against that, by ensuring that `Writable#stop` only stores the first exception assigned to it.
|
310
|
-
stream.on(:data) do |chunk|
|
311
|
-
begin
|
312
|
-
# If the body is stopped, write will fail...
|
313
|
-
body.write(chunk.to_s) unless chunk.empty?
|
314
|
-
rescue
|
315
|
-
# ... so, we close the stream:
|
316
|
-
stream.close(:internal_error)
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
|
-
write_request(request, stream)
|
321
|
-
|
322
|
-
Async.logger.debug(self) {"Request sent, waiting for signal."}
|
323
|
-
finished.wait
|
33
|
+
def self.client(stream, settings = DEFAULT_SETTINGS)
|
34
|
+
client = Client.new(stream)
|
324
35
|
|
325
|
-
|
326
|
-
|
327
|
-
end
|
36
|
+
client.send_connection_preface(settings)
|
37
|
+
client.start_connection
|
328
38
|
|
329
|
-
|
330
|
-
return response
|
39
|
+
return client
|
331
40
|
end
|
332
41
|
|
333
|
-
|
334
|
-
|
335
|
-
SCHEME => HTTPS,
|
336
|
-
METHOD => request.method,
|
337
|
-
PATH => request.path,
|
338
|
-
AUTHORITY => request.authority,
|
339
|
-
}, request.headers)
|
42
|
+
def self.server(stream, settings = DEFAULT_SETTINGS)
|
43
|
+
server = Server.new(stream)
|
340
44
|
|
341
|
-
|
342
|
-
|
343
|
-
request.body.read if request.body
|
344
|
-
else
|
345
|
-
begin
|
346
|
-
stream.headers(headers)
|
347
|
-
rescue
|
348
|
-
raise RequestFailed.new
|
349
|
-
end
|
350
|
-
|
351
|
-
request.body.each do |chunk|
|
352
|
-
stream.data(chunk, end_stream: false)
|
353
|
-
end
|
354
|
-
|
355
|
-
stream.data("")
|
356
|
-
end
|
45
|
+
server.read_connection_preface(settings)
|
46
|
+
server.start_connection
|
357
47
|
|
358
|
-
|
359
|
-
@stream.flush
|
48
|
+
return server
|
360
49
|
end
|
361
50
|
end
|
362
51
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'connection'
|
22
|
+
require_relative 'response'
|
23
|
+
|
24
|
+
require 'http/protocol/http2/client'
|
25
|
+
|
26
|
+
module Async
|
27
|
+
module HTTP
|
28
|
+
module Protocol
|
29
|
+
module HTTP2
|
30
|
+
class Client < ::HTTP::Protocol::HTTP2::Client
|
31
|
+
include Connection
|
32
|
+
|
33
|
+
def initialize(stream, *args)
|
34
|
+
@stream = stream
|
35
|
+
|
36
|
+
framer = ::HTTP::Protocol::HTTP2::Framer.new(@stream)
|
37
|
+
|
38
|
+
super(framer, *args)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Used by the client to send requests to the remote server.
|
42
|
+
def call(request)
|
43
|
+
@count += 1
|
44
|
+
|
45
|
+
response = Response.new(self, next_stream_id)
|
46
|
+
|
47
|
+
response.send_request(request)
|
48
|
+
|
49
|
+
response.wait
|
50
|
+
|
51
|
+
return response
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'stream'
|
22
|
+
|
23
|
+
module Async
|
24
|
+
module HTTP
|
25
|
+
module Protocol
|
26
|
+
module HTTP2
|
27
|
+
HTTPS = 'https'.freeze
|
28
|
+
SCHEME = ':scheme'.freeze
|
29
|
+
METHOD = ':method'.freeze
|
30
|
+
PATH = ':path'.freeze
|
31
|
+
AUTHORITY = ':authority'.freeze
|
32
|
+
REASON = ':reason'.freeze
|
33
|
+
STATUS = ':status'.freeze
|
34
|
+
VERSION = 'HTTP/2.0'.freeze
|
35
|
+
|
36
|
+
module Connection
|
37
|
+
def initialize(*)
|
38
|
+
super
|
39
|
+
|
40
|
+
@count = 0
|
41
|
+
@reader = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def start_connection
|
45
|
+
@reader ||= read_in_background
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_in_background(task: Task.current)
|
49
|
+
task.async do |nested_task|
|
50
|
+
nested_task.annotate("#{version} reading data")
|
51
|
+
|
52
|
+
while !self.closed?
|
53
|
+
self.read_frame
|
54
|
+
end
|
55
|
+
|
56
|
+
Async.logger.debug(self) {"Connection reset by peer!"}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def peer
|
61
|
+
@stream.io
|
62
|
+
end
|
63
|
+
|
64
|
+
attr :count
|
65
|
+
|
66
|
+
# Only one simultaneous connection at a time.
|
67
|
+
def multiplex
|
68
|
+
@remote_settings.maximum_concurrent_streams
|
69
|
+
end
|
70
|
+
|
71
|
+
# Can we use this connection to make requests?
|
72
|
+
def good?
|
73
|
+
@stream.connected?
|
74
|
+
end
|
75
|
+
|
76
|
+
def reusable?
|
77
|
+
!(self.closed? || @stream.closed?)
|
78
|
+
end
|
79
|
+
|
80
|
+
def version
|
81
|
+
VERSION
|
82
|
+
end
|
83
|
+
|
84
|
+
def close
|
85
|
+
Async.logger.debug(self) {"Closing connection"}
|
86
|
+
|
87
|
+
@reader.stop if @reader
|
88
|
+
@stream.close
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative '../request'
|
22
|
+
require_relative 'connection'
|
23
|
+
|
24
|
+
module Async
|
25
|
+
module HTTP
|
26
|
+
module Protocol
|
27
|
+
module HTTP2
|
28
|
+
class Request < Protocol::Request
|
29
|
+
def initialize(protocol, stream_id)
|
30
|
+
@input = Body::Writable.new
|
31
|
+
|
32
|
+
super(nil, nil, nil, VERSION, Headers.new, @input)
|
33
|
+
|
34
|
+
@protocol = protocol
|
35
|
+
@stream = Stream.new(self, protocol, stream_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
attr :stream
|
39
|
+
|
40
|
+
def hijack?
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def receive_headers(stream, headers, end_stream)
|
45
|
+
headers.each do |key, value|
|
46
|
+
if key == METHOD
|
47
|
+
return @stream.send_failure(400, "Request method already specified") if @method
|
48
|
+
|
49
|
+
@method = value
|
50
|
+
elsif key == PATH
|
51
|
+
return @stream.send_failure(400, "Request path already specified") if @path
|
52
|
+
|
53
|
+
@path = value
|
54
|
+
elsif key == AUTHORITY
|
55
|
+
return @stream.send_failure(400, "Request authority already specified") if @authority
|
56
|
+
|
57
|
+
@authority = value
|
58
|
+
else
|
59
|
+
@headers[key] = value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# We are ready for processing:
|
64
|
+
@protocol.requests.enqueue self
|
65
|
+
end
|
66
|
+
|
67
|
+
def receive_data(stream, data, end_stream)
|
68
|
+
unless data.empty?
|
69
|
+
@input.write(data)
|
70
|
+
end
|
71
|
+
|
72
|
+
if end_stream
|
73
|
+
@input.finish
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def receive_reset_stream(stream, error_code)
|
78
|
+
end
|
79
|
+
|
80
|
+
NO_RESPONSE = [[STATUS, '500'], [REASON, "No response generated"]]
|
81
|
+
|
82
|
+
def send_response(response)
|
83
|
+
if response.nil?
|
84
|
+
@stream.send_headers(nil, NO_RESPONSE, ::HTTP::Protocol::HTTP2::END_STREAM)
|
85
|
+
else
|
86
|
+
headers = Headers::Merged.new({
|
87
|
+
STATUS => response.status,
|
88
|
+
REASON => response.reason,
|
89
|
+
}, response.headers)
|
90
|
+
|
91
|
+
if response.body.nil?
|
92
|
+
@stream.send_headers(nil, headers, ::HTTP::Protocol::HTTP2::END_STREAM)
|
93
|
+
else
|
94
|
+
@stream.send_headers(nil, headers)
|
95
|
+
@stream.send_body(response.body)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative '../response'
|
22
|
+
|
23
|
+
module Async
|
24
|
+
module HTTP
|
25
|
+
module Protocol
|
26
|
+
module HTTP2
|
27
|
+
class Response < Protocol::Response
|
28
|
+
def initialize(protocol, stream_id)
|
29
|
+
@input = Body::Writable.new
|
30
|
+
|
31
|
+
super(protocol.version, nil, nil, Headers.new, @input)
|
32
|
+
|
33
|
+
@protocol = protocol
|
34
|
+
@stream = Stream.new(self, protocol, stream_id)
|
35
|
+
|
36
|
+
@notification = Async::Notification.new
|
37
|
+
@exception = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def wait
|
41
|
+
@notification.wait
|
42
|
+
|
43
|
+
if @exception
|
44
|
+
raise @exception
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def receive_headers(stream, headers, end_stream)
|
49
|
+
headers.each do |key, value|
|
50
|
+
if key == STATUS
|
51
|
+
@status = value.to_i
|
52
|
+
elsif key == REASON
|
53
|
+
@reason = value
|
54
|
+
else
|
55
|
+
@headers[key] = value
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if end_stream
|
60
|
+
@input.finish
|
61
|
+
end
|
62
|
+
|
63
|
+
# We are ready for processing:
|
64
|
+
@notification.signal
|
65
|
+
end
|
66
|
+
|
67
|
+
def receive_data(stream, data, end_stream)
|
68
|
+
unless data.empty?
|
69
|
+
@input.write(data)
|
70
|
+
end
|
71
|
+
|
72
|
+
if end_stream
|
73
|
+
@input.finish
|
74
|
+
end
|
75
|
+
rescue EOFError
|
76
|
+
@stream.send_reset_stream(0)
|
77
|
+
end
|
78
|
+
|
79
|
+
def receive_reset_stream(stream, error_code)
|
80
|
+
if error_code > 0
|
81
|
+
@exception = EOFError.new(error_code)
|
82
|
+
end
|
83
|
+
|
84
|
+
@notification.signal
|
85
|
+
end
|
86
|
+
|
87
|
+
def send_request(request)
|
88
|
+
headers = Headers::Merged.new({
|
89
|
+
SCHEME => HTTPS,
|
90
|
+
METHOD => request.method,
|
91
|
+
PATH => request.path,
|
92
|
+
AUTHORITY => request.authority,
|
93
|
+
}, request.headers)
|
94
|
+
|
95
|
+
if request.body.nil?
|
96
|
+
@stream.send_headers(nil, headers, ::HTTP::Protocol::HTTP2::END_STREAM)
|
97
|
+
else
|
98
|
+
begin
|
99
|
+
@stream.send_headers(nil, headers)
|
100
|
+
rescue
|
101
|
+
raise RequestFailed
|
102
|
+
end
|
103
|
+
|
104
|
+
@stream.send_body(request.body)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'connection'
|
22
|
+
require_relative 'request'
|
23
|
+
|
24
|
+
require 'http/protocol/http2/server'
|
25
|
+
|
26
|
+
module Async
|
27
|
+
module HTTP
|
28
|
+
module Protocol
|
29
|
+
module HTTP2
|
30
|
+
class Server < ::HTTP::Protocol::HTTP2::Server
|
31
|
+
include Connection
|
32
|
+
|
33
|
+
def initialize(stream, *args)
|
34
|
+
framer = ::HTTP::Protocol::HTTP2::Framer.new(stream)
|
35
|
+
|
36
|
+
super(framer, *args)
|
37
|
+
|
38
|
+
@requests = Async::Queue.new
|
39
|
+
end
|
40
|
+
|
41
|
+
attr :requests
|
42
|
+
|
43
|
+
def create_stream(stream_id)
|
44
|
+
request = Request.new(self, stream_id)
|
45
|
+
|
46
|
+
return request.stream
|
47
|
+
end
|
48
|
+
|
49
|
+
def receive_requests(task: Task.current)
|
50
|
+
while request = @requests.dequeue
|
51
|
+
@count += 1
|
52
|
+
|
53
|
+
# We need to close the stream if the user code blows up while generating a response:
|
54
|
+
response = begin
|
55
|
+
response = yield(request)
|
56
|
+
rescue
|
57
|
+
request.stream.send_reset_stream(::HTTP::Protocol::HTTP2::INTERNAL_ERROR)
|
58
|
+
Async.logger.error(request) {$!}
|
59
|
+
else
|
60
|
+
request.send_response(response)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'http/protocol/http2/stream'
|
22
|
+
|
23
|
+
module Async
|
24
|
+
module HTTP
|
25
|
+
module Protocol
|
26
|
+
module HTTP2
|
27
|
+
class Stream < ::HTTP::Protocol::HTTP2::Stream
|
28
|
+
def initialize(delegate, *args)
|
29
|
+
super(*args)
|
30
|
+
|
31
|
+
@delegate = delegate
|
32
|
+
|
33
|
+
@body = body
|
34
|
+
@remainder = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_accessor :delegate
|
38
|
+
attr :body
|
39
|
+
|
40
|
+
def send_body(body)
|
41
|
+
@body = body
|
42
|
+
|
43
|
+
window_updated
|
44
|
+
end
|
45
|
+
|
46
|
+
def send_chunk
|
47
|
+
maximum_size = self.available_frame_size
|
48
|
+
|
49
|
+
if maximum_size == 0
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
|
53
|
+
if @remainder
|
54
|
+
chunk = @remainder
|
55
|
+
@remainder = nil
|
56
|
+
elsif chunk = @body.read
|
57
|
+
else
|
58
|
+
@body = nil
|
59
|
+
|
60
|
+
# @body.read above might take a while and a stream reset might be received in the mean time.
|
61
|
+
unless @state == :closed
|
62
|
+
send_data(nil, ::HTTP::Protocol::HTTP2::END_STREAM)
|
63
|
+
end
|
64
|
+
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
|
68
|
+
return false if @state == :closed
|
69
|
+
|
70
|
+
if chunk.bytesize <= maximum_size
|
71
|
+
send_data(chunk, maximum_size: maximum_size)
|
72
|
+
|
73
|
+
return true
|
74
|
+
else
|
75
|
+
send_data(chunk.byteslice(0, maximum_size), padding_size: 0)
|
76
|
+
@remainder = chunk.byteslice(maximum_size, chunk.bytesize - maximum_size)
|
77
|
+
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def window_updated
|
83
|
+
return unless @body
|
84
|
+
|
85
|
+
while send_chunk; end
|
86
|
+
end
|
87
|
+
|
88
|
+
def receive_headers(frame)
|
89
|
+
headers = super
|
90
|
+
|
91
|
+
delegate.receive_headers(self, headers, frame.end_stream?)
|
92
|
+
|
93
|
+
return headers
|
94
|
+
end
|
95
|
+
|
96
|
+
def receive_data(frame)
|
97
|
+
data = super
|
98
|
+
|
99
|
+
if data
|
100
|
+
delegate.receive_data(self, data, frame.end_stream?)
|
101
|
+
end
|
102
|
+
|
103
|
+
return data
|
104
|
+
end
|
105
|
+
|
106
|
+
def receive_reset_stream(frame)
|
107
|
+
error_code = super
|
108
|
+
|
109
|
+
if @body
|
110
|
+
@body.stop(EOFError.new(error_code))
|
111
|
+
end
|
112
|
+
|
113
|
+
delegate.receive_reset_stream(self, error_code)
|
114
|
+
|
115
|
+
return error_code
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/async/http/reference.rb
CHANGED
@@ -168,11 +168,11 @@ module Async
|
|
168
168
|
def encode(value, prefix = nil)
|
169
169
|
case value
|
170
170
|
when Array
|
171
|
-
return value.map {
|
171
|
+
return value.map {|v|
|
172
172
|
encode(v, "#{prefix}[]")
|
173
173
|
}.join("&")
|
174
174
|
when Hash
|
175
|
-
return value.map {
|
175
|
+
return value.map {|k, v|
|
176
176
|
encode(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s))
|
177
177
|
}.reject(&:empty?).join('&')
|
178
178
|
when nil
|
data/lib/async/http/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.28.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-07-
|
11
|
+
date: 2018-07-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async
|
@@ -30,28 +30,28 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '1.
|
33
|
+
version: '1.14'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '1.
|
40
|
+
version: '1.14'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name: http-
|
42
|
+
name: http-protocol
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0.
|
47
|
+
version: 0.1.0
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0.
|
54
|
+
version: 0.1.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: async-rspec
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -162,6 +162,12 @@ files:
|
|
162
162
|
- lib/async/http/protocol/http10.rb
|
163
163
|
- lib/async/http/protocol/http11.rb
|
164
164
|
- lib/async/http/protocol/http2.rb
|
165
|
+
- lib/async/http/protocol/http2/client.rb
|
166
|
+
- lib/async/http/protocol/http2/connection.rb
|
167
|
+
- lib/async/http/protocol/http2/request.rb
|
168
|
+
- lib/async/http/protocol/http2/response.rb
|
169
|
+
- lib/async/http/protocol/http2/server.rb
|
170
|
+
- lib/async/http/protocol/http2/stream.rb
|
165
171
|
- lib/async/http/protocol/https.rb
|
166
172
|
- lib/async/http/protocol/request.rb
|
167
173
|
- lib/async/http/protocol/response.rb
|