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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/aws-sdk-core.rb +1 -0
  4. data/lib/aws-sdk-core/async_client_stubs.rb +80 -0
  5. data/lib/aws-sdk-core/binary.rb +3 -0
  6. data/lib/aws-sdk-core/binary/decode_handler.rb +21 -1
  7. data/lib/aws-sdk-core/binary/encode_handler.rb +32 -0
  8. data/lib/aws-sdk-core/binary/event_builder.rb +122 -0
  9. data/lib/aws-sdk-core/binary/event_parser.rb +48 -18
  10. data/lib/aws-sdk-core/binary/event_stream_decoder.rb +5 -2
  11. data/lib/aws-sdk-core/binary/event_stream_encoder.rb +53 -0
  12. data/lib/aws-sdk-core/client_stubs.rb +1 -1
  13. data/lib/aws-sdk-core/errors.rb +4 -0
  14. data/lib/aws-sdk-core/event_emitter.rb +42 -0
  15. data/lib/aws-sdk-core/json/handler.rb +19 -1
  16. data/lib/aws-sdk-core/param_validator.rb +9 -1
  17. data/lib/aws-sdk-core/plugins/event_stream_configuration.rb +14 -0
  18. data/lib/aws-sdk-core/plugins/invocation_id.rb +33 -0
  19. data/lib/aws-sdk-core/plugins/stub_responses.rb +19 -7
  20. data/lib/aws-sdk-core/stubbing/protocols/rest.rb +19 -0
  21. data/lib/aws-sdk-core/stubbing/stub_data.rb +1 -1
  22. data/lib/aws-sdk-sts.rb +1 -1
  23. data/lib/aws-sdk-sts/client.rb +1 -1
  24. data/lib/seahorse.rb +9 -0
  25. data/lib/seahorse/client/async_base.rb +50 -0
  26. data/lib/seahorse/client/async_response.rb +73 -0
  27. data/lib/seahorse/client/base.rb +1 -1
  28. data/lib/seahorse/client/h2/connection.rb +242 -0
  29. data/lib/seahorse/client/h2/handler.rb +149 -0
  30. data/lib/seahorse/client/http/async_response.rb +42 -0
  31. data/lib/seahorse/client/http/response.rb +10 -5
  32. data/lib/seahorse/client/networking_error.rb +28 -0
  33. data/lib/seahorse/client/plugins/h2.rb +64 -0
  34. data/lib/seahorse/model/api.rb +4 -0
  35. data/lib/seahorse/model/operation.rb +4 -0
  36. metadata +35 -4
@@ -46,7 +46,7 @@ module Seahorse
46
46
  # names. These are valid arguments to {#build_request} and are also
47
47
  # valid methods.
48
48
  def operation_names
49
- self.class.api.operation_names
49
+ self.class.api.operation_names - self.class.api.async_operation_names
50
50
  end
51
51
 
52
52
  private
@@ -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.rewind
46
- contents = body.read
47
- body.rewind
48
- contents
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