async-http 0.18.0 → 0.19.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.
@@ -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
- VERBS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']
59
+ include Verbs
58
60
 
59
- VERBS.each do |verb|
60
- define_method(verb.downcase) do |reference, *args, &block|
61
- self.request(verb, reference.to_str, *args, &block)
62
- end
63
- end
64
-
65
- def request(*args, &block)
66
- @connections.acquire do |connection|
67
- response = connection.send_request(@authority, *args)
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
- status, headers, body = yield request
41
+ response = yield request
42
42
 
43
- write_response(request.version, status, headers, body)
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
- buffer = String.new
57
- body.each{|chunk| buffer << chunk}
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 content_length = headers['content-length']
65
- return @stream.read(Integer(content_length))
66
- # elsif !keep_alive?(headers)
67
- # return @stream.read
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
- status, headers, body = yield request
74
+ response = yield request
75
+
76
+ response.version ||= request.version
72
77
 
73
- write_response(request.version, status, headers, body)
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
- # Client request.
89
- def send_request(authority, method, path, headers = {}, body = [])
90
- Async.logger.debug(self) {"#{method} #{path} #{headers.inspect}"}
93
+ def call(request)
94
+ request.version ||= self.version
91
95
 
92
- write_request(authority, method, path, version, headers, body)
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 chunked
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
- buffer = String.new
176
- body.each{|chunk| buffer << chunk}
183
+ body = Body::Buffered.for(body)
184
+
185
+ @stream.write("Content-Length: #{body.bytesize}\r\n\r\n")
177
186
 
178
- @stream.write("Content-Length: #{buffer.bytesize}\r\n\r\n")
179
- @stream.write(buffer)
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 ChunkedBody.new(self)
195
+ return Body::Chunked.new(self)
186
196
  elsif content_length = headers['content-length']
187
- return FixedBody.new(Integer(content_length), @stream)
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 = VERSION
108
+ request.version = self.version
106
109
  request.headers = {}
107
- request.body = Body.new
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.body.write(chunk.to_s) unless chunk.empty?
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
- request.body.finish
137
+ # puts "Finishing body..."
138
+ body.finish
131
139
 
140
+ # puts "Sending response..."
132
141
  # send response
133
- headers = {STATUS => response[0].to_s}
134
- headers.update(response[1])
142
+ headers = {STATUS => response.status.to_s}
143
+ headers.update(response.headers)
135
144
 
136
- stream.headers(headers, end_stream: false)
137
-
138
- response[2].each do |chunk|
139
- stream.data(chunk, end_stream: false)
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
- RESPONSE_VERSION = 'HTTP/2'.freeze
150
-
151
- def send_request(authority, method, path, headers = {}, body = nil)
168
+ def call(request)
169
+ request.version ||= self.version
170
+
152
171
  stream = @controller.new_stream
153
172
 
154
- internal_headers = {
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 = RESPONSE_VERSION
183
+ response.version = self.version
175
184
  response.headers = {}
176
- response.body = Body.new
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
- Async.logger.debug(self) {"Stream data: #{chunk.inspect}"}
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(:half_close) do
200
- Async.logger.debug(self) {"Stream half-closed."}
206
+ stream.on(:close) do
207
+ body.finish
201
208
  end
202
209
 
203
- stream.on(:close) do
204
- Async.logger.debug(self) {"Stream closed, sending signal."}
205
- # TODO should we prefer `response.finish`?
206
- response.body.finish
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."}