gorgon 0.5.0.rc1 → 0.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/Gemfile.lock +2 -4
  2. data/gorgon.gemspec +0 -1
  3. data/lib/gorgon/amqp_service.rb +5 -5
  4. data/lib/gorgon/gem_command_handler.rb +2 -1
  5. data/lib/gorgon/listener.rb +7 -5
  6. data/lib/gorgon/originator_protocol.rb +1 -0
  7. data/lib/gorgon/version.rb +1 -1
  8. data/lib/gorgon/worker_manager.rb +5 -2
  9. data/lib/gorgon_amq-protocol/.gitignore +15 -0
  10. data/lib/gorgon_amq-protocol/.gitmodules +3 -0
  11. data/lib/gorgon_amq-protocol/.rspec +3 -0
  12. data/lib/gorgon_amq-protocol/.travis.yml +19 -0
  13. data/lib/gorgon_amq-protocol/lib/gorgon_amq/bit_set.rb +82 -0
  14. data/lib/gorgon_amq-protocol/lib/gorgon_amq/endianness.rb +15 -0
  15. data/lib/gorgon_amq-protocol/lib/gorgon_amq/int_allocator.rb +96 -0
  16. data/lib/gorgon_amq-protocol/lib/gorgon_amq/pack.rb +53 -0
  17. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol.rb +4 -0
  18. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/client.rb +2322 -0
  19. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/constants.rb +22 -0
  20. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/exceptions.rb +60 -0
  21. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/float_32bit.rb +14 -0
  22. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/frame.rb +210 -0
  23. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table.rb +142 -0
  24. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table_value_decoder.rb +190 -0
  25. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table_value_encoder.rb +123 -0
  26. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/type_constants.rb +26 -0
  27. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/version.rb +5 -0
  28. data/lib/gorgon_amq-protocol/lib/gorgon_amq/settings.rb +114 -0
  29. data/lib/gorgon_amq-protocol/lib/gorgon_amq/uri.rb +37 -0
  30. data/lib/gorgon_bunny/lib/gorgon_amq/protocol/extensions.rb +16 -0
  31. data/lib/gorgon_bunny/lib/gorgon_bunny.rb +89 -0
  32. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/credentials_encoder.rb +55 -0
  33. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/external_mechanism_encoder.rb +27 -0
  34. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/plain_mechanism_encoder.rb +19 -0
  35. data/lib/gorgon_bunny/lib/gorgon_bunny/channel.rb +1875 -0
  36. data/lib/gorgon_bunny/lib/gorgon_bunny/channel_id_allocator.rb +80 -0
  37. data/lib/gorgon_bunny/lib/gorgon_bunny/compatibility.rb +24 -0
  38. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/atomic_fixnum.rb +74 -0
  39. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/condition.rb +66 -0
  40. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/continuation_queue.rb +41 -0
  41. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/linked_continuation_queue.rb +61 -0
  42. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/synchronized_sorted_set.rb +56 -0
  43. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer.rb +123 -0
  44. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer_tag_generator.rb +23 -0
  45. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer_work_pool.rb +94 -0
  46. data/lib/gorgon_bunny/lib/gorgon_bunny/delivery_info.rb +93 -0
  47. data/lib/gorgon_bunny/lib/gorgon_bunny/exceptions.rb +236 -0
  48. data/lib/gorgon_bunny/lib/gorgon_bunny/exchange.rb +271 -0
  49. data/lib/gorgon_bunny/lib/gorgon_bunny/framing.rb +56 -0
  50. data/lib/gorgon_bunny/lib/gorgon_bunny/heartbeat_sender.rb +70 -0
  51. data/lib/gorgon_bunny/lib/gorgon_bunny/message_properties.rb +119 -0
  52. data/lib/gorgon_bunny/lib/gorgon_bunny/queue.rb +387 -0
  53. data/lib/gorgon_bunny/lib/gorgon_bunny/reader_loop.rb +116 -0
  54. data/lib/gorgon_bunny/lib/gorgon_bunny/return_info.rb +74 -0
  55. data/lib/gorgon_bunny/lib/gorgon_bunny/session.rb +1044 -0
  56. data/lib/gorgon_bunny/lib/gorgon_bunny/socket.rb +83 -0
  57. data/lib/gorgon_bunny/lib/gorgon_bunny/ssl_socket.rb +57 -0
  58. data/lib/gorgon_bunny/lib/gorgon_bunny/system_timer.rb +20 -0
  59. data/lib/gorgon_bunny/lib/gorgon_bunny/test_kit.rb +27 -0
  60. data/lib/gorgon_bunny/lib/gorgon_bunny/timeout.rb +18 -0
  61. data/lib/gorgon_bunny/lib/gorgon_bunny/transport.rb +398 -0
  62. data/lib/gorgon_bunny/lib/gorgon_bunny/version.rb +6 -0
  63. data/lib/gorgon_bunny/lib/gorgon_bunny/versioned_delivery_tag.rb +28 -0
  64. data/spec/crash_reporter_spec.rb +1 -1
  65. data/spec/gem_command_handler_spec.rb +2 -2
  66. data/spec/listener_spec.rb +5 -5
  67. data/spec/worker_manager_spec.rb +3 -3
  68. metadata +56 -17
@@ -0,0 +1,83 @@
1
+ require "socket"
2
+
3
+ module GorgonBunny
4
+ # TCP socket extension that uses TCP_NODELAY and supports reading
5
+ # fully.
6
+ #
7
+ # Heavily inspired by Dalli by Mike Perham.
8
+ # @private
9
+ class Socket < TCPSocket
10
+ attr_accessor :options
11
+
12
+ # IO::WaitReadable is 1.9+ only
13
+ READ_RETRY_EXCEPTION_CLASSES = [Errno::EAGAIN, Errno::EWOULDBLOCK]
14
+ READ_RETRY_EXCEPTION_CLASSES << IO::WaitReadable if IO.const_defined?(:WaitReadable)
15
+
16
+ def self.open(host, port, options = {})
17
+ Timeout.timeout(options[:socket_timeout], ClientTimeout) do
18
+ sock = new(host, port)
19
+ if Socket.constants.include?('TCP_NODELAY') || Socket.constants.include?(:TCP_NODELAY)
20
+ sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
21
+ end
22
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options.fetch(:keepalive, true)
23
+ sock.options = {:host => host, :port => port}.merge(options)
24
+ sock
25
+ end
26
+ end
27
+
28
+ # Reads given number of bytes with an optional timeout
29
+ #
30
+ # @param [Integer] count How many bytes to read
31
+ # @param [Integer] timeout Timeout
32
+ #
33
+ # @return [String] Data read from the socket
34
+ # @api public
35
+ def read_fully(count, timeout = nil)
36
+ return nil if @__bunny_socket_eof_flag__
37
+
38
+ value = ''
39
+ begin
40
+ loop do
41
+ value << read_nonblock(count - value.bytesize)
42
+ break if value.bytesize >= count
43
+ end
44
+ rescue EOFError
45
+ # @eof will break Rubinius' TCPSocket implementation. MK.
46
+ @__bunny_socket_eof_flag__ = true
47
+ rescue *READ_RETRY_EXCEPTION_CLASSES
48
+ if IO.select([self], nil, nil, timeout)
49
+ retry
50
+ else
51
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
52
+ end
53
+ end
54
+ value
55
+ end # read_fully
56
+
57
+ # Writes provided data using IO#write_nonblock, taking care of handling
58
+ # of exceptions it raises when writing would fail (e.g. due to socket buffer
59
+ # being full).
60
+ #
61
+ # IMPORTANT: this method will mutate (slice) the argument. Pass in duplicates
62
+ # if this is not appropriate in your case.
63
+ #
64
+ # @param [String] data Data to write
65
+ #
66
+ # @api public
67
+ def write_nonblock_fully(data)
68
+ return nil if @__bunny_socket_eof_flag__
69
+
70
+ begin
71
+ while !data.empty?
72
+ written = self.write_nonblock(data)
73
+ data.slice!(0, written)
74
+ end
75
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN
76
+ IO.select([], [self])
77
+ retry
78
+ end
79
+
80
+ data.bytesize
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,57 @@
1
+ require "socket"
2
+
3
+ module GorgonBunny
4
+ begin
5
+ require "openssl"
6
+
7
+ # TLS-enabled TCP socket that implements convenience
8
+ # methods found in GorgonBunny::Socket.
9
+ class SSLSocket < OpenSSL::SSL::SSLSocket
10
+
11
+ # IO::WaitReadable is 1.9+ only
12
+ READ_RETRY_EXCEPTION_CLASSES = [Errno::EAGAIN, Errno::EWOULDBLOCK]
13
+ READ_RETRY_EXCEPTION_CLASSES << IO::WaitReadable if IO.const_defined?(:WaitReadable)
14
+
15
+
16
+ # Reads given number of bytes with an optional timeout
17
+ #
18
+ # @param [Integer] count How many bytes to read
19
+ # @param [Integer] timeout Timeout
20
+ #
21
+ # @return [String] Data read from the socket
22
+ # @api public
23
+ def read_fully(count, timeout = nil)
24
+ return nil if @__bunny_socket_eof_flag__
25
+
26
+ value = ''
27
+ begin
28
+ loop do
29
+ value << read_nonblock(count - value.bytesize)
30
+ break if value.bytesize >= count
31
+ end
32
+ rescue EOFError => e
33
+ @__bunny_socket_eof_flag__ = true
34
+ rescue OpenSSL::SSL::SSLError => e
35
+ if e.message == "read would block"
36
+ if IO.select([self], nil, nil, timeout)
37
+ retry
38
+ else
39
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
40
+ end
41
+ else
42
+ raise e
43
+ end
44
+ rescue *READ_RETRY_EXCEPTION_CLASSES => e
45
+ if IO.select([self], nil, nil, timeout)
46
+ retry
47
+ else
48
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
49
+ end
50
+ end
51
+ value
52
+ end
53
+ end
54
+ rescue LoadError => le
55
+ puts "Could not load OpenSSL"
56
+ end
57
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8; mode: ruby -*-
2
+
3
+ require "system_timer"
4
+
5
+ module GorgonBunny
6
+ # Used for Ruby before 1.9
7
+ class SystemTimer
8
+ # Executes a block of code, raising if the execution does not finish
9
+ # in the alloted period of time, in seconds.
10
+ def self.timeout(seconds, exception = nil)
11
+ if seconds
12
+ ::SystemTimer.timeout_after(seconds, exception) do
13
+ yield
14
+ end
15
+ else
16
+ yield
17
+ end
18
+ end
19
+ end # SystemTimer
20
+ end # GorgonBunny
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8 -*-
2
+ module GorgonBunny
3
+ # Unit, integration and stress testing toolkit
4
+ class TestKit
5
+ class << self
6
+
7
+ # @return [Integer] Random integer in the range of [a, b]
8
+ # @api private
9
+ def random_in_range(a, b)
10
+ Range.new(a, b).to_a.sample
11
+ end
12
+
13
+ # @param [Integer] a Lower bound of message size, in KB
14
+ # @param [Integer] b Upper bound of message size, in KB
15
+ # @param [Integer] i Random number to use in message generation
16
+ # @return [String] Message payload of length in the given range, with non-ASCII characters
17
+ # @api public
18
+ def message_in_kb(a, b, i)
19
+ s = "Ю#{i}"
20
+ n = random_in_range(a, b) / s.bytesize
21
+
22
+ s * n * 1024
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module GorgonBunny
2
+ # Unifies Ruby standard library's Timeout (which is not accurate on
3
+ # Ruby 1.8) and SystemTimer (the gem)
4
+ Timeout = if RUBY_VERSION < "1.9"
5
+ begin
6
+ require "gorgon_bunny/system_timer"
7
+ GorgonBunny::SystemTimer
8
+ rescue LoadError
9
+ Timeout
10
+ end
11
+ else
12
+ Timeout
13
+ end
14
+
15
+ # Backwards compatibility
16
+ # @private
17
+ Timer = Timeout
18
+ end
@@ -0,0 +1,398 @@
1
+ require "socket"
2
+ require "thread"
3
+ require "monitor"
4
+
5
+ begin
6
+ require "openssl"
7
+ rescue LoadError => le
8
+ $stderr.puts "Could not load OpenSSL"
9
+ end
10
+
11
+ require "gorgon_bunny/exceptions"
12
+ require "gorgon_bunny/socket"
13
+
14
+ module GorgonBunny
15
+ # @private
16
+ class Transport
17
+
18
+ #
19
+ # API
20
+ #
21
+
22
+ # Default TCP connection timeout
23
+ DEFAULT_CONNECTION_TIMEOUT = 5.0
24
+ # Default TLS protocol version to use.
25
+ # Currently SSLv3, same as in RabbitMQ Java client
26
+ DEFAULT_TLS_PROTOCOL = "SSLv3"
27
+
28
+
29
+ attr_reader :session, :host, :port, :socket, :connect_timeout, :read_write_timeout, :disconnect_timeout
30
+ attr_reader :tls_context
31
+
32
+ def initialize(session, host, port, opts)
33
+ @session = session
34
+ @session_thread = opts[:session_thread]
35
+ @host = host
36
+ @port = port
37
+ @opts = opts
38
+
39
+ @logger = session.logger
40
+ @tls_enabled = tls_enabled?(opts)
41
+
42
+ @read_write_timeout = opts[:socket_timeout] || 3
43
+ @read_write_timeout = nil if @read_write_timeout == 0
44
+ @connect_timeout = self.timeout_from(opts)
45
+ @connect_timeout = nil if @connect_timeout == 0
46
+ @disconnect_timeout = @read_write_timeout || @connect_timeout
47
+
48
+ @writes_mutex = @session.mutex_impl.new
49
+
50
+ maybe_initialize_socket
51
+ prepare_tls_context(opts) if @tls_enabled
52
+ end
53
+
54
+ def hostname
55
+ @host
56
+ end
57
+
58
+ def uses_tls?
59
+ @tls_enabled
60
+ end
61
+ alias tls? uses_tls?
62
+
63
+ def uses_ssl?
64
+ @tls_enabled
65
+ end
66
+ alias ssl? uses_ssl?
67
+
68
+
69
+ def connect
70
+ if uses_ssl?
71
+ @socket.connect
72
+ @socket.post_connection_check(host) if uses_tls? && @verify_peer
73
+
74
+ @status = :connected
75
+
76
+ @socket
77
+ else
78
+ # no-op
79
+ end
80
+ end
81
+
82
+ def connected?
83
+ :not_connected == @status && open?
84
+ end
85
+
86
+ def configure_socket(&block)
87
+ block.call(@socket) if @socket
88
+ end
89
+
90
+ def configure_tls_context(&block)
91
+ block.call(@tls_context) if @tls_context
92
+ end
93
+
94
+ # Writes data to the socket. If read/write timeout was specified, GorgonBunny::ClientTimeout will be raised
95
+ # if the operation times out.
96
+ #
97
+ # @raise [ClientTimeout]
98
+ def write(data)
99
+ begin
100
+ if @read_write_timeout
101
+ GorgonBunny::Timeout.timeout(@read_write_timeout, GorgonBunny::ClientTimeout) do
102
+ if open?
103
+ @writes_mutex.synchronize { @socket.write(data) }
104
+ @socket.flush
105
+ end
106
+ end
107
+ else
108
+ if open?
109
+ @writes_mutex.synchronize { @socket.write(data) }
110
+ @socket.flush
111
+ end
112
+ end
113
+ rescue SystemCallError, GorgonBunny::ClientTimeout, GorgonBunny::ConnectionError, IOError => e
114
+ @logger.error "Got an exception when sending data: #{e.message} (#{e.class.name})"
115
+ close
116
+ @status = :not_connected
117
+
118
+ if @session.automatically_recover?
119
+ @session.handle_network_failure(e)
120
+ else
121
+ @session_thread.raise(GorgonBunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
122
+ end
123
+ end
124
+ end
125
+
126
+ # Writes data to the socket without timeout checks
127
+ def write_without_timeout(data)
128
+ begin
129
+ @writes_mutex.synchronize { @socket.write(data) }
130
+ @socket.flush
131
+ rescue SystemCallError, GorgonBunny::ConnectionError, IOError => e
132
+ close
133
+
134
+ if @session.automatically_recover?
135
+ @session.handle_network_failure(e)
136
+ else
137
+ @session_thread.raise(GorgonBunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
138
+ end
139
+ end
140
+ end
141
+
142
+ # Sends frame to the peer.
143
+ #
144
+ # @raise [ConnectionClosedError]
145
+ # @private
146
+ def send_frame(frame)
147
+ if closed?
148
+ @session.handle_network_failure(ConnectionClosedError.new(frame))
149
+ else
150
+ write(frame.encode)
151
+ end
152
+ end
153
+
154
+ # Sends frame to the peer without timeout control.
155
+ #
156
+ # @raise [ConnectionClosedError]
157
+ # @private
158
+ def send_frame_without_timeout(frame)
159
+ if closed?
160
+ @session.handle_network_failure(ConnectionClosedError.new(frame))
161
+ else
162
+ write_without_timeout(frame.encode)
163
+ end
164
+ end
165
+
166
+
167
+ def close(reason = nil)
168
+ @socket.close if open?
169
+ end
170
+
171
+ def open?
172
+ @socket && !@socket.closed?
173
+ end
174
+
175
+ def closed?
176
+ !open?
177
+ end
178
+
179
+ def flush
180
+ @socket.flush if @socket
181
+ end
182
+
183
+ def read_fully(*args)
184
+ @socket.read_fully(*args)
185
+ end
186
+
187
+ def read_ready?(timeout = nil)
188
+ io = IO.select([@socket].compact, nil, nil, timeout)
189
+ io && io[0].include?(@socket)
190
+ end
191
+
192
+
193
+ # Exposed primarily for GorgonBunny::Channel
194
+ # @private
195
+ def read_next_frame(opts = {})
196
+ header = @socket.read_fully(7)
197
+ # TODO: network issues here will sometimes cause
198
+ # the socket method return an empty string. We need to log
199
+ # and handle this better.
200
+ # type, channel, size = begin
201
+ # GorgonAMQ::Protocol::Frame.decode_header(header)
202
+ # rescue GorgonAMQ::Protocol::EmptyResponseError => e
203
+ # puts "Got GorgonAMQ::Protocol::EmptyResponseError, header is #{header.inspect}"
204
+ # end
205
+ type, channel, size = GorgonAMQ::Protocol::Frame.decode_header(header)
206
+ payload = @socket.read_fully(size)
207
+ frame_end = @socket.read_fully(1)
208
+
209
+ # 1) the size is miscalculated
210
+ if payload.bytesize != size
211
+ raise BadLengthError.new(size, payload.bytesize)
212
+ end
213
+
214
+ # 2) the size is OK, but the string doesn't end with FINAL_OCTET
215
+ raise NoFinalOctetError.new if frame_end != GorgonAMQ::Protocol::Frame::FINAL_OCTET
216
+ GorgonAMQ::Protocol::Frame.new(type, payload, channel)
217
+ end
218
+
219
+
220
+ def self.reacheable?(host, port, timeout)
221
+ begin
222
+ s = GorgonBunny::Socket.open(host, port,
223
+ :socket_timeout => timeout)
224
+
225
+ true
226
+ rescue SocketError, Timeout::Error => e
227
+ false
228
+ ensure
229
+ s.close if s
230
+ end
231
+ end
232
+
233
+ def self.ping!(host, port, timeout)
234
+ raise ConnectionTimeout.new("#{host}:#{port} is unreachable") if !reacheable?(host, port, timeout)
235
+ end
236
+
237
+ def initialize_socket
238
+ begin
239
+ @socket = GorgonBunny::Timeout.timeout(@connect_timeout, ClientTimeout) do
240
+ GorgonBunny::Socket.open(@host, @port,
241
+ :keepalive => @opts[:keepalive],
242
+ :socket_timeout => @connect_timeout)
243
+ end
244
+ rescue StandardError, ClientTimeout => e
245
+ @status = :not_connected
246
+ raise GorgonBunny::TCPConnectionFailed.new(e, self.hostname, self.port)
247
+ end
248
+
249
+ @socket
250
+ end
251
+
252
+ def maybe_initialize_socket
253
+ initialize_socket if !@socket || closed?
254
+ end
255
+
256
+ def post_initialize_socket
257
+ @socket = if uses_tls?
258
+ wrap_in_tls_socket(@socket)
259
+ else
260
+ @socket
261
+ end
262
+ end
263
+
264
+ protected
265
+
266
+ def tls_enabled?(opts)
267
+ opts[:tls] || opts[:ssl] || (opts[:port] == GorgonAMQ::Protocol::TLS_PORT) || false
268
+ end
269
+
270
+ def tls_certificate_path_from(opts)
271
+ opts[:tls_cert] || opts[:ssl_cert] || opts[:tls_cert_path] || opts[:ssl_cert_path] || opts[:tls_certificate_path] || opts[:ssl_certificate_path]
272
+ end
273
+
274
+ def tls_key_path_from(opts)
275
+ opts[:tls_key] || opts[:ssl_key] || opts[:tls_key_path] || opts[:ssl_key_path]
276
+ end
277
+
278
+ def tls_certificate_from(opts)
279
+ begin
280
+ read_client_certificate!
281
+ rescue MissingTLSCertificateFile => e
282
+ inline_client_certificate_from(opts)
283
+ end
284
+ end
285
+
286
+ def tls_key_from(opts)
287
+ begin
288
+ read_client_key!
289
+ rescue MissingTLSKeyFile => e
290
+ inline_client_key_from(opts)
291
+ end
292
+ end
293
+
294
+
295
+ def inline_client_certificate_from(opts)
296
+ opts[:tls_certificate] || opts[:ssl_cert_string]
297
+ end
298
+
299
+ def inline_client_key_from(opts)
300
+ opts[:tls_key] || opts[:ssl_key_string]
301
+ end
302
+
303
+ def prepare_tls_context(opts)
304
+ # client cert/key paths
305
+ @tls_certificate_path = tls_certificate_path_from(opts)
306
+ @tls_key_path = tls_key_path_from(opts)
307
+ # client cert/key
308
+ @tls_certificate = tls_certificate_from(opts)
309
+ @tls_key = tls_key_from(opts)
310
+ @tls_certificate_store = opts[:tls_certificate_store]
311
+
312
+ default_ca_file = ENV[OpenSSL::X509::DEFAULT_CERT_FILE_ENV] || OpenSSL::X509::DEFAULT_CERT_FILE
313
+ default_ca_path = ENV[OpenSSL::X509::DEFAULT_CERT_DIR_ENV] || OpenSSL::X509::DEFAULT_CERT_DIR
314
+ @tls_ca_certificates = opts.fetch(:tls_ca_certificates, [
315
+ default_ca_file,
316
+ File.join(default_ca_path, 'ca-certificates.crt'), # Ubuntu/Debian
317
+ File.join(default_ca_path, 'ca-bundle.crt'), # Amazon Linux & Fedora/RHEL
318
+ File.join(default_ca_path, 'ca-bundle.pem') # OpenSUSE
319
+ ])
320
+ @verify_peer = opts[:verify_ssl] || opts[:verify_peer]
321
+
322
+ @tls_context = initialize_tls_context(OpenSSL::SSL::SSLContext.new)
323
+ end
324
+
325
+ def wrap_in_tls_socket(socket)
326
+ raise ArgumentError, "cannot wrap nil into TLS socket, @tls_context is nil. This is a GorgonBunny bug." unless socket
327
+ raise "cannot wrap a socket into TLS socket, @tls_context is nil. This is a GorgonBunny bug." unless @tls_context
328
+
329
+ s = GorgonBunny::SSLSocket.new(socket, @tls_context)
330
+ s.sync_close = true
331
+ s
332
+ end
333
+
334
+ def check_local_certificate_path!(s)
335
+ raise MissingTLSCertificateFile, "cannot read client TLS certificate from #{s}" unless File.file?(s) && File.readable?(s)
336
+ end
337
+
338
+ def check_local_key_path!(s)
339
+ raise MissingTLSKeyFile, "cannot read client TLS private key from #{s}" unless File.file?(s) && File.readable?(s)
340
+ end
341
+
342
+ def read_client_certificate!
343
+ if @tls_certificate_path
344
+ check_local_certificate_path!(@tls_certificate_path)
345
+ @tls_certificate = File.read(@tls_certificate_path)
346
+ end
347
+ end
348
+
349
+ def read_client_key!
350
+ if @tls_key_path
351
+ check_local_key_path!(@tls_key_path)
352
+ @tls_key = File.read(@tls_key_path)
353
+ end
354
+ end
355
+
356
+ def initialize_tls_context(ctx)
357
+ ctx.cert = OpenSSL::X509::Certificate.new(@tls_certificate) if @tls_certificate
358
+ ctx.key = OpenSSL::PKey::RSA.new(@tls_key) if @tls_key
359
+ ctx.cert_store = if @tls_certificate_store
360
+ @tls_certificate_store
361
+ else
362
+ initialize_tls_certificate_store(@tls_ca_certificates)
363
+ end
364
+
365
+ if !@tls_certificate
366
+ @logger.warn <<-MSG
367
+ Using TLS but no client certificate is provided! If RabbitMQ is configured to verify peer
368
+ certificate, connection upgrade will fail!
369
+ MSG
370
+ end
371
+ if @tls_certificate && !@tls_key
372
+ @logger.warn "Using TLS but no client private key is provided!"
373
+ end
374
+
375
+ # setting TLS/SSL version only works correctly when done
376
+ # vis set_params. MK.
377
+ ctx.set_params(:ssl_version => @opts.fetch(:tls_protocol, DEFAULT_TLS_PROTOCOL))
378
+ ctx.set_params(:verify_mode => OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT) if @verify_peer
379
+
380
+ ctx
381
+ end
382
+
383
+ def initialize_tls_certificate_store(certs)
384
+ certs = certs.select { |path| File.readable? path }
385
+ @logger.debug "Using CA certificates at #{certs.join(', ')}"
386
+ if certs.empty?
387
+ @logger.error "No CA certificates found, add one with :tls_ca_certificates"
388
+ end
389
+ OpenSSL::X509::Store.new.tap do |store|
390
+ certs.each { |path| store.add_file(path) }
391
+ end
392
+ end
393
+
394
+ def timeout_from(options)
395
+ options[:connect_timeout] || options[:connection_timeout] || options[:timeout] || DEFAULT_CONNECTION_TIMEOUT
396
+ end
397
+ end
398
+ end