httpx 0.14.1 → 0.15.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.
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