x-core 1.0.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 +7 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +353 -0
- data/LICENSE.txt +21 -0
- data/README.md +54 -0
- data/lib/x/core/app_only_authenticator.rb +198 -0
- data/lib/x/core/authenticator.rb +74 -0
- data/lib/x/core/authenticator_request.rb +56 -0
- data/lib/x/core/bearer_token_authenticator.rb +46 -0
- data/lib/x/core/built_response.rb +100 -0
- data/lib/x/core/client.rb +728 -0
- data/lib/x/core/client_app_only.rb +115 -0
- data/lib/x/core/client_credentials.rb +193 -0
- data/lib/x/core/client_internals.rb +315 -0
- data/lib/x/core/client_memo.rb +58 -0
- data/lib/x/core/client_settings.rb +166 -0
- data/lib/x/core/client_token_refresh.rb +281 -0
- data/lib/x/core/connection.rb +328 -0
- data/lib/x/core/connection_pool.rb +123 -0
- data/lib/x/core/connection_proxy.rb +151 -0
- data/lib/x/core/connection_request.rb +90 -0
- data/lib/x/core/credential_holder.rb +78 -0
- data/lib/x/core/credential_validator.rb +286 -0
- data/lib/x/core/errors/authorization_denied.rb +55 -0
- data/lib/x/core/errors/authorization_error.rb +41 -0
- data/lib/x/core/errors/bad_gateway.rb +9 -0
- data/lib/x/core/errors/bad_request.rb +10 -0
- data/lib/x/core/errors/callback_error.rb +88 -0
- data/lib/x/core/errors/client_error.rb +17 -0
- data/lib/x/core/errors/conflict.rb +9 -0
- data/lib/x/core/errors/error.rb +66 -0
- data/lib/x/core/errors/forbidden.rb +10 -0
- data/lib/x/core/errors/gateway_timeout.rb +9 -0
- data/lib/x/core/errors/gone.rb +9 -0
- data/lib/x/core/errors/http_error.rb +333 -0
- data/lib/x/core/errors/internal_server_error.rb +9 -0
- data/lib/x/core/errors/invalid_response.rb +96 -0
- data/lib/x/core/errors/method_not_allowed.rb +10 -0
- data/lib/x/core/errors/network_error.rb +59 -0
- data/lib/x/core/errors/not_acceptable.rb +10 -0
- data/lib/x/core/errors/not_found.rb +15 -0
- data/lib/x/core/errors/payload_too_large.rb +10 -0
- data/lib/x/core/errors/payment_required.rb +10 -0
- data/lib/x/core/errors/request_timeout.rb +9 -0
- data/lib/x/core/errors/server_error.rb +15 -0
- data/lib/x/core/errors/service_unavailable.rb +9 -0
- data/lib/x/core/errors/token_report_failed.rb +59 -0
- data/lib/x/core/errors/too_many_redirects.rb +53 -0
- data/lib/x/core/errors/too_many_requests.rb +80 -0
- data/lib/x/core/errors/unauthorized.rb +15 -0
- data/lib/x/core/errors/unavailable_for_legal_reasons.rb +10 -0
- data/lib/x/core/errors/unprocessable_entity.rb +10 -0
- data/lib/x/core/errors/unsupported_format.rb +22 -0
- data/lib/x/core/errors/unsupported_media_type.rb +10 -0
- data/lib/x/core/errors/unsupported_operation.rb +18 -0
- data/lib/x/core/oauth1_authenticator.rb +142 -0
- data/lib/x/core/oauth2_authenticator.rb +296 -0
- data/lib/x/core/oauth2_authorization.rb +405 -0
- data/lib/x/core/oauth2_refresh.rb +272 -0
- data/lib/x/core/oauth2_tokens.rb +277 -0
- data/lib/x/core/origin.rb +95 -0
- data/lib/x/core/problem.rb +302 -0
- data/lib/x/core/proxy_setting.rb +22 -0
- data/lib/x/core/rate_limit.rb +137 -0
- data/lib/x/core/rate_limit_handler.rb +166 -0
- data/lib/x/core/redirect_handler.rb +208 -0
- data/lib/x/core/refresh_reporter.rb +92 -0
- data/lib/x/core/request_builder.rb +155 -0
- data/lib/x/core/request_context.rb +85 -0
- data/lib/x/core/request_encoding.rb +135 -0
- data/lib/x/core/response.rb +196 -0
- data/lib/x/core/response_headers.rb +31 -0
- data/lib/x/core/response_parser.rb +155 -0
- data/lib/x/core/retry_handler.rb +149 -0
- data/lib/x/core/setting_validator.rb +399 -0
- data/lib/x/core/stream_body.rb +77 -0
- data/lib/x/core/stream_response.rb +267 -0
- data/lib/x/core/token_endpoint.rb +238 -0
- data/lib/x/core/version.rb +25 -0
- data/lib/x/core.rb +6 -0
- data/sig/manifest.yaml +8 -0
- data/sig/x-core.rbs +396 -0
- metadata +162 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require_relative "rate_limit_handler"
|
|
5
|
+
require_relative "redirect_handler"
|
|
6
|
+
require_relative "response"
|
|
7
|
+
require_relative "retry_handler"
|
|
8
|
+
require_relative "setting_validator"
|
|
9
|
+
|
|
10
|
+
module X
|
|
11
|
+
module Core
|
|
12
|
+
# The settings of a client other than its credentials: its base URL, parsing classes, hook, and the settings of
|
|
13
|
+
# its connection and handlers, included into ClientInternals
|
|
14
|
+
#
|
|
15
|
+
# A client is built with the settings it keeps for as long as it lives. {Client#with} derives a client whose
|
|
16
|
+
# settings differ, rather than replacing the ones a client holds, so that a request never runs under a setting
|
|
17
|
+
# another thread is halfway through changing.
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
module ClientSettings
|
|
21
|
+
extend Forwardable
|
|
22
|
+
|
|
23
|
+
# The base URL for API requests
|
|
24
|
+
#
|
|
25
|
+
# {Client#base_url} returns it.
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
28
|
+
# @return [String] the base URL for API requests, which ends with a slash
|
|
29
|
+
attr_reader :base_url
|
|
30
|
+
|
|
31
|
+
# The default class for parsing JSON arrays
|
|
32
|
+
#
|
|
33
|
+
# {Client#default_array_class} returns it.
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
36
|
+
# @return [Class] the default class for parsing JSON arrays
|
|
37
|
+
attr_reader :default_array_class
|
|
38
|
+
|
|
39
|
+
# The default class for parsing JSON objects
|
|
40
|
+
#
|
|
41
|
+
# {Client#default_object_class} returns it.
|
|
42
|
+
#
|
|
43
|
+
# @api private
|
|
44
|
+
# @return [Class, #from_response] the default class for parsing JSON objects
|
|
45
|
+
attr_reader :default_object_class
|
|
46
|
+
|
|
47
|
+
# The callable passed an X::Response after each request and streamed object
|
|
48
|
+
#
|
|
49
|
+
# {Client#on_response} returns it.
|
|
50
|
+
#
|
|
51
|
+
# @api private
|
|
52
|
+
# @return [#call, nil] the callable, or nil for none
|
|
53
|
+
attr_reader :on_response
|
|
54
|
+
|
|
55
|
+
# The headers sent with every request the client makes
|
|
56
|
+
#
|
|
57
|
+
# {Client#headers} returns it.
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
60
|
+
# @return [Hash{String => String}] the headers, frozen
|
|
61
|
+
attr_reader :headers
|
|
62
|
+
|
|
63
|
+
def_delegators :@connection, :open_timeout, :read_timeout, :write_timeout, :keep_alive_timeout, :debug_output
|
|
64
|
+
def_delegators :@redirect_handler, :max_redirects
|
|
65
|
+
def_delegators :@rate_limit_handler, :max_rate_limit_retries, :max_rate_limit_wait
|
|
66
|
+
def_delegators :@retry_handler, :max_retries
|
|
67
|
+
|
|
68
|
+
# Send a request that is safe to send twice again after a failure
|
|
69
|
+
#
|
|
70
|
+
# It is what Client#with_retries does.
|
|
71
|
+
#
|
|
72
|
+
# @api private
|
|
73
|
+
# @yield sends the request
|
|
74
|
+
# @return [Object] what the block returns
|
|
75
|
+
def with_retries(&) = @rate_limit_handler.handing_down { @retry_handler.handle(idempotent: true, resend_unanswered: true, &) }
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Share the connections kept open by the client this one was copied from
|
|
80
|
+
#
|
|
81
|
+
# They are shared when this client opens its connections as that one does; see Connection#share_pool_of.
|
|
82
|
+
# A copy that differs only in what it sends, such as its headers, base URL, or credentials, would otherwise
|
|
83
|
+
# open connections of its own, with a TCP and TLS handshake for each, and keep them open, idle, until it is
|
|
84
|
+
# collected, so a copy made for each request would leave connections to each host behind it.
|
|
85
|
+
#
|
|
86
|
+
# The internals of the client copied call it on those of the copy with __send__, since it is private.
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
# @param connection [Connection] the connection of the client this one was copied from
|
|
90
|
+
# @return [void]
|
|
91
|
+
def share_connection(connection) = @connection.share_pool_of(connection)
|
|
92
|
+
|
|
93
|
+
# The settings, as initialize accepts them
|
|
94
|
+
# @api private
|
|
95
|
+
# @return [Hash{Symbol => Object}] the settings
|
|
96
|
+
def settings
|
|
97
|
+
{base_url:, open_timeout:, read_timeout:, write_timeout:, keep_alive_timeout:, debug_output:, proxy_url:,
|
|
98
|
+
default_array_class:, default_object_class:, headers:, max_redirects:, max_rate_limit_retries:,
|
|
99
|
+
max_rate_limit_wait:, max_retries:, on_response:, save_tokens:, load_tokens:}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Initialize the settings, and the handlers of redirects, rate limits, and retries
|
|
103
|
+
#
|
|
104
|
+
# An endpoint is resolved against the base URL, which drops the last segment of a path that does not end with a
|
|
105
|
+
# slash, so a slash is added to a base URL without one. The headers are copied and frozen, so that changing the
|
|
106
|
+
# Hash a client was built with never changes what it sends.
|
|
107
|
+
#
|
|
108
|
+
# @api private
|
|
109
|
+
# @param base_url [String] the base URL for API requests
|
|
110
|
+
# @param default_array_class [Class] the default class for parsing JSON arrays
|
|
111
|
+
# @param default_object_class [Class, #from_response] the default class for parsing JSON objects
|
|
112
|
+
# @param headers [Hash{String, Symbol => String}] the headers sent with every request
|
|
113
|
+
# @param on_response [#call, nil] the callable passed an X::Response after every request and streamed object
|
|
114
|
+
# @param max_redirects [Integer] the maximum number of redirects to follow
|
|
115
|
+
# @param max_rate_limit_retries [Integer] the maximum number of times to retry a request refused for a rate limit
|
|
116
|
+
# @param max_rate_limit_wait [Integer, Float] the maximum number of seconds to wait for a rate limit to reset
|
|
117
|
+
# @param max_retries [Integer] the maximum number of times to send an idempotent request again after a failure
|
|
118
|
+
# @return [void]
|
|
119
|
+
# @raise [ArgumentError] if on_response is neither nil nor responds to call
|
|
120
|
+
# @raise [ArgumentError] if default_array_class is not a Class, or default_object_class is neither a Class nor
|
|
121
|
+
# responds to from_response
|
|
122
|
+
def initialize_settings(base_url:, default_array_class:, default_object_class:, headers:, on_response:,
|
|
123
|
+
max_redirects:, max_rate_limit_retries:, max_rate_limit_wait:, max_retries:)
|
|
124
|
+
base_url = SettingValidator.base_url!(base_url)
|
|
125
|
+
@base_url = (base_url.end_with?("/") ? base_url : "#{base_url}/").freeze
|
|
126
|
+
@default_array_class = SettingValidator.array_class!(:default_array_class, default_array_class)
|
|
127
|
+
@default_object_class = SettingValidator.object_class!(:default_object_class, default_object_class)
|
|
128
|
+
@headers = SettingValidator.headers!(headers).freeze
|
|
129
|
+
@on_response = SettingValidator.callable!(:on_response, on_response)
|
|
130
|
+
@redirect_handler = RedirectHandler.new(connection: @connection, request_builder: @request_builder, max_redirects:)
|
|
131
|
+
@rate_limit_handler = RateLimitHandler.new(max_rate_limit_retries:, max_rate_limit_wait:)
|
|
132
|
+
@retry_handler = RetryHandler.new(max_retries:)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The headers of a request, the client's under the request's own
|
|
136
|
+
#
|
|
137
|
+
# A header of the request is sent in place of one of the client whose name differs from it in case alone.
|
|
138
|
+
#
|
|
139
|
+
# @api private
|
|
140
|
+
# @param request_headers [Hash{String => String}] the headers passed to the request
|
|
141
|
+
# @return [Hash{String => String}] the headers to send
|
|
142
|
+
def headers_for(request_headers) = RequestBuilder.merge_headers(headers, request_headers)
|
|
143
|
+
|
|
144
|
+
# Pass a response to on_response and to the block of the request
|
|
145
|
+
#
|
|
146
|
+
# Both receive the one summary, the client's hook first, so that a hook which counts every request and a block
|
|
147
|
+
# which reads the response of one see the same object. Neither is built a summary when there is nothing to
|
|
148
|
+
# pass it to.
|
|
149
|
+
#
|
|
150
|
+
# @api private
|
|
151
|
+
# @param http_method [Symbol, String] the HTTP method of the request, in any case
|
|
152
|
+
# @param uri [URI::Generic] the URI of the request
|
|
153
|
+
# @param response [Net::HTTPResponse] the HTTP response
|
|
154
|
+
# @yieldparam response [Response] the summary of the response
|
|
155
|
+
# @return [void]
|
|
156
|
+
def report(http_method, uri, response, &block)
|
|
157
|
+
return unless on_response || block
|
|
158
|
+
|
|
159
|
+
summary = Response.new(http_response: response, http_method:, uri:)
|
|
160
|
+
on_response&.call(summary)
|
|
161
|
+
block&.call(summary)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
private_constant :ClientSettings
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "app_only_authenticator"
|
|
4
|
+
require_relative "oauth2_authenticator"
|
|
5
|
+
require_relative "setting_validator"
|
|
6
|
+
|
|
7
|
+
module X
|
|
8
|
+
module Core
|
|
9
|
+
# The OAuth 2.0 authenticator of a client, which its copies share, and the tokens it last refreshed, included into
|
|
10
|
+
# ClientInternals
|
|
11
|
+
# @api private
|
|
12
|
+
module ClientTokenRefresh
|
|
13
|
+
# The message of the error raised for a copy given an expiration time or scopes for the access token it shares
|
|
14
|
+
SHARED_EXPIRATION = "A copy that shares the access token of the client shares its expiration time and scopes, " \
|
|
15
|
+
"so it cannot be given %s. Pass it beside the access token and refresh token it belongs to"
|
|
16
|
+
private_constant :SHARED_EXPIRATION
|
|
17
|
+
# The options that belong to the tokens of the client, which a copy that does not keep the client's
|
|
18
|
+
# authenticator holds only when it is given them
|
|
19
|
+
TOKEN_OPTIONS = %i[refresh_token expires_at scopes save_tokens load_tokens].freeze
|
|
20
|
+
# The options that are credentials, any of which a copy that authenticates as the client is not given
|
|
21
|
+
CREDENTIAL_OPTIONS = %i[api_key api_key_secret access_token access_token_secret bearer_token client_id client_secret refresh_token].freeze
|
|
22
|
+
private_constant :TOKEN_OPTIONS, :CREDENTIAL_OPTIONS
|
|
23
|
+
|
|
24
|
+
# The time the OAuth 2.0 access token expires, as last refreshed
|
|
25
|
+
#
|
|
26
|
+
# {Client#expires_at} returns it.
|
|
27
|
+
#
|
|
28
|
+
# @api private
|
|
29
|
+
# @return [Time, nil] the expiration time, or nil if it is not known
|
|
30
|
+
def expires_at
|
|
31
|
+
current = oauth2_authenticator_in_use
|
|
32
|
+
current ? current.expires_at : @expires_at
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The scopes X granted the OAuth 2.0 access token, as last refreshed
|
|
36
|
+
#
|
|
37
|
+
# {Client#scopes} returns it.
|
|
38
|
+
#
|
|
39
|
+
# @api private
|
|
40
|
+
# @return [Array<String>, nil] the scopes, or nil if they are not known
|
|
41
|
+
def scopes
|
|
42
|
+
current = oauth2_authenticator_in_use
|
|
43
|
+
current ? current.scopes : @scopes
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Initialize the callables that store and load the tokens of a refresh
|
|
49
|
+
# @api private
|
|
50
|
+
# @param save_tokens [#call, nil] the callable passed the OAuth2Tokens of each refresh
|
|
51
|
+
# @param load_tokens [#call, nil] the callable that returns the OAuth2Tokens in the store
|
|
52
|
+
# @return [void]
|
|
53
|
+
# @raise [ArgumentError] if save_tokens or load_tokens is neither nil nor responds to call
|
|
54
|
+
def initialize_token_hooks(save_tokens:, load_tokens:)
|
|
55
|
+
@save_tokens = SettingValidator.callable!(:save_tokens, save_tokens)
|
|
56
|
+
@load_tokens = SettingValidator.callable!(:load_tokens, load_tokens)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Share the authenticator of the client this one was copied from
|
|
60
|
+
#
|
|
61
|
+
# A copy that holds the credentials of the client shares its OAuth 2.0 or app-only authenticator, so that the
|
|
62
|
+
# copy refreshes the tokens of the client, or sends the bearer token it fetched, rather than hold tokens of its
|
|
63
|
+
# own. A copy given an authenticator of its own keeps it. The internals of the client copied call it on those of
|
|
64
|
+
# the copy with __send__, since it is private.
|
|
65
|
+
#
|
|
66
|
+
# @api private
|
|
67
|
+
# @param copy [Client] the copy these are the internals of
|
|
68
|
+
# @param other [Authenticator] the authenticator of the client this one was copied from
|
|
69
|
+
# @param options [Hash] the options the copy was given in place of the client's
|
|
70
|
+
# @return [void]
|
|
71
|
+
def share_authenticator(copy, other, options)
|
|
72
|
+
return if options[:authenticator]
|
|
73
|
+
|
|
74
|
+
case other
|
|
75
|
+
when AppOnlyAuthenticator then share_app_only(other, options)
|
|
76
|
+
when OAuth2Authenticator then share_oauth2(copy, other, options)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Share the OAuth 2.0 authenticator of the client this one was copied from
|
|
81
|
+
#
|
|
82
|
+
# A refresh by either client then reaches both. X accepts a refresh token once, so a copy that refreshed with
|
|
83
|
+
# an authenticator of its own would leave the original client with a refresh token that no longer works.
|
|
84
|
+
#
|
|
85
|
+
# Whether the two share it is decided by the options the copy was given, not by the tokens it was built with:
|
|
86
|
+
# a refresh on another thread may replace them while it is built, and a copy that held on to the ones replaced
|
|
87
|
+
# could never refresh again. The expiration time and scopes are facts about the access token the two hold, so a
|
|
88
|
+
# copy that shares it is refused either, rather than set it for the client it was copied from.
|
|
89
|
+
#
|
|
90
|
+
# @api private
|
|
91
|
+
# @param copy [Client] the copy these are the internals of
|
|
92
|
+
# @param other [OAuth2Authenticator] the authenticator of the client this one was copied from
|
|
93
|
+
# @param options [Hash] the options the copy was given in place of the client's
|
|
94
|
+
# @return [void]
|
|
95
|
+
# @raise [ArgumentError] if the copy shares the authenticator and was given an expiration time or scopes
|
|
96
|
+
def share_oauth2(copy, other, options)
|
|
97
|
+
return unless oauth2_authenticator_in_use && other.__send__(:holds?, options)
|
|
98
|
+
|
|
99
|
+
given = options.keys & %i[expires_at scopes]
|
|
100
|
+
raise ArgumentError, format(SHARED_EXPIRATION, given.join(" or ")) unless given.empty?
|
|
101
|
+
|
|
102
|
+
@authenticator = join(copy, other)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The options of a copy, without the tokens of a client it does not share
|
|
106
|
+
#
|
|
107
|
+
# A copy that authenticates otherwise than the client authenticates with tokens that may be another user's. It
|
|
108
|
+
# holds the refresh token, expiration time, scopes, save_tokens, and load_tokens of the client only when it is
|
|
109
|
+
# given them: a refresh of the copy would otherwise spend the refresh token the client holds, which X accepts
|
|
110
|
+
# once, read the store of the client with its load_tokens and take the client's tokens in place of its own, or
|
|
111
|
+
# pass its tokens to the save_tokens of the client, which would store them in place of the client's.
|
|
112
|
+
#
|
|
113
|
+
# @api private
|
|
114
|
+
# @param copied [Hash{Symbol => Object}] the options the copy is built with
|
|
115
|
+
# @param options [Hash{Symbol => Object}] the options the copy was given
|
|
116
|
+
# @return [Hash{Symbol => Object}] the options, without those of the tokens of the client the copy was not given
|
|
117
|
+
def without_tokens_of_client(copied, options)
|
|
118
|
+
keeps_authenticator?(copied, options) ? copied : copied.except(*(TOKEN_OPTIONS - options.keys))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Check whether a copy keeps the authenticator of the client
|
|
122
|
+
#
|
|
123
|
+
# A copy of a client given its authenticator keeps it unless given a credential or an authenticator, which
|
|
124
|
+
# replaces it. A copy of a client built from credentials keeps its OAuth 2.0 authenticator unless given an
|
|
125
|
+
# authenticator, or a client ID, client secret, access token, or refresh token the authenticator does not hold,
|
|
126
|
+
# and keeps any other authenticator unless given an authenticator or any credential.
|
|
127
|
+
#
|
|
128
|
+
# @api private
|
|
129
|
+
# @param copied [Hash{Symbol => Object}] the options the copy is built with
|
|
130
|
+
# @param options [Hash{Symbol => Object}] the options the copy was given
|
|
131
|
+
# @return [Boolean] whether the copy authenticates as the client does
|
|
132
|
+
def keeps_authenticator?(copied, options)
|
|
133
|
+
return copied[:authenticator].equal?(@authenticator) if @given_authenticator
|
|
134
|
+
return false if options[:authenticator]
|
|
135
|
+
|
|
136
|
+
current = oauth2_authenticator_in_use
|
|
137
|
+
current ? current.__send__(:holds?, options) : !options.keys.intersect?(CREDENTIAL_OPTIONS)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Share the app-only authenticator of the client this one was copied from
|
|
141
|
+
#
|
|
142
|
+
# It is shared by a copy that authenticates as the app with the API key and secret the authenticator holds, at
|
|
143
|
+
# the origin it fetches the bearer token at, and was given no bearer token of its own, so the bearer token the
|
|
144
|
+
# client fetched, or fetches, is fetched once, and is sent to no other origin than the one that issued it.
|
|
145
|
+
#
|
|
146
|
+
# @api private
|
|
147
|
+
# @param other [AppOnlyAuthenticator] the authenticator of the client this one was copied from
|
|
148
|
+
# @param options [Hash] the options the copy was given in place of the client's
|
|
149
|
+
# @return [void]
|
|
150
|
+
def share_app_only(other, options)
|
|
151
|
+
return unless AppOnlyAuthenticator === @authenticator && other.__send__(:holds?, options)
|
|
152
|
+
|
|
153
|
+
@authenticator = other if other.__send__(:fetched_for?, base_url)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Take an authenticator the client was given, as it would one it built
|
|
157
|
+
#
|
|
158
|
+
# The token requests of an authenticator that makes them are sent over the connection of the first client
|
|
159
|
+
# given it, and the refreshes of an OAuth 2.0 one reach the save_tokens of each client that shares it.
|
|
160
|
+
#
|
|
161
|
+
# @api private
|
|
162
|
+
# @param client [Client] the client these are the internals of
|
|
163
|
+
# @param authenticator [Authenticator] the authenticator
|
|
164
|
+
# @return [Authenticator] the authenticator
|
|
165
|
+
def take(client, authenticator)
|
|
166
|
+
case authenticator
|
|
167
|
+
when OAuth2Authenticator then join(client, authenticator.__send__(:token_requests_over, @connection, base_url, headers))
|
|
168
|
+
when AppOnlyAuthenticator then authenticator.__send__(:token_requests_over, @connection, base_url, headers)
|
|
169
|
+
else authenticator
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Join the clients whose save_tokens an OAuth 2.0 authenticator reports to
|
|
174
|
+
# @api private
|
|
175
|
+
# @param client [Client] the client these are the internals of
|
|
176
|
+
# @param authenticator [OAuth2Authenticator] the authenticator
|
|
177
|
+
# @return [OAuth2Authenticator] the authenticator
|
|
178
|
+
def join(client, authenticator)
|
|
179
|
+
clients = authenticator.__send__(:clients)
|
|
180
|
+
clients[client] = true
|
|
181
|
+
authenticator.__send__(:report_refreshes_to, -> { clients.keys.filter_map(&:save_tokens).uniq })
|
|
182
|
+
authenticator
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# The OAuth 2.0 authenticator, if the client authenticates with one
|
|
186
|
+
# @api private
|
|
187
|
+
# @return [OAuth2Authenticator, nil] the authenticator or nil
|
|
188
|
+
def oauth2_authenticator_in_use
|
|
189
|
+
current = @authenticator
|
|
190
|
+
current if current.is_a?(OAuth2Authenticator)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The OAuth 2.0 authenticator of the client's credentials, if they form a set
|
|
194
|
+
#
|
|
195
|
+
# A copy of a client shares the authenticator of the client it was copied from, rather than the one this
|
|
196
|
+
# builds, when the two hold the same credentials; see share_authenticator. A client ID and access token without
|
|
197
|
+
# a refresh token, as an authorization without offline.access issues them, build an authenticator that acts
|
|
198
|
+
# for the user and cannot refresh.
|
|
199
|
+
#
|
|
200
|
+
# @api private
|
|
201
|
+
# @param client [Client] the client these are the internals of
|
|
202
|
+
# @return [OAuth2Authenticator, nil] the OAuth 2.0 authenticator or nil
|
|
203
|
+
def oauth2_authenticator(client)
|
|
204
|
+
client_id = @client_id
|
|
205
|
+
access_token = @access_token
|
|
206
|
+
return unless client_id && access_token
|
|
207
|
+
|
|
208
|
+
new_oauth2_authenticator(client, client_id:, access_token:, refresh_token: @refresh_token)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# The OAuth 2.0 authenticator of the client's credentials, as last refreshed
|
|
212
|
+
#
|
|
213
|
+
# It is the one the client built of them, or shares with the client it was copied from. A client given its
|
|
214
|
+
# authenticator holds no credentials, so the tokens of that authenticator are none of its credentials, which a
|
|
215
|
+
# copy would be built with beside it.
|
|
216
|
+
#
|
|
217
|
+
# @api private
|
|
218
|
+
# @return [OAuth2Authenticator, nil] the authenticator, or nil for a client that holds no OAuth 2.0 credentials
|
|
219
|
+
def oauth2_credentials_in_use = (oauth2_authenticator_in_use unless @given_authenticator)
|
|
220
|
+
|
|
221
|
+
# The access token for OAuth authentication, as last refreshed
|
|
222
|
+
#
|
|
223
|
+
# It is private, as {ClientCredentials#api_key_secret} is, and as the access token of the authenticator is. A
|
|
224
|
+
# hook given to save_tokens is passed the OAuth2Tokens of the refresh it reports.
|
|
225
|
+
#
|
|
226
|
+
# @api private
|
|
227
|
+
# @return [String, nil] the access token for OAuth authentication
|
|
228
|
+
def access_token
|
|
229
|
+
current = oauth2_credentials_in_use
|
|
230
|
+
current ? current.__send__(:access_token) : @access_token
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The OAuth 2.0 refresh token, as last refreshed
|
|
234
|
+
#
|
|
235
|
+
# It is private for the reason {#access_token} is.
|
|
236
|
+
#
|
|
237
|
+
# @api private
|
|
238
|
+
# @return [String, nil] the OAuth 2.0 refresh token
|
|
239
|
+
def refresh_token
|
|
240
|
+
current = oauth2_credentials_in_use
|
|
241
|
+
current ? current.__send__(:refresh_token) : @refresh_token
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Build an OAuth 2.0 authenticator whose refreshes reach the clients that share it
|
|
245
|
+
#
|
|
246
|
+
# It sends its token requests over the client's connection. A public client has no client secret, and refreshes
|
|
247
|
+
# its tokens with its client ID alone.
|
|
248
|
+
#
|
|
249
|
+
# @api private
|
|
250
|
+
# @param client [Client] the client these are the internals of
|
|
251
|
+
# @param client_id [String] the OAuth 2.0 client ID
|
|
252
|
+
# @param access_token [String] the OAuth 2.0 access token
|
|
253
|
+
# @param refresh_token [String, nil] the OAuth 2.0 refresh token, or nil for an access token that is not refreshed
|
|
254
|
+
# @return [OAuth2Authenticator] the OAuth 2.0 authenticator
|
|
255
|
+
def new_oauth2_authenticator(client, client_id:, access_token:, refresh_token:)
|
|
256
|
+
authenticator = OAuth2Authenticator.new(client_id:, client_secret: @client_secret, access_token:, refresh_token:, expires_at: @expires_at, scopes: @scopes)
|
|
257
|
+
join(client, authenticator.__send__(:token_requests_over, @connection, base_url, headers))
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Run a request, again if a refresh replaces an OAuth 2.0 token the API rejects
|
|
261
|
+
#
|
|
262
|
+
# Only a rejection by the origin of the base URL, which the token is sent to, refreshes it; see {Origin}. An
|
|
263
|
+
# app-only bearer token the API rejects is fetched again the same way; see {AppOnlyAuthenticator}. A stream
|
|
264
|
+
# that Client#get_stream opens runs through it too.
|
|
265
|
+
#
|
|
266
|
+
# @api private
|
|
267
|
+
# @param client [Client] the client these are the internals of, which a refresh that fails to report is raised
|
|
268
|
+
# with
|
|
269
|
+
# @yield runs the request
|
|
270
|
+
# @return [Object] what the block returns
|
|
271
|
+
def refreshing_rejected_token(client, &)
|
|
272
|
+
case (current = @authenticator)
|
|
273
|
+
when OAuth2Authenticator then current.__send__(:retrying_rejected_token, URI(base_url), @connection, client, &)
|
|
274
|
+
when AppOnlyAuthenticator then current.__send__(:retrying_rejected_token, URI(base_url), &)
|
|
275
|
+
else yield
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
private_constant :ClientTokenRefresh
|
|
280
|
+
end
|
|
281
|
+
end
|