httpx 0.14.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_13_2.md +1 -1
  3. data/doc/release_notes/0_14_1.md +1 -1
  4. data/doc/release_notes/0_14_2.md +6 -0
  5. data/doc/release_notes/0_14_3.md +5 -0
  6. data/doc/release_notes/0_14_4.md +5 -0
  7. data/doc/release_notes/0_14_5.md +11 -0
  8. data/doc/release_notes/0_15_0.md +44 -0
  9. data/lib/httpx.rb +1 -0
  10. data/lib/httpx/connection.rb +14 -3
  11. data/lib/httpx/connection/http1.rb +15 -2
  12. data/lib/httpx/connection/http2.rb +14 -0
  13. data/lib/httpx/domain_name.rb +0 -290
  14. data/lib/httpx/errors.rb +2 -0
  15. data/lib/httpx/extensions.rb +1 -1
  16. data/lib/httpx/options.rb +2 -0
  17. data/lib/httpx/plugins/digest_authentication.rb +19 -21
  18. data/lib/httpx/plugins/grpc.rb +1 -1
  19. data/lib/httpx/plugins/grpc/call.rb +1 -2
  20. data/lib/httpx/plugins/multipart/part.rb +1 -1
  21. data/lib/httpx/plugins/ntlm_authentication.rb +66 -0
  22. data/lib/httpx/plugins/proxy/socks4.rb +4 -0
  23. data/lib/httpx/plugins/proxy/socks5.rb +4 -0
  24. data/lib/httpx/pmatch_extensions.rb +33 -0
  25. data/lib/httpx/punycode.rb +304 -0
  26. data/lib/httpx/request.rb +1 -1
  27. data/lib/httpx/response.rb +2 -0
  28. data/lib/httpx/selector.rb +31 -31
  29. data/lib/httpx/utils.rb +6 -4
  30. data/lib/httpx/version.rb +1 -1
  31. data/sig/chainable.rbs +5 -0
  32. data/sig/connection/http1.rbs +2 -0
  33. data/sig/connection/http2.rbs +2 -0
  34. data/sig/options.rbs +1 -1
  35. data/sig/plugins/aws_sdk_authentication.rbs +2 -0
  36. data/sig/plugins/basic_authentication.rbs +1 -1
  37. data/sig/plugins/digest_authentication.rbs +1 -1
  38. data/sig/plugins/follow_redirects.rbs +1 -1
  39. data/sig/plugins/grpc.rbs +93 -0
  40. data/sig/plugins/multipart.rbs +2 -2
  41. data/sig/plugins/ntlm_authentication.rbs +27 -0
  42. data/sig/plugins/proxy/socks4.rbs +1 -0
  43. data/sig/plugins/proxy/socks5.rbs +1 -0
  44. data/sig/utils.rbs +7 -0
  45. metadata +18 -2
data/lib/httpx/errors.rb CHANGED
@@ -24,6 +24,8 @@ module HTTPX
24
24
 
25
25
  ConnectTimeoutError = Class.new(TimeoutError)
26
26
 
27
+ SettingsTimeoutError = Class.new(TimeoutError)
28
+
27
29
  ResolveTimeoutError = Class.new(TimeoutError)
28
30
 
29
31
  ResolveError = Class.new(Error)
@@ -78,7 +78,7 @@ module HTTPX
78
78
 
79
79
  def authority
80
80
  port_string = port == default_port ? nil : ":#{port}"
81
- "#{@non_ascii_hostname || host}#{port_string}"
81
+ "#{host}#{port_string}"
82
82
  end
83
83
 
84
84
  def origin
data/lib/httpx/options.rb CHANGED
@@ -7,6 +7,7 @@ module HTTPX
7
7
  CONNECT_TIMEOUT = 60
8
8
  OPERATION_TIMEOUT = 60
9
9
  KEEP_ALIVE_TIMEOUT = 20
10
+ SETTINGS_TIMEOUT = 10
10
11
 
11
12
  DEFAULT_OPTIONS = {
12
13
  :debug => ENV.key?("HTTPX_DEBUG") ? $stderr : nil,
@@ -16,6 +17,7 @@ module HTTPX
16
17
  :fallback_protocol => "http/1.1",
17
18
  :timeout => {
18
19
  connect_timeout: CONNECT_TIMEOUT,
20
+ settings_timeout: SETTINGS_TIMEOUT,
19
21
  operation_timeout: OPERATION_TIMEOUT,
20
22
  keep_alive_timeout: KEEP_ALIVE_TIMEOUT,
21
23
  },
@@ -10,6 +10,8 @@ module HTTPX
10
10
  # https://gitlab.com/honeyryderchuck/httpx/wikis/Authentication#authentication
11
11
  #
12
12
  module DigestAuthentication
13
+ using RegexpExtensions unless Regexp.method_defined?(:match?)
14
+
13
15
  DigestError = Class.new(Error)
14
16
 
15
17
  def self.extra_options(options)
@@ -19,7 +21,7 @@ module HTTPX
19
21
 
20
22
  value
21
23
  OUT
22
- end.new(options)
24
+ end.new(options).merge(max_concurrent_requests: 1)
23
25
  end
24
26
 
25
27
  def self.load_dependencies(*)
@@ -34,34 +36,30 @@ module HTTPX
34
36
 
35
37
  alias_method :digest_auth, :digest_authentication
36
38
 
37
- def request(*args, **options)
38
- requests = build_requests(*args, options)
39
- probe_request = requests.first
40
- digest = probe_request.options.digest
41
-
42
- return super unless digest
39
+ def send_requests(*requests, options)
40
+ requests.flat_map do |request|
41
+ digest = request.options.digest
43
42
 
44
- prev_response = wrap { send_requests(*probe_request, options).first }
43
+ if digest
44
+ probe_response = wrap { super(request, options).first }
45
45
 
46
- raise Error, "request doesn't require authentication (status: #{prev_response.status})" unless prev_response.status == 401
46
+ if digest && !probe_response.is_a?(ErrorResponse) &&
47
+ probe_response.status == 401 && probe_response.headers.key?("www-authenticate") &&
48
+ /Digest .*/.match?(probe_response.headers["www-authenticate"])
47
49
 
48
- probe_request.transition(:idle)
50
+ request.transition(:idle)
49
51
 
50
- responses = []
52
+ token = digest.generate_header(request, probe_response)
53
+ request.headers["authorization"] = "Digest #{token}"
51
54
 
52
- while (request = requests.shift)
53
- token = digest.generate_header(request, prev_response)
54
- request.headers["authorization"] = "Digest #{token}"
55
- response = if requests.empty?
56
- send_requests(*request, options).first
55
+ super(request, options)
56
+ else
57
+ probe_response
58
+ end
57
59
  else
58
- wrap { send_requests(*request, options).first }
60
+ super(request, options)
59
61
  end
60
- responses << response
61
- prev_response = response
62
62
  end
63
-
64
- responses.size == 1 ? responses.first : responses
65
63
  end
66
64
  end
67
65
 
@@ -160,7 +160,7 @@ module HTTPX
160
160
  grpc_request = build_grpc_request(rpc_method, input, deadline: deadline, metadata: metadata, **opts)
161
161
  response = request(grpc_request, **opts)
162
162
  response.raise_for_status
163
- GRPC::Call.new(response, opts)
163
+ GRPC::Call.new(response)
164
164
  end
165
165
 
166
166
  private
@@ -7,9 +7,8 @@ module HTTPX
7
7
  class Call
8
8
  attr_writer :decoder
9
9
 
10
- def initialize(response, options)
10
+ def initialize(response)
11
11
  @response = response
12
- @options = options
13
12
  @decoder = ->(z) { z }
14
13
  end
15
14
 
@@ -26,7 +26,7 @@ module HTTPX
26
26
  content_type ||= MimeTypeDetector.call(value, filename) || "application/octet-stream"
27
27
  [value, content_type, filename]
28
28
  else
29
- [StringIO.new(value.to_s), content_type || "text/plain"]
29
+ [StringIO.new(value.to_s), content_type || "text/plain", filename]
30
30
  end
31
31
  end
32
32
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # https://gitlab.com/honeyryderchuck/httpx/wikis/Authentication#ntlm-authentication
7
+ #
8
+ module NTLMAuthentication
9
+ NTLMParams = Struct.new(:user, :domain, :password)
10
+
11
+ class << self
12
+ def load_dependencies(_klass)
13
+ require "base64"
14
+ require "ntlm"
15
+ end
16
+
17
+ def extra_options(options)
18
+ Class.new(options.class) do
19
+ def_option(:ntlm, <<-OUT)
20
+ raise Error, ":ntlm must be a #{NTLMParams}" unless value.is_a?(#{NTLMParams})
21
+
22
+ value
23
+ OUT
24
+ end.new(options).merge(max_concurrent_requests: 1)
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+ def ntlm_authentication(user, password, domain = nil)
30
+ with(ntlm: NTLMParams.new(user, domain, password))
31
+ end
32
+
33
+ alias_method :ntlm_auth, :ntlm_authentication
34
+
35
+ def send_requests(*requests, options)
36
+ requests.flat_map do |request|
37
+ ntlm = request.options.ntlm
38
+
39
+ if ntlm
40
+ request.headers["authorization"] = "NTLM #{NTLM.negotiate(domain: ntlm.domain).to_base64}"
41
+ probe_response = wrap { super(request, options).first }
42
+
43
+ if !probe_response.is_a?(ErrorResponse) && probe_response.status == 401 &&
44
+ probe_response.headers.key?("www-authenticate") &&
45
+ (challenge = probe_response.headers["www-authenticate"][/NTLM (.*)/, 1])
46
+
47
+ challenge = Base64.decode64(challenge)
48
+ ntlm_challenge = NTLM.authenticate(challenge, ntlm.user, ntlm.domain, ntlm.password).to_base64
49
+
50
+ request.transition(:idle)
51
+
52
+ request.headers["authorization"] = "NTLM #{ntlm_challenge}"
53
+ super(request, options)
54
+ else
55
+ probe_response
56
+ end
57
+ else
58
+ super(request, options)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ register_plugin :ntlm_authentication, NTLMAuthentication
65
+ end
66
+ end
@@ -85,6 +85,10 @@ module HTTPX
85
85
  @options = Options.new(options)
86
86
  end
87
87
 
88
+ def timeout
89
+ @options.timeout[:operation_timeout]
90
+ end
91
+
88
92
  def close; end
89
93
 
90
94
  def consume(*); end
@@ -133,6 +133,10 @@ module HTTPX
133
133
  @options = Options.new(options)
134
134
  end
135
135
 
136
+ def timeout
137
+ @options.timeout[:operation_timeout]
138
+ end
139
+
136
140
  def close; end
137
141
 
138
142
  def consume(*); end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module ResponsePatternMatchExtensions
5
+ def deconstruct
6
+ [@status, @headers, @body]
7
+ end
8
+
9
+ def deconstruct_keys(_keys)
10
+ { status: @status, headers: @headers, body: @body }
11
+ end
12
+ end
13
+
14
+ module ErrorResponsePatternMatchExtensions
15
+ def deconstruct
16
+ [@error]
17
+ end
18
+
19
+ def deconstruct_keys(_keys)
20
+ { error: @error }
21
+ end
22
+ end
23
+
24
+ module HeadersPatternMatchExtensions
25
+ def deconstruct
26
+ to_a
27
+ end
28
+ end
29
+
30
+ Headers.include HeadersPatternMatchExtensions
31
+ Response.include ResponsePatternMatchExtensions
32
+ ErrorResponse.include ErrorResponsePatternMatchExtensions
33
+ end
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ begin
5
+ require "idnx"
6
+
7
+ module Punycode
8
+ module_function
9
+
10
+ def encode_hostname(hostname)
11
+ Idnx.to_punycode(hostname)
12
+ end
13
+ end
14
+
15
+ rescue LoadError
16
+ # :nocov:
17
+ # -*- coding: utf-8 -*-
18
+ #--
19
+ # punycode.rb - PunyCode encoder for the Domain Name library
20
+ #
21
+ # Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
22
+ #
23
+ # Ported from puny.c, a part of VeriSign XCode (encode/decode) IDN
24
+ # Library.
25
+ #
26
+ # Copyright (C) 2000-2002 Verisign Inc., All rights reserved.
27
+ #
28
+ # Redistribution and use in source and binary forms, with or
29
+ # without modification, are permitted provided that the following
30
+ # conditions are met:
31
+ #
32
+ # 1) Redistributions of source code must retain the above copyright
33
+ # notice, this list of conditions and the following disclaimer.
34
+ #
35
+ # 2) Redistributions in binary form must reproduce the above copyright
36
+ # notice, this list of conditions and the following disclaimer in
37
+ # the documentation and/or other materials provided with the
38
+ # distribution.
39
+ #
40
+ # 3) Neither the name of the VeriSign Inc. nor the names of its
41
+ # contributors may be used to endorse or promote products derived
42
+ # from this software without specific prior written permission.
43
+ #
44
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
45
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
46
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
47
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
48
+ # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
49
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
50
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
51
+ # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
52
+ # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
53
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
54
+ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
55
+ # POSSIBILITY OF SUCH DAMAGE.
56
+ #
57
+ # This software is licensed under the BSD open source license. For more
58
+ # information visit www.opensource.org.
59
+ #
60
+ # Authors:
61
+ # John Colosi (VeriSign)
62
+ # Srikanth Veeramachaneni (VeriSign)
63
+ # Nagesh Chigurupati (Verisign)
64
+ # Praveen Srinivasan(Verisign)
65
+ #++
66
+ module Punycode
67
+ BASE = 36
68
+ TMIN = 1
69
+ TMAX = 26
70
+ SKEW = 38
71
+ DAMP = 700
72
+ INITIAL_BIAS = 72
73
+ INITIAL_N = 0x80
74
+ DELIMITER = "-"
75
+
76
+ MAXINT = (1 << 32) - 1
77
+
78
+ LOBASE = BASE - TMIN
79
+ CUTOFF = LOBASE * TMAX / 2
80
+
81
+ RE_NONBASIC = /[^\x00-\x7f]/.freeze
82
+
83
+ # Returns the numeric value of a basic code point (for use in
84
+ # representing integers) in the range 0 to base-1, or nil if cp
85
+ # is does not represent a value.
86
+ DECODE_DIGIT = {}.tap do |map|
87
+ # ASCII A..Z map to 0..25
88
+ # ASCII a..z map to 0..25
89
+ (0..25).each { |i| map[65 + i] = map[97 + i] = i }
90
+ # ASCII 0..9 map to 26..35
91
+ (26..35).each { |i| map[22 + i] = i }
92
+ end
93
+
94
+ # Returns the basic code point whose value (when used for
95
+ # representing integers) is d, which must be in the range 0 to
96
+ # BASE-1. The lowercase form is used unless flag is true, in
97
+ # which case the uppercase form is used. The behavior is
98
+ # undefined if flag is nonzero and digit d has no uppercase
99
+ # form.
100
+ ENCODE_DIGIT = proc { |d, flag|
101
+ (d + 22 + (d < 26 ? 75 : 0) - (flag ? (1 << 5) : 0)).chr
102
+ # 0..25 map to ASCII a..z or A..Z
103
+ # 26..35 map to ASCII 0..9
104
+ }
105
+
106
+ DOT = "."
107
+ PREFIX = "xn--"
108
+
109
+ # Most errors we raise are basically kind of ArgumentError.
110
+ class ArgumentError < ::ArgumentError; end
111
+ class BufferOverflowError < ArgumentError; end
112
+
113
+ module_function
114
+
115
+ # Encode a +string+ in Punycode
116
+ def encode(string)
117
+ input = string.unpack("U*")
118
+ output = +""
119
+
120
+ # Initialize the state
121
+ n = INITIAL_N
122
+ delta = 0
123
+ bias = INITIAL_BIAS
124
+
125
+ # Handle the basic code points
126
+ input.each { |cp| output << cp.chr if cp < 0x80 }
127
+
128
+ h = b = output.length
129
+
130
+ # h is the number of code points that have been handled, b is the
131
+ # number of basic code points, and out is the number of characters
132
+ # that have been output.
133
+
134
+ output << DELIMITER if b > 0
135
+
136
+ # Main encoding loop
137
+
138
+ while h < input.length
139
+ # All non-basic code points < n have been handled already. Find
140
+ # the next larger one
141
+
142
+ m = MAXINT
143
+ input.each do |cp|
144
+ m = cp if (n...m) === cp
145
+ end
146
+
147
+ # Increase delta enough to advance the decoder's <n,i> state to
148
+ # <m,0>, but guard against overflow
149
+
150
+ delta += (m - n) * (h + 1)
151
+ raise BufferOverflowError if delta > MAXINT
152
+
153
+ n = m
154
+
155
+ input.each do |cp|
156
+ # AMC-ACE-Z can use this simplified version instead
157
+ if cp < n
158
+ delta += 1
159
+ raise BufferOverflowError if delta > MAXINT
160
+ elsif cp == n
161
+ # Represent delta as a generalized variable-length integer
162
+ q = delta
163
+ k = BASE
164
+ loop do
165
+ t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
166
+ break if q < t
167
+
168
+ q, r = (q - t).divmod(BASE - t)
169
+ output << ENCODE_DIGIT[t + r, false]
170
+ k += BASE
171
+ end
172
+
173
+ output << ENCODE_DIGIT[q, false]
174
+
175
+ # Adapt the bias
176
+ delta = h == b ? delta / DAMP : delta >> 1
177
+ delta += delta / (h + 1)
178
+ bias = 0
179
+ while delta > CUTOFF
180
+ delta /= LOBASE
181
+ bias += BASE
182
+ end
183
+ bias += (LOBASE + 1) * delta / (delta + SKEW)
184
+
185
+ delta = 0
186
+ h += 1
187
+ end
188
+ end
189
+
190
+ delta += 1
191
+ n += 1
192
+ end
193
+
194
+ output
195
+ end
196
+
197
+ # Encode a hostname using IDN/Punycode algorithms
198
+ def encode_hostname(hostname)
199
+ hostname.match(RE_NONBASIC) || (return hostname)
200
+
201
+ hostname.split(DOT).map do |name|
202
+ if name.match(RE_NONBASIC)
203
+ PREFIX + encode(name)
204
+ else
205
+ name
206
+ end
207
+ end.join(DOT)
208
+ end
209
+
210
+ # Decode a +string+ encoded in Punycode
211
+ def decode(string)
212
+ # Initialize the state
213
+ n = INITIAL_N
214
+ i = 0
215
+ bias = INITIAL_BIAS
216
+
217
+ if j = string.rindex(DELIMITER)
218
+ b = string[0...j]
219
+
220
+ b.match(RE_NONBASIC) &&
221
+ raise(ArgumentError, "Illegal character is found in basic part: #{string.inspect}")
222
+
223
+ # Handle the basic code points
224
+
225
+ output = b.unpack("U*")
226
+ u = string[(j + 1)..-1]
227
+ else
228
+ output = []
229
+ u = string
230
+ end
231
+
232
+ # Main decoding loop: Start just after the last delimiter if any
233
+ # basic code points were copied; start at the beginning
234
+ # otherwise.
235
+
236
+ input = u.unpack("C*")
237
+ input_length = input.length
238
+ h = 0
239
+ out = output.length
240
+
241
+ while h < input_length
242
+ # Decode a generalized variable-length integer into delta,
243
+ # which gets added to i. The overflow checking is easier
244
+ # if we increase i as we go, then subtract off its starting
245
+ # value at the end to obtain delta.
246
+
247
+ oldi = i
248
+ w = 1
249
+ k = BASE
250
+
251
+ loop do
252
+ (digit = DECODE_DIGIT[input[h]]) ||
253
+ raise(ArgumentError, "Illegal character is found in non-basic part: #{string.inspect}")
254
+ h += 1
255
+ i += digit * w
256
+ raise BufferOverflowError if i > MAXINT
257
+
258
+ t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
259
+ break if digit < t
260
+
261
+ w *= BASE - t
262
+ raise BufferOverflowError if w > MAXINT
263
+
264
+ k += BASE
265
+ (h < input_length) || raise(ArgumentError, "Malformed input given: #{string.inspect}")
266
+ end
267
+
268
+ # Adapt the bias
269
+ delta = oldi == 0 ? i / DAMP : (i - oldi) >> 1
270
+ delta += delta / (out + 1)
271
+ bias = 0
272
+ while delta > CUTOFF
273
+ delta /= LOBASE
274
+ bias += BASE
275
+ end
276
+ bias += (LOBASE + 1) * delta / (delta + SKEW)
277
+
278
+ # i was supposed to wrap around from out+1 to 0, incrementing
279
+ # n each time, so we'll fix that now:
280
+
281
+ q, i = i.divmod(out + 1)
282
+ n += q
283
+ raise BufferOverflowError if n > MAXINT
284
+
285
+ # Insert n at position i of the output:
286
+
287
+ output[i, 0] = n
288
+
289
+ out += 1
290
+ i += 1
291
+ end
292
+ output.pack("U*")
293
+ end
294
+
295
+ # Decode a hostname using IDN/Punycode algorithms
296
+ def decode_hostname(hostname)
297
+ hostname.gsub(/(\A|#{Regexp.quote(DOT)})#{Regexp.quote(PREFIX)}([^#{Regexp.quote(DOT)}]*)/o) do
298
+ Regexp.last_match(1) << decode(Regexp.last_match(2))
299
+ end
300
+ end
301
+ end
302
+ # :nocov:
303
+ end
304
+ end