aws-sdk-core 3.46.2 → 3.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/aws-sdk-core.rb +1 -0
- data/lib/aws-sdk-core/async_client_stubs.rb +80 -0
- data/lib/aws-sdk-core/binary.rb +3 -0
- data/lib/aws-sdk-core/binary/decode_handler.rb +21 -1
- data/lib/aws-sdk-core/binary/encode_handler.rb +32 -0
- data/lib/aws-sdk-core/binary/event_builder.rb +122 -0
- data/lib/aws-sdk-core/binary/event_parser.rb +48 -18
- data/lib/aws-sdk-core/binary/event_stream_decoder.rb +5 -2
- data/lib/aws-sdk-core/binary/event_stream_encoder.rb +53 -0
- data/lib/aws-sdk-core/client_stubs.rb +1 -1
- data/lib/aws-sdk-core/errors.rb +4 -0
- data/lib/aws-sdk-core/event_emitter.rb +42 -0
- data/lib/aws-sdk-core/json/handler.rb +19 -1
- data/lib/aws-sdk-core/param_validator.rb +9 -1
- data/lib/aws-sdk-core/plugins/event_stream_configuration.rb +14 -0
- data/lib/aws-sdk-core/plugins/invocation_id.rb +33 -0
- data/lib/aws-sdk-core/plugins/stub_responses.rb +19 -7
- data/lib/aws-sdk-core/stubbing/protocols/rest.rb +19 -0
- data/lib/aws-sdk-core/stubbing/stub_data.rb +1 -1
- data/lib/aws-sdk-sts.rb +1 -1
- data/lib/aws-sdk-sts/client.rb +1 -1
- data/lib/seahorse.rb +9 -0
- data/lib/seahorse/client/async_base.rb +50 -0
- data/lib/seahorse/client/async_response.rb +73 -0
- data/lib/seahorse/client/base.rb +1 -1
- data/lib/seahorse/client/h2/connection.rb +242 -0
- data/lib/seahorse/client/h2/handler.rb +149 -0
- data/lib/seahorse/client/http/async_response.rb +42 -0
- data/lib/seahorse/client/http/response.rb +10 -5
- data/lib/seahorse/client/networking_error.rb +28 -0
- data/lib/seahorse/client/plugins/h2.rb +64 -0
- data/lib/seahorse/model/api.rb +4 -0
- data/lib/seahorse/model/operation.rb +4 -0
- metadata +35 -4
data/lib/seahorse/client/base.rb
CHANGED
@@ -0,0 +1,242 @@
|
|
1
|
+
if RUBY_VERSION >= '2.1'
|
2
|
+
require 'http/2'
|
3
|
+
end
|
4
|
+
require 'openssl'
|
5
|
+
require 'socket'
|
6
|
+
|
7
|
+
module Seahorse
|
8
|
+
module Client
|
9
|
+
# @api private
|
10
|
+
module H2
|
11
|
+
|
12
|
+
# H2 Connection build on top of `http/2` gem
|
13
|
+
# (requires Ruby >= 2.1)
|
14
|
+
# with TLS layer plus ALPN, requires:
|
15
|
+
# Ruby >= 2.3 and OpenSSL >= 1.0.2
|
16
|
+
class Connection
|
17
|
+
|
18
|
+
OPTIONS = {
|
19
|
+
max_concurrent_streams: 100,
|
20
|
+
connection_timeout: 60,
|
21
|
+
connection_read_timeout: 60,
|
22
|
+
http_wire_trace: false,
|
23
|
+
logger: nil,
|
24
|
+
ssl_verify_peer: true,
|
25
|
+
ssl_ca_bundle: nil,
|
26
|
+
ssl_ca_directory: nil,
|
27
|
+
ssl_ca_store: nil,
|
28
|
+
enable_alpn: false
|
29
|
+
}
|
30
|
+
|
31
|
+
# chunk read size at socket
|
32
|
+
CHUNKSIZE = 1024
|
33
|
+
|
34
|
+
SOCKET_FAMILY = ::Socket::AF_INET
|
35
|
+
|
36
|
+
def initialize(options = {})
|
37
|
+
OPTIONS.each_pair do |opt_name, default_value|
|
38
|
+
value = options[opt_name].nil? ? default_value : options[opt_name]
|
39
|
+
instance_variable_set("@#{opt_name}", value)
|
40
|
+
end
|
41
|
+
@h2_client = HTTP2::Client.new(
|
42
|
+
settings_max_concurrent_streams: max_concurrent_streams
|
43
|
+
)
|
44
|
+
@logger = options[:logger] || Logger.new($stdout) if @http_wire_trace
|
45
|
+
@chunk_size = options[:read_chunk_size] || CHUNKSIZE
|
46
|
+
@errors = []
|
47
|
+
@status = :ready
|
48
|
+
@mutex = Mutex.new # connection can be shared across requests
|
49
|
+
end
|
50
|
+
|
51
|
+
OPTIONS.keys.each do |attr_name|
|
52
|
+
attr_reader(attr_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
alias ssl_verify_peer? ssl_verify_peer
|
56
|
+
|
57
|
+
attr_reader :errors
|
58
|
+
|
59
|
+
attr_accessor :input_signal_thread
|
60
|
+
|
61
|
+
def new_stream
|
62
|
+
begin
|
63
|
+
@h2_client.new_stream
|
64
|
+
rescue => error
|
65
|
+
raise Http2StreamInitializeError.new(error)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def connect(endpoint)
|
70
|
+
@mutex.synchronize {
|
71
|
+
if @status == :ready
|
72
|
+
tcp, addr = _tcp_socket(endpoint)
|
73
|
+
debug_output("opening connection to #{endpoint.host}:#{endpoint.port} ...")
|
74
|
+
_nonblocking_connect(tcp, addr)
|
75
|
+
debug_output("opened")
|
76
|
+
|
77
|
+
@socket = OpenSSL::SSL::SSLSocket.new(tcp, _tls_context)
|
78
|
+
@socket.sync_close = true
|
79
|
+
@socket.hostname = endpoint.host
|
80
|
+
|
81
|
+
debug_output("starting TLS for #{endpoint.host}:#{endpoint.port} ...")
|
82
|
+
@socket.connect
|
83
|
+
debug_output("TLS established")
|
84
|
+
_register_h2_callbacks
|
85
|
+
@status = :active
|
86
|
+
elsif @status == :closed
|
87
|
+
msg = "Async Client HTTP2 Connection is closed, you may"\
|
88
|
+
" use #new_connection to create a new HTTP2 Connection for this client"
|
89
|
+
raise Http2ConnectionClosedError.new(msg)
|
90
|
+
end
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def start(stream)
|
95
|
+
@mutex.synchronize {
|
96
|
+
return if @socket_thread
|
97
|
+
@socket_thread = Thread.new do
|
98
|
+
while !@socket.closed?
|
99
|
+
begin
|
100
|
+
data = @socket.read_nonblock(@chunk_size)
|
101
|
+
@h2_client << data
|
102
|
+
rescue IO::WaitReadable
|
103
|
+
begin
|
104
|
+
unless IO.select([@socket], nil, nil, connection_read_timeout)
|
105
|
+
self.debug_output("socket connection read time out")
|
106
|
+
self.close!
|
107
|
+
else
|
108
|
+
# available, retry to start reading
|
109
|
+
retry
|
110
|
+
end
|
111
|
+
rescue
|
112
|
+
# error can happen when closing the socket
|
113
|
+
# while it's waiting for read
|
114
|
+
self.close!
|
115
|
+
end
|
116
|
+
rescue EOFError
|
117
|
+
self.close!
|
118
|
+
rescue => error
|
119
|
+
self.debug_output(error.inspect)
|
120
|
+
@errors << error
|
121
|
+
self.close!
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
@socket_thread.abort_on_exception = true
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
def close!
|
130
|
+
@mutex.synchronize {
|
131
|
+
self.debug_output("closing connection ...")
|
132
|
+
if @socket
|
133
|
+
@socket.close
|
134
|
+
@socket = nil
|
135
|
+
end
|
136
|
+
if @socket_thread
|
137
|
+
Thread.kill(@socket_thread)
|
138
|
+
@socket_thread = nil
|
139
|
+
end
|
140
|
+
@status = :closed
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
def closed?
|
145
|
+
@status == :closed
|
146
|
+
end
|
147
|
+
|
148
|
+
def debug_output(msg, type = nil)
|
149
|
+
prefix = case type
|
150
|
+
when :send then "-> "
|
151
|
+
when :receive then "<- "
|
152
|
+
else
|
153
|
+
""
|
154
|
+
end
|
155
|
+
return unless @logger
|
156
|
+
_debug_entry(prefix + msg)
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def _debug_entry(str)
|
162
|
+
@logger << str
|
163
|
+
@logger << "\n"
|
164
|
+
end
|
165
|
+
|
166
|
+
def _register_h2_callbacks
|
167
|
+
@h2_client.on(:frame) do |bytes|
|
168
|
+
if @socket.nil?
|
169
|
+
msg = "Connection is closed due to errors, "\
|
170
|
+
"you can find errors at async_client.connection.errors"
|
171
|
+
raise Http2ConnectionClosedError.new(msg)
|
172
|
+
else
|
173
|
+
@socket.print(bytes)
|
174
|
+
@socket.flush
|
175
|
+
end
|
176
|
+
end
|
177
|
+
@h2_client.on(:frame_sent) do |frame|
|
178
|
+
debug_output("frame: #{frame.inspect}", :send)
|
179
|
+
end
|
180
|
+
@h2_client.on(:frame_received) do |frame|
|
181
|
+
debug_output("frame: #{frame.inspect}", :receive)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def _tcp_socket(endpoint)
|
186
|
+
tcp = ::Socket.new(SOCKET_FAMILY, ::Socket::SOCK_STREAM, 0)
|
187
|
+
tcp.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
|
188
|
+
|
189
|
+
address = ::Socket.getaddrinfo(endpoint.host, nil, SOCKET_FAMILY).first[3]
|
190
|
+
sockaddr = ::Socket.sockaddr_in(endpoint.port, address)
|
191
|
+
|
192
|
+
[tcp, sockaddr]
|
193
|
+
end
|
194
|
+
|
195
|
+
def _nonblocking_connect(tcp, addr)
|
196
|
+
begin
|
197
|
+
tcp.connect_nonblock(addr)
|
198
|
+
rescue IO::WaitWritable
|
199
|
+
unless IO.select(nil, [tcp], nil, connection_timeout)
|
200
|
+
tcp.close
|
201
|
+
raise
|
202
|
+
end
|
203
|
+
begin
|
204
|
+
tcp.connect_nonblock(addr)
|
205
|
+
rescue Errno::EISCONN
|
206
|
+
# tcp socket connected, continue
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def _tls_context
|
212
|
+
ssl_ctx = OpenSSL::SSL::SSLContext.new(:TLSv1_2)
|
213
|
+
if ssl_verify_peer?
|
214
|
+
ssl_ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
215
|
+
ssl_ctx.ca_file = ssl_ca_bundle ? ssl_ca_bundle : _default_ca_bundle
|
216
|
+
ssl_ctx.ca_path = ssl_ca_directory ? ssl_ca_directory : _default_ca_directory
|
217
|
+
ssl_ctx.cert_store = ssl_ca_store if ssl_ca_store
|
218
|
+
else
|
219
|
+
ssl_ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
220
|
+
end
|
221
|
+
if enable_alpn
|
222
|
+
debug_output("enabling ALPN for TLS ...")
|
223
|
+
ssl_ctx.alpn_protocols = ['h2']
|
224
|
+
end
|
225
|
+
ssl_ctx
|
226
|
+
end
|
227
|
+
|
228
|
+
def _default_ca_bundle
|
229
|
+
File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE) ?
|
230
|
+
OpenSSL::X509::DEFAULT_CERT_FILE : nil
|
231
|
+
end
|
232
|
+
|
233
|
+
def _default_ca_directory
|
234
|
+
Dir.exist?(OpenSSL::X509::DEFAULT_CERT_DIR) ?
|
235
|
+
OpenSSL::X509::DEFAULT_CERT_DIR : nil
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
@@ -0,0 +1,149 @@
|
|
1
|
+
if RUBY_VERSION >= '2.1'
|
2
|
+
require 'http/2'
|
3
|
+
end
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module Seahorse
|
7
|
+
module Client
|
8
|
+
# @api private
|
9
|
+
module H2
|
10
|
+
|
11
|
+
NETWORK_ERRORS = [
|
12
|
+
SocketError, EOFError, IOError, Timeout::Error,
|
13
|
+
Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE,
|
14
|
+
Errno::EINVAL, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError,
|
15
|
+
Errno::EHOSTUNREACH, Errno::ECONNREFUSED,# OpenSSL::SSL::SSLErrorWaitReadable
|
16
|
+
]
|
17
|
+
|
18
|
+
# @api private
|
19
|
+
DNS_ERROR_MESSAGES = [
|
20
|
+
'getaddrinfo: nodename nor servname provided, or not known', # MacOS
|
21
|
+
'getaddrinfo: Name or service not known' # GNU
|
22
|
+
]
|
23
|
+
|
24
|
+
class Handler < Client::Handler
|
25
|
+
|
26
|
+
def call(context)
|
27
|
+
stream = nil
|
28
|
+
begin
|
29
|
+
conn = context.client.connection
|
30
|
+
stream = conn.new_stream
|
31
|
+
|
32
|
+
stream_mutex = Mutex.new
|
33
|
+
close_condition = ConditionVariable.new
|
34
|
+
sync_queue = Queue.new
|
35
|
+
|
36
|
+
conn.connect(context.http_request.endpoint)
|
37
|
+
_register_callbacks(
|
38
|
+
context.http_response,
|
39
|
+
stream,
|
40
|
+
stream_mutex,
|
41
|
+
close_condition,
|
42
|
+
sync_queue
|
43
|
+
)
|
44
|
+
|
45
|
+
conn.debug_output("sending initial request ...")
|
46
|
+
if input_emitter = context[:input_event_emitter]
|
47
|
+
_send_initial_headers(context.http_request, stream)
|
48
|
+
|
49
|
+
# prepare for sending events later
|
50
|
+
input_emitter.stream = stream
|
51
|
+
# request sigv4 serves as the initial #prior_signature
|
52
|
+
input_emitter.encoder.prior_signature =
|
53
|
+
context.http_request.headers['authorization'].split('Signature=').last
|
54
|
+
input_emitter.validate_event = context.config.validate_params
|
55
|
+
else
|
56
|
+
_send_initial_headers(context.http_request, stream)
|
57
|
+
_send_initial_data(context.http_request, stream)
|
58
|
+
end
|
59
|
+
|
60
|
+
conn.start(stream)
|
61
|
+
rescue *NETWORK_ERRORS => error
|
62
|
+
error = NetworkingError.new(
|
63
|
+
error, error_message(context.http_request, error))
|
64
|
+
context.http_response.signal_error(error)
|
65
|
+
rescue => error
|
66
|
+
conn.debug_output(error.inspect)
|
67
|
+
# not retryable
|
68
|
+
context.http_response.signal_error(error)
|
69
|
+
end
|
70
|
+
|
71
|
+
AsyncResponse.new(
|
72
|
+
context: context,
|
73
|
+
stream: stream,
|
74
|
+
stream_mutex: stream_mutex,
|
75
|
+
close_condition: close_condition,
|
76
|
+
sync_queue: sync_queue
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def _register_callbacks(resp, stream, stream_mutex, close_condition, sync_queue)
|
83
|
+
stream.on(:headers) do |headers|
|
84
|
+
resp.signal_headers(headers)
|
85
|
+
end
|
86
|
+
|
87
|
+
stream.on(:data) do |data|
|
88
|
+
resp.signal_data(data)
|
89
|
+
end
|
90
|
+
|
91
|
+
stream.on(:close) do
|
92
|
+
resp.signal_done
|
93
|
+
# block until #wait is ready for signal
|
94
|
+
# else deadlock may happen because #signal happened
|
95
|
+
# eariler than #wait (see AsyncResponse#wait)
|
96
|
+
sync_queue.pop
|
97
|
+
stream_mutex.synchronize {
|
98
|
+
close_condition.signal
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def _send_initial_headers(req, stream)
|
104
|
+
begin
|
105
|
+
headers = _h2_headers(req)
|
106
|
+
stream.headers(headers, end_stream: false)
|
107
|
+
rescue => e
|
108
|
+
raise Http2InitialRequestError.new(e)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def _send_initial_data(req, stream)
|
113
|
+
begin
|
114
|
+
data = req.body.read
|
115
|
+
stream.data(data, end_stream: true)
|
116
|
+
rescue => e
|
117
|
+
raise Http2InitialRequestError.new(e)
|
118
|
+
end
|
119
|
+
data
|
120
|
+
end
|
121
|
+
|
122
|
+
# H2 pseudo headers
|
123
|
+
# https://http2.github.io/http2-spec/#rfc.section.8.1.2.3
|
124
|
+
def _h2_headers(req)
|
125
|
+
headers = {}
|
126
|
+
headers[':method'] = req.http_method.upcase
|
127
|
+
headers[':scheme'] = req.endpoint.scheme
|
128
|
+
headers[':path'] = req.endpoint.path.empty? ? '/' : req.endpoint.path
|
129
|
+
if req.endpoint.query && !req.endpoint.query.empty?
|
130
|
+
headers[':path'] += "?#{req.endpoint.query}"
|
131
|
+
end
|
132
|
+
req.headers.each {|k, v| headers[k.downcase] = v }
|
133
|
+
headers
|
134
|
+
end
|
135
|
+
|
136
|
+
def error_message(req, error)
|
137
|
+
if error.is_a?(SocketError) && DNS_ERROR_MESSAGES.include?(error.message)
|
138
|
+
host = req.endpoint.host
|
139
|
+
"unable to connect to `#{host}`; SocketError: #{error.message}"
|
140
|
+
else
|
141
|
+
error.message
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Seahorse
|
2
|
+
module Client
|
3
|
+
module Http
|
4
|
+
class AsyncResponse < Seahorse::Client::Http::Response
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def signal_headers(headers)
|
11
|
+
# H2 headers arrive as array of pair
|
12
|
+
hash = headers.inject({}) do |h, pair|
|
13
|
+
key, value = pair
|
14
|
+
h[key] = value
|
15
|
+
h
|
16
|
+
end
|
17
|
+
@status_code = hash[":status"].to_i
|
18
|
+
@headers = Headers.new(hash)
|
19
|
+
emit(:headers, @status_code, @headers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def signal_done(options = {})
|
23
|
+
# H2 only has header and body
|
24
|
+
# ':status' header will be sent back
|
25
|
+
if options.keys.sort == [:body, :headers]
|
26
|
+
signal_headers(options[:headers])
|
27
|
+
signal_data(options[:body])
|
28
|
+
signal_done
|
29
|
+
elsif options.empty?
|
30
|
+
@body.rewind if @body.respond_to?(:rewind)
|
31
|
+
@done = true
|
32
|
+
emit(:done)
|
33
|
+
else
|
34
|
+
msg = "options must be empty or must contain :headers and :body"
|
35
|
+
raise ArgumentError, msg
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -40,12 +40,17 @@ module Seahorse
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
# @return [String]
|
43
|
+
# @return [String|Array]
|
44
44
|
def body_contents
|
45
|
-
body.
|
46
|
-
|
47
|
-
|
48
|
-
|
45
|
+
if body.is_a?(Array)
|
46
|
+
# an array of parsed events
|
47
|
+
body
|
48
|
+
else
|
49
|
+
body.rewind
|
50
|
+
contents = body.read
|
51
|
+
body.rewind
|
52
|
+
contents
|
53
|
+
end
|
49
54
|
end
|
50
55
|
|
51
56
|
# @param [Integer] status_code
|