async-http 0.23.3 → 0.24.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 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