smithy-client 1.0.0.pre0 → 1.0.0.pre1
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/CHANGELOG.md +2 -0
- data/VERSION +1 -1
- data/lib/smithy-client/anonymous_provider.rb +12 -0
- data/lib/smithy-client/auth_option.rb +23 -0
- data/lib/smithy-client/auth_scheme.rb +25 -0
- data/lib/smithy-client/auth_schemes/anonymous.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_api_key.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_basic.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_bearer.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_digest.rb +18 -0
- data/lib/smithy-client/base.rb +200 -0
- data/lib/smithy-client/block_io.rb +36 -0
- data/lib/smithy-client/configuration.rb +222 -0
- data/lib/smithy-client/default_params.rb +91 -0
- data/lib/smithy-client/dynamic_errors.rb +82 -0
- data/lib/smithy-client/endpoint_rules.rb +186 -0
- data/lib/smithy-client/handler.rb +29 -0
- data/lib/smithy-client/handler_builder.rb +33 -0
- data/lib/smithy-client/handler_context.rb +67 -0
- data/lib/smithy-client/handler_list.rb +197 -0
- data/lib/smithy-client/handler_list_entry.rb +102 -0
- data/lib/smithy-client/http/error_inspector.rb +87 -0
- data/lib/smithy-client/http/headers.rb +122 -0
- data/lib/smithy-client/http/request.rb +57 -0
- data/lib/smithy-client/http/response.rb +178 -0
- data/lib/smithy-client/http_api_key_provider.rb +18 -0
- data/lib/smithy-client/http_bearer_provider.rb +18 -0
- data/lib/smithy-client/http_login_provider.rb +19 -0
- data/lib/smithy-client/identities/anonymous.rb +10 -0
- data/lib/smithy-client/identities/http_api_key.rb +18 -0
- data/lib/smithy-client/identities/http_bearer.rb +18 -0
- data/lib/smithy-client/identities/http_login.rb +22 -0
- data/lib/smithy-client/identity.rb +15 -0
- data/lib/smithy-client/log_formatter.rb +215 -0
- data/lib/smithy-client/log_param_filter.rb +88 -0
- data/lib/smithy-client/log_param_formatter.rb +65 -0
- data/lib/smithy-client/managed_file.rb +14 -0
- data/lib/smithy-client/net_http/connection_pool.rb +297 -0
- data/lib/smithy-client/net_http/handler.rb +160 -0
- data/lib/smithy-client/net_http/patches.rb +28 -0
- data/lib/smithy-client/networking_error.rb +16 -0
- data/lib/smithy-client/pageable_response.rb +138 -0
- data/lib/smithy-client/param_converter.rb +243 -0
- data/lib/smithy-client/param_validator.rb +213 -0
- data/lib/smithy-client/plugin.rb +144 -0
- data/lib/smithy-client/plugin_list.rb +141 -0
- data/lib/smithy-client/plugins/anonymous_auth.rb +23 -0
- data/lib/smithy-client/plugins/checksum_required.rb +51 -0
- data/lib/smithy-client/plugins/content_length.rb +26 -0
- data/lib/smithy-client/plugins/default_params.rb +22 -0
- data/lib/smithy-client/plugins/host_prefix.rb +69 -0
- data/lib/smithy-client/plugins/http_api_key_auth.rb +37 -0
- data/lib/smithy-client/plugins/http_basic_auth.rb +47 -0
- data/lib/smithy-client/plugins/http_bearer_auth.rb +37 -0
- data/lib/smithy-client/plugins/http_digest_auth.rb +60 -0
- data/lib/smithy-client/plugins/idempotency_token.rb +34 -0
- data/lib/smithy-client/plugins/logging.rb +56 -0
- data/lib/smithy-client/plugins/net_http.rb +163 -0
- data/lib/smithy-client/plugins/pageable_response.rb +37 -0
- data/lib/smithy-client/plugins/param_converter.rb +32 -0
- data/lib/smithy-client/plugins/param_validator.rb +30 -0
- data/lib/smithy-client/plugins/protocol.rb +66 -0
- data/lib/smithy-client/plugins/raise_response_errors.rb +33 -0
- data/lib/smithy-client/plugins/request_compression.rb +200 -0
- data/lib/smithy-client/plugins/response_target.rb +71 -0
- data/lib/smithy-client/plugins/retry_errors.rb +125 -0
- data/lib/smithy-client/plugins/sign_requests.rb +24 -0
- data/lib/smithy-client/plugins/stub_responses.rb +102 -0
- data/lib/smithy-client/protocol_spec_matcher.rb +60 -0
- data/lib/smithy-client/refreshing_identity_provider.rb +65 -0
- data/lib/smithy-client/request.rb +76 -0
- data/lib/smithy-client/response.rb +48 -0
- data/lib/smithy-client/retry/adaptive.rb +66 -0
- data/lib/smithy-client/retry/client_rate_limiter.rb +142 -0
- data/lib/smithy-client/retry/quota.rb +58 -0
- data/lib/smithy-client/retry/standard.rb +52 -0
- data/lib/smithy-client/retry.rb +36 -0
- data/lib/smithy-client/rpc_v2_cbor/protocol.rb +38 -0
- data/lib/smithy-client/rpc_v2_cbor/request_builder.rb +76 -0
- data/lib/smithy-client/rpc_v2_cbor/response_parser.rb +86 -0
- data/lib/smithy-client/rpc_v2_cbor/response_stubber.rb +34 -0
- data/lib/smithy-client/service_error.rb +57 -0
- data/lib/smithy-client/signer.rb +16 -0
- data/lib/smithy-client/signers/anonymous.rb +13 -0
- data/lib/smithy-client/signers/http_api_key.rb +52 -0
- data/lib/smithy-client/signers/http_basic.rb +23 -0
- data/lib/smithy-client/signers/http_bearer.rb +19 -0
- data/lib/smithy-client/signers/http_digest.rb +21 -0
- data/lib/smithy-client/stubbing/data_applicator.rb +61 -0
- data/lib/smithy-client/stubbing/empty_stub.rb +69 -0
- data/lib/smithy-client/stubbing/endpoint_provider.rb +22 -0
- data/lib/smithy-client/stubbing/protocol.rb +29 -0
- data/lib/smithy-client/stubbing/stub_data.rb +25 -0
- data/lib/smithy-client/stubbing.rb +14 -0
- data/lib/smithy-client/stubs.rb +212 -0
- data/lib/smithy-client/util.rb +15 -0
- data/lib/smithy-client/waiters/poller.rb +93 -0
- data/lib/smithy-client/waiters/waiter.rb +113 -0
- data/lib/smithy-client.rb +66 -1
- metadata +163 -9
@@ -0,0 +1,215 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Smithy
|
6
|
+
module Client
|
7
|
+
# A log formatter generates a string for logging from output. This is
|
8
|
+
# accomplished with a log pattern string:
|
9
|
+
#
|
10
|
+
# pattern = ':operation :http_response_status_code :time'
|
11
|
+
# formatter = Smithy::Client::LogFormatter.new(pattern)
|
12
|
+
# formatter.format(response)
|
13
|
+
# #=> 'list_cities 200 0.0352'
|
14
|
+
#
|
15
|
+
# # Canned Formatters
|
16
|
+
#
|
17
|
+
# Instead of providing your own pattern, you can choose a canned log
|
18
|
+
# formatter.
|
19
|
+
#
|
20
|
+
# * {Formatter.default}
|
21
|
+
# * {Formatter.colored}
|
22
|
+
# * {Formatter.short}
|
23
|
+
#
|
24
|
+
# # Pattern Substitutions
|
25
|
+
#
|
26
|
+
# You can put any of these placeholders into your pattern.
|
27
|
+
#
|
28
|
+
# * `:client_class` - The name of the client class.
|
29
|
+
# * `:operation` - The name of the client request method.
|
30
|
+
# * `:request_params` - The user provided parameters. Long strings are truncated/summarized if
|
31
|
+
# they exceed the `:max_string_size`. Other objects are inspected.
|
32
|
+
# * `:time` - The total time in seconds spent on the request.
|
33
|
+
# This includes client side time spent building the request and parsing the response.
|
34
|
+
# * `:retries` - The number of times a client request was retried.
|
35
|
+
# * `:http_request_method` - The http request verb, e.g., `POST`, `PUT`, `GET`, etc.
|
36
|
+
# * `:http_request_endpoint` - The request endpoint. This includes the scheme, host and port, but not the path.
|
37
|
+
# * `:http_request_scheme` - This is replaced by `http` or `https`.
|
38
|
+
# * `:http_request_host` - The host name of the http request endpoint (e.g. 'example.com').
|
39
|
+
# * `:http_request_port` - The port number (e.g. '443' or '80').
|
40
|
+
# * `:http_request_headers` - The http request headers, inspected.
|
41
|
+
# * `:http_request_body` - The http request payload.
|
42
|
+
# * `:http_response_status_code` - The http response status code, e.g., `200`, `404`, `500`, etc.
|
43
|
+
# * `:http_response_headers` - The http response headers, inspected.
|
44
|
+
# * `:http_response_body` - The http response body contents.
|
45
|
+
# * `:error_class` - The class of the error that occurred, if any.
|
46
|
+
# * `:error_message` - The error message, if an error occurred.
|
47
|
+
#
|
48
|
+
class LogFormatter
|
49
|
+
# @param [String] pattern The log format pattern should be a string and may contain substitutions.
|
50
|
+
# @option options [Integer] :max_string_size (1000) When summarizing
|
51
|
+
# request parameters, strings longer than this value will be truncated.
|
52
|
+
# @option options [Boolean] :filter_sensitive_params (true) When false, sensitive params will
|
53
|
+
# not be filtered when logging `:params`.
|
54
|
+
def initialize(pattern, options = {})
|
55
|
+
@pattern = pattern
|
56
|
+
@log_param_formatter = LogParamFormatter.new(options)
|
57
|
+
@log_param_filter = LogParamFilter.new(options)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [String]
|
61
|
+
attr_reader :pattern
|
62
|
+
|
63
|
+
# Given a response, this will format a log message and return it as a string according to {#pattern}.
|
64
|
+
# @param [Smithy::Client::Response] response
|
65
|
+
# @return [String]
|
66
|
+
def format(response)
|
67
|
+
pattern.gsub(/:(\w+)/) { |sym| send("_#{sym[1..]}", response) }
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def _client_class(response)
|
73
|
+
response.context.client.class.name
|
74
|
+
end
|
75
|
+
|
76
|
+
def _operation(response)
|
77
|
+
response.context.operation_name
|
78
|
+
end
|
79
|
+
|
80
|
+
def _request_params(response)
|
81
|
+
params = response.context.params
|
82
|
+
input = response.context.operation.input
|
83
|
+
@log_param_formatter.summarize(@log_param_filter.filter(input, params))
|
84
|
+
end
|
85
|
+
|
86
|
+
def _time(response)
|
87
|
+
duration = response.context[:logging_completed_at] - response.context[:logging_started_at]
|
88
|
+
Kernel.format('%.06f', duration).sub(/0+$/, '')
|
89
|
+
end
|
90
|
+
|
91
|
+
def _retries(response)
|
92
|
+
response.context.retries
|
93
|
+
end
|
94
|
+
|
95
|
+
def _http_request_endpoint(response)
|
96
|
+
response.context.http_request.endpoint.to_s
|
97
|
+
end
|
98
|
+
|
99
|
+
def _http_request_scheme(response)
|
100
|
+
response.context.http_request.endpoint.scheme
|
101
|
+
end
|
102
|
+
|
103
|
+
def _http_request_host(response)
|
104
|
+
response.context.http_request.endpoint.host
|
105
|
+
end
|
106
|
+
|
107
|
+
def _http_request_port(response)
|
108
|
+
response.context.http_request.endpoint.port
|
109
|
+
end
|
110
|
+
|
111
|
+
def _http_request_method(response)
|
112
|
+
response.context.http_request.http_method
|
113
|
+
end
|
114
|
+
|
115
|
+
def _http_request_headers(response)
|
116
|
+
response.context.http_request.headers.inspect
|
117
|
+
end
|
118
|
+
|
119
|
+
def _http_request_body(response)
|
120
|
+
body = response.context.http_request.body
|
121
|
+
return '' unless body.respond_to?(:rewind)
|
122
|
+
|
123
|
+
body_contents = body.read
|
124
|
+
body.rewind
|
125
|
+
@log_param_formatter.summarize(body_contents)
|
126
|
+
end
|
127
|
+
|
128
|
+
def _http_response_status_code(response)
|
129
|
+
response.context.http_response.status_code
|
130
|
+
end
|
131
|
+
|
132
|
+
def _http_response_headers(response)
|
133
|
+
response.context.http_response.headers.inspect
|
134
|
+
end
|
135
|
+
|
136
|
+
def _http_response_body(response)
|
137
|
+
body = response.context.http_response.body
|
138
|
+
return '' unless body.respond_to?(:rewind)
|
139
|
+
|
140
|
+
body_contents = body.read
|
141
|
+
body.rewind
|
142
|
+
@log_param_formatter.summarize(body_contents)
|
143
|
+
end
|
144
|
+
|
145
|
+
def _error_class(response)
|
146
|
+
response.error ? response.error.class.name : ''
|
147
|
+
end
|
148
|
+
|
149
|
+
def _error_message(response)
|
150
|
+
response.error ? response.error.message : ''
|
151
|
+
end
|
152
|
+
|
153
|
+
class << self
|
154
|
+
# The default log format.
|
155
|
+
# @option (see #initialize)
|
156
|
+
# @example A sample of the default format.
|
157
|
+
#
|
158
|
+
# [ClientClass 200 0.580066 0 retries] list_objects(:bucket_name => 'bucket')
|
159
|
+
#
|
160
|
+
# @return [Formatter]
|
161
|
+
def default(options = {})
|
162
|
+
pattern = []
|
163
|
+
pattern << '[:client_class'
|
164
|
+
pattern << ':http_response_status_code'
|
165
|
+
pattern << ':time'
|
166
|
+
pattern << ':retries retries]'
|
167
|
+
pattern << ':operation(:request_params)'
|
168
|
+
pattern << ':error_class'
|
169
|
+
pattern << ':error_message'
|
170
|
+
LogFormatter.new("#{pattern.join(' ')}\n", options)
|
171
|
+
end
|
172
|
+
|
173
|
+
# The short log format. Similar to default, but it does not
|
174
|
+
# inspect the request params or report on retries.
|
175
|
+
# @option (see #initialize)
|
176
|
+
# @example A sample of the short format
|
177
|
+
#
|
178
|
+
# [ClientClass 200 0.494532] list_buckets
|
179
|
+
#
|
180
|
+
# @return [Formatter]
|
181
|
+
def short(options = {})
|
182
|
+
pattern = []
|
183
|
+
pattern << '[:client_class'
|
184
|
+
pattern << ':http_response_status_code'
|
185
|
+
pattern << ':time]'
|
186
|
+
pattern << ':operation'
|
187
|
+
pattern << ':error_class'
|
188
|
+
LogFormatter.new("#{pattern.join(' ')}\n", options)
|
189
|
+
end
|
190
|
+
|
191
|
+
# The default log format with ANSI colors.
|
192
|
+
# @option (see #initialize)
|
193
|
+
# @example A sample of the colored format (sans the ansi colors).
|
194
|
+
#
|
195
|
+
# [ClientClass 200 0.580066 0 retries] list_objects(:bucket_name => 'bucket')
|
196
|
+
#
|
197
|
+
# @return [Formatter]
|
198
|
+
def colored(options = {})
|
199
|
+
bold = "\x1b[1m"
|
200
|
+
color = "\x1b[34m"
|
201
|
+
reset = "\x1b[0m"
|
202
|
+
pattern = []
|
203
|
+
pattern << "#{bold}#{color}[:client_class"
|
204
|
+
pattern << ':http_response_status_code'
|
205
|
+
pattern << ':time'
|
206
|
+
pattern << ":retries retries]#{reset}#{bold}"
|
207
|
+
pattern << ':operation(:request_params)'
|
208
|
+
pattern << ':error_class'
|
209
|
+
pattern << ":error_message#{reset}"
|
210
|
+
LogFormatter.new("#{pattern.join(' ')}\n", options)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
# @api private
|
6
|
+
class LogParamFilter
|
7
|
+
include Schema::Shapes
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@filter_sensitive_params = options.fetch(:filter_sensitive_params, true)
|
11
|
+
end
|
12
|
+
|
13
|
+
def filter(ref, values)
|
14
|
+
case ref.shape
|
15
|
+
when ListShape then list(ref, values)
|
16
|
+
when MapShape then map(ref, values)
|
17
|
+
when StructureShape then structure(ref, values)
|
18
|
+
when UnionShape then union(ref, values)
|
19
|
+
else scalar(ref, values)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def list(ref, values)
|
26
|
+
shape = ref.shape
|
27
|
+
return '[FILTERED]' if sensitive?(shape)
|
28
|
+
|
29
|
+
member_ref = shape.member
|
30
|
+
values.collect { |value| filter(member_ref, value) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def map(ref, values)
|
34
|
+
shape = ref.shape
|
35
|
+
return '[FILTERED]' if sensitive?(shape)
|
36
|
+
|
37
|
+
filtered = {}
|
38
|
+
value_ref = shape.value
|
39
|
+
values.each_pair do |key, value|
|
40
|
+
filtered[key] = filter(value_ref, value)
|
41
|
+
end
|
42
|
+
filtered
|
43
|
+
end
|
44
|
+
|
45
|
+
def scalar(ref, value)
|
46
|
+
return '[FILTERED]' if sensitive?(ref.shape)
|
47
|
+
|
48
|
+
value
|
49
|
+
end
|
50
|
+
|
51
|
+
def structure(ref, values)
|
52
|
+
shape = ref.shape
|
53
|
+
return '[FILTERED]' if sensitive?(shape)
|
54
|
+
|
55
|
+
filtered = {}
|
56
|
+
values.each_pair do |key, value|
|
57
|
+
next unless shape.member?(key)
|
58
|
+
|
59
|
+
member_ref = shape.member(key)
|
60
|
+
filtered[key] = filter(member_ref, value)
|
61
|
+
end
|
62
|
+
filtered
|
63
|
+
end
|
64
|
+
|
65
|
+
def union(ref, values) # rubocop:disable Metrics/AbcSize
|
66
|
+
shape = ref.shape
|
67
|
+
return '[FILTERED]' if sensitive?(shape)
|
68
|
+
|
69
|
+
filtered = {}
|
70
|
+
if values.is_a?(Schema::Union)
|
71
|
+
name, member_ref = ref.shape.member_by_type(values.class)
|
72
|
+
filtered[name] = filter(member_ref, values.value)
|
73
|
+
else
|
74
|
+
key, value = values.first
|
75
|
+
if ref.shape.member?(key)
|
76
|
+
member_ref = ref.shape.member(key)
|
77
|
+
filtered[key] = filter(member_ref, value)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
filtered
|
81
|
+
end
|
82
|
+
|
83
|
+
def sensitive?(shape)
|
84
|
+
@filter_sensitive_params && shape.traits.key?('smithy.api#sensitive')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Smithy
|
6
|
+
module Client
|
7
|
+
# @api private
|
8
|
+
class LogParamFormatter
|
9
|
+
# String longer than the max string size are truncated
|
10
|
+
MAX_STRING_SIZE = 1000
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@max_string_size = options[:max_string_size] || MAX_STRING_SIZE
|
14
|
+
end
|
15
|
+
|
16
|
+
def summarize(value)
|
17
|
+
case value
|
18
|
+
when Array then "[#{array(value)}]"
|
19
|
+
when File then file(value)
|
20
|
+
when Hash then "{ #{hash(value)} }"
|
21
|
+
when Pathname then pathname(value)
|
22
|
+
when String then string(value)
|
23
|
+
when Tempfile then tempfile(value)
|
24
|
+
else value.inspect
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def string(str)
|
31
|
+
if str.size > @max_string_size
|
32
|
+
"#<String #{str[0...@max_string_size].inspect} ... (#{str.size} bytes)>"
|
33
|
+
else
|
34
|
+
str.inspect
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def hash(hash)
|
39
|
+
hash.map do |key, value|
|
40
|
+
if key.is_a?(String)
|
41
|
+
"#{key.inspect} => #{summarize(value)}"
|
42
|
+
else
|
43
|
+
"#{key}: #{summarize(value)}"
|
44
|
+
end
|
45
|
+
end.join(', ')
|
46
|
+
end
|
47
|
+
|
48
|
+
def array(array)
|
49
|
+
array.map { |v| summarize(v) }.join(', ')
|
50
|
+
end
|
51
|
+
|
52
|
+
def file(file)
|
53
|
+
"#<File:#{file.path} (#{file.size} bytes)>"
|
54
|
+
end
|
55
|
+
|
56
|
+
def tempfile(file)
|
57
|
+
"#<Tempfile:#{file.path} (#{file.size} bytes)>"
|
58
|
+
end
|
59
|
+
|
60
|
+
def pathname(path)
|
61
|
+
"#<Pathname:#{path} (#{File.size(path)} bytes)>"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
# This utility class is used to track files opened by Smithy::Client.
|
6
|
+
# @api private
|
7
|
+
class ManagedFile < File
|
8
|
+
# @return [Boolean]
|
9
|
+
def open?
|
10
|
+
!closed?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'net/http'
|
5
|
+
require 'net/https'
|
6
|
+
require 'delegate'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
require_relative 'patches'
|
10
|
+
|
11
|
+
Smithy::Client::NetHTTP::Patches.apply!
|
12
|
+
|
13
|
+
module Smithy
|
14
|
+
module Client
|
15
|
+
module NetHTTP
|
16
|
+
# @api private
|
17
|
+
class ConnectionPool
|
18
|
+
@pools_mutex = Mutex.new
|
19
|
+
@pools = {}
|
20
|
+
|
21
|
+
class << self
|
22
|
+
# Returns a connection pool constructed from the given options.
|
23
|
+
# Calling this method twice with the same options will return
|
24
|
+
# the same pool.
|
25
|
+
#
|
26
|
+
# @option options [Numeric] :http_continue_timeout (nil)
|
27
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-continue_timeout
|
28
|
+
# @option options [Numeric] :http_keep_alive_timeout (nil)
|
29
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-keep_alive_timeout
|
30
|
+
# @option options [Numeric] :http_open_timeout (nil)
|
31
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-open_timeout
|
32
|
+
# @option options [Numeric] :http_read_timeout (nil)
|
33
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-read_timeout
|
34
|
+
# @option options [Numeric] :http_ssl_timeout (nil)
|
35
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-ssl_timeout
|
36
|
+
# @option options [Numeric] :http_write_timeout (nil)
|
37
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-write_timeout
|
38
|
+
# @option options [String] :http_ca_file (nil)
|
39
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-ca_file
|
40
|
+
# @option options [String] :http_ca_path (nil)
|
41
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-ca_path
|
42
|
+
# @option options [OpenSSL::X509::Certificate] :http_cert (nil)
|
43
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-cert
|
44
|
+
# @option options [OpenSSL::X509::Store] :http_cert_store (nil)
|
45
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-cert_store
|
46
|
+
# @option options [OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] :http_key (nil)
|
47
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-key
|
48
|
+
# @option options [Integer] :http_verify_mode (OpenSSL::SSL::VERIFY_PEER)
|
49
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-verify_mode
|
50
|
+
# @option options [Boolean] :http_debug_output (false)
|
51
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#method-i-set_debug_output
|
52
|
+
# @option options [URI::HTTP, String] :http_proxy (nil)
|
53
|
+
# @see https://docs.ruby-lang.org/en/master/Net/HTTP.html#attribute-i-proxy
|
54
|
+
# @option options [Logger] :logger (nil) Where debug output is sent.
|
55
|
+
# Defaults to `Logger.new($stdout)` when `:http_wire_trace` is `true`.
|
56
|
+
# @return [ConnectionPool]
|
57
|
+
def for(options = {})
|
58
|
+
options = pool_options(options)
|
59
|
+
@pools_mutex.synchronize do
|
60
|
+
@pools[options] ||= new(options)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Array<ConnectionPool>] Returns a list of the
|
65
|
+
# constructed connection pools.
|
66
|
+
def pools
|
67
|
+
@pools_mutex.synchronize do
|
68
|
+
@pools.values
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Filters an option hash, merging in default values.
|
75
|
+
# @return [Hash]
|
76
|
+
def pool_options(options)
|
77
|
+
options.delete(:logger) unless options[:http_debug_output]
|
78
|
+
options[:http_proxy] = URI.parse(options[:http_proxy].to_s)
|
79
|
+
options
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
OPTIONS = {
|
84
|
+
# Connections
|
85
|
+
http_continue_timeout: nil,
|
86
|
+
http_keep_alive_timeout: nil,
|
87
|
+
http_open_timeout: nil,
|
88
|
+
http_read_timeout: nil,
|
89
|
+
http_ssl_timeout: nil,
|
90
|
+
http_write_timeout: nil,
|
91
|
+
# Security
|
92
|
+
http_ca_file: nil,
|
93
|
+
http_ca_path: nil,
|
94
|
+
http_cert: nil,
|
95
|
+
http_cert_store: nil,
|
96
|
+
http_key: nil,
|
97
|
+
http_verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
98
|
+
# Debugging
|
99
|
+
http_debug_output: nil,
|
100
|
+
# Proxies
|
101
|
+
http_proxy: nil,
|
102
|
+
# Other
|
103
|
+
logger: Logger.new($stdout)
|
104
|
+
}.freeze
|
105
|
+
|
106
|
+
# @api private
|
107
|
+
def initialize(options = {})
|
108
|
+
OPTIONS.each_pair do |opt_name, default_value|
|
109
|
+
value = options[opt_name].nil? ? default_value : options[opt_name]
|
110
|
+
instance_variable_set("@#{opt_name}", value)
|
111
|
+
end
|
112
|
+
@pool_mutex = Mutex.new
|
113
|
+
@pool = {}
|
114
|
+
end
|
115
|
+
private_class_method :new
|
116
|
+
|
117
|
+
OPTIONS.each_key do |attr_name|
|
118
|
+
attr_reader(attr_name)
|
119
|
+
end
|
120
|
+
|
121
|
+
# @param [URI::HTTP, URI::HTTPS] endpoint The HTTP(S) endpoint
|
122
|
+
# to connect to (e.g. 'https://domain.com').
|
123
|
+
#
|
124
|
+
# @yieldparam [Net::HTTPSession] session
|
125
|
+
#
|
126
|
+
# @return [nil]
|
127
|
+
def session_for(endpoint)
|
128
|
+
endpoint = remove_path_and_query(endpoint)
|
129
|
+
session = nil
|
130
|
+
|
131
|
+
# attempt to recycle an already open session
|
132
|
+
@pool_mutex.synchronize do
|
133
|
+
_clean
|
134
|
+
session = @pool[endpoint].shift if @pool.key?(endpoint)
|
135
|
+
end
|
136
|
+
|
137
|
+
begin
|
138
|
+
session ||= start_session(endpoint)
|
139
|
+
yield(session)
|
140
|
+
rescue StandardError
|
141
|
+
session&.finish
|
142
|
+
raise
|
143
|
+
else
|
144
|
+
@pool_mutex.synchronize do
|
145
|
+
@pool[endpoint] = [] unless @pool.key?(endpoint)
|
146
|
+
@pool[endpoint] << session
|
147
|
+
end
|
148
|
+
end
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
|
152
|
+
# @return [Integer] Returns the count of sessions currently in the
|
153
|
+
# pool, not counting those currently in use.
|
154
|
+
def size
|
155
|
+
@pool_mutex.synchronize do
|
156
|
+
@pool.values.flatten.size
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Removes stale http sessions from the pool (that have exceeded the idle timeout).
|
161
|
+
# @return [nil]
|
162
|
+
def clean!
|
163
|
+
@pool_mutex.synchronize { _clean }
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
167
|
+
# Closes and removes all sessions from the pool. If empty! is called while
|
168
|
+
# there are outstanding requests they may get checked back into the pool,
|
169
|
+
# leaving the pool in a non-empty state.
|
170
|
+
# @return [nil]
|
171
|
+
def empty!
|
172
|
+
@pool_mutex.synchronize do
|
173
|
+
@pool.values.flatten.map(&:finish)
|
174
|
+
@pool.clear
|
175
|
+
end
|
176
|
+
nil
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def remove_path_and_query(endpoint)
|
182
|
+
endpoint.dup.tap do |e|
|
183
|
+
e.path = ''
|
184
|
+
e.query = nil
|
185
|
+
end.to_s
|
186
|
+
end
|
187
|
+
|
188
|
+
# Extract the parts of the http_proxy URI
|
189
|
+
# @return [Array<String>]
|
190
|
+
def http_proxy_parts
|
191
|
+
[
|
192
|
+
http_proxy.host,
|
193
|
+
http_proxy.port,
|
194
|
+
http_proxy.user && CGI.unescape(http_proxy.user),
|
195
|
+
http_proxy.password && CGI.unescape(http_proxy.password)
|
196
|
+
]
|
197
|
+
end
|
198
|
+
|
199
|
+
# Starts and returns a new HTTP(S) session.
|
200
|
+
# @param [String] endpoint
|
201
|
+
# @return [Net::HTTPSession]
|
202
|
+
def start_session(endpoint)
|
203
|
+
endpoint = URI.parse(endpoint)
|
204
|
+
|
205
|
+
args = []
|
206
|
+
args << endpoint.host
|
207
|
+
args << endpoint.port
|
208
|
+
args += http_proxy_parts
|
209
|
+
|
210
|
+
http = ExtendedSession.new(Net::HTTP.new(*args.compact))
|
211
|
+
# Prefer SDK retries and do not retry at the http layer
|
212
|
+
http.max_retries = 0
|
213
|
+
configure_session(http, endpoint)
|
214
|
+
http.set_debug_output(logger) if http_debug_output
|
215
|
+
|
216
|
+
http.start
|
217
|
+
http
|
218
|
+
end
|
219
|
+
|
220
|
+
def configure_session(http, endpoint)
|
221
|
+
configure_http_connections(http)
|
222
|
+
|
223
|
+
if endpoint.scheme == 'https'
|
224
|
+
configure_ssl(http)
|
225
|
+
else
|
226
|
+
http.use_ssl = false
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def configure_http_connections(http)
|
231
|
+
http.continue_timeout = http_continue_timeout if http_continue_timeout
|
232
|
+
http.keep_alive_timeout = http_keep_alive_timeout if http_keep_alive_timeout
|
233
|
+
http.open_timeout = http_open_timeout if http_open_timeout
|
234
|
+
http.read_timeout = http_read_timeout if http_read_timeout
|
235
|
+
http.write_timeout = http_write_timeout if http_write_timeout
|
236
|
+
end
|
237
|
+
|
238
|
+
def configure_ssl(http)
|
239
|
+
http.use_ssl = true
|
240
|
+
http.ssl_timeout = http_ssl_timeout if http_ssl_timeout
|
241
|
+
return unless http_verify_mode == OpenSSL::SSL::VERIFY_PEER
|
242
|
+
|
243
|
+
configure_ssl_cert(http)
|
244
|
+
end
|
245
|
+
|
246
|
+
def configure_ssl_cert(http)
|
247
|
+
http.ca_file = http_ca_file if http_ca_file
|
248
|
+
http.ca_path = http_ca_path if http_ca_path
|
249
|
+
http.cert = http_cert if http_cert
|
250
|
+
http.cert_store = http_cert_store if http_cert_store
|
251
|
+
http.key = http_key if http_key
|
252
|
+
end
|
253
|
+
|
254
|
+
# Removes stale sessions from the pool. This method *must* be called.
|
255
|
+
# @note **Must** be called behind a `@pool_mutex` synchronize block.
|
256
|
+
def _clean
|
257
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
258
|
+
@pool.each_value do |sessions|
|
259
|
+
sessions.delete_if do |session|
|
260
|
+
if session.last_used.nil? ||
|
261
|
+
(now - session.last_used > session.keep_alive_timeout * 1000)
|
262
|
+
session.finish
|
263
|
+
true
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Helper methods extended onto Net::HTTPSession objects opened by the
|
270
|
+
# connection pool.
|
271
|
+
# @api private
|
272
|
+
class ExtendedSession < SimpleDelegator
|
273
|
+
def initialize(http)
|
274
|
+
super
|
275
|
+
@http = http
|
276
|
+
end
|
277
|
+
|
278
|
+
# @return [Integer, nil]
|
279
|
+
attr_reader :last_used
|
280
|
+
|
281
|
+
# Sends the request and tracks that this session has been used.
|
282
|
+
def request(...)
|
283
|
+
@http.request(...)
|
284
|
+
@last_used = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
285
|
+
end
|
286
|
+
|
287
|
+
# Attempts to close/finish the session without raising an error.
|
288
|
+
def finish
|
289
|
+
@http.finish
|
290
|
+
rescue IOError
|
291
|
+
nil
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|