async-http 0.23.3 → 0.24.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: deadf37dcc1031b4d90620691dcbba62a5053dad924d8be2ac74f0066ccef5e2
4
- data.tar.gz: d5ddc775312cdf35f8fd9d388d93eb02dc5e7134edb11e19ad43fbdced14c92e
3
+ metadata.gz: 2e35c6673eda6e9dd937957b447502b7d6ad4e715508185ead1d9ef2f03db7b3
4
+ data.tar.gz: 597da90259cdb9611a5b70701d6eefc2ede3deaee51f905bdbbc78cd14feffd8
5
5
  SHA512:
6
- metadata.gz: 72870ae703d379f1de5e2a025cc1096703b786e118306bb196aaca40a2bbb946609dfa6bc0ac9e1615620b1792ee86745db7fe0bcd3aa9a9906570198cf39dc6
7
- data.tar.gz: 176290c981e692d4851e5959f1b73e74b6a84a5dc13d96852e32b516971fe05a57a5a2ca93ba197098eab832d45202b563278829f0b2f0d387712257e2d45ec8
6
+ metadata.gz: 03ab49f9a1363ca6b2ee627a4ea437ead245103f7d6ec0feec290ea25a3ef7601f5afa19417c8fe0656c9b4bdde89868d051babec2234c35c1908ebacf41ccb6
7
+ data.tar.gz: a7e24ce9ba42e98bd8ebcfeaf043a1b7e3748b8e50400365a8a1ec6214dc20f33b46ad553e3156bb320fa376d0fb1c0a030d50b7fbee3bc4ef69187f01af8dab
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.add_dependency("async", "~> 1.6")
20
- spec.add_dependency("async-io", "~> 1.11")
20
+ spec.add_dependency("async-io", "~> 1.12")
21
21
 
22
22
  spec.add_dependency("http-2", "~> 0.9.0")
23
23
  # spec.add_dependency("openssl")
@@ -44,12 +44,10 @@ module Async
44
44
  response = super
45
45
 
46
46
  if !response.body.empty? and content_encoding = response.headers['content-encoding']
47
- encodings = content_encoding.split(/\s*,\s*/)
48
-
49
47
  body = response.body
50
48
 
51
49
  # We want to unwrap all encodings
52
- encodings.reverse_each do |name|
50
+ content_encoding.reverse_each do |name|
53
51
  if wrapper = @wrappers[name]
54
52
  body = wrapper.call(body)
55
53
  end
@@ -0,0 +1,76 @@
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
+ require_relative 'readable'
22
+
23
+ module Async
24
+ module HTTP
25
+ module Body
26
+ class File < Readable
27
+ def initialize(path, range = nil, block_size: Async::IO::Stream::BLOCK_SIZE)
28
+ @path = path
29
+ @file = File.open(path)
30
+
31
+ @block_size = block_size
32
+
33
+ if range
34
+ @offset = range.min
35
+ @length = @remaining = range.size
36
+ else
37
+ @offset = 0
38
+ @length = @remaining = @file.size
39
+ end
40
+ end
41
+
42
+ attr :length
43
+
44
+ def empty?
45
+ @remaining == 0
46
+ end
47
+
48
+ def read
49
+ if @remaining > 0
50
+ amount = [@remaining, @block_size].min
51
+
52
+ if chunk = @file.read(amount)
53
+ @remaining -= chunk.bytesize
54
+
55
+ return chunk
56
+ else
57
+ @file.close
58
+ end
59
+ end
60
+ end
61
+
62
+ def join
63
+ buffer = @file.read(@remaining)
64
+
65
+ @remaining = 0
66
+
67
+ return buffer
68
+ end
69
+
70
+ def inspect
71
+ "\#<#{self.class} path=#{@path} offset=#{@offset} remaining=#{@remaining}>"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -30,6 +30,9 @@ module Async
30
30
  @remaining = length
31
31
  end
32
32
 
33
+ attr :length
34
+ attr :remaining
35
+
33
36
  def empty?
34
37
  @remaining == 0
35
38
  end
@@ -35,6 +35,10 @@ module Async
35
35
  false
36
36
  end
37
37
 
38
+ def length
39
+ nil
40
+ end
41
+
38
42
  # Read the next available chunk.
39
43
  def read
40
44
  nil
@@ -63,7 +63,7 @@ module Async
63
63
  request.authority ||= @authority
64
64
  attempt = 0
65
65
 
66
- # There is a challenge with how this works. If you have 8 connections in the connection pool and they've all expired, retrying 3 times isn't going to work. We need to, perhaps, on the last retry, initiate a completely new connection.
66
+ # We may retry the request if it is possible to do so. https://tools.ietf.org/html/draft-nottingham-httpbis-retry-01 is a good guide for how retrying requests should work.
67
67
  begin
68
68
  attempt += 1
69
69
 
@@ -21,6 +21,38 @@
21
21
  module Async
22
22
  module HTTP
23
23
  class Headers
24
+ class Split < Array
25
+ COMMA = /\s*,\s*/
26
+
27
+ def initialize(value)
28
+ super(value.split(COMMA))
29
+ end
30
+
31
+ def << value
32
+ super value.split(COMMA)
33
+ end
34
+
35
+ def to_s
36
+ join(", ")
37
+ end
38
+ end
39
+
40
+ class Multiple < Array
41
+ def initialize(value)
42
+ super()
43
+
44
+ self << value
45
+ end
46
+
47
+ def to_s
48
+ join("\n")
49
+ end
50
+ end
51
+
52
+ def self.[] hash
53
+ self.new(hash.to_a)
54
+ end
55
+
24
56
  def initialize(fields = [])
25
57
  @fields = fields
26
58
  @indexed = to_h
@@ -36,6 +68,10 @@ module Async
36
68
  super
37
69
  end
38
70
 
71
+ def empty?
72
+ @fields.empty?
73
+ end
74
+
39
75
  def each(&block)
40
76
  @fields.each(&block)
41
77
  end
@@ -59,17 +95,69 @@ module Async
59
95
  end
60
96
  end
61
97
 
98
+ def slice!(keys)
99
+ values, @fields = @fields.partition do |field|
100
+ keys.include?(field.first.downcase)
101
+ end
102
+
103
+ if @indexed
104
+ keys.each do |key|
105
+ @indexed.delete(key)
106
+ end
107
+ end
108
+ end
109
+
110
+ def add(key, value)
111
+ self[key] = value
112
+ end
113
+
62
114
  def []= key, value
63
115
  @fields << [key, value]
64
116
 
65
117
  if @indexed
66
- key = key.downcase
67
-
68
- if current_value = @indexed[key]
69
- @indexed[key] = Array(current_value) << value
118
+ # It would be good to do some kind of validation here.
119
+ merge(@indexed, key.downcase, value)
120
+ end
121
+ end
122
+
123
+ MERGE_POLICY = {
124
+ # Headers which may only be specified once.
125
+ 'content-type' => false,
126
+ 'content-disposition' => false,
127
+ 'content-length' => false,
128
+ 'user-agent' => false,
129
+ 'referer' => false,
130
+ 'host' => false,
131
+ 'authorization' => false,
132
+ 'proxy-authorization' => false,
133
+ 'if-modified-since' => false,
134
+ 'if-unmodified-since' => false,
135
+ 'from' => false,
136
+ 'location' => false,
137
+ 'max-forwards' => false,
138
+
139
+ 'connection' => Split,
140
+
141
+ # Headers specifically for proxies:
142
+ 'via' => Split,
143
+ 'x-forwarded-for' => Split,
144
+
145
+ # Headers which may be specified multiple times, but which can't be concatenated.
146
+ 'set-cookie' => Multiple,
147
+ 'www-authenticate' => Multiple,
148
+ 'proxy-authenticate' => Multiple
149
+ }.tap{|hash| hash.default = Split}
150
+
151
+ def merge(hash, key, value)
152
+ if policy = MERGE_POLICY[key]
153
+ if current_value = hash[key]
154
+ current_value << value
70
155
  else
71
- @indexed[key] = value
156
+ hash[key] = policy.new(value)
72
157
  end
158
+ else
159
+ # We can't merge these, we only expose the last one set.
160
+ hash[key] = value
73
161
  end
74
162
  end
75
163
 
@@ -81,13 +169,7 @@ module Async
81
169
 
82
170
  def to_h
83
171
  @fields.inject({}) do |hash, (key, value)|
84
- key = key.downcase
85
-
86
- if current_value = hash[key]
87
- hash[key] = Array(current_value) << value
88
- else
89
- hash[key] = value
90
- end
172
+ merge(hash, key.downcase, value)
91
173
 
92
174
  hash
93
175
  end
@@ -100,6 +182,20 @@ module Async
100
182
  @fields == other.fields
101
183
  end
102
184
  end
185
+
186
+ class Merged
187
+ def initialize(*all)
188
+ @all = all
189
+ end
190
+
191
+ def each(&block)
192
+ @all.each do |headers|
193
+ headers.each do |key, value|
194
+ yield key, value.to_s
195
+ end
196
+ end
197
+ end
198
+ end
103
199
  end
104
200
  end
105
201
  end
@@ -34,7 +34,11 @@ module Async
34
34
  end
35
35
 
36
36
  def persistent?(headers)
37
- headers.delete(CONNECTION) == KEEP_ALIVE
37
+ if connection = headers[CONNECTION]
38
+ return connection.include?(KEEP_ALIVE)
39
+ else
40
+ return false
41
+ end
38
42
  end
39
43
 
40
44
  # Server loop.
@@ -38,7 +38,6 @@ module Async
38
38
  CONNECTION = 'connection'.freeze
39
39
  HOST = 'host'.freeze
40
40
  CLOSE = 'close'.freeze
41
-
42
41
  VERSION = "HTTP/1.1".freeze
43
42
 
44
43
  def initialize(stream)
@@ -74,7 +73,11 @@ module Async
74
73
  end
75
74
 
76
75
  def persistent?(headers)
77
- headers.delete(CONNECTION) != CLOSE
76
+ if connection = headers[CONNECTION]
77
+ return !connection.include?(CLOSE)
78
+ else
79
+ return true
80
+ end
78
81
  end
79
82
 
80
83
  # Server loop.
@@ -193,6 +196,12 @@ module Async
193
196
  if body.nil? or body.empty?
194
197
  @stream.write("Content-Length: 0\r\n\r\n")
195
198
  body.read if body
199
+ 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
196
205
  elsif chunked
197
206
  @stream.write("Transfer-Encoding: chunked\r\n\r\n")
198
207
 
@@ -219,11 +228,25 @@ module Async
219
228
  @stream.flush
220
229
  end
221
230
 
231
+ TRANSFER_ENCODING = 'transfer-encoding'.freeze
232
+ CONTENT_LENGTH = 'content-length'.freeze
233
+ CHUNKED = 'chunked'.freeze
234
+
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
240
+ end
241
+ end
242
+
222
243
  def read_body(headers)
223
- if headers.delete('transfer-encoding') == 'chunked'
244
+ if chunked?(headers)
224
245
  return Body::Chunked.new(self)
225
- elsif content_length = headers.delete('content-length')
226
- return Body::Fixed.new(@stream, Integer(content_length))
246
+ elsif content_length = headers[CONTENT_LENGTH]
247
+ if content_length != 0
248
+ return Body::Fixed.new(@stream, Integer(content_length))
249
+ end
227
250
  end
228
251
  end
229
252
  end
@@ -66,11 +66,12 @@ module Async
66
66
  Async.logger.debug(self) {"Received frame: #{frame.inspect}"}
67
67
  end
68
68
 
69
+ @goaway = false
70
+
69
71
  @controller.on(:goaway) do |payload|
70
72
  Async.logger.error(self) {"goaway: #{payload.inspect}"}
71
73
 
72
- @reader.stop
73
- @stream.close
74
+ @goaway = true
74
75
  end
75
76
 
76
77
  @count = 0
@@ -89,7 +90,7 @@ module Async
89
90
  end
90
91
 
91
92
  def reusable?
92
- !@stream.closed?
93
+ !@goaway || !@stream.closed?
93
94
  end
94
95
 
95
96
  def version
@@ -197,19 +198,19 @@ module Async
197
198
  stream = @controller.new_stream
198
199
  @count += 1
199
200
 
200
- headers = {
201
+ headers = Headers::Merged.new({
201
202
  SCHEME => HTTPS,
202
- METHOD => request.method.to_s,
203
- PATH => request.path.to_s,
204
- AUTHORITY => request.authority.to_s,
205
- }.merge(request.headers)
203
+ METHOD => request.method,
204
+ PATH => request.path,
205
+ AUTHORITY => request.authority,
206
+ }, request.headers)
206
207
 
207
208
  finished = Async::Notification.new
208
209
 
209
210
  exception = nil
210
211
  response = Response.new
211
212
  response.version = self.version
212
- response.headers = {}
213
+ response.headers = Headers.new
213
214
  body = Body::Writable.new
214
215
  response.body = body
215
216
 
@@ -254,7 +255,7 @@ module Async
254
255
  request.body.read if request.body
255
256
  else
256
257
  begin
257
- stream.headers(headers, end_stream: false)
258
+ stream.headers(headers)
258
259
  rescue
259
260
  raise RequestFailed.new
260
261
  end
@@ -263,7 +264,7 @@ module Async
263
264
  stream.data(chunk, end_stream: false)
264
265
  end
265
266
 
266
- stream.data("", end_stream: true)
267
+ stream.data("")
267
268
  end
268
269
 
269
270
  start_connection
@@ -94,9 +94,13 @@ module Async
94
94
  end
95
95
  end
96
96
 
97
+ def tcp_options
98
+ {reuse_port: @options[:reuse_port] ? true : false}
99
+ end
100
+
97
101
  def endpoint
98
102
  unless @endpoint
99
- @endpoint = Async::IO::Endpoint.tcp(hostname, port)
103
+ @endpoint = Async::IO::Endpoint.tcp(hostname, port, tcp_options)
100
104
 
101
105
  if secure?
102
106
  # Wrap it in SSL:
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module HTTP
23
- VERSION = "0.23.3"
23
+ VERSION = "0.24.0"
24
24
  end
25
25
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.3
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.11'
33
+ version: '1.12'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.11'
40
+ version: '1.12'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: http-2
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -128,6 +128,7 @@ files:
128
128
  - lib/async/http/body/buffered.rb
129
129
  - lib/async/http/body/chunked.rb
130
130
  - lib/async/http/body/deflate.rb
131
+ - lib/async/http/body/file.rb
131
132
  - lib/async/http/body/fixed.rb
132
133
  - lib/async/http/body/inflate.rb
133
134
  - lib/async/http/body/readable.rb