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 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