httpx 1.6.2 → 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 +47 -0
- 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/adapters/sentry.rb +1 -1
- data/lib/httpx/altsvc.rb +3 -1
- data/lib/httpx/connection/http1.rb +14 -15
- data/lib/httpx/connection/http2.rb +16 -15
- data/lib/httpx/connection.rb +118 -110
- data/lib/httpx/domain_name.rb +1 -1
- data/lib/httpx/extensions.rb +0 -14
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/ssl.rb +1 -1
- data/lib/httpx/loggable.rb +14 -2
- data/lib/httpx/options.rb +60 -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/callbacks.rb +15 -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/proxy/http.rb +37 -9
- data/lib/httpx/plugins/rate_limiter.rb +2 -2
- data/lib/httpx/plugins/response_cache/file_store.rb +1 -0
- data/lib/httpx/plugins/response_cache.rb +16 -9
- 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 +87 -22
- data/lib/httpx/pool.rb +65 -21
- data/lib/httpx/request.rb +13 -14
- data/lib/httpx/resolver/https.rb +100 -34
- data/lib/httpx/resolver/multi.rb +12 -27
- data/lib/httpx/resolver/native.rb +68 -38
- data/lib/httpx/resolver/resolver.rb +46 -29
- data/lib/httpx/resolver/system.rb +63 -39
- 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 +44 -20
- data/lib/httpx/session.rb +23 -33
- data/lib/httpx/transcoder/body.rb +1 -1
- data/lib/httpx/transcoder/deflate.rb +13 -8
- 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/utils/body_reader.rb +1 -2
- data/lib/httpx/transcoder/utils/deflater.rb +1 -2
- 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 +13 -6
- data/sig/loggable.rbs +5 -1
- data/sig/options.rbs +6 -2
- 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/callbacks.rbs +3 -0
- 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 +5 -7
- data/sig/pool.rbs +1 -1
- data/sig/resolver/https.rbs +7 -0
- data/sig/resolver/multi.rbs +2 -9
- data/sig/resolver/native.rbs +1 -1
- data/sig/resolver/resolver.rbs +9 -8
- data/sig/resolver/system.rbs +4 -2
- data/sig/resolver.rbs +12 -3
- data/sig/response.rbs +3 -0
- data/sig/selector.rbs +2 -0
- data/sig/session.rbs +8 -8
- data/sig/transcoder/multipart.rbs +4 -2
- data/sig/transcoder.rbs +5 -1
- metadata +5 -1
|
@@ -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
|
|
@@ -64,7 +64,7 @@ module HTTPX
|
|
|
64
64
|
|
|
65
65
|
emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
|
|
66
66
|
end
|
|
67
|
-
connection.on(:
|
|
67
|
+
connection.on(:callback_connection_closed) do
|
|
68
68
|
next unless connection.current_session == self
|
|
69
69
|
|
|
70
70
|
emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
|
|
@@ -121,6 +121,20 @@ module HTTPX
|
|
|
121
121
|
raise e.cause
|
|
122
122
|
end
|
|
123
123
|
end
|
|
124
|
+
|
|
125
|
+
module ConnectionMethods
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def disconnect
|
|
129
|
+
return if @exhausted
|
|
130
|
+
|
|
131
|
+
return unless @current_session && @current_selector
|
|
132
|
+
|
|
133
|
+
emit(:callback_connection_closed)
|
|
134
|
+
|
|
135
|
+
super
|
|
136
|
+
end
|
|
137
|
+
end
|
|
124
138
|
end
|
|
125
139
|
register_plugin :callbacks, Callbacks
|
|
126
140
|
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 probe_response unless probe_response.is_a?(Response)
|
|
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
|
|
@@ -6,12 +6,13 @@ module HTTPX
|
|
|
6
6
|
#
|
|
7
7
|
# This enables integration with fiber scheduler implementations such as [async](https://github.com/async).
|
|
8
8
|
#
|
|
9
|
-
# # https://gitlab.com/os85/httpx/wikis/
|
|
9
|
+
# # https://gitlab.com/os85/httpx/wikis/Fiber-Concurrency
|
|
10
10
|
#
|
|
11
11
|
module FiberConcurrency
|
|
12
12
|
def self.subplugins
|
|
13
13
|
{
|
|
14
14
|
h2c: FiberConcurrencyH2C,
|
|
15
|
+
stream: FiberConcurrencyStream,
|
|
15
16
|
}
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -188,6 +189,20 @@ module HTTPX
|
|
|
188
189
|
end
|
|
189
190
|
end
|
|
190
191
|
end
|
|
192
|
+
|
|
193
|
+
module FiberConcurrencyStream
|
|
194
|
+
module StreamResponseMethods
|
|
195
|
+
def close
|
|
196
|
+
unless @request.current_context?
|
|
197
|
+
@request.close
|
|
198
|
+
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
super
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
191
206
|
end
|
|
192
207
|
|
|
193
208
|
register_plugin :fiber_concurrency, FiberConcurrency
|
|
@@ -48,7 +48,7 @@ module HTTPX
|
|
|
48
48
|
until message.empty?
|
|
49
49
|
compressed, size = message.unpack("CL>")
|
|
50
50
|
|
|
51
|
-
encoded_data = message.byteslice(5..size + 5 - 1)
|
|
51
|
+
encoded_data = message.byteslice(5..(size + 5 - 1))
|
|
52
52
|
|
|
53
53
|
if compressed == 1
|
|
54
54
|
grpc_encodings.reverse_each do |encoding|
|
data/lib/httpx/plugins/grpc.rb
CHANGED
|
@@ -249,7 +249,7 @@ module HTTPX
|
|
|
249
249
|
call
|
|
250
250
|
end
|
|
251
251
|
|
|
252
|
-
def build_grpc_request(rpc_method, input, deadline:, metadata: nil, **)
|
|
252
|
+
def build_grpc_request(rpc_method, input, deadline:, metadata: nil, **opts)
|
|
253
253
|
uri = @options.origin.dup
|
|
254
254
|
rpc_method = "/#{rpc_method}" unless rpc_method.start_with?("/")
|
|
255
255
|
rpc_method = "/#{@options.grpc_service}#{rpc_method}" if @options.grpc_service
|
|
@@ -273,7 +273,7 @@ module HTTPX
|
|
|
273
273
|
|
|
274
274
|
headers.merge!(@options.call_credentials.call.transform_keys(&:to_s)) if @options.call_credentials
|
|
275
275
|
|
|
276
|
-
build_request("POST", uri, headers: headers, body: input)
|
|
276
|
+
build_request("POST", uri, headers: headers, body: input, **opts)
|
|
277
277
|
end
|
|
278
278
|
end
|
|
279
279
|
end
|
|
@@ -7,8 +7,9 @@ module HTTPX
|
|
|
7
7
|
#
|
|
8
8
|
module NTLMAuth
|
|
9
9
|
class << self
|
|
10
|
-
def load_dependencies(
|
|
10
|
+
def load_dependencies(klass)
|
|
11
11
|
require_relative "auth/ntlm"
|
|
12
|
+
klass.plugin(:auth)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def extra_options(options)
|
|
@@ -38,14 +39,15 @@ module HTTPX
|
|
|
38
39
|
ntlm = request.options.ntlm
|
|
39
40
|
|
|
40
41
|
if ntlm
|
|
41
|
-
request.
|
|
42
|
+
request.authorize(ntlm.negotiate)
|
|
42
43
|
probe_response = wrap { super(request).first }
|
|
43
44
|
|
|
44
45
|
return probe_response unless probe_response.is_a?(Response)
|
|
45
46
|
|
|
46
47
|
if probe_response.status == 401 && ntlm.can_authenticate?(probe_response.headers["www-authenticate"])
|
|
47
48
|
request.transition(:idle)
|
|
48
|
-
request.headers
|
|
49
|
+
request.headers.get("authorization").pop
|
|
50
|
+
request.authorize(ntlm.authenticate(request, probe_response.headers["www-authenticate"]).encode("utf-8"))
|
|
49
51
|
super(request)
|
|
50
52
|
else
|
|
51
53
|
probe_response
|
data/lib/httpx/plugins/oauth.rb
CHANGED
|
@@ -2,21 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
module HTTPX
|
|
4
4
|
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds support for managing an OAuth Session associated with the given session.
|
|
7
|
+
#
|
|
8
|
+
# The scope of OAuth support is limited to the `client_crendentials` and `refresh_token` grants.
|
|
5
9
|
#
|
|
6
10
|
# https://gitlab.com/os85/httpx/wikis/OAuth
|
|
7
11
|
#
|
|
8
12
|
module OAuth
|
|
9
13
|
class << self
|
|
10
|
-
def load_dependencies(
|
|
14
|
+
def load_dependencies(klass)
|
|
11
15
|
require_relative "auth/basic"
|
|
16
|
+
klass.plugin(:auth)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def subplugins
|
|
20
|
+
{
|
|
21
|
+
retries: OAuthRetries,
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def extra_options(options)
|
|
26
|
+
options.merge(auth_header_type: "Bearer")
|
|
12
27
|
end
|
|
13
28
|
end
|
|
14
29
|
|
|
15
30
|
SUPPORTED_GRANT_TYPES = %w[client_credentials refresh_token].freeze
|
|
16
31
|
SUPPORTED_AUTH_METHODS = %w[client_secret_basic client_secret_post].freeze
|
|
17
32
|
|
|
33
|
+
# Implements the bulk of functionality and maintains the state associated with the
|
|
34
|
+
# management of the the lifecycle of an OAuth session.
|
|
18
35
|
class OAuthSession
|
|
19
|
-
attr_reader :
|
|
36
|
+
attr_reader :access_token, :refresh_token
|
|
20
37
|
|
|
21
38
|
def initialize(
|
|
22
39
|
issuer:,
|
|
@@ -27,7 +44,6 @@ module HTTPX
|
|
|
27
44
|
scope: nil,
|
|
28
45
|
audience: nil,
|
|
29
46
|
token_endpoint: nil,
|
|
30
|
-
response_type: nil,
|
|
31
47
|
grant_type: nil,
|
|
32
48
|
token_endpoint_auth_method: nil
|
|
33
49
|
)
|
|
@@ -35,7 +51,6 @@ module HTTPX
|
|
|
35
51
|
@client_id = client_id
|
|
36
52
|
@client_secret = client_secret
|
|
37
53
|
@token_endpoint = URI(token_endpoint) if token_endpoint
|
|
38
|
-
@response_type = response_type
|
|
39
54
|
@scope = case scope
|
|
40
55
|
when String
|
|
41
56
|
scope.split
|
|
@@ -47,6 +62,8 @@ module HTTPX
|
|
|
47
62
|
@refresh_token = refresh_token
|
|
48
63
|
@token_endpoint_auth_method = String(token_endpoint_auth_method) if token_endpoint_auth_method
|
|
49
64
|
@grant_type = grant_type || (@refresh_token ? "refresh_token" : "client_credentials")
|
|
65
|
+
@access_token = access_token
|
|
66
|
+
@refresh_token = refresh_token
|
|
50
67
|
|
|
51
68
|
unless @token_endpoint_auth_method.nil? || SUPPORTED_AUTH_METHODS.include?(@token_endpoint_auth_method)
|
|
52
69
|
raise Error, "#{@token_endpoint_auth_method} is not a supported auth method"
|
|
@@ -57,28 +74,75 @@ module HTTPX
|
|
|
57
74
|
raise Error, "#{@grant_type} is not a supported grant type"
|
|
58
75
|
end
|
|
59
76
|
|
|
77
|
+
# returns the URL where to request access and refresh tokens from.
|
|
60
78
|
def token_endpoint
|
|
61
79
|
@token_endpoint || "#{@issuer}/token"
|
|
62
80
|
end
|
|
63
81
|
|
|
82
|
+
# returns the oauth-documented authorization method to use when requesting a token.
|
|
64
83
|
def token_endpoint_auth_method
|
|
65
84
|
@token_endpoint_auth_method || "client_secret_basic"
|
|
66
85
|
end
|
|
67
86
|
|
|
68
|
-
def
|
|
69
|
-
|
|
87
|
+
def reset!
|
|
88
|
+
@access_token = nil
|
|
89
|
+
end
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
# when not available, it uses the +http+ object to request new access and refresh tokens.
|
|
92
|
+
def fetch_access_token(http)
|
|
93
|
+
return access_token if access_token
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
load(http)
|
|
96
|
+
|
|
97
|
+
# always prefer refresh token grant if a refresh token is available
|
|
98
|
+
grant_type = @refresh_token ? "refresh_token" : @grant_type
|
|
99
|
+
|
|
100
|
+
headers = {} # : Hash[String ,String]
|
|
101
|
+
form_post = {
|
|
102
|
+
"grant_type" => @grant_type,
|
|
103
|
+
"scope" => Array(@scope).join(" "),
|
|
104
|
+
"audience" => @audience,
|
|
105
|
+
}.compact
|
|
106
|
+
|
|
107
|
+
# auth
|
|
108
|
+
case token_endpoint_auth_method
|
|
109
|
+
when "client_secret_post"
|
|
110
|
+
form_post["client_id"] = @client_id
|
|
111
|
+
form_post["client_secret"] = @client_secret
|
|
112
|
+
when "client_secret_basic"
|
|
113
|
+
headers["authorization"] = Authentication::Basic.new(@client_id, @client_secret).authenticate
|
|
78
114
|
end
|
|
79
|
-
|
|
115
|
+
|
|
116
|
+
case grant_type
|
|
117
|
+
when "client_credentials"
|
|
118
|
+
# do nothing
|
|
119
|
+
when "refresh_token"
|
|
120
|
+
raise Error, "cannot use the `\"refresh_token\"` grant type without a refresh token" unless refresh_token
|
|
121
|
+
|
|
122
|
+
form_post["refresh_token"] = refresh_token
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# POST /token
|
|
126
|
+
token_request = http.build_request("POST", token_endpoint, headers: headers, form: form_post)
|
|
127
|
+
|
|
128
|
+
token_request.headers.delete("authorization") unless token_endpoint_auth_method == "client_secret_basic"
|
|
129
|
+
|
|
130
|
+
token_response = http.skip_auth_header { http.request(token_request) }
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
token_response.raise_for_status
|
|
134
|
+
rescue HTTPError => e
|
|
135
|
+
@refresh_token = nil if e.response.status == 401 && (grant_type == "refresh_token")
|
|
136
|
+
raise e
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
payload = token_response.json
|
|
140
|
+
|
|
141
|
+
@refresh_token = payload["refresh_token"] || @refresh_token
|
|
142
|
+
@access_token = payload["access_token"]
|
|
80
143
|
end
|
|
81
144
|
|
|
145
|
+
# TODO: remove this after deprecating the `:oauth_session` option
|
|
82
146
|
def merge(other)
|
|
83
147
|
obj = dup
|
|
84
148
|
|
|
@@ -97,12 +161,32 @@ module HTTPX
|
|
|
97
161
|
end
|
|
98
162
|
obj
|
|
99
163
|
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
# uses +http+ to fetch for the oauth server metadata.
|
|
168
|
+
def load(http)
|
|
169
|
+
return if @grant_type && @scope
|
|
170
|
+
|
|
171
|
+
metadata = http.skip_auth_header { http.get("#{@issuer}/.well-known/oauth-authorization-server").raise_for_status.json }
|
|
172
|
+
|
|
173
|
+
@token_endpoint = metadata["token_endpoint"]
|
|
174
|
+
@scope = metadata["scopes_supported"]
|
|
175
|
+
@grant_type = Array(metadata["grant_types_supported"]).find { |gr| SUPPORTED_GRANT_TYPES.include?(gr) }
|
|
176
|
+
@token_endpoint_auth_method = Array(metadata["token_endpoint_auth_methods_supported"]).find do |am|
|
|
177
|
+
SUPPORTED_AUTH_METHODS.include?(am)
|
|
178
|
+
end
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
100
181
|
end
|
|
101
182
|
|
|
102
183
|
module OptionsMethods
|
|
103
184
|
private
|
|
104
185
|
|
|
105
186
|
def option_oauth_session(value)
|
|
187
|
+
warn "DEPRECATION WARNING: option `:oauth_session` is deprecated. " \
|
|
188
|
+
"Use `:oauth_options` instead."
|
|
189
|
+
|
|
106
190
|
case value
|
|
107
191
|
when Hash
|
|
108
192
|
OAuthSession.new(**value)
|
|
@@ -112,69 +196,91 @@ module HTTPX
|
|
|
112
196
|
raise TypeError, ":oauth_session must be a #{OAuthSession}"
|
|
113
197
|
end
|
|
114
198
|
end
|
|
199
|
+
|
|
200
|
+
def option_oauth_options(value)
|
|
201
|
+
value = Hash[value] unless value.is_a?(Hash)
|
|
202
|
+
value
|
|
203
|
+
end
|
|
115
204
|
end
|
|
116
205
|
|
|
117
206
|
module InstanceMethods
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
end
|
|
207
|
+
attr_reader :oauth_session
|
|
208
|
+
protected :oauth_session
|
|
121
209
|
|
|
122
|
-
def
|
|
123
|
-
|
|
210
|
+
def initialize(*)
|
|
211
|
+
super
|
|
124
212
|
|
|
125
|
-
oauth_session.
|
|
213
|
+
@oauth_session = if @options.oauth_options
|
|
214
|
+
OAuthSession.new(**@options.oauth_options)
|
|
215
|
+
elsif @options.oauth_session
|
|
216
|
+
@oauth_session = @options.oauth_session.dup
|
|
217
|
+
end
|
|
218
|
+
end
|
|
126
219
|
|
|
127
|
-
|
|
220
|
+
def initialize_dup(other)
|
|
221
|
+
super
|
|
222
|
+
@oauth_session = other.instance_variable_get(:@oauth_session).dup
|
|
223
|
+
end
|
|
128
224
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"scope" => Array(oauth_session.scope).join(" "),
|
|
133
|
-
"audience" => oauth_session.audience,
|
|
134
|
-
}.compact
|
|
225
|
+
def oauth_auth(**args)
|
|
226
|
+
warn "DEPRECATION WARNING: `#{__method__}` is deprecated. " \
|
|
227
|
+
"Use `with(oauth_options: options)` instead."
|
|
135
228
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
when "client_secret_post"
|
|
139
|
-
form_post["client_id"] = oauth_session.client_id
|
|
140
|
-
form_post["client_secret"] = oauth_session.client_secret
|
|
141
|
-
when "client_secret_basic"
|
|
142
|
-
headers["authorization"] = Authentication::Basic.new(oauth_session.client_id, oauth_session.client_secret).authenticate
|
|
143
|
-
end
|
|
229
|
+
with(oauth_options: args)
|
|
230
|
+
end
|
|
144
231
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
232
|
+
# will eagerly negotiate new oauth tokens with the issuer
|
|
233
|
+
def refresh_oauth_tokens!
|
|
234
|
+
return unless @oauth_session
|
|
235
|
+
|
|
236
|
+
@oauth_session.reset!
|
|
237
|
+
@oauth_session.fetch_access_token(self)
|
|
238
|
+
end
|
|
151
239
|
|
|
152
|
-
|
|
153
|
-
|
|
240
|
+
# TODO: deprecate
|
|
241
|
+
def with_access_token
|
|
242
|
+
warn "DEPRECATION WARNING: `#{__method__}` is deprecated. " \
|
|
243
|
+
"The session will automatically handle token lifecycles for you."
|
|
154
244
|
|
|
155
|
-
|
|
156
|
-
|
|
245
|
+
other_session = dup # : instance
|
|
246
|
+
oauth_session = other_session.oauth_session
|
|
247
|
+
oauth_session.fetch_access_token(other_session)
|
|
248
|
+
other_session
|
|
249
|
+
end
|
|
157
250
|
|
|
158
|
-
|
|
251
|
+
private
|
|
159
252
|
|
|
160
|
-
|
|
161
|
-
|
|
253
|
+
def generate_auth_token
|
|
254
|
+
return unless @oauth_session
|
|
162
255
|
|
|
163
|
-
|
|
256
|
+
@oauth_session.fetch_access_token(self)
|
|
164
257
|
end
|
|
258
|
+
end
|
|
165
259
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
260
|
+
module OAuthRetries
|
|
261
|
+
class << self
|
|
262
|
+
def extra_options(options)
|
|
263
|
+
options.merge(
|
|
264
|
+
retry_on: method(:response_oauth_error?),
|
|
265
|
+
generate_auth_value_on_retry: method(:response_oauth_error?)
|
|
266
|
+
)
|
|
267
|
+
end
|
|
170
268
|
|
|
171
|
-
|
|
269
|
+
def response_oauth_error?(res)
|
|
270
|
+
res.is_a?(Response) && res.status == 401
|
|
271
|
+
end
|
|
272
|
+
end
|
|
172
273
|
|
|
173
|
-
|
|
274
|
+
module InstanceMethods
|
|
275
|
+
def prepare_to_retry(_request, response)
|
|
276
|
+
unless @oauth_session && @options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response)
|
|
277
|
+
return super
|
|
278
|
+
end
|
|
174
279
|
|
|
175
|
-
|
|
280
|
+
@oauth_session.reset!
|
|
176
281
|
|
|
177
|
-
|
|
282
|
+
super
|
|
283
|
+
end
|
|
178
284
|
end
|
|
179
285
|
end
|
|
180
286
|
end
|