async-http 0.24.3 → 0.25.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: 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