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,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "simple_oauth"
|
|
4
|
+
require_relative "authenticator"
|
|
5
|
+
require_relative "connection"
|
|
6
|
+
require_relative "credential_validator"
|
|
7
|
+
require_relative "setting_validator"
|
|
8
|
+
require_relative "errors/authorization_error"
|
|
9
|
+
require_relative "errors/unauthorized"
|
|
10
|
+
require_relative "origin"
|
|
11
|
+
require_relative "token_endpoint"
|
|
12
|
+
|
|
13
|
+
module X
|
|
14
|
+
module Core
|
|
15
|
+
# Authenticates as an app with a bearer token, fetched with the API key and secret when first needed
|
|
16
|
+
#
|
|
17
|
+
# A bearer token the API rejects with 401 Unauthorized, as it does one that was invalidated, is dropped, and the
|
|
18
|
+
# request is sent again with one fetched in its place.
|
|
19
|
+
#
|
|
20
|
+
# @api public
|
|
21
|
+
class ::X::AppOnlyAuthenticator < Authenticator
|
|
22
|
+
# The endpoint that exchanges an API key and secret for a bearer token, at X, whose path is requested at the
|
|
23
|
+
# origin of the base URL of the client that takes the authenticator
|
|
24
|
+
TOKEN_URL = "https://api.x.com/oauth2/token"
|
|
25
|
+
# The message raised when the token endpoint describes no reason for the failure
|
|
26
|
+
DEFAULT_ERROR_MESSAGE = "Bearer token request failed"
|
|
27
|
+
private_constant :TOKEN_URL, :DEFAULT_ERROR_MESSAGE
|
|
28
|
+
|
|
29
|
+
# The API key
|
|
30
|
+
# @api public
|
|
31
|
+
# @return [String] the API key
|
|
32
|
+
# @example Get the API key
|
|
33
|
+
# authenticator.api_key
|
|
34
|
+
attr_reader :api_key
|
|
35
|
+
|
|
36
|
+
# Initialize a new app-only authenticator
|
|
37
|
+
#
|
|
38
|
+
# @api public
|
|
39
|
+
# @param api_key [String] the API key
|
|
40
|
+
# @param api_key_secret [String] the API key secret
|
|
41
|
+
# @param bearer_token [String, nil] a bearer token already fetched with these credentials
|
|
42
|
+
# @return [AppOnlyAuthenticator] a new authenticator
|
|
43
|
+
# @raise [ArgumentError] if the API key or secret is nil or empty, or the bearer token is empty
|
|
44
|
+
# @example Create an app-only authenticator
|
|
45
|
+
# X::AppOnlyAuthenticator.new(api_key: "key", api_key_secret: "secret")
|
|
46
|
+
def initialize(api_key:, api_key_secret:, bearer_token: nil)
|
|
47
|
+
CredentialValidator.validate_required!({api_key:, api_key_secret:}, {bearer_token:})
|
|
48
|
+
@api_key, @api_key_secret = SettingValidator.frozen(api_key), SettingValidator.frozen(api_key_secret)
|
|
49
|
+
@bearer_token = SettingValidator.frozen(bearer_token)
|
|
50
|
+
@connection, @token_url, @token_headers = Connection.new, TOKEN_URL, {}
|
|
51
|
+
@mutex = Mutex.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Generate the authentication headers, fetching the bearer token first if needed
|
|
55
|
+
#
|
|
56
|
+
# @api public
|
|
57
|
+
# @param _request [#http_method, #uri, #body, #[], nil] the request, which app-only authentication does not sign
|
|
58
|
+
# @return [Hash{String => String}] the authorization header
|
|
59
|
+
# @raise [AuthorizationError] if X refuses to issue the bearer token
|
|
60
|
+
# @raise [HTTPError, InvalidResponse] if the token endpoint limits the rate of the request or fails to answer,
|
|
61
|
+
# as a server error, a redirect, or the page of a proxy says
|
|
62
|
+
# @example Generate the header
|
|
63
|
+
# authenticator.headers(request) # => {"Authorization" => "Bearer ..."}
|
|
64
|
+
def headers(_request)
|
|
65
|
+
{AUTHENTICATION_HEADER => "Bearer #{bearer_token}"}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# The bearer token, fetched once with the API key and secret
|
|
71
|
+
#
|
|
72
|
+
# It is a secret, so it is private, as the bearer token of a BearerTokenAuthenticator is. Internal to x-core:
|
|
73
|
+
# a client that authenticates as the app fetches its token through it, and calls it with __send__.
|
|
74
|
+
#
|
|
75
|
+
# @api private
|
|
76
|
+
# @return [String] the bearer token
|
|
77
|
+
# @raise [AuthorizationError] if X refuses to issue the bearer token
|
|
78
|
+
# @raise [HTTPError, InvalidResponse] if the token endpoint limits the rate of the request or fails to answer,
|
|
79
|
+
# as a server error, a redirect, or the page of a proxy says
|
|
80
|
+
# @example Get the bearer token
|
|
81
|
+
# bearer_token
|
|
82
|
+
def bearer_token
|
|
83
|
+
@mutex.synchronize { @bearer_token ||= fetch_bearer_token }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The connection the bearer token is fetched over
|
|
87
|
+
# @api private
|
|
88
|
+
# @return [Core::Connection] the connection
|
|
89
|
+
attr_reader :connection
|
|
90
|
+
|
|
91
|
+
# Fetch the bearer token over the connection of the first client that takes it
|
|
92
|
+
#
|
|
93
|
+
# The token endpoint is requested at the scheme, host, and port of the base URL of the client, rather than of
|
|
94
|
+
# TOKEN_URL, so that a client pointed at another host sends the API key and secret there, as it sends its
|
|
95
|
+
# requests. A client that takes it after the first leaves it fetching over the connection of the first; see
|
|
96
|
+
# {OAuth2Authenticator}. Internal to x-core: a client fetches the token of the authenticator it builds, or is
|
|
97
|
+
# given, over its own connection, with its proxy, timeouts, debug output, and headers, and calls it with
|
|
98
|
+
# __send__, since it is private.
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
# @param connection [Core::Connection] the connection to fetch the token over
|
|
102
|
+
# @param base_url [String] the base URL of the client, at whose origin the token endpoint is requested
|
|
103
|
+
# @param headers [Hash{String => String}] the headers of the client, which the token request is sent with
|
|
104
|
+
# @return [AppOnlyAuthenticator] the authenticator
|
|
105
|
+
def token_requests_over(connection, base_url, headers)
|
|
106
|
+
@mutex.synchronize do
|
|
107
|
+
unless @taken
|
|
108
|
+
@connection = connection
|
|
109
|
+
@token_url = TokenEndpoint.url_at(base_url, TOKEN_URL)
|
|
110
|
+
@token_headers = headers
|
|
111
|
+
end
|
|
112
|
+
@taken = true
|
|
113
|
+
end
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The API key secret, which buys the bearer token
|
|
118
|
+
# @api private
|
|
119
|
+
# @return [String] the API key secret
|
|
120
|
+
# @example Buy a bearer token with the API key secret
|
|
121
|
+
# api_key_secret
|
|
122
|
+
attr_reader :api_key_secret
|
|
123
|
+
|
|
124
|
+
# Check whether a copy of a client, given these options, holds these credentials
|
|
125
|
+
#
|
|
126
|
+
# Internal to x-core: Client shares the authenticator with a copy that holds its API key and secret and was given
|
|
127
|
+
# no bearer token, and calls it with __send__, since it is private.
|
|
128
|
+
#
|
|
129
|
+
# @api private
|
|
130
|
+
# @param options [Hash] the options the copy was given in place of the client's
|
|
131
|
+
# @return [Boolean] true if the options name no bearer token, and no API key or secret but the ones held
|
|
132
|
+
def holds?(options)
|
|
133
|
+
!options.key?(:bearer_token) && options.slice(:api_key, :api_key_secret) <= {api_key:, api_key_secret:}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check whether the bearer token is fetched at the origin of a base URL
|
|
137
|
+
#
|
|
138
|
+
# A token is fetched at the origin of the client that took the authenticator first, and sent there alone, so a
|
|
139
|
+
# copy of the client pointed at another origin fetches one of its own, rather than send it the client's.
|
|
140
|
+
# Internal to x-core: Client shares the authenticator with a copy at the same origin alone, and calls it with
|
|
141
|
+
# __send__, since it is private.
|
|
142
|
+
#
|
|
143
|
+
# @api private
|
|
144
|
+
# @param base_url [String] the base URL of the copy
|
|
145
|
+
# @return [Boolean] true if the token endpoint is at the origin of the base URL
|
|
146
|
+
def fetched_for?(base_url) = Origin.same?(URI(@token_url), URI(base_url))
|
|
147
|
+
|
|
148
|
+
# Run a request, again with a bearer token fetched in place of one the API rejects
|
|
149
|
+
#
|
|
150
|
+
# Only a rejection by the origin the token is sent to drops it; see {Core::Origin}. A token fetched for the
|
|
151
|
+
# request itself is not fetched again, since the endpoint that just issued it would issue it again. Internal to
|
|
152
|
+
# x-core: Client runs each request through it, and calls it with __send__, since it is private.
|
|
153
|
+
#
|
|
154
|
+
# @api private
|
|
155
|
+
# @param origin [URI::Generic] the base URL of the client, the origin the token is sent to
|
|
156
|
+
# @yield runs the request
|
|
157
|
+
# @return [Object] what the block returns
|
|
158
|
+
# @raise [Unauthorized] if the API rejects the token fetched in place of the one it rejected
|
|
159
|
+
def retrying_rejected_token(origin)
|
|
160
|
+
token = @bearer_token
|
|
161
|
+
begin
|
|
162
|
+
yield
|
|
163
|
+
rescue Unauthorized => e
|
|
164
|
+
raise unless token && Origin.answered?(e, origin)
|
|
165
|
+
|
|
166
|
+
drop_bearer_token(token)
|
|
167
|
+
yield
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Drop a bearer token the API rejected, unless another request already replaced it
|
|
172
|
+
# @api private
|
|
173
|
+
# @param rejected [String] the bearer token the API rejected
|
|
174
|
+
# @return [void]
|
|
175
|
+
def drop_bearer_token(rejected)
|
|
176
|
+
@mutex.synchronize { @bearer_token = nil if @bearer_token.eql?(rejected) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Exchange the API key and secret for a bearer token
|
|
180
|
+
# @api private
|
|
181
|
+
# @return [String] the bearer token
|
|
182
|
+
# @raise [AuthorizationError] if the token endpoint rejects the request
|
|
183
|
+
# @raise [HTTPError, InvalidResponse] if the token endpoint limits the rate of the request or fails to answer,
|
|
184
|
+
# as a server error, a redirect, or the page of a proxy says
|
|
185
|
+
def fetch_bearer_token
|
|
186
|
+
TokenEndpoint.fetch(token_request, connection:, refusal: DEFAULT_ERROR_MESSAGE, headers: @token_headers).access_token
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Build the client credentials request
|
|
190
|
+
# @api private
|
|
191
|
+
# @return [SimpleOAuth::OAuth2::Request] the token request
|
|
192
|
+
def token_request
|
|
193
|
+
SimpleOAuth::OAuth2::Client.new(client_id: api_key, client_secret: api_key_secret, token_endpoint: @token_url)
|
|
194
|
+
.client_credentials_request
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "credential_holder"
|
|
4
|
+
|
|
5
|
+
# A Ruby client for the X API
|
|
6
|
+
module X
|
|
7
|
+
module Core
|
|
8
|
+
# Base class for authentication
|
|
9
|
+
#
|
|
10
|
+
# Subclass it to authenticate with a scheme of your own, overriding {#headers}, as the authenticators of x-core do.
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
13
|
+
class ::X::Authenticator
|
|
14
|
+
include CredentialHolder
|
|
15
|
+
|
|
16
|
+
# The HTTP header name for authentication
|
|
17
|
+
AUTHENTICATION_HEADER = "Authorization"
|
|
18
|
+
|
|
19
|
+
# Generate the authentication headers for a request, which authenticates as no one
|
|
20
|
+
#
|
|
21
|
+
# A client calls it for every request it sends to the origin of its base URL, and every stream it opens there,
|
|
22
|
+
# once the request is built, just before it is sent, and sends each header it returns, in place of any header of
|
|
23
|
+
# the same name. Its method, URI, headers, and body are set, so an authenticator of your own can sign any of them:
|
|
24
|
+
# subclass X::Authenticator and override this method, and give an instance to the authenticator: of
|
|
25
|
+
# X::Client.new or X::Client#with. A request to another origin, such as one a redirect leads to, is not passed to
|
|
26
|
+
# it, so its headers never leave the origin they were built for. It is called on the thread that sends the
|
|
27
|
+
# request, so one a client shares across threads must be thread-safe.
|
|
28
|
+
#
|
|
29
|
+
# The request answers four methods alone, which are all the authenticators of x-core read of it: http_method, the
|
|
30
|
+
# HTTP method as a Symbol, such as :post, as a response and an error name it; uri, the URI::Generic it is sent
|
|
31
|
+
# to, query included; body, the String it sends, or nil for none; and [], the value of a header by its name, in
|
|
32
|
+
# any case, or nil for one it does not send. It answers them alone whatever request the client sends, so an
|
|
33
|
+
# authenticator of your own reads nothing a later version of 1.x could take away.
|
|
34
|
+
#
|
|
35
|
+
# A client refreshes the token of none but its own OAuth 2.0 authenticator, so an authenticator of your own that
|
|
36
|
+
# holds a token that expires refreshes it here. A client given one takes it to authenticate as the app, as it
|
|
37
|
+
# does an X::Authenticator itself, so app_only returns the client, and a stream is opened with it.
|
|
38
|
+
#
|
|
39
|
+
# @api public
|
|
40
|
+
# @param _request [#http_method, #uri, #body, #[]] the request, which answers http_method, uri, body, and [] alone
|
|
41
|
+
# @return [Hash{String => String}] the headers that authenticate the request, empty for none
|
|
42
|
+
# @example Authenticate every request with a token an application keeps
|
|
43
|
+
# class VaultAuthenticator < X::Authenticator
|
|
44
|
+
# def headers(_request) = {AUTHENTICATION_HEADER => "Bearer #{Vault.read("x/bearer_token")}"}
|
|
45
|
+
# end
|
|
46
|
+
# client = X::Client.new(authenticator: VaultAuthenticator.new)
|
|
47
|
+
def headers(_request)
|
|
48
|
+
{}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The identifier of the user the credentials act for, when they name one
|
|
52
|
+
#
|
|
53
|
+
# Only an OAuth 1.0a access token names its user, so every other set of credentials answers nil, and the caller
|
|
54
|
+
# that wants the user of such a client asks the API for it.
|
|
55
|
+
#
|
|
56
|
+
# @api public
|
|
57
|
+
# @return [Integer, nil] the identifier, or nil for credentials that name no user
|
|
58
|
+
# @example Read the user a client acts for without a request
|
|
59
|
+
# client.authenticator.user_id # => nil
|
|
60
|
+
def user_id
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Summarize the authenticator for the console without revealing credentials
|
|
64
|
+
#
|
|
65
|
+
# @api public
|
|
66
|
+
# @return [String] the class name
|
|
67
|
+
# @example Inspect an authenticator
|
|
68
|
+
# authenticator.inspect # => #<X::BearerTokenAuthenticator>
|
|
69
|
+
def inspect
|
|
70
|
+
"#<#{self.class}>"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module X
|
|
4
|
+
module Core
|
|
5
|
+
# The request an authenticator is passed: what it may read of the request a client is about to send
|
|
6
|
+
#
|
|
7
|
+
# It answers the four methods an authenticator is guaranteed, and no more, so that an authenticator of your own
|
|
8
|
+
# reads nothing of the Net::HTTPRequest a client sends that a later version of 1.x, which may send another, could
|
|
9
|
+
# take away.
|
|
10
|
+
#
|
|
11
|
+
# Internal to x-core: RequestBuilder passes one to the authenticator of each request it builds.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class AuthenticatorRequest
|
|
15
|
+
# Initialize the request an authenticator reads
|
|
16
|
+
# @api private
|
|
17
|
+
# @param request [Net::HTTPRequest] the request a client is about to send
|
|
18
|
+
# @return [AuthenticatorRequest] a new instance
|
|
19
|
+
# @example Pass a request to an authenticator
|
|
20
|
+
# authenticator.headers(X::Core::AuthenticatorRequest.new(request))
|
|
21
|
+
def initialize(request)
|
|
22
|
+
@request = request
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The HTTP method the request is sent with
|
|
26
|
+
# @api private
|
|
27
|
+
# @return [Symbol] the method, as :get, :post, :put, or :delete
|
|
28
|
+
# @example Read the method
|
|
29
|
+
# request.http_method # => :post
|
|
30
|
+
def http_method = @request.method.downcase.to_sym
|
|
31
|
+
|
|
32
|
+
# The URI the request is sent to, query included
|
|
33
|
+
# @api private
|
|
34
|
+
# @return [URI::Generic] the URI
|
|
35
|
+
# @example Read the URI
|
|
36
|
+
# request.uri # => #<URI::HTTPS https://api.x.com/2/tweets>
|
|
37
|
+
def uri = @request.uri
|
|
38
|
+
|
|
39
|
+
# The body the request sends
|
|
40
|
+
# @api private
|
|
41
|
+
# @return [String, nil] the body, or nil for none
|
|
42
|
+
# @example Read the body
|
|
43
|
+
# request.body # => "{\"text\":\"Hello\"}"
|
|
44
|
+
def body = @request.body
|
|
45
|
+
|
|
46
|
+
# The value of a header the request sends
|
|
47
|
+
# @api private
|
|
48
|
+
# @param name [String] the name of the header, in any case
|
|
49
|
+
# @return [String, nil] the value, or nil for a header the request does not send
|
|
50
|
+
# @example Read the content type
|
|
51
|
+
# request["Content-Type"] # => "application/json; charset=utf-8"
|
|
52
|
+
def [](name) = @request[name]
|
|
53
|
+
end
|
|
54
|
+
private_constant :AuthenticatorRequest
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "authenticator"
|
|
4
|
+
require_relative "credential_validator"
|
|
5
|
+
require_relative "setting_validator"
|
|
6
|
+
|
|
7
|
+
module X
|
|
8
|
+
module Core
|
|
9
|
+
# Authenticator for Bearer token authentication
|
|
10
|
+
# @api public
|
|
11
|
+
class ::X::BearerTokenAuthenticator < Authenticator
|
|
12
|
+
# Initialize a new BearerTokenAuthenticator
|
|
13
|
+
#
|
|
14
|
+
# @api public
|
|
15
|
+
# @param bearer_token [String] the bearer token for authentication
|
|
16
|
+
# @return [BearerTokenAuthenticator] a new instance
|
|
17
|
+
# @raise [ArgumentError] if the bearer token is nil or empty
|
|
18
|
+
# @example Create a new bearer token authenticator
|
|
19
|
+
# authenticator = X::BearerTokenAuthenticator.new(bearer_token: "token")
|
|
20
|
+
def initialize(bearer_token:)
|
|
21
|
+
CredentialValidator.validate_required!({bearer_token:})
|
|
22
|
+
@bearer_token = SettingValidator.frozen(bearer_token)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Generate the authentication headers for a request
|
|
26
|
+
#
|
|
27
|
+
# @api public
|
|
28
|
+
# @param _request [#http_method, #uri, #body, #[], nil] the request, which a bearer token does not sign
|
|
29
|
+
# @return [Hash{String => String}] the authentication header with bearer token
|
|
30
|
+
# @example Generate a bearer authentication header
|
|
31
|
+
# authenticator.headers(request)
|
|
32
|
+
def headers(_request)
|
|
33
|
+
{AUTHENTICATION_HEADER => "Bearer #{bearer_token}"}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# The bearer token, which authenticates a request
|
|
39
|
+
# @api private
|
|
40
|
+
# @return [String] the bearer token
|
|
41
|
+
# @example Authenticate with the bearer token
|
|
42
|
+
# bearer_token
|
|
43
|
+
attr_reader :bearer_token
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require_relative "setting_validator"
|
|
5
|
+
|
|
6
|
+
module X
|
|
7
|
+
module Core
|
|
8
|
+
# The HTTP response of an error or a summary built by hand, from its status, headers, and body
|
|
9
|
+
#
|
|
10
|
+
# Internal to x-core: HTTPError, InvalidResponse, and Response are built from the Net::HTTP response a client
|
|
11
|
+
# received, or, so that code that rescues one, or an on_response hook, can be tested without building one, from
|
|
12
|
+
# the status, headers, and body of a response, which this builds the Net::HTTP response of.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
module BuiltResponse
|
|
16
|
+
extend self
|
|
17
|
+
|
|
18
|
+
# The message of the error raised for a status that is not one HTTP defines
|
|
19
|
+
INVALID_STATUS = "status must be an Integer from 100 to 599, not %s"
|
|
20
|
+
private_constant :INVALID_STATUS
|
|
21
|
+
# The message of the error raised for a response given beside what one is built of
|
|
22
|
+
RESPONSE_AND_STATUS = "Pass the http_response:, or the status:, headers:, and body: one is built of, not both"
|
|
23
|
+
private_constant :RESPONSE_AND_STATUS
|
|
24
|
+
# The message of the error raised for neither a response nor a status
|
|
25
|
+
NO_RESPONSE = "Pass the status: of the response, with its headers: and body: if it has any, or the http_response: itself"
|
|
26
|
+
private_constant :NO_RESPONSE
|
|
27
|
+
# The statuses HTTP defines
|
|
28
|
+
STATUSES = 100..599
|
|
29
|
+
private_constant :STATUSES
|
|
30
|
+
|
|
31
|
+
# The HTTP response given, or the one built of a status, headers, and body
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
34
|
+
# @param http_response [Net::HTTPResponse, nil] the HTTP response, or nil to build one
|
|
35
|
+
# @param status [Integer, nil] the status of the response to build, or nil for the one given
|
|
36
|
+
# @param headers [Hash{String => String}, nil] the headers of the response to build, or nil for none
|
|
37
|
+
# @param body [String, nil] the body of the response to build, or nil for none
|
|
38
|
+
# @return [Net::HTTPResponse] the HTTP response
|
|
39
|
+
# @raise [ArgumentError] if a response is given beside a status, headers, or a body, or neither a response nor a
|
|
40
|
+
# status is given, or the status is not one HTTP defines, or the headers are not a Hash of names to values
|
|
41
|
+
# @example Build a response the API refused a request with
|
|
42
|
+
# X::Core::BuiltResponse.of(nil, status: 404, headers: nil, body: "{}") # => #<Net::HTTPNotFound 404 Not Found>
|
|
43
|
+
def of(http_response, status:, headers:, body:)
|
|
44
|
+
return build(status, headers || {}, body) if http_response.nil? && !status.nil?
|
|
45
|
+
raise ArgumentError, NO_RESPONSE if http_response.nil?
|
|
46
|
+
raise ArgumentError, RESPONSE_AND_STATUS unless [status, headers, body].none?
|
|
47
|
+
|
|
48
|
+
http_response
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Build the HTTP response of a status, headers, and body
|
|
54
|
+
#
|
|
55
|
+
# Its body is tagged UTF-8, as the body of a response the client reads is.
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
# @param status [Integer] the status
|
|
59
|
+
# @param headers [Hash{String => String}] the headers
|
|
60
|
+
# @param body [String, nil] the body
|
|
61
|
+
# @return [Net::HTTPResponse] the HTTP response
|
|
62
|
+
# @raise [ArgumentError] if the status is not one HTTP defines, or the headers are not a Hash of names to values
|
|
63
|
+
def build(status, headers, body)
|
|
64
|
+
response_class = response_class_of(status)
|
|
65
|
+
response_class.new("1.1", status.to_s, reason_of(response_class)).tap do |response|
|
|
66
|
+
SettingValidator.headers!(headers).each { |name, value| response[name] = value }
|
|
67
|
+
response.body = body.dup&.force_encoding(Encoding::UTF_8)
|
|
68
|
+
response.instance_variable_set(:@read, true)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The class Net::HTTP reads a response of a status as
|
|
73
|
+
#
|
|
74
|
+
# It is the class of the status, such as Net::HTTPNotFound, or, for a status Net::HTTP names no class of, the
|
|
75
|
+
# class of its kind of status, such as Net::HTTPClientError for a 4xx.
|
|
76
|
+
#
|
|
77
|
+
# @api private
|
|
78
|
+
# @param status [Integer] the status
|
|
79
|
+
# @return [Class] the class
|
|
80
|
+
# @raise [ArgumentError] if the status is not one HTTP defines
|
|
81
|
+
def response_class_of(status)
|
|
82
|
+
raise ArgumentError, format(INVALID_STATUS, status.inspect) unless status.instance_of?(Integer) && STATUSES.cover?(status)
|
|
83
|
+
|
|
84
|
+
code = status.to_s
|
|
85
|
+
Net::HTTPResponse::CODE_TO_OBJ.fetch(code) { Net::HTTPResponse::CODE_CLASS_TO_OBJ.fetch(code[0]) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The reason phrase of the status of a Net::HTTP response class
|
|
89
|
+
#
|
|
90
|
+
# @api private
|
|
91
|
+
# @param response_class [Class] the class, such as Net::HTTPNotFound
|
|
92
|
+
# @return [String] the reason phrase, such as "Not Found"
|
|
93
|
+
def reason_of(response_class)
|
|
94
|
+
name = response_class.name #: String
|
|
95
|
+
name.delete_prefix("Net::HTTP").gsub(/(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/, " ")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
private_constant :BuiltResponse
|
|
99
|
+
end
|
|
100
|
+
end
|