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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c02c5af2eb616dc015cd2897b79e5a02953e462345cb598f1b8f91bcff978cc
4
- data.tar.gz: c1af0f5da029636d55a310d1cdc915a9eff057ba0b0ca5878b016ff59874e0dc
3
+ metadata.gz: b0902e29085dc6e9908afb45613d5a8d923f60767fb111b2a1ef5d8a549b75dd
4
+ data.tar.gz: ff5f7aaf1141e192d95b53a197ef12b5ad4404fad761d09201367d0f029decf5
5
5
  SHA512:
6
- metadata.gz: 5409d5bb69202a4cba94af3daae5c79f12044fcaa8fb4fe4e2ab8da418ea714c452c70351e3d4386143fad92df0bbfac3cc3ac6bc1a70f251b53ebf4fc5117a5
7
- data.tar.gz: 15173bb7a9bb74ceadfe73f8c2d9720ba8b195b7f39f068eb8ae5831a7554240b2a84bbab74d005ab5aefcdd129e429c0f650d779a6e62afd60d41fed888a3cc
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, 2015, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
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/}) { |f| File.basename(f) }
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.13")
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 'request'
22
- require_relative 'response'
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
- # A server that supports both HTTP1.0 and HTTP1.1 semantics by detecting the version of the request.
34
- class HTTP2
35
- def self.client(stream)
36
- self.new(::HTTP2::Client.new, stream)
37
- end
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
- # Generate a response to the request. If this fails, the stream is terminated and the error is reported.
210
- private def generate_response(request, stream, &block)
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
- if exception
326
- raise exception
327
- end
36
+ client.send_connection_preface(settings)
37
+ client.start_connection
328
38
 
329
- Async.logger.debug(self) {"Stream finished: #{response.inspect}"}
330
- return response
39
+ return client
331
40
  end
332
41
 
333
- private def write_request(request, stream)
334
- headers = Headers::Merged.new({
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
- if request.body.nil? or request.body.empty?
342
- stream.headers(headers, end_stream: true)
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
- start_connection
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
@@ -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 { |v|
171
+ return value.map {|v|
172
172
  encode(v, "#{prefix}[]")
173
173
  }.join("&")
174
174
  when Hash
175
- return value.map { |k, v|
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
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module HTTP
23
- VERSION = "0.27.13"
23
+ VERSION = "0.28.0"
24
24
  end
25
25
  end
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.27.13
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-21 00:00:00.000000000 Z
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.13'
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.13'
40
+ version: '1.14'
41
41
  - !ruby/object:Gem::Dependency
42
- name: http-2
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.9.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.9.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