async-http 0.18.0 → 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/async-http.gemspec +1 -1
- data/lib/async/http/accept_encoding.rb +65 -0
- data/lib/async/http/body.rb +2 -244
- data/lib/async/http/body/buffered.rb +107 -0
- data/lib/async/http/body/chunked.rb +67 -0
- data/lib/async/http/body/deflate.rb +106 -0
- data/lib/async/http/body/fixed.rb +83 -0
- data/lib/async/http/body/inflate.rb +55 -0
- data/lib/async/http/body/readable.rb +73 -0
- data/lib/async/http/body/streamable.rb +52 -0
- data/lib/async/http/body/wrapper.rb +58 -0
- data/lib/async/http/body/writable.rb +81 -0
- data/lib/async/http/client.rb +14 -19
- data/lib/async/http/content_encoding.rb +71 -0
- data/lib/async/http/middleware.rb +55 -0
- data/lib/async/http/middleware/builder.rb +50 -0
- data/lib/async/http/protocol/http10.rb +13 -11
- data/lib/async/http/protocol/http11.rb +24 -14
- data/lib/async/http/protocol/http2.rb +62 -48
- data/lib/async/http/reference.rb +173 -0
- data/lib/async/http/relative_location.rb +75 -0
- data/lib/async/http/request.rb +8 -2
- data/lib/async/http/response.rb +12 -2
- data/lib/async/http/server.rb +4 -2
- data/lib/async/http/statistics.rb +131 -0
- data/lib/async/http/url_endpoint.rb +17 -1
- data/lib/async/http/version.rb +1 -1
- metadata +20 -5
- data/lib/async/http/deflate_body.rb +0 -124
data/lib/async/http/client.rb
CHANGED
@@ -21,6 +21,8 @@
|
|
21
21
|
require 'async/io/endpoint'
|
22
22
|
|
23
23
|
require_relative 'protocol'
|
24
|
+
require_relative 'body/streamable'
|
25
|
+
require_relative 'middleware'
|
24
26
|
|
25
27
|
module Async
|
26
28
|
module HTTP
|
@@ -54,27 +56,20 @@ module Async
|
|
54
56
|
@connections.close
|
55
57
|
end
|
56
58
|
|
57
|
-
|
59
|
+
include Verbs
|
58
60
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
begin
|
70
|
-
return yield response if block_given?
|
71
|
-
ensure
|
72
|
-
# This forces the stream to complete reading.
|
73
|
-
response.finish
|
74
|
-
end
|
75
|
-
|
76
|
-
return response
|
61
|
+
def call(request)
|
62
|
+
connection = @connections.acquire
|
63
|
+
|
64
|
+
request.authority ||= @authority
|
65
|
+
response = connection.call(request)
|
66
|
+
|
67
|
+
# The connection won't be released until the body is completely read/released.
|
68
|
+
Body::Streamable.wrap(response) do
|
69
|
+
@connections.release(connection)
|
77
70
|
end
|
71
|
+
|
72
|
+
return response
|
78
73
|
end
|
79
74
|
|
80
75
|
protected
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'middleware'
|
22
|
+
|
23
|
+
require_relative 'body/buffered'
|
24
|
+
require_relative 'body/deflate'
|
25
|
+
|
26
|
+
module Async
|
27
|
+
module HTTP
|
28
|
+
# Encode a response according the the request's acceptable encodings.
|
29
|
+
class ContentEncoding < Middleware
|
30
|
+
DEFAULT_WRAPPERS = {
|
31
|
+
'gzip' => Body::Deflate.method(:for)
|
32
|
+
}
|
33
|
+
|
34
|
+
DEFAULT_CONTENT_TYPES = %r{^(text/.*?)|(.*?/json)|(.*?/javascript)$}
|
35
|
+
|
36
|
+
def initialize(app, content_types = DEFAULT_CONTENT_TYPES, wrappers = DEFAULT_WRAPPERS)
|
37
|
+
super(app)
|
38
|
+
|
39
|
+
@content_types = content_types
|
40
|
+
@wrappers = wrappers
|
41
|
+
end
|
42
|
+
|
43
|
+
def call(request, *)
|
44
|
+
response = super
|
45
|
+
|
46
|
+
if !response.body.empty? and accept_encoding = request.headers['accept-encoding']
|
47
|
+
if content_type = response.headers['content-type'] and @content_types.match?(content_type)
|
48
|
+
# TODO use http-accept and sort by priority
|
49
|
+
encodings = accept_encoding.split(/\s*,\s*/)
|
50
|
+
|
51
|
+
body = response.body
|
52
|
+
|
53
|
+
encodings.each do |name|
|
54
|
+
if wrapper = @wrappers[name]
|
55
|
+
response.headers['content-encoding'] = name
|
56
|
+
|
57
|
+
body = wrapper.call(body)
|
58
|
+
|
59
|
+
break
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
response.body = body
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
return response
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# Copyright, 2017, 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
|
+
VERBS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']
|
24
|
+
|
25
|
+
module Verbs
|
26
|
+
VERBS.each do |verb|
|
27
|
+
define_method(verb.downcase) do |location, headers = {}, body = []|
|
28
|
+
self.call(Request[verb, location.to_str, headers, body])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Middleware
|
34
|
+
def initialize(app)
|
35
|
+
@app = app
|
36
|
+
end
|
37
|
+
|
38
|
+
def close
|
39
|
+
@app.close
|
40
|
+
end
|
41
|
+
|
42
|
+
include Verbs
|
43
|
+
|
44
|
+
def call(*args)
|
45
|
+
@app.call(*args)
|
46
|
+
end
|
47
|
+
|
48
|
+
module Okay
|
49
|
+
def self.call
|
50
|
+
Response.local(200, {}, [])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright, 2017, 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
|
+
class Middleware
|
24
|
+
class Builder
|
25
|
+
def initialize(default_app = nil, &block)
|
26
|
+
@use = []
|
27
|
+
@run = default_app
|
28
|
+
|
29
|
+
instance_eval(&block) if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
def use(middleware, *args, &block)
|
33
|
+
@use << proc {|app| middleware.new(app, *args, &block)}
|
34
|
+
end
|
35
|
+
|
36
|
+
def run(app)
|
37
|
+
@app = app
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_app
|
41
|
+
app = @use.reverse.inject(app) {|app, use| use.call(app).freeze}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.build(&block)
|
46
|
+
Builder.new(&block).to_app
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -38,9 +38,11 @@ module Async
|
|
38
38
|
# Server loop.
|
39
39
|
def receive_requests
|
40
40
|
while request = Request.new(*self.read_request)
|
41
|
-
|
41
|
+
response = yield request
|
42
42
|
|
43
|
-
|
43
|
+
response.version ||= request.version
|
44
|
+
|
45
|
+
write_response(response.version, response.status, response.headers, response.body)
|
44
46
|
|
45
47
|
unless keep_alive?(request.headers) && keep_alive?(headers)
|
46
48
|
@keep_alive = false
|
@@ -53,18 +55,18 @@ module Async
|
|
53
55
|
end
|
54
56
|
|
55
57
|
def write_body(body, chunked = true)
|
56
|
-
|
57
|
-
body
|
58
|
-
|
59
|
-
@stream.write("Content-Length: #{buffer.bytesize}\r\n\r\n")
|
60
|
-
@stream.write(buffer)
|
58
|
+
# We don't support chunked encoding.
|
59
|
+
super(body, false)
|
61
60
|
end
|
62
61
|
|
63
62
|
def read_body(headers)
|
64
|
-
if
|
65
|
-
return
|
66
|
-
|
67
|
-
|
63
|
+
if body = super
|
64
|
+
return body
|
65
|
+
end
|
66
|
+
|
67
|
+
# Technically, with HTTP/1.0, if no content-length is specified, we just need to read everything until the connection is closed.
|
68
|
+
if !keep_alive?(headers)
|
69
|
+
return Body::Remainder.new(@stream)
|
68
70
|
end
|
69
71
|
end
|
70
72
|
end
|
@@ -23,6 +23,9 @@ require 'async/io/protocol/line'
|
|
23
23
|
require_relative '../request'
|
24
24
|
require_relative '../response'
|
25
25
|
|
26
|
+
require_relative '../body/chunked'
|
27
|
+
require_relative '../body/fixed'
|
28
|
+
|
26
29
|
module Async
|
27
30
|
module HTTP
|
28
31
|
module Protocol
|
@@ -68,13 +71,15 @@ module Async
|
|
68
71
|
while true
|
69
72
|
request = Request.new(*read_request)
|
70
73
|
|
71
|
-
|
74
|
+
response = yield request
|
75
|
+
|
76
|
+
response.version ||= request.version
|
72
77
|
|
73
|
-
write_response(
|
78
|
+
write_response(response.version, response.status, response.headers, response.body)
|
74
79
|
|
75
80
|
request.finish
|
76
81
|
|
77
|
-
unless keep_alive?(request.headers) and keep_alive?(headers)
|
82
|
+
unless keep_alive?(request.headers) and keep_alive?(response.headers)
|
78
83
|
@keep_alive = false
|
79
84
|
|
80
85
|
break
|
@@ -85,11 +90,11 @@ module Async
|
|
85
90
|
end
|
86
91
|
end
|
87
92
|
|
88
|
-
|
89
|
-
|
90
|
-
Async.logger.debug(self) {"#{method} #{path} #{headers.inspect}"}
|
93
|
+
def call(request)
|
94
|
+
request.version ||= self.version
|
91
95
|
|
92
|
-
|
96
|
+
Async.logger.debug(self) {"#{request.method} #{request.path} #{request.headers.inspect}"}
|
97
|
+
write_request(request.authority, request.method, request.path, request.version, request.headers, request.body)
|
93
98
|
|
94
99
|
return Response.new(*read_response)
|
95
100
|
rescue EOFError
|
@@ -158,7 +163,10 @@ module Async
|
|
158
163
|
end
|
159
164
|
|
160
165
|
def write_body(body, chunked = true)
|
161
|
-
if
|
166
|
+
if body.empty?
|
167
|
+
@stream.write("Content-Length: 0\r\n\r\n")
|
168
|
+
body.read
|
169
|
+
elsif chunked
|
162
170
|
@stream.write("Transfer-Encoding: chunked\r\n\r\n")
|
163
171
|
|
164
172
|
body.each do |chunk|
|
@@ -172,19 +180,21 @@ module Async
|
|
172
180
|
|
173
181
|
@stream.write("0\r\n\r\n")
|
174
182
|
else
|
175
|
-
|
176
|
-
|
183
|
+
body = Body::Buffered.for(body)
|
184
|
+
|
185
|
+
@stream.write("Content-Length: #{body.bytesize}\r\n\r\n")
|
177
186
|
|
178
|
-
|
179
|
-
|
187
|
+
body.each do |chunk|
|
188
|
+
@stream.write(chunk)
|
189
|
+
end
|
180
190
|
end
|
181
191
|
end
|
182
192
|
|
183
193
|
def read_body(headers)
|
184
194
|
if headers['transfer-encoding'] == 'chunked'
|
185
|
-
return
|
195
|
+
return Body::Chunked.new(self)
|
186
196
|
elsif content_length = headers['content-length']
|
187
|
-
return
|
197
|
+
return Body::Fixed.new(@stream, Integer(content_length))
|
188
198
|
end
|
189
199
|
end
|
190
200
|
end
|
@@ -20,6 +20,7 @@
|
|
20
20
|
|
21
21
|
require_relative '../request'
|
22
22
|
require_relative '../response'
|
23
|
+
require_relative '../body/writable'
|
23
24
|
|
24
25
|
require 'async/notification'
|
25
26
|
|
@@ -63,12 +64,6 @@ module Async
|
|
63
64
|
@controller.on(:frame_received) do |frame|
|
64
65
|
Async.logger.debug(self) {"Received frame: #{frame.inspect}"}
|
65
66
|
end
|
66
|
-
|
67
|
-
if @controller.is_a? ::HTTP2::Client
|
68
|
-
@controller.send_connection_preface
|
69
|
-
end
|
70
|
-
|
71
|
-
@reader = read_in_background
|
72
67
|
end
|
73
68
|
|
74
69
|
# Multiple requests can be processed at the same time.
|
@@ -80,6 +75,14 @@ module Async
|
|
80
75
|
@reader.alive?
|
81
76
|
end
|
82
77
|
|
78
|
+
def version
|
79
|
+
VERSION
|
80
|
+
end
|
81
|
+
|
82
|
+
def start_connection
|
83
|
+
@reader ||= read_in_background
|
84
|
+
end
|
85
|
+
|
83
86
|
def read_in_background(task: Task.current)
|
84
87
|
task.async do |nested_task|
|
85
88
|
buffer = Async::IO::BinaryString.new
|
@@ -102,11 +105,13 @@ module Async
|
|
102
105
|
# emits new streams opened by the client
|
103
106
|
@controller.on(:stream) do |stream|
|
104
107
|
request = Request.new
|
105
|
-
request.version =
|
108
|
+
request.version = self.version
|
106
109
|
request.headers = {}
|
107
|
-
|
110
|
+
body = Body::Writable.new
|
111
|
+
request.body = body
|
108
112
|
|
109
113
|
stream.on(:headers) do |headers|
|
114
|
+
# puts "Got request headers: #{headers.inspect}"
|
110
115
|
headers.each do |key, value|
|
111
116
|
if key == METHOD
|
112
117
|
request.method = value
|
@@ -121,63 +126,66 @@ module Async
|
|
121
126
|
end
|
122
127
|
|
123
128
|
stream.on(:data) do |chunk|
|
124
|
-
request
|
129
|
+
# puts "Got request data: #{chunk.inspect}"
|
130
|
+
body.write(chunk.to_s) unless chunk.empty?
|
125
131
|
end
|
126
132
|
|
127
133
|
stream.on(:half_close) do
|
134
|
+
# puts "Generating response..."
|
128
135
|
response = yield request
|
129
136
|
|
130
|
-
|
137
|
+
# puts "Finishing body..."
|
138
|
+
body.finish
|
131
139
|
|
140
|
+
# puts "Sending response..."
|
132
141
|
# send response
|
133
|
-
headers = {STATUS => response
|
134
|
-
headers.update(response
|
142
|
+
headers = {STATUS => response.status.to_s}
|
143
|
+
headers.update(response.headers)
|
135
144
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
145
|
+
# puts "Sending headers #{headers}"
|
146
|
+
if response.body.empty?
|
147
|
+
stream.headers(headers, end_stream: true)
|
148
|
+
response.body.read
|
149
|
+
else
|
150
|
+
stream.headers(headers, end_stream: false)
|
151
|
+
|
152
|
+
# puts "Streaming body..."
|
153
|
+
response.body.each do |chunk|
|
154
|
+
# puts "Sending chunk #{chunk.inspect}"
|
155
|
+
stream.data(chunk, end_stream: false)
|
156
|
+
end
|
157
|
+
|
158
|
+
# puts "Ending stream..."
|
159
|
+
stream.data("", end_stream: true)
|
140
160
|
end
|
141
|
-
|
142
|
-
stream.data("", end_stream: true)
|
143
161
|
end
|
144
162
|
end
|
145
163
|
|
164
|
+
start_connection
|
146
165
|
@reader.wait
|
147
166
|
end
|
148
167
|
|
149
|
-
|
150
|
-
|
151
|
-
|
168
|
+
def call(request)
|
169
|
+
request.version ||= self.version
|
170
|
+
|
152
171
|
stream = @controller.new_stream
|
153
172
|
|
154
|
-
|
173
|
+
headers = {
|
155
174
|
SCHEME => HTTPS,
|
156
|
-
METHOD => method,
|
157
|
-
PATH => path,
|
158
|
-
AUTHORITY => authority,
|
159
|
-
}.merge(headers)
|
160
|
-
|
161
|
-
stream.headers(internal_headers, end_stream: body.nil?)
|
162
|
-
|
163
|
-
if body
|
164
|
-
body.each do |chunk|
|
165
|
-
stream.data(chunk, end_stream: false)
|
166
|
-
end
|
167
|
-
|
168
|
-
stream.data("", end_stream: true)
|
169
|
-
end
|
175
|
+
METHOD => request.method.to_s,
|
176
|
+
PATH => request.path.to_s,
|
177
|
+
AUTHORITY => request.authority.to_s,
|
178
|
+
}.merge(request.headers)
|
170
179
|
|
171
180
|
finished = Async::Notification.new
|
172
181
|
|
173
182
|
response = Response.new
|
174
|
-
response.version =
|
183
|
+
response.version = self.version
|
175
184
|
response.headers = {}
|
176
|
-
|
185
|
+
body = Body::Writable.new
|
186
|
+
response.body = body
|
177
187
|
|
178
188
|
stream.on(:headers) do |headers|
|
179
|
-
# Async.logger.debug(self) {"Stream headers: #{headers.inspect}"}
|
180
|
-
|
181
189
|
headers.each do |key, value|
|
182
190
|
if key == STATUS
|
183
191
|
response.status = value.to_i
|
@@ -192,20 +200,26 @@ module Async
|
|
192
200
|
end
|
193
201
|
|
194
202
|
stream.on(:data) do |chunk|
|
195
|
-
|
196
|
-
response.body.write(chunk.to_s) unless chunk.empty?
|
203
|
+
body.write(chunk.to_s) unless chunk.empty?
|
197
204
|
end
|
198
205
|
|
199
|
-
stream.on(:
|
200
|
-
|
206
|
+
stream.on(:close) do
|
207
|
+
body.finish
|
201
208
|
end
|
202
209
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
210
|
+
if request.body.empty?
|
211
|
+
stream.headers(headers, end_stream: true)
|
212
|
+
else
|
213
|
+
stream.headers(headers, end_stream: false)
|
214
|
+
|
215
|
+
request.body.each do |chunk|
|
216
|
+
stream.data(chunk, end_stream: false)
|
217
|
+
end
|
218
|
+
|
219
|
+
stream.data("", end_stream: true)
|
207
220
|
end
|
208
221
|
|
222
|
+
start_connection
|
209
223
|
@stream.flush
|
210
224
|
|
211
225
|
# Async.logger.debug(self) {"Stream flushed, waiting for signal."}
|