async-http 0.27.13 → 0.28.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|