httpx 1.6.3 → 1.7.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 +4 -4
- data/doc/release_notes/0_11_0.md +3 -3
- data/doc/release_notes/1_6_3.md +2 -2
- data/doc/release_notes/1_7_0.md +149 -0
- data/lib/httpx/adapters/datadog.rb +1 -1
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/altsvc.rb +3 -1
- data/lib/httpx/connection/http1.rb +5 -6
- data/lib/httpx/connection/http2.rb +2 -0
- data/lib/httpx/connection.rb +3 -8
- data/lib/httpx/domain_name.rb +1 -1
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/loggable.rb +2 -0
- data/lib/httpx/options.rb +40 -17
- data/lib/httpx/plugins/auth/digest.rb +44 -4
- data/lib/httpx/plugins/auth.rb +87 -4
- data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
- data/lib/httpx/plugins/cookies/cookie.rb +1 -0
- data/lib/httpx/plugins/digest_auth.rb +4 -5
- data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
- data/lib/httpx/plugins/grpc.rb +2 -2
- data/lib/httpx/plugins/internal_telemetry.rb +1 -1
- data/lib/httpx/plugins/ntlm_auth.rb +5 -3
- data/lib/httpx/plugins/oauth.rb +162 -56
- data/lib/httpx/plugins/rate_limiter.rb +2 -2
- data/lib/httpx/plugins/response_cache.rb +3 -7
- data/lib/httpx/plugins/retries.rb +55 -16
- data/lib/httpx/plugins/ssrf_filter.rb +1 -1
- data/lib/httpx/plugins/stream.rb +59 -8
- data/lib/httpx/plugins/stream_bidi.rb +73 -17
- data/lib/httpx/pool.rb +12 -2
- data/lib/httpx/request.rb +10 -1
- data/lib/httpx/resolver/https.rb +67 -17
- data/lib/httpx/resolver/multi.rb +4 -0
- data/lib/httpx/resolver/native.rb +26 -4
- data/lib/httpx/resolver/resolver.rb +2 -2
- data/lib/httpx/resolver.rb +97 -29
- data/lib/httpx/response/body.rb +2 -0
- data/lib/httpx/response.rb +22 -6
- data/lib/httpx/selector.rb +9 -0
- data/lib/httpx/session.rb +6 -6
- data/lib/httpx/transcoder/body.rb +1 -1
- data/lib/httpx/transcoder/json.rb +1 -1
- data/lib/httpx/transcoder/multipart/decoder.rb +4 -4
- data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
- data/lib/httpx/transcoder/multipart.rb +16 -8
- data/lib/httpx/transcoder.rb +4 -6
- data/lib/httpx/version.rb +1 -1
- data/sig/altsvc.rbs +3 -0
- data/sig/chainable.rbs +3 -3
- data/sig/connection.rbs +1 -3
- data/sig/options.rbs +1 -1
- data/sig/plugins/auth/digest.rbs +6 -0
- data/sig/plugins/auth.rbs +28 -4
- data/sig/plugins/basic_auth.rbs +3 -3
- data/sig/plugins/digest_auth.rbs +2 -4
- data/sig/plugins/fiber_concurrency.rbs +6 -0
- data/sig/plugins/ntlm_auth.rbs +2 -2
- data/sig/plugins/oauth.rbs +46 -15
- data/sig/plugins/rate_limiter.rbs +1 -1
- data/sig/plugins/response_cache/file_store.rbs +2 -0
- data/sig/plugins/response_cache.rbs +4 -0
- data/sig/plugins/retries.rbs +8 -2
- data/sig/plugins/stream.rbs +13 -3
- data/sig/plugins/stream_bidi.rbs +2 -2
- data/sig/pool.rbs +1 -1
- data/sig/resolver/https.rbs +5 -0
- data/sig/resolver/multi.rbs +2 -0
- data/sig/resolver/native.rbs +2 -0
- data/sig/resolver.rbs +12 -3
- data/sig/response.rbs +3 -0
- data/sig/session.rbs +3 -5
- data/sig/transcoder/multipart.rbs +4 -2
- data/sig/transcoder.rbs +5 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2af63a63fe08211db58764570618b2e7c234d3473a28fc1ad6f5a31c3d0fb13d
|
|
4
|
+
data.tar.gz: 6b2671b85ac69e4817b8b764dfdabf638dacf94cfdcab6c44627ad64a57bcb50
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50a2d2d3c0cb27f3bf84cc34553b06bd9fadf46ad789f9cfa08091cc458ac07751cb5593e01d242d88e444f10ac2b3014ea3e4e56f7c616ebc860517d08ef90d
|
|
7
|
+
data.tar.gz: 6b15f21262e85639c6d32b9851926d7e717b6f9bcbc226b9af3872d67461799fd24847a2d563dfbad1967d5349d371c18a1a6e1a28c721c5f33387f2fc1446a7
|
data/doc/release_notes/0_11_0.md
CHANGED
|
@@ -21,7 +21,7 @@ stub_http_request(:get, "https://www.google.com").and_return(status: 200, body:
|
|
|
21
21
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Read more about it in the [webmock integration documentation](https://
|
|
24
|
+
Read more about it in the [webmock integration documentation](https://honeyryderchuck.gitlab.io/httpx/wiki/Webmock-Adapter).
|
|
25
25
|
|
|
26
26
|
### Datadog Adapter
|
|
27
27
|
|
|
@@ -40,7 +40,7 @@ A trace will be emitted for every request, so this should be an interesting visu
|
|
|
40
40
|
|
|
41
41
|
Customization options and traces are similar to what [the net-http adapter provides](https://docs.datadoghq.com/tracing/setup_overview/setup/ruby/#nethttp).
|
|
42
42
|
|
|
43
|
-
Read more about it in the [datadog integration documentation](https://
|
|
43
|
+
Read more about it in the [datadog integration documentation](https://honeyryderchuck.gitlab.io/httpx/wiki/Datadog-Adapter).
|
|
44
44
|
|
|
45
45
|
## Improvements
|
|
46
46
|
|
|
@@ -52,7 +52,7 @@ Read more about it in the [datadog integration documentation](https://os85.gitla
|
|
|
52
52
|
HTTPX.plugin(:multipart).post(uri, form: {file: File.new("path/to/file")})
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Read more about it in the [multipart plugin documentation](https://
|
|
55
|
+
Read more about it in the [multipart plugin documentation](https://honeyryderchuck.gitlab.io/httpx/wiki/Multipart-Uploads), including also about why this was made.
|
|
56
56
|
|
|
57
57
|
### Expect Plugin
|
|
58
58
|
|
data/doc/release_notes/1_6_3.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
## Improvements
|
|
8
8
|
|
|
9
9
|
* `system` resolver now works in a non-blocking manner, initiating the dns query in a separate thread and waiting on the pipe after that (it was blocking the main thread during resolution before).
|
|
10
|
-
* reduce allocation to a
|
|
11
|
-
*
|
|
10
|
+
* reduce allocation to a single shared option object when headers are passed as a session-level option, like `HTTPX.with(headers: headers).get(...)`
|
|
11
|
+
* favour using `String#replace` in buffer operations (instead of "clean-then-append").
|
|
12
12
|
* using `Array#unshift` instead of `Array#concat` in order to ensure that request ordering is respected in the face of an in-between error which requires reconnect-and-resend.
|
|
13
13
|
* replaced more internal callback indirection with plain method calls.
|
|
14
14
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# 1.7.0
|
|
2
|
+
|
|
3
|
+
## Features
|
|
4
|
+
|
|
5
|
+
### All AUTH plugin improvements!!
|
|
6
|
+
|
|
7
|
+
#### `:auth`
|
|
8
|
+
|
|
9
|
+
The `:auth` plugin can now be used with a dynamic callable object (methods, procs...) to generate the token.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# static token, pre 1.7.0
|
|
13
|
+
HTTPX.plugin(:auth).authorization("API-TOKEN")
|
|
14
|
+
# dynamically generate token!
|
|
15
|
+
HTTPX.plugin(:auth).authorization { generate_new_ephemeral_token }
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The `.authorization` method is now syntactic sugar for a new option, `:auth_header_value`, which can be used directly, alongside a `:auth_header_type`:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
HTTPX.plugin(:auth).authorization("API-TOKEN")
|
|
22
|
+
HTTPX.plugin(:auth).authorization { generate_new_ephemeral_token }
|
|
23
|
+
HTTPX.plugin(:auth).authorization("Bearer API-TOKEN")
|
|
24
|
+
# same as
|
|
25
|
+
HTTPX.plugin(:auth, auth_header_value: "API-TOKEN")
|
|
26
|
+
HTTPX.plugin(:auth, auth_header_value: -> { generate_new_ephemeral_token })
|
|
27
|
+
HTTPX.plugin(:auth, auth_header_type: "Bearer", auth_header_value: "API-TOKEN")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A new option `:generate_auth_value_on_retry` (which can be passed a callable receiving a response object) is now available; when used alongside the `:retries` plugin, it'll use the callable passed to the `.authorization` method to generate a new token before retrying the request:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
authed = HTTPX.plugin(:retries).plugin(:auth, generate_auth_value_on_retry: ->(res) {
|
|
34
|
+
res.status == 401
|
|
35
|
+
}).authorization { generate_new_ephemeral_token }
|
|
36
|
+
authed.get("https://example.com")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Read more about it in the [auth plugin wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/Auth).
|
|
40
|
+
|
|
41
|
+
#### `:oauth`
|
|
42
|
+
|
|
43
|
+
The `:oauth` plugin implementation was revamped to make use of the `:auth` plugin new functionality, in order to make managing an oauth session more seamless.
|
|
44
|
+
|
|
45
|
+
Take the following example:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
session = HTTPX.plugin(:oauth).with_oauth_options(
|
|
49
|
+
issuer: server.origin,
|
|
50
|
+
client_id: "CLIENT_ID",
|
|
51
|
+
client_secret: "SECRET",
|
|
52
|
+
)
|
|
53
|
+
session.get("https://example.com") #=> will load server metadata, request an access token, and perform the request with the access token.
|
|
54
|
+
# 2 hours later...
|
|
55
|
+
session.get("https://example.com")
|
|
56
|
+
# it'll reuse the same acces token, and if the request fails with 401, it'll request a new
|
|
57
|
+
# access token using the refresh token grant (when supported by the token issuer), and
|
|
58
|
+
# reperform the original request with the new access token.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A new option, `:oauth_options`, is now available. The same parameters previously supported by the `:oauth_session` options are supported.
|
|
62
|
+
|
|
63
|
+
The following components are therefore deprecated and scheduled for removal in a future major version:
|
|
64
|
+
|
|
65
|
+
* `:oauth_session` option
|
|
66
|
+
* `.oauth_auth` session method
|
|
67
|
+
* `.with_access_token` session method
|
|
68
|
+
|
|
69
|
+
#### `:bearer_auth`, `:digest_auth`; `:ntlm_auth`
|
|
70
|
+
|
|
71
|
+
The `:auth` plugin is now the foundation of each of these plugins, which haven't suffered major API changes.
|
|
72
|
+
|
|
73
|
+
Read more about it in the [auth plugin wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/OAuth).
|
|
74
|
+
|
|
75
|
+
### `:retries` plugin: `:retry_after` backoff algorithms
|
|
76
|
+
|
|
77
|
+
The `:retries` plugins supports two new possible values for the `:retry_after` option: `:exponential_backoff` and `:polynomial_backoff`. They'll implement the respective calculation per each retry of a given request.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# will wait 1, 2, 4, 8, 16 seconds... depending of how many retries it can wait for
|
|
81
|
+
session = HTTPX.plugin(:retries, retry_after: :exponential_backoff)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Read more about it in the [retries plugin wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/Retries).
|
|
85
|
+
|
|
86
|
+
### Ractor compatibility
|
|
87
|
+
|
|
88
|
+
`httpx` can be used within a ractor:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
# ruby 4.0 syntax
|
|
92
|
+
response = Ractor.new(uri) do |uri|
|
|
93
|
+
HTTPX.get(uri)
|
|
94
|
+
end.value
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Bear in mind that, if you're connection via HTTPS, you'll need make sure you're using version 4.0 or higher of the `openssl` gem.
|
|
98
|
+
|
|
99
|
+
The test suite isn't exhaustive for ractors yet, but most plugins should also be ractor-compatible. If they don't work, that's a bug, and you're recommended to report it.
|
|
100
|
+
|
|
101
|
+
## Improvements
|
|
102
|
+
|
|
103
|
+
* When encoding the `:json` param to send it as `application/json` payload, (example: `HTTPX.post("https://example.com", json: { foo: "bar })`), and the method uses the `json` standard library, it'll use `JSON.generate` (instead of `JSON.dump`) to encode the JSON payload. The reason is that, unlike `JSON.dump`, it doesn't rely on access to a global mutable hash, and is therefore ractor-safe.
|
|
104
|
+
* `:stream` plugin: the stream response class (the object that is returned in request calls is a stream response) can be extended now. You can add a `StreamResponseMethods` method to your plugin. Read more about it in the documentation.
|
|
105
|
+
* The resolver name cache (used by the native and https resolvers) was remade into a LRU cache, and will therefore not keep on growing when `httpx` is used to connect to a huge number of hostnames in a process.
|
|
106
|
+
* the native and https DNS resolvers will ignore answers with SERVFAIL code while there are retries left (some resolvers use such error code for rate limiting).
|
|
107
|
+
* `:timeout` option values are now validated, and an error is raised when passing an unrecognized timeout option (which is a good layer of protection for typos).
|
|
108
|
+
* pool: try passing the scheduler to a thread waiting on a connection, to avoid the current case where a connection may be checked-in-then-immediately-out-after when doing multiple requests in a loop, never giving a chance to others and potentially making the pool time out.
|
|
109
|
+
* headers deep-freeze and dup.
|
|
110
|
+
|
|
111
|
+
## Bugfixes
|
|
112
|
+
|
|
113
|
+
* recover and close connection when an `IOError` is raised while waiting for IO readiness (could cause busy loops during HTTP/2 termination handshake).
|
|
114
|
+
* `:stream_bidi` plugin: improve thread-safety of buffer operations when the session is used from multiple threads.
|
|
115
|
+
* `:stream_bidi` plugin: added missing methods to signal in order to comply with the Selectable API (it was reported as raising `NoMethodError` under certain conditions).
|
|
116
|
+
* `:stream_bidi` plugin: can support non-bidirectional stream requests using the same session.
|
|
117
|
+
* `:stream` plugin: is now compatible with fiber scheduler engines (via the `:fiber_concurrency` plugin).
|
|
118
|
+
* `:stream` plugin: make sure that stream long-running requests do not share the same connection as regular threads.
|
|
119
|
+
* `:digest_auth` plugin: can now support qop values wrapped inside parentheses in the `www-authenticate` header (i.e. `qop="('auth',)"`).
|
|
120
|
+
* https resolver: handle 3XX redirect responses in HTTP DNS queries.
|
|
121
|
+
* https resolver: do not close HTTP connections whhich are shared across AAAA and A resolution paths when its in use by one of them.
|
|
122
|
+
* fix access to private method from `http-2` which was made public in more recent versions, but not in older still-supported versions.
|
|
123
|
+
* fixed resolver log message using a "connection" label.
|
|
124
|
+
* `HTTPX::Response.copy_to` will explicitly close the response at the end; given that the body file can be moved as a result, there is no guarantee that the response is still usable, so might as well just close it altogether.
|
|
125
|
+
* selector: avoid skipping persistent connections in the selector to deactivate due to iterate-and-modify.
|
|
126
|
+
|
|
127
|
+
## Breaking Changes
|
|
128
|
+
|
|
129
|
+
### `:digest_auth` error
|
|
130
|
+
|
|
131
|
+
The main error class for the `:digest_auth` plugin has been moved to a different location. If you were rescuing the `HTTPX::Plugins::DigestAuth::DigestError` error, you should now point to the `HTTPX::Authentication::Digest::Error`.
|
|
132
|
+
|
|
133
|
+
### `:stream` plugin: `build_request` should receive `stream: true` for stream requests
|
|
134
|
+
|
|
135
|
+
In case you're building request objects before passing them to the session, you're now forced to create them with the `:stream` option on:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
session = HTTPX.plugin(:stream)
|
|
139
|
+
|
|
140
|
+
# before
|
|
141
|
+
req = session.build_request("GET", "https://example.com/stream")
|
|
142
|
+
session.request(req, stream: true)
|
|
143
|
+
|
|
144
|
+
# after
|
|
145
|
+
req = session.build_request("GET", "https://example.com/stream", stream: true)
|
|
146
|
+
session.request(req)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Previous code may still work in a few cases, but it is not guaranteed to work on all cases.
|
|
@@ -80,7 +80,7 @@ module Datadog::Tracing
|
|
|
80
80
|
else
|
|
81
81
|
span.set_tag(TAG_STATUS_CODE, response.status.to_s)
|
|
82
82
|
|
|
83
|
-
span.set_error(::HTTPX::HTTPError.new(response)) if response.status
|
|
83
|
+
span.set_error(::HTTPX::HTTPError.new(response)) if response.status.between?(400, 599)
|
|
84
84
|
|
|
85
85
|
span.set_tags(
|
|
86
86
|
Datadog.configuration.tracing.header_tags.response_tags(response.headers.to_h)
|
data/lib/httpx/altsvc.rb
CHANGED
|
@@ -8,6 +8,8 @@ module HTTPX
|
|
|
8
8
|
module ConnectionMixin
|
|
9
9
|
using URIExtensions
|
|
10
10
|
|
|
11
|
+
H2_ALTSVC_SCHEMES = %w[https h2].freeze
|
|
12
|
+
|
|
11
13
|
def send(request)
|
|
12
14
|
request.headers["alt-used"] = @origin.authority if @parser && !@write_buffer.full? && match_altsvcs?(request.uri)
|
|
13
15
|
|
|
@@ -46,7 +48,7 @@ module HTTPX
|
|
|
46
48
|
uri.origin == other_uri.origin || begin
|
|
47
49
|
case uri.scheme
|
|
48
50
|
when "h2"
|
|
49
|
-
(other_uri.scheme
|
|
51
|
+
H2_ALTSVC_SCHEMES.include?(other_uri.scheme) &&
|
|
50
52
|
uri.host == other_uri.host &&
|
|
51
53
|
uri.port == other_uri.port
|
|
52
54
|
else
|
|
@@ -10,6 +10,11 @@ module HTTPX
|
|
|
10
10
|
MAX_REQUESTS = 200
|
|
11
11
|
CRLF = "\r\n"
|
|
12
12
|
|
|
13
|
+
UPCASED = {
|
|
14
|
+
"www-authenticate" => "WWW-Authenticate",
|
|
15
|
+
"http2-settings" => "HTTP2-Settings",
|
|
16
|
+
"content-md5" => "Content-MD5",
|
|
17
|
+
}.freeze
|
|
13
18
|
attr_reader :pending, :requests
|
|
14
19
|
|
|
15
20
|
attr_accessor :max_concurrent_requests
|
|
@@ -386,12 +391,6 @@ module HTTPX
|
|
|
386
391
|
end
|
|
387
392
|
end
|
|
388
393
|
|
|
389
|
-
UPCASED = {
|
|
390
|
-
"www-authenticate" => "WWW-Authenticate",
|
|
391
|
-
"http2-settings" => "HTTP2-Settings",
|
|
392
|
-
"content-md5" => "Content-MD5",
|
|
393
|
-
}.freeze
|
|
394
|
-
|
|
395
394
|
def capitalized(field)
|
|
396
395
|
UPCASED[field] || field.split("-").map(&:capitalize).join("-")
|
|
397
396
|
end
|
data/lib/httpx/connection.rb
CHANGED
|
@@ -100,14 +100,13 @@ module HTTPX
|
|
|
100
100
|
def match?(uri, options)
|
|
101
101
|
return false if !used? && (@state == :closing || @state == :closed)
|
|
102
102
|
|
|
103
|
-
(
|
|
104
|
-
@origins.include?(uri.origin) &&
|
|
103
|
+
@origins.include?(uri.origin) &&
|
|
105
104
|
# if there is more than one origin to match, it means that this connection
|
|
106
105
|
# was the result of coalescing. To prevent blind trust in the case where the
|
|
107
106
|
# origin came from an ORIGIN frame, we're going to verify the hostname with the
|
|
108
107
|
# SSL certificate
|
|
109
|
-
(@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
|
|
110
|
-
|
|
108
|
+
(@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host))) &&
|
|
109
|
+
@options == options
|
|
111
110
|
end
|
|
112
111
|
|
|
113
112
|
def mergeable?(connection)
|
|
@@ -146,10 +145,6 @@ module HTTPX
|
|
|
146
145
|
end
|
|
147
146
|
end
|
|
148
147
|
|
|
149
|
-
def create_idle(options = {})
|
|
150
|
-
self.class.new(@origin, @options.merge(options))
|
|
151
|
-
end
|
|
152
|
-
|
|
153
148
|
def merge(connection)
|
|
154
149
|
@origins |= connection.instance_variable_get(:@origins)
|
|
155
150
|
if @ssl_session.nil? && connection.ssl_session
|
data/lib/httpx/domain_name.rb
CHANGED
data/lib/httpx/headers.rb
CHANGED
|
@@ -42,12 +42,12 @@ module HTTPX
|
|
|
42
42
|
# dupped initialization
|
|
43
43
|
def initialize_dup(orig)
|
|
44
44
|
super
|
|
45
|
-
@headers = orig.instance_variable_get(:@headers).dup
|
|
45
|
+
@headers = orig.instance_variable_get(:@headers).transform_values(&:dup)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# freezes the headers hash
|
|
49
49
|
def freeze
|
|
50
|
-
@headers.freeze
|
|
50
|
+
@headers.each_value(&:freeze).freeze
|
|
51
51
|
super
|
|
52
52
|
end
|
|
53
53
|
|
data/lib/httpx/loggable.rb
CHANGED
data/lib/httpx/options.rb
CHANGED
|
@@ -188,33 +188,40 @@ module HTTPX
|
|
|
188
188
|
end
|
|
189
189
|
|
|
190
190
|
def merge(other)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
ivar_map.keys
|
|
199
|
-
end
|
|
191
|
+
if (is_options = other.is_a?(Options))
|
|
192
|
+
|
|
193
|
+
return self if eql?(other)
|
|
194
|
+
|
|
195
|
+
opts_names = other.class.options_names
|
|
196
|
+
|
|
197
|
+
return self if opts_names.all? { |opt| public_send(opt) == other.public_send(opt) }
|
|
200
198
|
|
|
201
|
-
|
|
199
|
+
other_opts = opts_names
|
|
200
|
+
else
|
|
201
|
+
other_opts = other # : Hash[Symbol, untyped]
|
|
202
|
+
other_opts = Hash[other] unless other.is_a?(Hash)
|
|
203
|
+
|
|
204
|
+
return self if other_opts.empty?
|
|
202
205
|
|
|
203
|
-
|
|
206
|
+
return self if other_opts.all? { |opt, v| !respond_to?(opt) || public_send(opt) == v }
|
|
207
|
+
end
|
|
204
208
|
|
|
205
209
|
opts = dup
|
|
206
210
|
|
|
207
|
-
|
|
208
|
-
|
|
211
|
+
other_opts.each do |opt, v|
|
|
212
|
+
next unless respond_to?(opt)
|
|
213
|
+
|
|
214
|
+
v = other.public_send(opt) if is_options
|
|
215
|
+
ivar = :"@#{opt}"
|
|
209
216
|
|
|
210
217
|
unless v
|
|
211
218
|
opts.instance_variable_set(ivar, v)
|
|
212
219
|
next
|
|
213
220
|
end
|
|
214
221
|
|
|
215
|
-
v = opts.__send__(:"option_#{
|
|
222
|
+
v = opts.__send__(:"option_#{opt}", v)
|
|
216
223
|
|
|
217
|
-
orig_v =
|
|
224
|
+
orig_v = public_send(opt)
|
|
218
225
|
|
|
219
226
|
v = orig_v.merge(v) if orig_v.respond_to?(:merge) && v.respond_to?(:merge)
|
|
220
227
|
|
|
@@ -379,7 +386,20 @@ module HTTPX
|
|
|
379
386
|
end
|
|
380
387
|
|
|
381
388
|
def option_timeout(value)
|
|
382
|
-
Hash[value]
|
|
389
|
+
timeout_hash = Hash[value]
|
|
390
|
+
|
|
391
|
+
default_timeouts = DEFAULT_OPTIONS[:timeout]
|
|
392
|
+
|
|
393
|
+
# Validate keys and values
|
|
394
|
+
timeout_hash.each do |key, val|
|
|
395
|
+
raise TypeError, "invalid timeout: :#{key}" unless default_timeouts.key?(key)
|
|
396
|
+
|
|
397
|
+
next if val.nil?
|
|
398
|
+
|
|
399
|
+
raise TypeError, ":#{key} must be numeric" unless val.is_a?(Numeric)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
timeout_hash
|
|
383
403
|
end
|
|
384
404
|
|
|
385
405
|
def option_supported_compression_formats(value)
|
|
@@ -424,6 +444,8 @@ module HTTPX
|
|
|
424
444
|
end
|
|
425
445
|
end
|
|
426
446
|
|
|
447
|
+
# rubocop:disable Lint/UselessConstantScoping
|
|
448
|
+
# these really need to be defined at the end of the class
|
|
427
449
|
SET_TEMPORARY_NAME = ->(klass, pl = nil) do
|
|
428
450
|
if klass.respond_to?(:set_temporary_name) # ruby 3.4 only
|
|
429
451
|
name = klass.name || "#{klass.superclass.name}(plugin)"
|
|
@@ -478,6 +500,7 @@ module HTTPX
|
|
|
478
500
|
:pool_options => EMPTY_HASH,
|
|
479
501
|
:ip_families => nil,
|
|
480
502
|
:close_on_fork => false,
|
|
481
|
-
}.freeze
|
|
503
|
+
}.each_value(&:freeze).freeze
|
|
504
|
+
# rubocop:enable Lint/UselessConstantScoping
|
|
482
505
|
end
|
|
483
506
|
end
|
|
@@ -8,6 +8,8 @@ module HTTPX
|
|
|
8
8
|
module Plugins
|
|
9
9
|
module Authentication
|
|
10
10
|
class Digest
|
|
11
|
+
Error = Class.new(Error)
|
|
12
|
+
|
|
11
13
|
def initialize(user, password, hashed: false, **)
|
|
12
14
|
@user = user
|
|
13
15
|
@password = password
|
|
@@ -29,19 +31,53 @@ module HTTPX
|
|
|
29
31
|
# discard first token, it's Digest
|
|
30
32
|
auth_info = authenticate[/^(\w+) (.*)/, 2]
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
raise_format_error unless auth_info
|
|
35
|
+
|
|
36
|
+
s = StringScanner.new(auth_info)
|
|
37
|
+
|
|
38
|
+
params = {}
|
|
39
|
+
until s.eos?
|
|
40
|
+
k = s.scan_until(/=/)
|
|
41
|
+
raise_format_error unless k&.end_with?("=")
|
|
42
|
+
|
|
43
|
+
if s.peek(1) == "\""
|
|
44
|
+
s.skip("\"")
|
|
45
|
+
v = s.scan_until(/"/)
|
|
46
|
+
raise_format_error unless v&.end_with?("\"")
|
|
47
|
+
|
|
48
|
+
v = v[0..-2]
|
|
49
|
+
s.skip_until(/,/)
|
|
50
|
+
else
|
|
51
|
+
v = s.scan_until(/,|$/)
|
|
52
|
+
|
|
53
|
+
if v&.end_with?(",")
|
|
54
|
+
v = v[0..-2]
|
|
55
|
+
else
|
|
56
|
+
raise_format_error unless s.eos?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
v = v[0..-2] if v&.end_with?(",")
|
|
60
|
+
end
|
|
61
|
+
params[k[0..-2]] = v
|
|
62
|
+
s.skip(/\s/)
|
|
63
|
+
end
|
|
64
|
+
|
|
35
65
|
nonce = params["nonce"]
|
|
36
66
|
nc = next_nonce
|
|
37
67
|
|
|
38
68
|
# verify qop
|
|
39
69
|
qop = params["qop"]
|
|
40
70
|
|
|
71
|
+
if qop
|
|
72
|
+
# some servers send multiple values wrapped in parentheses (i.e. "(qauth,)")
|
|
73
|
+
qop = qop[/\(?([^)]+)\)?/, 1]
|
|
74
|
+
qop = qop.split(",").map { |s| s.delete_prefix("'").delete_suffix("'") }.delete_if(&:empty?).map.first
|
|
75
|
+
end
|
|
76
|
+
|
|
41
77
|
if params["algorithm"] =~ /(.*?)(-sess)?$/
|
|
42
78
|
alg = Regexp.last_match(1)
|
|
43
79
|
algorithm = ::Digest.const_get(alg)
|
|
44
|
-
raise
|
|
80
|
+
raise Error, "unknown algorithm \"#{alg}\"" unless algorithm
|
|
45
81
|
|
|
46
82
|
sess = Regexp.last_match(2)
|
|
47
83
|
else
|
|
@@ -96,6 +132,10 @@ module HTTPX
|
|
|
96
132
|
def next_nonce
|
|
97
133
|
@nonce += 1
|
|
98
134
|
end
|
|
135
|
+
|
|
136
|
+
def raise_format_error
|
|
137
|
+
raise Error, "unsupported digest header format"
|
|
138
|
+
end
|
|
99
139
|
end
|
|
100
140
|
end
|
|
101
141
|
end
|
data/lib/httpx/plugins/auth.rb
CHANGED
|
@@ -10,13 +10,96 @@ module HTTPX
|
|
|
10
10
|
# https://gitlab.com/os85/httpx/wikis/Auth#auth
|
|
11
11
|
#
|
|
12
12
|
module Auth
|
|
13
|
+
def self.subplugins
|
|
14
|
+
{
|
|
15
|
+
retries: AuthRetries,
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module OptionsMethods
|
|
20
|
+
def option_auth_header_value(value)
|
|
21
|
+
value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def option_auth_header_type(value)
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def option_generate_auth_value_on_retry(value)
|
|
29
|
+
raise TypeError, "`:generate_auth_value_on_retry` must be a callable" unless value.respond_to?(:call)
|
|
30
|
+
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
13
35
|
module InstanceMethods
|
|
14
|
-
def
|
|
15
|
-
|
|
36
|
+
def initialize(*)
|
|
37
|
+
super
|
|
38
|
+
|
|
39
|
+
@auth_header_value = nil
|
|
40
|
+
@skip_auth_header_value = false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def authorization(token = nil, auth_header_type: nil, &blk)
|
|
44
|
+
with(auth_header_type: auth_header_type, auth_header_value: token || blk)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def bearer_auth(token = nil, &blk)
|
|
48
|
+
authorization(token, auth_header_type: "Bearer", &blk)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def skip_auth_header
|
|
52
|
+
@skip_auth_header_value = true
|
|
53
|
+
yield
|
|
54
|
+
ensure
|
|
55
|
+
@skip_auth_header_value = false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reset_auth_header_value!
|
|
59
|
+
@auth_header_value = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def send_request(request, *)
|
|
65
|
+
return super if @skip_auth_header_value
|
|
66
|
+
|
|
67
|
+
@auth_header_value ||= generate_auth_token
|
|
68
|
+
|
|
69
|
+
request.authorize(@auth_header_value) if @auth_header_value
|
|
70
|
+
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def generate_auth_token
|
|
75
|
+
return unless (auth_value = @options.auth_header_value)
|
|
76
|
+
|
|
77
|
+
auth_value = auth_value.call(self) if auth_value.respond_to?(:call)
|
|
78
|
+
|
|
79
|
+
auth_value
|
|
16
80
|
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
module RequestMethods
|
|
84
|
+
def authorize(auth_value)
|
|
85
|
+
if (auth_type = @options.auth_header_type)
|
|
86
|
+
auth_value = "#{auth_type} #{auth_value}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@headers.add("authorization", auth_value)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
module AuthRetries
|
|
94
|
+
module InstanceMethods
|
|
95
|
+
def prepare_to_retry(request, response)
|
|
96
|
+
super
|
|
97
|
+
|
|
98
|
+
return unless @options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response)
|
|
17
99
|
|
|
18
|
-
|
|
19
|
-
|
|
100
|
+
request.headers.get("authorization").pop
|
|
101
|
+
@auth_header_value = generate_auth_token
|
|
102
|
+
end
|
|
20
103
|
end
|
|
21
104
|
end
|
|
22
105
|
end
|
|
@@ -8,15 +8,14 @@ module HTTPX
|
|
|
8
8
|
# https://gitlab.com/os85/httpx/wikis/Auth#digest-auth
|
|
9
9
|
#
|
|
10
10
|
module DigestAuth
|
|
11
|
-
DigestError = Class.new(Error)
|
|
12
|
-
|
|
13
11
|
class << self
|
|
14
12
|
def extra_options(options)
|
|
15
13
|
options.merge(max_concurrent_requests: 1)
|
|
16
14
|
end
|
|
17
15
|
|
|
18
|
-
def load_dependencies(
|
|
16
|
+
def load_dependencies(klass)
|
|
19
17
|
require_relative "auth/digest"
|
|
18
|
+
klass.plugin(:auth)
|
|
20
19
|
end
|
|
21
20
|
end
|
|
22
21
|
|
|
@@ -48,11 +47,11 @@ module HTTPX
|
|
|
48
47
|
|
|
49
48
|
probe_response = wrap { super(request).first }
|
|
50
49
|
|
|
51
|
-
return
|
|
50
|
+
return [probe_response] * requests.size unless probe_response.is_a?(Response)
|
|
52
51
|
|
|
53
52
|
if probe_response.status == 401 && digest.can_authenticate?(probe_response.headers["www-authenticate"])
|
|
54
53
|
request.transition(:idle)
|
|
55
|
-
request.
|
|
54
|
+
request.authorize(digest.authenticate(request, probe_response.headers["www-authenticate"]))
|
|
56
55
|
super(request)
|
|
57
56
|
else
|
|
58
57
|
probe_response
|