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 +4 -4
- data/Rakefile +12 -19
- data/lib/async/http/body/buffered.rb +3 -3
- data/lib/async/http/body/chunked.rb +12 -8
- data/lib/async/http/body/deflate.rb +17 -8
- data/lib/async/http/body/fixed.rb +9 -1
- data/lib/async/http/body/inflate.rb +3 -3
- data/lib/async/http/body/wrapper.rb +4 -0
- data/lib/async/http/headers.rb +2 -0
- data/lib/async/http/middleware.rb +9 -1
- data/lib/async/http/protocol/bad_request.rb +29 -0
- data/lib/async/http/protocol/http10.rb +1 -32
- data/lib/async/http/protocol/http11.rb +207 -67
- data/lib/async/http/protocol/http2.rb +89 -50
- data/lib/async/http/request.rb +31 -2
- data/lib/async/http/response.rb +23 -1
- data/lib/async/http/server.rb +4 -11
- data/lib/async/http/statistics.rb +4 -4
- data/lib/async/http/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 232e1b617ad6dea60e2b7d5ccbd1838bd1a507d6896292dab3345cb439ee7638
|
4
|
+
data.tar.gz: 51877e29175a10fabe6bd36b10d675b09eb90447b16a4e59ba3a8c128f4b9940
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
61
|
-
|
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
|
-
|
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
|
-
|
79
|
-
|
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
|
-
@
|
51
|
+
@length = nil
|
52
52
|
|
53
53
|
@index = 0
|
54
54
|
end
|
55
55
|
|
56
|
-
def
|
57
|
-
@
|
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
|
-
@
|
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
|
-
|
47
|
+
length = @protocol.read_line.to_i(16)
|
43
48
|
|
44
|
-
if
|
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(
|
56
|
+
chunk = @protocol.stream.read(length)
|
53
57
|
@protocol.read_line # Consume the trailing CRLF
|
54
58
|
|
55
|
-
@
|
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} #{@
|
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
|
-
@
|
55
|
-
@
|
54
|
+
@input_length = 0
|
55
|
+
@output_length = 0
|
56
56
|
end
|
57
57
|
|
58
|
-
|
59
|
-
|
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
|
-
@
|
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
|
-
@
|
95
|
+
@input_length += chunk.bytesize
|
87
96
|
|
88
97
|
chunk = @stream.deflate(chunk, Zlib::SYNC_FLUSH)
|
89
98
|
|
90
|
-
@
|
99
|
+
@output_length += chunk.bytesize
|
91
100
|
|
92
101
|
return chunk
|
93
102
|
else
|
94
103
|
chunk = @stream.finish
|
95
104
|
|
96
|
-
@
|
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.
|
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
|
-
@
|
37
|
+
@input_length += chunk.bytesize
|
38
38
|
|
39
39
|
chunk = @stream.inflate(chunk)
|
40
40
|
|
41
|
-
@
|
41
|
+
@output_length += chunk.bytesize
|
42
42
|
else
|
43
43
|
chunk = @stream.finish
|
44
44
|
|
45
|
-
@
|
45
|
+
@output_length += chunk.bytesize
|
46
46
|
|
47
47
|
@stream.close
|
48
48
|
end
|
data/lib/async/http/headers.rb
CHANGED
@@ -20,7 +20,15 @@
|
|
20
20
|
|
21
21
|
module Async
|
22
22
|
module HTTP
|
23
|
-
|
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("
|
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
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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("
|
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
|
-
|
140
|
-
body = read_body(headers)
|
181
|
+
Async.logger.debug(self) {"#{version} #{status} #{reason}"}
|
141
182
|
|
142
|
-
|
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
|
-
|
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("
|
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
|
-
|
198
|
-
body.read if body
|
296
|
+
write_empty_body(body)
|
199
297
|
elsif length = body.length
|
200
|
-
|
201
|
-
|
202
|
-
body.each do |chunk|
|
203
|
-
@stream.write(chunk)
|
204
|
-
end
|
298
|
+
write_fixed_length_body(body, length)
|
205
299
|
elsif chunked
|
206
|
-
|
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
|
-
|
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
|
-
|
232
|
-
|
233
|
-
|
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
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
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
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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(:
|
152
|
-
|
153
|
-
|
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(:
|
158
|
-
|
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
|
|
data/lib/async/http/request.rb
CHANGED
@@ -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
|
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 !=
|
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
|
data/lib/async/http/response.rb
CHANGED
@@ -22,9 +22,23 @@ require_relative 'body/buffered'
|
|
22
22
|
|
23
23
|
module Async
|
24
24
|
module HTTP
|
25
|
-
class Response
|
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
|
data/lib/async/http/server.rb
CHANGED
@@ -48,18 +48,11 @@ module Async
|
|
48
48
|
|
49
49
|
Async.logger.debug(self) {"Incoming connnection from #{address.inspect} to #{protocol}"}
|
50
50
|
|
51
|
-
|
52
|
-
|
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
|
-
#
|
58
|
-
|
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
|
-
@
|
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 :
|
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
|
-
@
|
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 #{@
|
97
|
+
parts = ["sent #{@length} bytes"]
|
98
98
|
|
99
99
|
if duration = self.total_duration
|
100
100
|
parts << "took #{format_duration(duration)} in total"
|
data/lib/async/http/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.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-
|
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
|