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.
@@ -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."}