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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/VERSION +1 -1
  4. data/lib/smithy-client/anonymous_provider.rb +12 -0
  5. data/lib/smithy-client/auth_option.rb +23 -0
  6. data/lib/smithy-client/auth_scheme.rb +25 -0
  7. data/lib/smithy-client/auth_schemes/anonymous.rb +18 -0
  8. data/lib/smithy-client/auth_schemes/http_api_key.rb +18 -0
  9. data/lib/smithy-client/auth_schemes/http_basic.rb +18 -0
  10. data/lib/smithy-client/auth_schemes/http_bearer.rb +18 -0
  11. data/lib/smithy-client/auth_schemes/http_digest.rb +18 -0
  12. data/lib/smithy-client/base.rb +200 -0
  13. data/lib/smithy-client/block_io.rb +36 -0
  14. data/lib/smithy-client/configuration.rb +222 -0
  15. data/lib/smithy-client/default_params.rb +91 -0
  16. data/lib/smithy-client/dynamic_errors.rb +82 -0
  17. data/lib/smithy-client/endpoint_rules.rb +186 -0
  18. data/lib/smithy-client/handler.rb +29 -0
  19. data/lib/smithy-client/handler_builder.rb +33 -0
  20. data/lib/smithy-client/handler_context.rb +67 -0
  21. data/lib/smithy-client/handler_list.rb +197 -0
  22. data/lib/smithy-client/handler_list_entry.rb +102 -0
  23. data/lib/smithy-client/http/error_inspector.rb +87 -0
  24. data/lib/smithy-client/http/headers.rb +122 -0
  25. data/lib/smithy-client/http/request.rb +57 -0
  26. data/lib/smithy-client/http/response.rb +178 -0
  27. data/lib/smithy-client/http_api_key_provider.rb +18 -0
  28. data/lib/smithy-client/http_bearer_provider.rb +18 -0
  29. data/lib/smithy-client/http_login_provider.rb +19 -0
  30. data/lib/smithy-client/identities/anonymous.rb +10 -0
  31. data/lib/smithy-client/identities/http_api_key.rb +18 -0
  32. data/lib/smithy-client/identities/http_bearer.rb +18 -0
  33. data/lib/smithy-client/identities/http_login.rb +22 -0
  34. data/lib/smithy-client/identity.rb +15 -0
  35. data/lib/smithy-client/log_formatter.rb +215 -0
  36. data/lib/smithy-client/log_param_filter.rb +88 -0
  37. data/lib/smithy-client/log_param_formatter.rb +65 -0
  38. data/lib/smithy-client/managed_file.rb +14 -0
  39. data/lib/smithy-client/net_http/connection_pool.rb +297 -0
  40. data/lib/smithy-client/net_http/handler.rb +160 -0
  41. data/lib/smithy-client/net_http/patches.rb +28 -0
  42. data/lib/smithy-client/networking_error.rb +16 -0
  43. data/lib/smithy-client/pageable_response.rb +138 -0
  44. data/lib/smithy-client/param_converter.rb +243 -0
  45. data/lib/smithy-client/param_validator.rb +213 -0
  46. data/lib/smithy-client/plugin.rb +144 -0
  47. data/lib/smithy-client/plugin_list.rb +141 -0
  48. data/lib/smithy-client/plugins/anonymous_auth.rb +23 -0
  49. data/lib/smithy-client/plugins/checksum_required.rb +51 -0
  50. data/lib/smithy-client/plugins/content_length.rb +26 -0
  51. data/lib/smithy-client/plugins/default_params.rb +22 -0
  52. data/lib/smithy-client/plugins/host_prefix.rb +69 -0
  53. data/lib/smithy-client/plugins/http_api_key_auth.rb +37 -0
  54. data/lib/smithy-client/plugins/http_basic_auth.rb +47 -0
  55. data/lib/smithy-client/plugins/http_bearer_auth.rb +37 -0
  56. data/lib/smithy-client/plugins/http_digest_auth.rb +60 -0
  57. data/lib/smithy-client/plugins/idempotency_token.rb +34 -0
  58. data/lib/smithy-client/plugins/logging.rb +56 -0
  59. data/lib/smithy-client/plugins/net_http.rb +163 -0
  60. data/lib/smithy-client/plugins/pageable_response.rb +37 -0
  61. data/lib/smithy-client/plugins/param_converter.rb +32 -0
  62. data/lib/smithy-client/plugins/param_validator.rb +30 -0
  63. data/lib/smithy-client/plugins/protocol.rb +66 -0
  64. data/lib/smithy-client/plugins/raise_response_errors.rb +33 -0
  65. data/lib/smithy-client/plugins/request_compression.rb +200 -0
  66. data/lib/smithy-client/plugins/response_target.rb +71 -0
  67. data/lib/smithy-client/plugins/retry_errors.rb +125 -0
  68. data/lib/smithy-client/plugins/sign_requests.rb +24 -0
  69. data/lib/smithy-client/plugins/stub_responses.rb +102 -0
  70. data/lib/smithy-client/protocol_spec_matcher.rb +60 -0
  71. data/lib/smithy-client/refreshing_identity_provider.rb +65 -0
  72. data/lib/smithy-client/request.rb +76 -0
  73. data/lib/smithy-client/response.rb +48 -0
  74. data/lib/smithy-client/retry/adaptive.rb +66 -0
  75. data/lib/smithy-client/retry/client_rate_limiter.rb +142 -0
  76. data/lib/smithy-client/retry/quota.rb +58 -0
  77. data/lib/smithy-client/retry/standard.rb +52 -0
  78. data/lib/smithy-client/retry.rb +36 -0
  79. data/lib/smithy-client/rpc_v2_cbor/protocol.rb +38 -0
  80. data/lib/smithy-client/rpc_v2_cbor/request_builder.rb +76 -0
  81. data/lib/smithy-client/rpc_v2_cbor/response_parser.rb +86 -0
  82. data/lib/smithy-client/rpc_v2_cbor/response_stubber.rb +34 -0
  83. data/lib/smithy-client/service_error.rb +57 -0
  84. data/lib/smithy-client/signer.rb +16 -0
  85. data/lib/smithy-client/signers/anonymous.rb +13 -0
  86. data/lib/smithy-client/signers/http_api_key.rb +52 -0
  87. data/lib/smithy-client/signers/http_basic.rb +23 -0
  88. data/lib/smithy-client/signers/http_bearer.rb +19 -0
  89. data/lib/smithy-client/signers/http_digest.rb +21 -0
  90. data/lib/smithy-client/stubbing/data_applicator.rb +61 -0
  91. data/lib/smithy-client/stubbing/empty_stub.rb +69 -0
  92. data/lib/smithy-client/stubbing/endpoint_provider.rb +22 -0
  93. data/lib/smithy-client/stubbing/protocol.rb +29 -0
  94. data/lib/smithy-client/stubbing/stub_data.rb +25 -0
  95. data/lib/smithy-client/stubbing.rb +14 -0
  96. data/lib/smithy-client/stubs.rb +212 -0
  97. data/lib/smithy-client/util.rb +15 -0
  98. data/lib/smithy-client/waiters/poller.rb +93 -0
  99. data/lib/smithy-client/waiters/waiter.rb +113 -0
  100. data/lib/smithy-client.rb +66 -1
  101. 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