async-http 0.24.3 → 0.25.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: 570eb9bb6a0b2a283e9149bb2e158e1f72caedae249e872622c335ef2676bba2
4
- data.tar.gz: 93049e1d6687cdc102725553a48e9401ebc8ef743c3a54343b5de3b41ef8c0b7
3
+ metadata.gz: 232e1b617ad6dea60e2b7d5ccbd1838bd1a507d6896292dab3345cb439ee7638
4
+ data.tar.gz: 51877e29175a10fabe6bd36b10d675b09eb90447b16a4e59ba3a8c128f4b9940
5
5
  SHA512:
6
- metadata.gz: 8694e40e0e0eb6f1a1bebeef6b41385bc05ecd87549feb2943306828037e6a24c11113c0271ce26b3ab9a4f2e8512d1cf19c22d03fb59c61b5606cc253d60798
7
- data.tar.gz: f785ab8a1e9f71bdfd2a24f12f30de4474cbefcd08745a8ec4976a94b84c702f94319ef5c30b31fc898506c24df1206e41cec25b929bf7d8faf2229f2989ceac
6
+ metadata.gz: 61215be764f44c9b46378c7eb6582850936f3cbf0d424f8967fa38141ee109469c3cde72f343807919fb0a5a9fc31a191a6441c15832c6370279a395da8e1a16
7
+ data.tar.gz: 63f6fdd40cfc107ef03234615ed92d8a9a7f76305e48bbb6638a177d148a38f626650aa3d83671d0438ac0a2652f301e722d08f9626db5886288028359fbf682
data/Rakefile CHANGED
@@ -56,30 +56,23 @@ end
56
56
  task :wrk do
57
57
  require 'async/reactor'
58
58
  require 'async/http/server'
59
-
60
- app = lambda do |env|
61
- [200, {}, ["Hello World"]]
59
+ require 'async/container/forked'
60
+
61
+ server = Async::HTTP::Server.new(Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true), PROTOCOL) do |request, peer, address|
62
+ return Async::HTTP::Response[200, {'content-type' => 'text/plain'}, ["Hello World"]]
62
63
  end
64
+
65
+ concurrency = 1
63
66
 
64
- server = Async::HTTP::Server.new(Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true), app)
65
-
66
- process_count = Etc.nprocessors
67
-
68
- pids = process_count.times.collect do
69
- fork do
70
- Async::Reactor.run do
71
- server.run
72
- end
73
- end
67
+ container = Async::Container::Forked.new(concurrency: concurrency) do
68
+ server.run
74
69
  end
75
70
 
76
71
  url = "http://127.0.0.1:9294/"
77
72
 
78
- connections = process_count
79
- system("wrk", "-c", connections.to_s, "-d", "2", "-t", connections.to_s, url)
80
-
81
- pids.each do |pid|
82
- Process.kill(:KILL, pid)
83
- Process.wait pid
73
+ 5.times do
74
+ system("wrk", "-c", concurrency.to_s, "-d", "10", "-t", concurrency.to_s, url)
84
75
  end
76
+
77
+ container.stop
85
78
  end
@@ -48,13 +48,13 @@ module Async
48
48
 
49
49
  def initialize(chunks)
50
50
  @chunks = chunks
51
- @bytesize = nil
51
+ @length = nil
52
52
 
53
53
  @index = 0
54
54
  end
55
55
 
56
- def bytesize
57
- @bytesize ||= @chunks.inject(0) {|sum, chunk| sum + chunk.bytesize}
56
+ def length
57
+ @length ||= @chunks.inject(0) {|sum, chunk| sum + chunk.bytesize}
58
58
  end
59
59
 
60
60
  def empty?
@@ -28,7 +28,7 @@ module Async
28
28
  @protocol = protocol
29
29
  @finished = false
30
30
 
31
- @bytesize = 0
31
+ @length = 0
32
32
  @count = 0
33
33
  end
34
34
 
@@ -36,30 +36,34 @@ module Async
36
36
  @finished
37
37
  end
38
38
 
39
+ def stop(error)
40
+ @protocol.close
41
+ @finished = true
42
+ end
43
+
39
44
  def read
40
45
  return nil if @finished
41
46
 
42
- size = @protocol.read_line.to_i(16)
47
+ length = @protocol.read_line.to_i(16)
43
48
 
44
- if size == 0
45
- @protocol.read_line
46
-
49
+ if length == 0
47
50
  @finished = true
51
+ @protocol.read_line
48
52
 
49
53
  return nil
50
54
  end
51
55
 
52
- chunk = @protocol.stream.read(size)
56
+ chunk = @protocol.stream.read(length)
53
57
  @protocol.read_line # Consume the trailing CRLF
54
58
 
55
- @bytesize += size
59
+ @length += length
56
60
  @count += 1
57
61
 
58
62
  return chunk
59
63
  end
60
64
 
61
65
  def inspect
62
- "\#<#{self.class} #{@bytesize} bytes read in #{@count} chunks>"
66
+ "\#<#{self.class} #{@length} bytes read in #{@count} chunks>"
63
67
  end
64
68
  end
65
69
  end
@@ -51,15 +51,24 @@ module Async
51
51
 
52
52
  @stream = stream
53
53
 
54
- @input_size = 0
55
- @output_size = 0
54
+ @input_length = 0
55
+ @output_length = 0
56
56
  end
57
57
 
58
- attr :input_size
59
- attr :output_size
58
+ def length
59
+ # We don't know the length of the output until after it's been compressed.
60
+ nil
61
+ end
62
+
63
+ attr :input_length
64
+ attr :output_length
60
65
 
61
66
  def ratio
62
- @output_size.to_f / @input_size.to_f
67
+ if @input_length != 0
68
+ @output_length.to_f / @input_length.to_f
69
+ else
70
+ 1.0
71
+ end
63
72
  end
64
73
 
65
74
  def stop(error)
@@ -83,17 +92,17 @@ module Async
83
92
  return if @stream.closed?
84
93
 
85
94
  if chunk = super
86
- @input_size += chunk.bytesize
95
+ @input_length += chunk.bytesize
87
96
 
88
97
  chunk = @stream.deflate(chunk, Zlib::SYNC_FLUSH)
89
98
 
90
- @output_size += chunk.bytesize
99
+ @output_length += chunk.bytesize
91
100
 
92
101
  return chunk
93
102
  else
94
103
  chunk = @stream.finish
95
104
 
96
- @output_size += chunk.bytesize
105
+ @output_length += chunk.bytesize
97
106
 
98
107
  @stream.close
99
108
 
@@ -37,6 +37,10 @@ module Async
37
37
  @remaining == 0
38
38
  end
39
39
 
40
+ def stop(error)
41
+ @stream.close
42
+ end
43
+
40
44
  def read
41
45
  if @remaining > 0
42
46
  if chunk = @stream.read_partial(@remaining)
@@ -69,8 +73,12 @@ module Async
69
73
  @stream.closed?
70
74
  end
71
75
 
76
+ def stop(error)
77
+ @stream.close
78
+ end
79
+
72
80
  def read
73
- @stream.read unless @stream.closed?
81
+ @stream.read_partial
74
82
  end
75
83
 
76
84
  def join
@@ -34,15 +34,15 @@ module Async
34
34
  return if @stream.closed?
35
35
 
36
36
  if chunk = super
37
- @input_size += chunk.bytesize
37
+ @input_length += chunk.bytesize
38
38
 
39
39
  chunk = @stream.inflate(chunk)
40
40
 
41
- @output_size += chunk.bytesize
41
+ @output_length += chunk.bytesize
42
42
  else
43
43
  chunk = @stream.finish
44
44
 
45
- @output_size += chunk.bytesize
45
+ @output_length += chunk.bytesize
46
46
 
47
47
  @stream.close
48
48
  end
@@ -33,6 +33,10 @@ module Async
33
33
  @body.empty?
34
34
  end
35
35
 
36
+ def length
37
+ @body.length
38
+ end
39
+
36
40
  # Buffer any remaining body.
37
41
  def close
38
42
  @body = @body.close
@@ -156,6 +156,8 @@ module Async
156
156
  hash[key] = policy.new(value)
157
157
  end
158
158
  else
159
+ raise ArgumentError, "Header #{key} can only be set once!" if hash.include?(key)
160
+
159
161
  # We can't merge these, we only expose the last one set.
160
162
  hash[key] = value
161
163
  end
@@ -20,7 +20,15 @@
20
20
 
21
21
  module Async
22
22
  module HTTP
23
- VERBS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']
23
+ GET = 'GET'.freeze
24
+ HEAD = 'HEAD'.freeze
25
+ POST = 'POST'.freeze
26
+ PUT = 'PUT'.freeze
27
+ PATCH = 'PATCH'.freeze
28
+ DELETE = 'DELETE'.freeze
29
+ CONNECT = 'CONNECT'.freeze
30
+
31
+ VERBS = [GET, HEAD, POST, PUT, PATCH, DELETE, CONNECT].freeze
24
32
 
25
33
  module Verbs
26
34
  VERBS.each do |verb|
@@ -0,0 +1,29 @@
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
+ module Async
22
+ module HTTP
23
+ module Protocol
24
+ # The request was invalid/malformed in some way.
25
+ class BadRequest < StandardError
26
+ end
27
+ end
28
+ end
29
+ end
@@ -41,45 +41,14 @@ module Async
41
41
  end
42
42
  end
43
43
 
44
- # Server loop.
45
- def receive_requests(task: Task.current)
46
- while @persistent
47
- request = Request.new(*self.read_request)
48
-
49
- unless persistent?(request.headers)
50
- @persistent = false
51
- end
52
-
53
- response = yield request
54
-
55
- response.version ||= request.version
56
-
57
- write_response(response.version, response.status, response.headers, response.body)
58
-
59
- # This ensures we yield at least once every iteration of the loop and allow other fibers to execute.
60
- task.yield
61
- end
62
- end
63
-
64
44
  def write_persistent_header
65
- @stream.write("Connection: keep-alive\r\n") if @persistent
45
+ @stream.write("connection: keep-alive\r\n") if @persistent
66
46
  end
67
47
 
68
48
  def write_body(body, chunked = false)
69
49
  # We don't support chunked encoding.
70
50
  super(body, chunked)
71
51
  end
72
-
73
- def read_body(headers)
74
- if body = super
75
- return body
76
- end
77
-
78
- # Technically, with HTTP/1.0, if no content-length is specified, we just need to read everything until the connection is closed.
79
- unless @persistent
80
- return Body::Remainder.new(@stream)
81
- end
82
- end
83
52
  end
84
53
  end
85
54
  end
@@ -21,6 +21,7 @@
21
21
  require 'async/io/protocol/line'
22
22
 
23
23
  require_relative 'request_failed'
24
+ require_relative 'bad_request'
24
25
 
25
26
  require_relative '../request'
26
27
  require_relative '../response'
@@ -32,6 +33,10 @@ require_relative '../body/fixed'
32
33
  module Async
33
34
  module HTTP
34
35
  module Protocol
36
+ TRANSFER_ENCODING = 'transfer-encoding'.freeze
37
+ CONTENT_LENGTH = 'content-length'.freeze
38
+ CHUNKED = 'chunked'.freeze
39
+
35
40
  # Implements basic HTTP/1.1 request/response.
36
41
  class HTTP11 < Async::IO::Protocol::Line
37
42
  CRLF = "\r\n".freeze
@@ -80,25 +85,62 @@ module Async
80
85
  end
81
86
  end
82
87
 
88
+ def hijack
89
+ @persistent = false
90
+
91
+ @stream.flush
92
+
93
+ return @stream.io
94
+ end
95
+
96
+ class Request < HTTP::Request
97
+ def initialize(protocol)
98
+ super(*protocol.read_request)
99
+
100
+ @protocol = protocol
101
+ end
102
+
103
+ attr :protocol
104
+
105
+ def hijack?
106
+ true
107
+ end
108
+
109
+ def hijack
110
+ @protocol.hijack
111
+ end
112
+ end
113
+
114
+ def next_request
115
+ # The default is true.
116
+ return nil unless @persistent
117
+
118
+ request = Request.new(self)
119
+
120
+ unless persistent?(request.headers)
121
+ @persistent = false
122
+ end
123
+
124
+ return request
125
+ rescue
126
+ # Bad Request
127
+ write_response(self.version, 400, {}, nil)
128
+
129
+ raise
130
+ end
131
+
83
132
  # Server loop.
84
133
  def receive_requests(task: Task.current)
85
- while @persistent
86
- request = Request.new(*read_request)
87
-
88
- unless persistent?(request.headers)
89
- @persistent = false
134
+ while request = next_request
135
+ if response = yield(request, self)
136
+ write_response(response.version || self.version, response.status, response.headers, response.body)
137
+ request.finish
138
+
139
+ # This ensures we yield at least once every iteration of the loop and allow other fibers to execute.
140
+ task.yield
141
+ else
142
+ break
90
143
  end
91
-
92
- response = yield request
93
-
94
- response.version ||= request.version
95
-
96
- write_response(response.version, response.status, response.headers, response.body)
97
-
98
- request.finish
99
-
100
- # This ensures we yield at least once every iteration of the loop and allow other fibers to execute.
101
- task.yield
102
144
  end
103
145
  end
104
146
 
@@ -118,7 +160,7 @@ module Async
118
160
  # Once we start writing the body, we can't recover if the request fails. That's because the body might be generated dynamically, streaming, etc.
119
161
  write_body(request.body)
120
162
 
121
- return Response.new(*read_response)
163
+ return Response.new(*read_response(request))
122
164
  rescue
123
165
  # This will ensure that #reusable? returns false.
124
166
  @stream.close
@@ -128,28 +170,34 @@ module Async
128
170
 
129
171
  def write_request(authority, method, path, version, headers)
130
172
  @stream.write("#{method} #{path} #{version}\r\n")
131
- @stream.write("Host: #{authority}\r\n")
173
+ @stream.write("host: #{authority}\r\n")
132
174
  write_headers(headers)
133
175
 
134
176
  @stream.flush
135
177
  end
136
178
 
137
- def read_response
179
+ def read_response(request)
138
180
  version, status, reason = read_line.split(/\s+/, 3)
139
- headers = read_headers
140
- body = read_body(headers)
181
+ Async.logger.debug(self) {"#{version} #{status} #{reason}"}
141
182
 
142
- @count += 1
183
+ headers = read_headers
143
184
 
144
185
  @persistent = persistent?(headers)
145
186
 
187
+ body = read_response_body(request, status, headers)
188
+
189
+ @count += 1
190
+
146
191
  return version, Integer(status), reason, headers, body
147
192
  end
148
193
 
149
194
  def read_request
150
195
  method, path, version = read_line.split(/\s+/, 3)
151
196
  headers = read_headers
152
- body = read_body(headers)
197
+
198
+ @persistent = persistent?(headers)
199
+
200
+ body = read_request_body(headers)
153
201
 
154
202
  @count += 1
155
203
 
@@ -167,15 +215,13 @@ module Async
167
215
  protected
168
216
 
169
217
  def write_persistent_header
170
- @stream.write("Connection: close\r\n") unless @persistent
218
+ @stream.write("connection: close\r\n") unless @persistent
171
219
  end
172
220
 
173
221
  def write_headers(headers)
174
222
  headers.each do |name, value|
175
223
  @stream.write("#{name}: #{value}\r\n")
176
224
  end
177
-
178
- write_persistent_header
179
225
  end
180
226
 
181
227
  def read_headers
@@ -192,60 +238,154 @@ module Async
192
238
  return Headers.new(fields)
193
239
  end
194
240
 
241
+ def write_empty_body(body)
242
+ # Write empty body:
243
+ write_persistent_header
244
+ @stream.write("content-length: 0\r\n\r\n")
245
+
246
+ body.read if body
247
+
248
+ @stream.flush
249
+ end
250
+
251
+ def write_fixed_length_body(body, length)
252
+ write_persistent_header
253
+ @stream.write("content-length: #{length}\r\n\r\n")
254
+
255
+ body.each do |chunk|
256
+ @stream.write(chunk)
257
+ end
258
+
259
+ @stream.flush
260
+ end
261
+
262
+ def write_chunked_body(body)
263
+ write_persistent_header
264
+ @stream.write("transfer-encoding: chunked\r\n\r\n")
265
+
266
+ body.each do |chunk|
267
+ next if chunk.size == 0
268
+
269
+ @stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n")
270
+ @stream.write(chunk)
271
+ @stream.write(CRLF)
272
+ @stream.flush
273
+ end
274
+
275
+ @stream.write("0\r\n\r\n")
276
+ @stream.flush
277
+ end
278
+
279
+ def write_body_and_close(body)
280
+ # We can't be persistent because we don't know the data length:
281
+ @persistent = false
282
+ write_persistent_header
283
+
284
+ @stream.write("\r\n")
285
+
286
+ body.each do |chunk|
287
+ @stream.write(chunk)
288
+ @stream.flush
289
+ end
290
+
291
+ @stream.io.close_write
292
+ end
293
+
195
294
  def write_body(body, chunked = true)
196
295
  if body.nil? or body.empty?
197
- @stream.write("Content-Length: 0\r\n\r\n")
198
- body.read if body
296
+ write_empty_body(body)
199
297
  elsif length = body.length
200
- @stream.write("Content-Length: #{length}\r\n\r\n")
201
-
202
- body.each do |chunk|
203
- @stream.write(chunk)
204
- end
298
+ write_fixed_length_body(body, length)
205
299
  elsif chunked
206
- @stream.write("Transfer-Encoding: chunked\r\n\r\n")
207
-
208
- body.each do |chunk|
209
- next if chunk.size == 0
210
-
211
- @stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n")
212
- @stream.write(chunk)
213
- @stream.write(CRLF)
214
- @stream.flush
215
- end
216
-
217
- @stream.write("0\r\n\r\n")
300
+ write_chunked_body(body)
218
301
  else
219
- body = Body::Buffered.for(body)
220
-
221
- @stream.write("Content-Length: #{body.bytesize}\r\n\r\n")
222
-
223
- body.each do |chunk|
224
- @stream.write(chunk)
225
- end
302
+ write_body_and_close(body)
226
303
  end
227
-
228
- @stream.flush
229
304
  end
230
305
 
231
- TRANSFER_ENCODING = 'transfer-encoding'.freeze
232
- CONTENT_LENGTH = 'content-length'.freeze
233
- CHUNKED = 'chunked'.freeze
306
+ def read_response_body(request, status, headers)
307
+ # RFC 7230 3.3.3
308
+ # 1. Any response to a HEAD request and any response with a 1xx
309
+ # (Informational), 204 (No Content), or 304 (Not Modified) status
310
+ # code is always terminated by the first empty line after the
311
+ # header fields, regardless of the header fields present in the
312
+ # message, and thus cannot contain a message body.
313
+ if request.head? or status == 204 or status == 304
314
+ return nil
315
+ end
316
+
317
+ # 2. Any 2xx (Successful) response to a CONNECT request implies that
318
+ # the connection will become a tunnel immediately after the empty
319
+ # line that concludes the header fields. A client MUST ignore any
320
+ # Content-Length or Transfer-Encoding header fields received in
321
+ # such a message.
322
+ if request.connect? and status == 200
323
+ return Body::Remainder.new(@stream)
324
+ end
325
+
326
+ if body = read_body(headers)
327
+ return body
328
+ else
329
+ # 7. Otherwise, this is a response message without a declared message
330
+ # body length, so the message body length is determined by the
331
+ # number of octets received prior to the server closing the
332
+ # connection.
333
+ return Body::Remainder.new(@stream)
334
+ end
335
+ end
234
336
 
235
- def chunked?(headers)
236
- if transfer_encoding = headers[TRANSFER_ENCODING]
237
- if transfer_encoding.count == 1
238
- return transfer_encoding.first == CHUNKED
239
- end
337
+ def read_request_body(headers)
338
+ # 6. If this is a request message and none of the above are true, then
339
+ # the message body length is zero (no message body is present).
340
+ if body = read_body(headers)
341
+ return body
240
342
  end
241
343
  end
242
344
 
243
345
  def read_body(headers)
244
- if chunked?(headers)
245
- return Body::Chunked.new(self)
246
- elsif content_length = headers[CONTENT_LENGTH]
247
- if content_length != 0
248
- return Body::Fixed.new(@stream, Integer(content_length))
346
+ # 3. If a Transfer-Encoding header field is present and the chunked
347
+ # transfer coding (Section 4.1) is the final encoding, the message
348
+ # body length is determined by reading and decoding the chunked
349
+ # data until the transfer coding indicates the data is complete.
350
+ if transfer_encoding = headers[TRANSFER_ENCODING]
351
+ # If a message is received with both a Transfer-Encoding and a
352
+ # Content-Length header field, the Transfer-Encoding overrides the
353
+ # Content-Length. Such a message might indicate an attempt to
354
+ # perform request smuggling (Section 9.5) or response splitting
355
+ # (Section 9.4) and ought to be handled as an error. A sender MUST
356
+ # remove the received Content-Length field prior to forwarding such
357
+ # a message downstream.
358
+ if headers[CONTENT_LENGTH]
359
+ raise BadRequest, "Message contains both transfer encoding and content length!"
360
+ end
361
+
362
+ if transfer_encoding.last == CHUNKED
363
+ return Body::Chunked.new(self)
364
+ else
365
+ # If a Transfer-Encoding header field is present in a response and
366
+ # the chunked transfer coding is not the final encoding, the
367
+ # message body length is determined by reading the connection until
368
+ # it is closed by the server. If a Transfer-Encoding header field
369
+ # is present in a request and the chunked transfer coding is not
370
+ # the final encoding, the message body length cannot be determined
371
+ # reliably; the server MUST respond with the 400 (Bad Request)
372
+ # status code and then close the connection.
373
+ return Body::Remainder.new(@stream)
374
+ end
375
+ end
376
+
377
+ # 5. If a valid Content-Length header field is present without
378
+ # Transfer-Encoding, its decimal value defines the expected message
379
+ # body length in octets. If the sender closes the connection or
380
+ # the recipient times out before the indicated number of octets are
381
+ # received, the recipient MUST consider the message to be
382
+ # incomplete and close the connection.
383
+ if content_length = headers[CONTENT_LENGTH]
384
+ length = Integer(content_length)
385
+ if length >= 0
386
+ return Body::Fixed.new(@stream, length)
387
+ else
388
+ raise BadRequest, "Invalid content length: #{content_length}"
249
389
  end
250
390
  end
251
391
  end
@@ -23,6 +23,8 @@ require_relative '../response'
23
23
  require_relative '../headers'
24
24
  require_relative '../body/writable'
25
25
 
26
+ require_relative 'http11'
27
+
26
28
  require 'async/notification'
27
29
 
28
30
  require 'http/2'
@@ -120,71 +122,73 @@ module Async
120
122
  @stream.close
121
123
  end
122
124
 
125
+ class Request < HTTP::Request
126
+ def initialize(stream)
127
+ super(nil, nil, nil, VERSION, Headers.new, Body::Writable.new)
128
+
129
+ @stream = stream
130
+ end
131
+
132
+ attr :stream
133
+
134
+ def assign_headers(headers)
135
+ headers.each do |key, value|
136
+ if key == METHOD
137
+ raise BadRequest, "Request method already specified" if @method
138
+
139
+ @method = value
140
+ elsif key == PATH
141
+ raise BadRequest, "Request path already specified" if @path
142
+
143
+ @path = value
144
+ elsif key == AUTHORITY
145
+ raise BadRequest, "Request authority already specified" if @authority
146
+
147
+ @authority = value
148
+ else
149
+ @headers[key] = value
150
+ end
151
+ end
152
+ end
153
+
154
+ def hijack?
155
+ false
156
+ end
157
+ end
158
+
123
159
  def receive_requests(task: Task.current, &block)
124
160
  # emits new streams opened by the client
125
161
  @controller.on(:stream) do |stream|
126
- request = Request.new
127
- request.version = self.version
128
- request.headers = Headers.new
129
- body = Body::Writable.new
130
- request.body = body
162
+ request = Request.new(stream)
163
+ body = request.body
131
164
 
132
165
  stream.on(:headers) do |headers|
133
- headers.each do |key, value|
134
- if key == METHOD
135
- request.method = value
136
- elsif key == PATH
137
- request.path = value
138
- elsif key == AUTHORITY
139
- request.authority = value
140
- else
141
- request.headers[key] = value
166
+ begin
167
+ request.assign_headers(headers)
168
+ rescue
169
+ Async.logger.error(self) {$!}
170
+
171
+ stream.headers({
172
+ STATUS => "400"
173
+ }, end_stream: true)
174
+ else
175
+ task.async do
176
+ generate_response(request, stream, &block)
142
177
  end
143
178
  end
144
179
  end
145
180
 
146
181
  stream.on(:data) do |chunk|
147
- # puts "Got request data: #{chunk.inspect}"
148
182
  body.write(chunk.to_s) unless chunk.empty?
149
183
  end
150
184
 
151
- stream.on(:close) do |error|
152
- if error
153
- body.stop(EOFError.new(error))
154
- end
185
+ stream.on(:half_close) do
186
+ # We are no longer receiving any more data frames:
187
+ body.finish
155
188
  end
156
189
 
157
- stream.on(:half_close) do
158
- # The requirements for this to be in lock-step with other opertaions is minimal.
159
- # TODO consider putting this in it's own async task.
160
- begin
161
- # We are no longer receiving any more data frames:
162
- body.finish
163
-
164
- # Generate the response:
165
- response = yield request
166
-
167
- headers = {STATUS => response.status.to_s}
168
- headers.update(response.headers)
169
-
170
- if response.body.nil? or response.body.empty?
171
- stream.headers(headers, end_stream: true)
172
- response.body.read if response.body
173
- else
174
- stream.headers(headers, end_stream: false)
175
-
176
- response.body.each do |chunk|
177
- stream.data(chunk, end_stream: false)
178
- end
179
-
180
- stream.data("", end_stream: true)
181
- end
182
- rescue
183
- Async.logger.error(self) {$!}
184
-
185
- # Generating the response failed.
186
- stream.close(:internal_error)
187
- end
190
+ stream.on(:close) do |error|
191
+ body.stop(EOFError.new(error)) if error
188
192
  end
189
193
  end
190
194
 
@@ -192,6 +196,41 @@ module Async
192
196
  @reader.wait
193
197
  end
194
198
 
199
+ # Generate a response to the request. If this fails, the stream is terminated and the error is reported.
200
+ private def generate_response(request, stream, &block)
201
+ # We need to close the stream if the user code blows up while generating a response:
202
+ response = begin
203
+ yield(request, stream)
204
+ rescue
205
+ stream.close(:internal_error)
206
+
207
+ raise
208
+ end
209
+
210
+ if response
211
+ headers = Headers::Merged.new({
212
+ STATUS => response.status,
213
+ }, response.headers)
214
+
215
+ if response.body.nil? or response.body.empty?
216
+ stream.headers(headers, end_stream: true)
217
+ response.body.read if response.body
218
+ else
219
+ stream.headers(headers, end_stream: false)
220
+
221
+ response.body.each do |chunk|
222
+ stream.data(chunk, end_stream: false)
223
+ end
224
+
225
+ stream.data("", end_stream: true)
226
+ end
227
+ else
228
+ stream.close(:internal_error) unless stream.state == :closed
229
+ end
230
+ rescue
231
+ Async.logger.error(request) {$!}
232
+ end
233
+
195
234
  def call(request)
196
235
  request.version ||= self.version
197
236
 
@@ -19,12 +19,37 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require_relative 'body/buffered'
22
+ require_relative 'middleware'
22
23
 
23
24
  module Async
24
25
  module HTTP
25
- class Request < Struct.new(:authority, :method, :path, :version, :headers, :body)
26
+ class Request
26
27
  prepend Body::Buffered::Reader
27
28
 
29
+ def initialize(authority = nil, method = nil, path = nil, version = nil, headers = [], body = nil)
30
+ @authority = authority
31
+ @method = method
32
+ @path = path
33
+ @version = version
34
+ @headers = headers
35
+ @body = body
36
+ end
37
+
38
+ attr_accessor :authority
39
+ attr_accessor :method
40
+ attr_accessor :path
41
+ attr_accessor :version
42
+ attr_accessor :headers
43
+ attr_accessor :body
44
+
45
+ def head?
46
+ self.method == HEAD
47
+ end
48
+
49
+ def connect?
50
+ self.method == CONNECT
51
+ end
52
+
28
53
  def self.[](method, path, headers, body)
29
54
  body = Body::Buffered.wrap(body)
30
55
 
@@ -32,7 +57,11 @@ module Async
32
57
  end
33
58
 
34
59
  def idempotent?
35
- method != 'POST' && (body.nil? || body.empty?)
60
+ method != POST && (body.nil? || body.empty?)
61
+ end
62
+
63
+ def to_s
64
+ "#{@method} #{@path} #{@version}"
36
65
  end
37
66
  end
38
67
  end
@@ -22,9 +22,23 @@ require_relative 'body/buffered'
22
22
 
23
23
  module Async
24
24
  module HTTP
25
- class Response < Struct.new(:version, :status, :reason, :headers, :body)
25
+ class Response
26
26
  prepend Body::Buffered::Reader
27
27
 
28
+ def initialize(version = nil, status = 200, reason = nil, headers = [], body = nil)
29
+ @version = version
30
+ @status = status
31
+ @reason = reason
32
+ @headers = headers
33
+ @body = body
34
+ end
35
+
36
+ attr_accessor :version
37
+ attr_accessor :status
38
+ attr_accessor :reason
39
+ attr_accessor :headers
40
+ attr_accessor :body
41
+
28
42
  def continue?
29
43
  status == 100
30
44
  end
@@ -45,6 +59,10 @@ module Async
45
59
  status >= 400 && status < 600
46
60
  end
47
61
 
62
+ def bad_request?
63
+ status == 400
64
+ end
65
+
48
66
  def self.[](status, headers = {}, body = [])
49
67
  body = Body::Buffered.wrap(body)
50
68
 
@@ -54,6 +72,10 @@ module Async
54
72
  def self.for_exception(exception)
55
73
  Async::HTTP::Response[500, {'content-type' => 'text/plain'}, ["#{exception.class}: #{exception.message}"]]
56
74
  end
75
+
76
+ def to_s
77
+ "#{@status} #{@reason} #{@version}"
78
+ end
57
79
  end
58
80
  end
59
81
  end
@@ -48,18 +48,11 @@ module Async
48
48
 
49
49
  Async.logger.debug(self) {"Incoming connnection from #{address.inspect} to #{protocol}"}
50
50
 
51
- hijack = catch(:hijack) do
52
- protocol.receive_requests do |request|
53
- # Async.logger.debug(self) {"Incoming request from #{address.inspect}: #{request.method} #{request.path}"}
54
- handle_request(request, peer, address)
55
- end
51
+ protocol.receive_requests do |request|
52
+ # Async.logger.debug(self) {"Incoming request from #{address.inspect}: #{request.method} #{request.path}"}
56
53
 
57
- # hijack should be false by default.
58
- false
59
- end
60
-
61
- if hijack
62
- hijack.call(peer)
54
+ # If this returns nil, we assume that the connection has been hijacked.
55
+ handle_request(request, peer, address)
63
56
  end
64
57
  rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
65
58
  # Sometimes client will disconnect without completing a result or reading the entire buffer. That means we are done.
@@ -46,7 +46,7 @@ module Async
46
46
  def initialize(start_time, body, callback)
47
47
  super(body)
48
48
 
49
- @bytesize = 0
49
+ @length = 0
50
50
 
51
51
  @start_time = start_time
52
52
  @first_chunk_time = nil
@@ -59,7 +59,7 @@ module Async
59
59
  attr :first_chunk_time
60
60
  attr :end_time
61
61
 
62
- attr :bytesize
62
+ attr :length
63
63
 
64
64
  def total_duration
65
65
  if @end_time
@@ -85,7 +85,7 @@ module Async
85
85
  @first_chunk_time ||= Clock.now
86
86
 
87
87
  if chunk
88
- @bytesize += chunk.bytesize
88
+ @length += chunk.length
89
89
  else
90
90
  complete_statistics
91
91
  end
@@ -94,7 +94,7 @@ module Async
94
94
  end
95
95
 
96
96
  def to_s
97
- parts = ["sent #{@bytesize} bytes"]
97
+ parts = ["sent #{@length} bytes"]
98
98
 
99
99
  if duration = self.total_duration
100
100
  parts << "took #{format_duration(duration)} in total"
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module HTTP
23
- VERSION = "0.24.3"
23
+ VERSION = "0.25.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.24.3
4
+ version: 0.25.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-05-22 00:00:00.000000000 Z
11
+ date: 2018-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -156,6 +156,7 @@ files:
156
156
  - lib/async/http/middleware/builder.rb
157
157
  - lib/async/http/pool.rb
158
158
  - lib/async/http/protocol.rb
159
+ - lib/async/http/protocol/bad_request.rb
159
160
  - lib/async/http/protocol/http1.rb
160
161
  - lib/async/http/protocol/http10.rb
161
162
  - lib/async/http/protocol/http11.rb