httpx 0.11.3 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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