httpx 0.11.3 → 0.14.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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/doc/release_notes/0_10_1.md +1 -1
  4. data/doc/release_notes/0_11_1.md +5 -1
  5. data/doc/release_notes/0_12_0.md +55 -0
  6. data/doc/release_notes/0_13_0.md +58 -0
  7. data/doc/release_notes/0_13_1.md +5 -0
  8. data/doc/release_notes/0_13_2.md +9 -0
  9. data/doc/release_notes/0_14_0.md +79 -0
  10. data/lib/httpx.rb +3 -3
  11. data/lib/httpx/adapters/faraday.rb +4 -6
  12. data/lib/httpx/altsvc.rb +1 -0
  13. data/lib/httpx/callbacks.rb +12 -3
  14. data/lib/httpx/chainable.rb +2 -2
  15. data/lib/httpx/connection.rb +92 -37
  16. data/lib/httpx/connection/http1.rb +37 -19
  17. data/lib/httpx/connection/http2.rb +82 -31
  18. data/lib/httpx/headers.rb +1 -1
  19. data/lib/httpx/io.rb +16 -3
  20. data/lib/httpx/io/ssl.rb +35 -24
  21. data/lib/httpx/io/tcp.rb +50 -28
  22. data/lib/httpx/io/tls.rb +218 -0
  23. data/lib/httpx/io/tls/box.rb +365 -0
  24. data/lib/httpx/io/tls/context.rb +199 -0
  25. data/lib/httpx/io/tls/ffi.rb +390 -0
  26. data/lib/httpx/io/udp.rb +31 -7
  27. data/lib/httpx/io/unix.rb +27 -12
  28. data/lib/httpx/options.rb +97 -74
  29. data/lib/httpx/parser/http1.rb +4 -4
  30. data/lib/httpx/plugins/aws_sdk_authentication.rb +84 -0
  31. data/lib/httpx/plugins/aws_sigv4.rb +219 -0
  32. data/lib/httpx/plugins/basic_authentication.rb +8 -3
  33. data/lib/httpx/plugins/compression.rb +24 -12
  34. data/lib/httpx/plugins/compression/brotli.rb +10 -7
  35. data/lib/httpx/plugins/compression/deflate.rb +8 -10
  36. data/lib/httpx/plugins/compression/gzip.rb +4 -3
  37. data/lib/httpx/plugins/cookies.rb +3 -7
  38. data/lib/httpx/plugins/digest_authentication.rb +5 -5
  39. data/lib/httpx/plugins/expect.rb +6 -6
  40. data/lib/httpx/plugins/follow_redirects.rb +4 -4
  41. data/lib/httpx/plugins/grpc.rb +247 -0
  42. data/lib/httpx/plugins/grpc/call.rb +62 -0
  43. data/lib/httpx/plugins/grpc/message.rb +85 -0
  44. data/lib/httpx/plugins/h2c.rb +43 -58
  45. data/lib/httpx/plugins/internal_telemetry.rb +93 -0
  46. data/lib/httpx/plugins/multipart.rb +2 -0
  47. data/lib/httpx/plugins/multipart/encoder.rb +4 -9
  48. data/lib/httpx/plugins/multipart/part.rb +1 -1
  49. data/lib/httpx/plugins/proxy.rb +4 -8
  50. data/lib/httpx/plugins/proxy/http.rb +1 -1
  51. data/lib/httpx/plugins/proxy/socks4.rb +8 -0
  52. data/lib/httpx/plugins/proxy/socks5.rb +8 -0
  53. data/lib/httpx/plugins/proxy/ssh.rb +3 -3
  54. data/lib/httpx/plugins/push_promise.rb +3 -2
  55. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  56. data/lib/httpx/plugins/retries.rb +15 -16
  57. data/lib/httpx/plugins/stream.rb +99 -77
  58. data/lib/httpx/plugins/upgrade.rb +84 -0
  59. data/lib/httpx/plugins/upgrade/h2.rb +54 -0
  60. data/lib/httpx/pool.rb +14 -6
  61. data/lib/httpx/registry.rb +1 -7
  62. data/lib/httpx/request.rb +36 -3
  63. data/lib/httpx/resolver/https.rb +3 -11
  64. data/lib/httpx/resolver/native.rb +7 -3
  65. data/lib/httpx/response.rb +18 -7
  66. data/lib/httpx/selector.rb +5 -0
  67. data/lib/httpx/session.rb +41 -8
  68. data/lib/httpx/transcoder/body.rb +3 -5
  69. data/lib/httpx/transcoder/chunker.rb +1 -1
  70. data/lib/httpx/version.rb +1 -1
  71. data/sig/callbacks.rbs +2 -0
  72. data/sig/chainable.rbs +2 -1
  73. data/sig/connection/http1.rbs +7 -2
  74. data/sig/connection/http2.rbs +10 -4
  75. data/sig/options.rbs +16 -22
  76. data/sig/plugins/aws_sdk_authentication.rbs +19 -0
  77. data/sig/plugins/aws_sigv4.rbs +64 -0
  78. data/sig/plugins/basic_authentication.rbs +2 -0
  79. data/sig/plugins/compression.rbs +7 -5
  80. data/sig/plugins/compression/brotli.rbs +1 -1
  81. data/sig/plugins/compression/deflate.rbs +1 -1
  82. data/sig/plugins/compression/gzip.rbs +1 -1
  83. data/sig/plugins/cookies.rbs +0 -1
  84. data/sig/plugins/digest_authentication.rbs +0 -1
  85. data/sig/plugins/expect.rbs +0 -2
  86. data/sig/plugins/follow_redirects.rbs +0 -2
  87. data/sig/plugins/h2c.rbs +5 -10
  88. data/sig/plugins/persistent.rbs +0 -1
  89. data/sig/plugins/proxy.rbs +0 -1
  90. data/sig/plugins/push_promise.rbs +1 -1
  91. data/sig/plugins/retries.rbs +0 -4
  92. data/sig/plugins/stream.rbs +17 -16
  93. data/sig/plugins/upgrade.rbs +23 -0
  94. data/sig/request.rbs +7 -2
  95. data/sig/response.rbs +4 -1
  96. data/sig/session.rbs +4 -0
  97. metadata +56 -33
  98. data/lib/httpx/timeout.rb +0 -67
  99. data/sig/timeout.rbs +0 -29
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "aws-sdk-s3"
5
+
6
+ module HTTPX
7
+ module Plugins
8
+ #
9
+ # This plugin adds AWS Sigv4 authentication.
10
+ #
11
+ # https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
12
+ #
13
+ # https://gitlab.com/honeyryderchuck/httpx/wikis/AWS-SigV4
14
+ #
15
+ module AWSSigV4
16
+ Credentials = Struct.new(:username, :password, :security_token)
17
+
18
+ class Signer
19
+ def initialize(
20
+ service:,
21
+ region:,
22
+ credentials: nil,
23
+ username: nil,
24
+ password: nil,
25
+ security_token: nil,
26
+ provider_prefix: "aws",
27
+ header_provider_field: "amz",
28
+ unsigned_headers: [],
29
+ apply_checksum_header: true,
30
+ algorithm: "SHA256"
31
+ )
32
+ @credentials = credentials || Credentials.new(username, password, security_token)
33
+ @service = service
34
+ @region = region
35
+
36
+ @unsigned_headers = Set.new(unsigned_headers.map(&:downcase))
37
+ @unsigned_headers << "authorization"
38
+ @unsigned_headers << "x-amzn-trace-id"
39
+ @unsigned_headers << "expect"
40
+
41
+ @apply_checksum_header = apply_checksum_header
42
+ @provider_prefix = provider_prefix
43
+ @header_provider_field = header_provider_field
44
+
45
+ @algorithm = algorithm
46
+ end
47
+
48
+ def sign!(request)
49
+ lower_provider_prefix = "#{@provider_prefix}4"
50
+ upper_provider_prefix = lower_provider_prefix.upcase
51
+
52
+ downcased_algorithm = @algorithm.downcase
53
+
54
+ datetime = (request.headers["x-#{@header_provider_field}-date"] ||= Time.now.utc.strftime("%Y%m%dT%H%M%SZ"))
55
+ date = datetime[0, 8]
56
+
57
+ content_hashed = request.headers["x-#{@header_provider_field}-content-#{downcased_algorithm}"] || hexdigest(request.body)
58
+
59
+ request.headers["x-#{@header_provider_field}-content-#{downcased_algorithm}"] ||= content_hashed if @apply_checksum_header
60
+ request.headers["x-#{@header_provider_field}-security-token"] ||= @credentials.security_token if @credentials.security_token
61
+
62
+ signature_headers = request.headers.each.reject do |k, _|
63
+ @unsigned_headers.include?(k)
64
+ end
65
+ # aws sigv4 needs to declare the host, regardless of protocol version
66
+ signature_headers << ["host", request.authority] unless request.headers.key?("host")
67
+ signature_headers.sort_by!(&:first)
68
+
69
+ signed_headers = signature_headers.map(&:first).join(";")
70
+
71
+ canonical_headers = signature_headers.map do |k, v|
72
+ # eliminate whitespace between value fields, unless it's a quoted value
73
+ "#{k}:#{v.start_with?("\"") && v.end_with?("\"") ? v : v.gsub(/\s+/, " ").strip}\n"
74
+ end.join
75
+
76
+ # canonical request
77
+ creq = "#{request.verb.to_s.upcase}" \
78
+ "\n#{request.canonical_path}" \
79
+ "\n#{request.canonical_query}" \
80
+ "\n#{canonical_headers}" \
81
+ "\n#{signed_headers}" \
82
+ "\n#{content_hashed}"
83
+
84
+ credential_scope = "#{date}" \
85
+ "/#{@region}" \
86
+ "/#{@service}" \
87
+ "/#{lower_provider_prefix}_request"
88
+
89
+ algo_line = "#{upper_provider_prefix}-HMAC-#{@algorithm}"
90
+ # string to sign
91
+ sts = "#{algo_line}" \
92
+ "\n#{datetime}" \
93
+ "\n#{credential_scope}" \
94
+ "\n#{hexdigest(creq)}"
95
+
96
+ # signature
97
+ k_date = hmac("#{upper_provider_prefix}#{@credentials.password}", date)
98
+ k_region = hmac(k_date, @region)
99
+ k_service = hmac(k_region, @service)
100
+ k_credentials = hmac(k_service, "#{lower_provider_prefix}_request")
101
+ sig = hexhmac(k_credentials, sts)
102
+
103
+ credential = "#{@credentials.username}/#{credential_scope}"
104
+ # apply signature
105
+ request.headers["authorization"] =
106
+ "#{algo_line} " \
107
+ "Credential=#{credential}, " \
108
+ "SignedHeaders=#{signed_headers}, " \
109
+ "Signature=#{sig}"
110
+ end
111
+
112
+ private
113
+
114
+ def hexdigest(value)
115
+ if value.respond_to?(:to_path)
116
+ # files, pathnames
117
+ OpenSSL::Digest.new(@algorithm).file(value.to_path).hexdigest
118
+ elsif value.respond_to?(:each)
119
+ digest = OpenSSL::Digest.new(@algorithm)
120
+
121
+ mb_buffer = value.each.each_with_object("".b) do |chunk, buffer|
122
+ buffer << chunk
123
+ break if buffer.bytesize >= 1024 * 1024
124
+ end
125
+
126
+ digest.update(mb_buffer)
127
+ value.rewind
128
+ digest.hexdigest
129
+ else
130
+ OpenSSL::Digest.new(@algorithm).hexdigest(value)
131
+ end
132
+ end
133
+
134
+ def hmac(key, value)
135
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new(@algorithm), key, value)
136
+ end
137
+
138
+ def hexhmac(key, value)
139
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new(@algorithm), key, value)
140
+ end
141
+ end
142
+
143
+ class << self
144
+ def extra_options(options)
145
+ Class.new(options.class) do
146
+ def_option(:sigv4_signer, <<-OUT)
147
+ value.is_a?(#{Signer}) ? value : #{Signer}.new(value)
148
+ OUT
149
+ end.new(options)
150
+ end
151
+
152
+ def load_dependencies(klass)
153
+ require "digest/sha2"
154
+ require "openssl"
155
+ klass.plugin(:expect)
156
+ klass.plugin(:compression)
157
+ end
158
+ end
159
+
160
+ module InstanceMethods
161
+ def aws_sigv4_authentication(**options)
162
+ with(sigv4_signer: Signer.new(**options))
163
+ end
164
+
165
+ def build_request(*, _)
166
+ request = super
167
+
168
+ return request if request.headers.key?("authorization")
169
+
170
+ signer = request.options.sigv4_signer
171
+
172
+ return request unless signer
173
+
174
+ signer.sign!(request)
175
+
176
+ request
177
+ end
178
+ end
179
+
180
+ module RequestMethods
181
+ def canonical_path
182
+ path = uri.path.dup
183
+ path << "/" if path.empty?
184
+ path.gsub(%r{[^/]+}) { |part| CGI.escape(part.encode("UTF-8")).gsub("+", "%20").gsub("%7E", "~") }
185
+ end
186
+
187
+ def canonical_query
188
+ params = query.split("&")
189
+ # params = params.map { |p| p.match(/=/) ? p : p + '=' }
190
+ # From: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
191
+ # Sort the parameter names by character code point in ascending order.
192
+ # Parameters with duplicate names should be sorted by value.
193
+ #
194
+ # Default sort <=> in JRuby will swap members
195
+ # occasionally when <=> is 0 (considered still sorted), but this
196
+ # causes our normalized query string to not match the sent querystring.
197
+ # When names match, we then sort by their values. When values also
198
+ # match then we sort by their original order
199
+ params.each.with_index.sort do |a, b|
200
+ a, a_offset = a
201
+ b, b_offset = b
202
+ a_name, a_value = a.split("=")
203
+ b_name, b_value = b.split("=")
204
+ if a_name == b_name
205
+ if a_value == b_value
206
+ a_offset <=> b_offset
207
+ else
208
+ a_value <=> b_value
209
+ end
210
+ else
211
+ a_name <=> b_name
212
+ end
213
+ end.map(&:first).join("&")
214
+ end
215
+ end
216
+ end
217
+ register_plugin :aws_sigv4, AWSSigV4
218
+ end
219
+ end
@@ -8,9 +8,14 @@ module HTTPX
8
8
  # https://gitlab.com/honeyryderchuck/httpx/wikis/Authentication#basic-authentication
9
9
  #
10
10
  module BasicAuthentication
11
- def self.load_dependencies(klass)
12
- require "base64"
13
- klass.plugin(:authentication)
11
+ class << self
12
+ def load_dependencies(_klass)
13
+ require "base64"
14
+ end
15
+
16
+ def configure(klass)
17
+ klass.plugin(:authentication)
18
+ end
14
19
  end
15
20
 
16
21
  module InstanceMethods
@@ -13,23 +13,31 @@ module HTTPX
13
13
  # https://gitlab.com/honeyryderchuck/httpx/wikis/Compression
14
14
  #
15
15
  module Compression
16
- extend Registry
17
-
18
16
  class << self
19
- def load_dependencies(klass)
17
+ def configure(klass)
20
18
  klass.plugin(:"compression/gzip")
21
19
  klass.plugin(:"compression/deflate")
22
20
  end
23
21
 
24
22
  def extra_options(options)
23
+ encodings = Module.new do
24
+ extend Registry
25
+ end
26
+
25
27
  Class.new(options.class) do
26
- def_option(:compression_threshold_size) do |bytes|
27
- bytes = Integer(bytes)
28
+ def_option(:compression_threshold_size, <<-OUT)
29
+ bytes = Integer(value)
28
30
  raise Error, ":expect_threshold_size must be positive" unless bytes.positive?
29
31
 
30
32
  bytes
31
- end
32
- end.new(options).merge(headers: { "accept-encoding" => Compression.registry.keys })
33
+ OUT
34
+
35
+ def_option(:encodings, <<-OUT)
36
+ raise Error, ":encodings must be a registry" unless value.respond_to?(:registry)
37
+
38
+ value
39
+ OUT
40
+ end.new(options).merge(encodings: encodings)
33
41
  end
34
42
  end
35
43
 
@@ -37,7 +45,11 @@ module HTTPX
37
45
  def initialize(*)
38
46
  super
39
47
  # forego compression in the Range cases
40
- @headers.delete("accept-encoding") if @headers.key?("range")
48
+ if @headers.key?("range")
49
+ @headers.delete("accept-encoding")
50
+ else
51
+ @headers["accept-encoding"] ||= @options.encodings.registry.keys
52
+ end
41
53
  end
42
54
  end
43
55
 
@@ -52,16 +64,16 @@ module HTTPX
52
64
  @headers.get("content-encoding").each do |encoding|
53
65
  next if encoding == "identity"
54
66
 
55
- @body = Encoder.new(@body, Compression.registry(encoding).deflater)
67
+ @body = Encoder.new(@body, options.encodings.registry(encoding).deflater)
56
68
  end
57
- @headers["content-length"] = @body.bytesize unless chunked?
69
+ @headers["content-length"] = @body.bytesize unless unbounded_body?
58
70
  end
59
71
  end
60
72
 
61
73
  module ResponseBodyMethods
62
74
  attr_reader :encodings
63
75
 
64
- def initialize(*, **)
76
+ def initialize(*)
65
77
  @encodings = []
66
78
 
67
79
  super
@@ -80,7 +92,7 @@ module HTTPX
80
92
  @_inflaters = @headers.get("content-encoding").map do |encoding|
81
93
  next if encoding == "identity"
82
94
 
83
- inflater = Compression.registry(encoding).inflater(compressed_length)
95
+ inflater = @options.encodings.registry(encoding).inflater(compressed_length)
84
96
  # do not uncompress if there is no decoder available. In fact, we can't reliably
85
97
  # continue decompressing beyond that, so ignore.
86
98
  break unless inflater
@@ -4,24 +4,27 @@ module HTTPX
4
4
  module Plugins
5
5
  module Compression
6
6
  module Brotli
7
- def self.load_dependencies(klass)
8
- klass.plugin(:compression)
9
- require "brotli"
10
- end
7
+ class << self
8
+ def load_dependencies(_klass)
9
+ require "brotli"
10
+ end
11
11
 
12
- def self.configure(*)
13
- Compression.register "br", self
12
+ def configure(klass)
13
+ klass.plugin(:compression)
14
+ klass.default_options.encodings.register "br", self
15
+ end
14
16
  end
15
17
 
16
18
  module Deflater
17
19
  module_function
18
20
 
19
- def deflate(raw, buffer, chunk_size:)
21
+ def deflate(raw, buffer = "".b, chunk_size: 16_384)
20
22
  while (chunk = raw.read(chunk_size))
21
23
  compressed = ::Brotli.deflate(chunk)
22
24
  buffer << compressed
23
25
  yield compressed if block_given?
24
26
  end
27
+ buffer
25
28
  end
26
29
  end
27
30
 
@@ -4,24 +4,21 @@ module HTTPX
4
4
  module Plugins
5
5
  module Compression
6
6
  module Deflate
7
- def self.load_dependencies(klass)
7
+ def self.load_dependencies(_klass)
8
8
  require "stringio"
9
9
  require "zlib"
10
- klass.plugin(:"compression/gzip")
11
10
  end
12
11
 
13
- def self.configure(*)
14
- Compression.register "deflate", self
12
+ def self.configure(klass)
13
+ klass.plugin(:"compression/gzip")
14
+ klass.default_options.encodings.register "deflate", self
15
15
  end
16
16
 
17
17
  module Deflater
18
18
  module_function
19
19
 
20
- def deflate(raw, buffer, chunk_size:)
21
- deflater = Zlib::Deflate.new(Zlib::BEST_COMPRESSION,
22
- Zlib::MAX_WBITS,
23
- Zlib::MAX_MEM_LEVEL,
24
- Zlib::HUFFMAN_ONLY)
20
+ def deflate(raw, buffer = "".b, chunk_size: 16_384)
21
+ deflater = Zlib::Deflate.new
25
22
  while (chunk = raw.read(chunk_size))
26
23
  compressed = deflater.deflate(chunk)
27
24
  buffer << compressed
@@ -30,8 +27,9 @@ module HTTPX
30
27
  last = deflater.finish
31
28
  buffer << last
32
29
  yield last if block_given?
30
+ buffer
33
31
  ensure
34
- deflater.close
32
+ deflater.close if deflater
35
33
  end
36
34
  end
37
35
 
@@ -10,8 +10,8 @@ module HTTPX
10
10
  require "zlib"
11
11
  end
12
12
 
13
- def self.configure(*)
14
- Compression.register "gzip", self
13
+ def self.configure(klass)
14
+ klass.default_options.encodings.register "gzip", self
15
15
  end
16
16
 
17
17
  class Deflater
@@ -19,7 +19,7 @@ module HTTPX
19
19
  @compressed_chunk = "".b
20
20
  end
21
21
 
22
- def deflate(raw, buffer, chunk_size:)
22
+ def deflate(raw, buffer = "".b, chunk_size: 16_384)
23
23
  gzip = Zlib::GzipWriter.new(self)
24
24
 
25
25
  begin
@@ -38,6 +38,7 @@ module HTTPX
38
38
 
39
39
  buffer << compressed
40
40
  yield compressed if block_given?
41
+ buffer
41
42
  end
42
43
 
43
44
  private
@@ -20,13 +20,9 @@ module HTTPX
20
20
 
21
21
  def self.extra_options(options)
22
22
  Class.new(options.class) do
23
- def_option(:cookies) do |cookies|
24
- if cookies.is_a?(Jar)
25
- cookies
26
- else
27
- Jar.new(cookies)
28
- end
29
- end
23
+ def_option(:cookies, <<-OUT)
24
+ value.is_a?(#{Jar}) ? value : #{Jar}.new(value)
25
+ OUT
30
26
  end.new(options)
31
27
  end
32
28
 
@@ -14,11 +14,11 @@ module HTTPX
14
14
 
15
15
  def self.extra_options(options)
16
16
  Class.new(options.class) do
17
- def_option(:digest) do |digest|
18
- raise Error, ":digest must be a Digest" unless digest.is_a?(Digest)
17
+ def_option(:digest, <<-OUT)
18
+ raise Error, ":digest must be a Digest" unless value.is_a?(#{Digest})
19
19
 
20
- digest
21
- end
20
+ value
21
+ OUT
22
22
  end.new(options)
23
23
  end
24
24
 
@@ -29,7 +29,7 @@ module HTTPX
29
29
 
30
30
  module InstanceMethods
31
31
  def digest_authentication(user, password)
32
- branch(default_options.with_digest(Digest.new(user, password)))
32
+ with(digest: Digest.new(user, password))
33
33
  end
34
34
 
35
35
  alias_method :digest_auth, :digest_authentication