aws-sdk-core 3.46.2 → 3.47.0

Sign up to get free protection for your applications and to get access to all the features.
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