bunny 1.3.0 → 2.17.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 (143) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE.md +18 -0
  3. data/.gitignore +7 -1
  4. data/.rspec +1 -3
  5. data/.travis.yml +21 -14
  6. data/CONTRIBUTING.md +132 -0
  7. data/ChangeLog.md +887 -1
  8. data/Gemfile +13 -13
  9. data/LICENSE +1 -1
  10. data/README.md +46 -60
  11. data/Rakefile +54 -0
  12. data/bunny.gemspec +5 -11
  13. data/docker-compose.yml +28 -0
  14. data/docker/Dockerfile +24 -0
  15. data/docker/apt/preferences.d/erlang +3 -0
  16. data/docker/apt/sources.list.d/bintray.rabbitmq.list +2 -0
  17. data/docker/docker-entrypoint.sh +26 -0
  18. data/docker/rabbitmq.conf +29 -0
  19. data/examples/connection/automatic_recovery_with_basic_get.rb +1 -1
  20. data/examples/connection/automatic_recovery_with_client_named_queues.rb +1 -1
  21. data/examples/connection/automatic_recovery_with_multiple_consumers.rb +1 -1
  22. data/examples/connection/automatic_recovery_with_republishing.rb +1 -1
  23. data/examples/connection/automatic_recovery_with_server_named_queues.rb +1 -1
  24. data/examples/connection/channel_level_exception.rb +1 -9
  25. data/examples/connection/disabled_automatic_recovery.rb +1 -1
  26. data/examples/connection/heartbeat.rb +1 -1
  27. data/examples/consumers/high_and_low_priority.rb +1 -1
  28. data/examples/guides/extensions/alternate_exchange.rb +2 -0
  29. data/examples/guides/extensions/basic_nack.rb +1 -1
  30. data/examples/guides/extensions/dead_letter_exchange.rb +1 -1
  31. data/examples/guides/getting_started/hello_world.rb +2 -0
  32. data/examples/guides/getting_started/weathr.rb +2 -0
  33. data/examples/guides/queues/one_off_consumer.rb +2 -0
  34. data/examples/guides/queues/redeliveries.rb +4 -2
  35. data/lib/bunny.rb +8 -4
  36. data/lib/bunny/channel.rb +268 -153
  37. data/lib/bunny/channel_id_allocator.rb +6 -4
  38. data/lib/bunny/concurrent/continuation_queue.rb +34 -13
  39. data/lib/bunny/consumer_work_pool.rb +34 -6
  40. data/lib/bunny/cruby/socket.rb +48 -21
  41. data/lib/bunny/cruby/ssl_socket.rb +65 -4
  42. data/lib/bunny/exceptions.rb +25 -4
  43. data/lib/bunny/exchange.rb +24 -28
  44. data/lib/bunny/get_response.rb +1 -1
  45. data/lib/bunny/heartbeat_sender.rb +3 -2
  46. data/lib/bunny/jruby/socket.rb +23 -6
  47. data/lib/bunny/jruby/ssl_socket.rb +5 -0
  48. data/lib/bunny/queue.rb +31 -22
  49. data/lib/bunny/reader_loop.rb +31 -18
  50. data/lib/bunny/session.rb +448 -159
  51. data/lib/bunny/test_kit.rb +14 -0
  52. data/lib/bunny/timeout.rb +1 -12
  53. data/lib/bunny/transport.rb +205 -98
  54. data/lib/bunny/version.rb +1 -1
  55. data/repl +1 -1
  56. data/spec/config/enabled_plugins +1 -0
  57. data/spec/config/rabbitmq.conf +13 -0
  58. data/spec/higher_level_api/integration/basic_ack_spec.rb +175 -16
  59. data/spec/higher_level_api/integration/basic_cancel_spec.rb +77 -11
  60. data/spec/higher_level_api/integration/basic_consume_spec.rb +60 -55
  61. data/spec/higher_level_api/integration/basic_consume_with_objects_spec.rb +6 -6
  62. data/spec/higher_level_api/integration/basic_get_spec.rb +31 -7
  63. data/spec/higher_level_api/integration/basic_nack_spec.rb +22 -19
  64. data/spec/higher_level_api/integration/basic_publish_spec.rb +11 -100
  65. data/spec/higher_level_api/integration/basic_qos_spec.rb +32 -4
  66. data/spec/higher_level_api/integration/basic_reject_spec.rb +94 -16
  67. data/spec/higher_level_api/integration/basic_return_spec.rb +4 -4
  68. data/spec/higher_level_api/integration/channel_close_spec.rb +51 -10
  69. data/spec/higher_level_api/integration/channel_open_spec.rb +12 -12
  70. data/spec/higher_level_api/integration/connection_recovery_spec.rb +424 -221
  71. data/spec/higher_level_api/integration/connection_spec.rb +300 -126
  72. data/spec/higher_level_api/integration/connection_stop_spec.rb +31 -19
  73. data/spec/higher_level_api/integration/consumer_cancellation_notification_spec.rb +17 -17
  74. data/spec/higher_level_api/integration/dead_lettering_spec.rb +34 -11
  75. data/spec/higher_level_api/integration/exchange_bind_spec.rb +5 -5
  76. data/spec/higher_level_api/integration/exchange_declare_spec.rb +32 -31
  77. data/spec/higher_level_api/integration/exchange_delete_spec.rb +12 -12
  78. data/spec/higher_level_api/integration/exchange_unbind_spec.rb +5 -5
  79. data/spec/higher_level_api/integration/exclusive_queue_spec.rb +5 -5
  80. data/spec/higher_level_api/integration/heartbeat_spec.rb +26 -8
  81. data/spec/higher_level_api/integration/message_properties_access_spec.rb +49 -49
  82. data/spec/higher_level_api/integration/predeclared_exchanges_spec.rb +2 -2
  83. data/spec/higher_level_api/integration/publisher_confirms_spec.rb +156 -42
  84. data/spec/higher_level_api/integration/publishing_edge_cases_spec.rb +19 -19
  85. data/spec/higher_level_api/integration/queue_bind_spec.rb +23 -23
  86. data/spec/higher_level_api/integration/queue_declare_spec.rb +129 -34
  87. data/spec/higher_level_api/integration/queue_delete_spec.rb +2 -2
  88. data/spec/higher_level_api/integration/queue_purge_spec.rb +5 -5
  89. data/spec/higher_level_api/integration/queue_unbind_spec.rb +6 -6
  90. data/spec/higher_level_api/integration/read_only_consumer_spec.rb +9 -9
  91. data/spec/higher_level_api/integration/sender_selected_distribution_spec.rb +10 -10
  92. data/spec/higher_level_api/integration/tls_connection_spec.rb +224 -89
  93. data/spec/higher_level_api/integration/toxiproxy_spec.rb +76 -0
  94. data/spec/higher_level_api/integration/tx_commit_spec.rb +1 -1
  95. data/spec/higher_level_api/integration/tx_rollback_spec.rb +1 -1
  96. data/spec/higher_level_api/integration/with_channel_spec.rb +2 -2
  97. data/spec/issues/issue100_spec.rb +11 -11
  98. data/spec/issues/issue141_spec.rb +13 -14
  99. data/spec/issues/issue202_spec.rb +1 -1
  100. data/spec/issues/issue224_spec.rb +40 -0
  101. data/spec/issues/issue465_spec.rb +32 -0
  102. data/spec/issues/issue549_spec.rb +30 -0
  103. data/spec/issues/issue78_spec.rb +21 -24
  104. data/spec/issues/issue83_spec.rb +5 -6
  105. data/spec/issues/issue97_spec.rb +44 -45
  106. data/spec/lower_level_api/integration/basic_cancel_spec.rb +15 -16
  107. data/spec/lower_level_api/integration/basic_consume_spec.rb +20 -21
  108. data/spec/spec_helper.rb +8 -26
  109. data/spec/stress/channel_close_stress_spec.rb +64 -0
  110. data/spec/stress/channel_open_stress_spec.rb +15 -9
  111. data/spec/stress/channel_open_stress_with_single_threaded_connection_spec.rb +7 -7
  112. data/spec/stress/concurrent_consumers_stress_spec.rb +18 -16
  113. data/spec/stress/concurrent_publishers_stress_spec.rb +16 -19
  114. data/spec/stress/connection_open_close_spec.rb +9 -9
  115. data/spec/stress/merry_go_round_spec.rb +105 -0
  116. data/spec/tls/client_key.pem +49 -25
  117. data/spec/tls/generate-server-cert.sh +8 -0
  118. data/spec/tls/server-openssl.cnf +10 -0
  119. data/spec/tls/server.csr +16 -0
  120. data/spec/tls/server_key.pem +49 -25
  121. data/spec/toxiproxy_helper.rb +28 -0
  122. data/spec/unit/bunny_spec.rb +5 -5
  123. data/spec/unit/concurrent/atomic_fixnum_spec.rb +6 -6
  124. data/spec/unit/concurrent/condition_spec.rb +8 -8
  125. data/spec/unit/concurrent/linked_continuation_queue_spec.rb +2 -2
  126. data/spec/unit/concurrent/synchronized_sorted_set_spec.rb +16 -16
  127. data/spec/unit/exchange_recovery_spec.rb +39 -0
  128. data/spec/unit/version_delivery_tag_spec.rb +3 -3
  129. metadata +65 -47
  130. data/.ruby-version +0 -1
  131. data/lib/bunny/compatibility.rb +0 -24
  132. data/lib/bunny/system_timer.rb +0 -20
  133. data/spec/compatibility/queue_declare_spec.rb +0 -44
  134. data/spec/compatibility/queue_declare_with_default_channel_spec.rb +0 -33
  135. data/spec/higher_level_api/integration/basic_recover_spec.rb +0 -18
  136. data/spec/higher_level_api/integration/confirm_select_spec.rb +0 -19
  137. data/spec/higher_level_api/integration/consistent_hash_exchange_spec.rb +0 -50
  138. data/spec/higher_level_api/integration/merry_go_round_spec.rb +0 -85
  139. data/spec/stress/long_running_consumer_spec.rb +0 -83
  140. data/spec/tls/cacert.pem +0 -18
  141. data/spec/tls/client_cert.pem +0 -18
  142. data/spec/tls/server_cert.pem +0 -18
  143. data/spec/unit/system_timer_spec.rb +0 -10
@@ -1,9 +1,23 @@
1
1
  # -*- coding: utf-8 -*-
2
+
3
+ require "timeout"
4
+
2
5
  module Bunny
3
6
  # Unit, integration and stress testing toolkit
4
7
  class TestKit
5
8
  class << self
6
9
 
10
+ def poll_while(timeout = 60, &probe)
11
+ Timeout.timeout(timeout) {
12
+ sleep 0.1 while probe.call
13
+ }
14
+ end
15
+ def poll_until(timeout = 60, &probe)
16
+ Timeout.timeout(timeout) {
17
+ sleep 0.1 until probe.call
18
+ }
19
+ end
20
+
7
21
  # @return [Integer] Random integer in the range of [a, b]
8
22
  # @api private
9
23
  def random_in_range(a, b)
data/lib/bunny/timeout.rb CHANGED
@@ -1,16 +1,5 @@
1
1
  module Bunny
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 "bunny/system_timer"
7
- Bunny::SystemTimer
8
- rescue LoadError
9
- Timeout
10
- end
11
- else
12
- Timeout
13
- end
2
+ Timeout = ::Timeout
14
3
 
15
4
  # Backwards compatibility
16
5
  # @private
@@ -4,7 +4,7 @@ require "monitor"
4
4
 
5
5
  begin
6
6
  require "openssl"
7
- rescue LoadError => le
7
+ rescue LoadError => _le
8
8
  $stderr.puts "Could not load OpenSSL"
9
9
  end
10
10
 
@@ -20,18 +20,22 @@ module Bunny
20
20
  #
21
21
 
22
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"
23
+ DEFAULT_CONNECTION_TIMEOUT = 30.0
27
24
 
25
+ DEFAULT_READ_TIMEOUT = 30.0
26
+ DEFAULT_WRITE_TIMEOUT = 30.0
28
27
 
29
- attr_reader :session, :host, :port, :socket, :connect_timeout, :read_write_timeout, :disconnect_timeout
30
- attr_reader :tls_context
28
+ attr_reader :session, :host, :port, :socket, :connect_timeout, :read_timeout, :write_timeout, :disconnect_timeout
29
+ attr_reader :tls_context, :verify_peer, :tls_ca_certificates, :tls_certificate_path, :tls_key_path
30
+
31
+ def read_timeout=(v)
32
+ @read_timeout = v
33
+ @read_timeout = nil if @read_timeout == 0
34
+ end
31
35
 
32
36
  def initialize(session, host, port, opts)
33
37
  @session = session
34
- @session_thread = opts[:session_thread]
38
+ @session_error_handler = opts[:session_error_handler]
35
39
  @host = host
36
40
  @port = port
37
41
  @opts = opts
@@ -39,14 +43,22 @@ module Bunny
39
43
  @logger = session.logger
40
44
  @tls_enabled = tls_enabled?(opts)
41
45
 
42
- @read_write_timeout = opts[:socket_timeout] || 3
43
- @read_write_timeout = nil if @read_write_timeout == 0
46
+ @read_timeout = opts[:read_timeout] || DEFAULT_READ_TIMEOUT
47
+ @read_timeout = nil if @read_timeout == 0
48
+
49
+ @write_timeout = opts[:socket_timeout] # Backwards compatability
50
+
51
+ @write_timeout ||= opts[:write_timeout] || DEFAULT_WRITE_TIMEOUT
52
+ @write_timeout = nil if @write_timeout == 0
53
+
44
54
  @connect_timeout = self.timeout_from(opts)
45
55
  @connect_timeout = nil if @connect_timeout == 0
46
- @disconnect_timeout = @read_write_timeout || @connect_timeout
56
+ @disconnect_timeout = @write_timeout || @read_timeout || @connect_timeout
47
57
 
48
58
  @writes_mutex = @session.mutex_impl.new
49
59
 
60
+ @socket = nil
61
+
50
62
  prepare_tls_context(opts) if @tls_enabled
51
63
  end
52
64
 
@@ -70,9 +82,30 @@ module Bunny
70
82
 
71
83
 
72
84
  def connect
73
- if uses_ssl?
74
- @socket.connect
75
- @socket.post_connection_check(host) if uses_tls? && @verify_peer
85
+ if uses_tls?
86
+ begin
87
+ @socket.connect
88
+ rescue OpenSSL::SSL::SSLError => e
89
+ @logger.error { "TLS connection failed: #{e.message}" }
90
+ raise e
91
+ end
92
+
93
+ log_peer_certificate_info(Logger::DEBUG, @socket.peer_cert)
94
+ log_peer_certificate_chain_info(Logger::DEBUG, @socket.peer_cert_chain)
95
+
96
+ begin
97
+ @socket.post_connection_check(host) if @verify_peer
98
+ rescue OpenSSL::SSL::SSLError => e
99
+ @logger.error do
100
+ msg = "Peer verification of target server failed: #{e.message}. "
101
+ msg += "Target hostname: #{hostname}, see peer certificate chain details below."
102
+ msg
103
+ end
104
+ log_peer_certificate_info(Logger::ERROR, @socket.peer_cert)
105
+ log_peer_certificate_chain_info(Logger::ERROR, @socket.peer_cert_chain)
106
+
107
+ raise e
108
+ end
76
109
 
77
110
  @status = :connected
78
111
 
@@ -83,7 +116,7 @@ module Bunny
83
116
  end
84
117
 
85
118
  def connected?
86
- :not_connected == @status && open?
119
+ :connected == @status && open?
87
120
  end
88
121
 
89
122
  def configure_socket(&block)
@@ -94,50 +127,68 @@ module Bunny
94
127
  block.call(@tls_context) if @tls_context
95
128
  end
96
129
 
97
- # Writes data to the socket. If read/write timeout was specified, Bunny::ClientTimeout will be raised
98
- # if the operation times out.
99
- #
100
- # @raise [ClientTimeout]
101
- def write(data)
102
- begin
103
- if @read_write_timeout
104
- Bunny::Timeout.timeout(@read_write_timeout, Bunny::ClientTimeout) do
105
- if open?
106
- @writes_mutex.synchronize { @socket.write(data) }
107
- @socket.flush
130
+ if defined?(JRUBY_VERSION)
131
+ # Writes data to the socket.
132
+ def write(data)
133
+ return write_without_timeout(data) unless @write_timeout
134
+
135
+ begin
136
+ if open?
137
+ @writes_mutex.synchronize do
138
+ @socket.write(data)
108
139
  end
109
140
  end
110
- else
111
- if open?
112
- @writes_mutex.synchronize { @socket.write(data) }
113
- @socket.flush
141
+ rescue SystemCallError, Timeout::Error, Bunny::ConnectionError, IOError => e
142
+ @logger.error "Got an exception when sending data: #{e.message} (#{e.class.name})"
143
+ close
144
+ @status = :not_connected
145
+
146
+ if @session.automatically_recover?
147
+ @session.handle_network_failure(e)
148
+ else
149
+ @session_error_handler.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
114
150
  end
115
151
  end
116
- rescue SystemCallError, Bunny::ClientTimeout, Bunny::ConnectionError, IOError => e
117
- @logger.error "Got an exception when sending data: #{e.message} (#{e.class.name})"
118
- close
119
- @status = :not_connected
152
+ end
153
+ else
154
+ # Writes data to the socket. If read/write timeout was specified the operation will return after that
155
+ # amount of time has elapsed waiting for the socket.
156
+ def write(data)
157
+ return write_without_timeout(data) unless @write_timeout
120
158
 
121
- if @session.automatically_recover?
122
- @session.handle_network_failure(e)
123
- else
124
- @session_thread.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
159
+ begin
160
+ if open?
161
+ @writes_mutex.synchronize do
162
+ @socket.write_nonblock_fully(data, @write_timeout)
163
+ end
164
+ end
165
+ rescue SystemCallError, Timeout::Error, Bunny::ConnectionError, IOError => e
166
+ @logger.error "Got an exception when sending data: #{e.message} (#{e.class.name})"
167
+ close
168
+ @status = :not_connected
169
+
170
+ if @session.automatically_recover?
171
+ @session.handle_network_failure(e)
172
+ else
173
+ @session_error_handler.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
174
+ end
125
175
  end
126
176
  end
127
177
  end
128
178
 
129
179
  # Writes data to the socket without timeout checks
130
- def write_without_timeout(data)
180
+ def write_without_timeout(data, raise_exceptions = false)
131
181
  begin
132
182
  @writes_mutex.synchronize { @socket.write(data) }
133
183
  @socket.flush
134
184
  rescue SystemCallError, Bunny::ConnectionError, IOError => e
135
185
  close
186
+ raise e if raise_exceptions
136
187
 
137
188
  if @session.automatically_recover?
138
189
  @session.handle_network_failure(e)
139
190
  else
140
- @session_thread.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
191
+ @session_error_handler.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
141
192
  end
142
193
  end
143
194
  end
@@ -183,8 +234,20 @@ module Bunny
183
234
  @socket.flush if @socket
184
235
  end
185
236
 
186
- def read_fully(*args)
187
- @socket.read_fully(*args)
237
+ def read_fully(count)
238
+ begin
239
+ @socket.read_fully(count, @read_timeout)
240
+ rescue SystemCallError, Timeout::Error, Bunny::ConnectionError, IOError => e
241
+ @logger.error "Got an exception when receiving data: #{e.message} (#{e.class.name})"
242
+ close
243
+ @status = :not_connected
244
+
245
+ if @session.automatically_recover?
246
+ raise
247
+ else
248
+ @session_error_handler.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
249
+ end
250
+ end
188
251
  end
189
252
 
190
253
  def read_ready?(timeout = nil)
@@ -192,22 +255,17 @@ module Bunny
192
255
  io && io[0].include?(@socket)
193
256
  end
194
257
 
195
-
196
258
  # Exposed primarily for Bunny::Channel
197
259
  # @private
198
260
  def read_next_frame(opts = {})
199
- header = @socket.read_fully(7)
200
- # TODO: network issues here will sometimes cause
201
- # the socket method return an empty string. We need to log
202
- # and handle this better.
203
- # type, channel, size = begin
204
- # AMQ::Protocol::Frame.decode_header(header)
205
- # rescue AMQ::Protocol::EmptyResponseError => e
206
- # puts "Got AMQ::Protocol::EmptyResponseError, header is #{header.inspect}"
207
- # end
261
+ header = read_fully(7)
208
262
  type, channel, size = AMQ::Protocol::Frame.decode_header(header)
209
- payload = @socket.read_fully(size)
210
- frame_end = @socket.read_fully(1)
263
+ payload = if size > 0
264
+ read_fully(size)
265
+ else
266
+ ''
267
+ end
268
+ frame_end = read_fully(1)
211
269
 
212
270
  # 1) the size is miscalculated
213
271
  if payload.bytesize != size
@@ -223,10 +281,10 @@ module Bunny
223
281
  def self.reacheable?(host, port, timeout)
224
282
  begin
225
283
  s = Bunny::SocketImpl.open(host, port,
226
- :socket_timeout => timeout)
284
+ :connect_timeout => timeout)
227
285
 
228
286
  true
229
- rescue SocketError, Timeout::Error => e
287
+ rescue SocketError, Timeout::Error => _e
230
288
  false
231
289
  ensure
232
290
  s.close if s
@@ -241,7 +299,7 @@ module Bunny
241
299
  begin
242
300
  @socket = Bunny::SocketImpl.open(@host, @port,
243
301
  :keepalive => @opts[:keepalive],
244
- :socket_timeout => @connect_timeout)
302
+ :connect_timeout => @connect_timeout)
245
303
  rescue StandardError, ClientTimeout => e
246
304
  @status = :not_connected
247
305
  raise Bunny::TCPConnectionFailed.new(e, self.hostname, self.port)
@@ -255,7 +313,7 @@ module Bunny
255
313
  end
256
314
 
257
315
  def post_initialize_socket
258
- @socket = if uses_tls?
316
+ @socket = if uses_tls? and !@socket.is_a?(Bunny::SSLSocketImpl)
259
317
  wrap_in_tls_socket(@socket)
260
318
  else
261
319
  @socket
@@ -265,23 +323,27 @@ module Bunny
265
323
  protected
266
324
 
267
325
  def tls_enabled?(opts)
268
- return opts[:tls] unless opts[:tls].nil?
269
- return opts[:ssl] unless opts[:ssl].nil?
326
+ return !!opts[:tls] unless opts[:tls].nil?
327
+ return !!opts[:ssl] unless opts[:ssl].nil?
270
328
  (opts[:port] == AMQ::Protocol::TLS_PORT) || false
271
329
  end
272
330
 
331
+ def tls_ca_certificates_paths_from(opts)
332
+ Array(opts[:cacertfile] || opts[:tls_ca_certificates] || opts[:ssl_ca_certificates])
333
+ end
334
+
273
335
  def tls_certificate_path_from(opts)
274
- opts[:tls_cert] || opts[:ssl_cert] || opts[:tls_cert_path] || opts[:ssl_cert_path] || opts[:tls_certificate_path] || opts[:ssl_certificate_path]
336
+ opts[:certfile] || opts[:tls_cert] || opts[:ssl_cert] || opts[:tls_cert_path] || opts[:ssl_cert_path] || opts[:tls_certificate_path] || opts[:ssl_certificate_path]
275
337
  end
276
338
 
277
339
  def tls_key_path_from(opts)
278
- opts[:tls_key] || opts[:ssl_key] || opts[:tls_key_path] || opts[:ssl_key_path]
340
+ opts[:keyfile] || opts[:tls_key] || opts[:ssl_key] || opts[:tls_key_path] || opts[:ssl_key_path]
279
341
  end
280
342
 
281
343
  def tls_certificate_from(opts)
282
344
  begin
283
345
  read_client_certificate!
284
- rescue MissingTLSCertificateFile => e
346
+ rescue MissingTLSCertificateFile => _e
285
347
  inline_client_certificate_from(opts)
286
348
  end
287
349
  end
@@ -289,14 +351,37 @@ module Bunny
289
351
  def tls_key_from(opts)
290
352
  begin
291
353
  read_client_key!
292
- rescue MissingTLSKeyFile => e
354
+ rescue MissingTLSKeyFile => _e
293
355
  inline_client_key_from(opts)
294
356
  end
295
357
  end
296
358
 
359
+ def peer_certificate_info(peer_cert, prefix = "Peer's leaf certificate")
360
+ exts = peer_cert.extensions.map { |x| x.value }
361
+ # Subject Alternative Names
362
+ sans = exts.select { |s| s =~ /^DNS/ }.map { |s| s.gsub(/^DNS:/, "") }
363
+
364
+ msg = "#{prefix} subject: #{peer_cert.subject}, "
365
+ msg += "subject alternative names: #{sans.join(', ')}, "
366
+ msg += "issuer: #{peer_cert.issuer}, "
367
+ msg += "not valid after: #{peer_cert.not_after}, "
368
+ msg += "X.509 usage extensions: #{exts.join(', ')}"
369
+
370
+ msg
371
+ end
372
+
373
+ def log_peer_certificate_info(severity, peer_cert, prefix = "Peer's leaf certificate")
374
+ @logger.add(severity) { peer_certificate_info(peer_cert, prefix) }
375
+ end
376
+
377
+ def log_peer_certificate_chain_info(severity, chain)
378
+ chain.each do |cert|
379
+ self.log_peer_certificate_info(severity, cert, "Peer's certificate chain entry")
380
+ end
381
+ end
297
382
 
298
383
  def inline_client_certificate_from(opts)
299
- opts[:tls_certificate] || opts[:ssl_cert_string]
384
+ opts[:tls_certificate] || opts[:ssl_cert_string] || opts[:tls_cert]
300
385
  end
301
386
 
302
387
  def inline_client_key_from(opts)
@@ -304,6 +389,10 @@ module Bunny
304
389
  end
305
390
 
306
391
  def prepare_tls_context(opts)
392
+ if opts.values_at(:verify_ssl, :verify_peer, :verify).all?(&:nil?)
393
+ opts[:verify_peer] = true
394
+ end
395
+
307
396
  # client cert/key paths
308
397
  @tls_certificate_path = tls_certificate_path_from(opts)
309
398
  @tls_key_path = tls_key_path_from(opts)
@@ -312,10 +401,20 @@ module Bunny
312
401
  @tls_key = tls_key_from(opts)
313
402
  @tls_certificate_store = opts[:tls_certificate_store]
314
403
 
315
- @tls_ca_certificates = opts.fetch(:tls_ca_certificates, default_tls_certificates)
316
- @verify_peer = opts[:verify_ssl] || opts[:verify_peer]
404
+ @verify_peer = as_boolean(opts[:verify_ssl] || opts[:verify_peer] || opts[:verify])
317
405
 
318
- @tls_context = initialize_tls_context(OpenSSL::SSL::SSLContext.new)
406
+ @tls_context = initialize_tls_context(OpenSSL::SSL::SSLContext.new, opts)
407
+ end
408
+
409
+ def as_boolean(val)
410
+ case val
411
+ when true then true
412
+ when false then false
413
+ when "true" then true
414
+ when "false" then false
415
+ else
416
+ !!val
417
+ end
319
418
  end
320
419
 
321
420
  def wrap_in_tls_socket(socket)
@@ -323,6 +422,11 @@ module Bunny
323
422
  raise "cannot wrap a socket into TLS socket, @tls_context is nil. This is a Bunny bug." unless @tls_context
324
423
 
325
424
  s = Bunny::SSLSocketImpl.new(socket, @tls_context)
425
+
426
+ # always set the SNI server name if possible since RFC 3546 and RFC 6066 both state
427
+ # that TLS clients supporting the extensions can talk to TLS servers that do not
428
+ s.hostname = @host if s.respond_to?(:hostname)
429
+
326
430
  s.sync_close = true
327
431
  s
328
432
  end
@@ -349,65 +453,68 @@ module Bunny
349
453
  end
350
454
  end
351
455
 
352
- def initialize_tls_context(ctx)
456
+ def initialize_tls_context(ctx, opts={})
353
457
  ctx.cert = OpenSSL::X509::Certificate.new(@tls_certificate) if @tls_certificate
354
458
  ctx.key = OpenSSL::PKey::RSA.new(@tls_key) if @tls_key
355
459
  ctx.cert_store = if @tls_certificate_store
356
460
  @tls_certificate_store
357
461
  else
462
+ # this ivar exists so that this value can be exposed in the API
463
+ @tls_ca_certificates = tls_ca_certificates_paths_from(opts)
358
464
  initialize_tls_certificate_store(@tls_ca_certificates)
359
465
  end
360
466
 
361
467
  if !@tls_certificate
362
468
  @logger.warn <<-MSG
363
- Using TLS but no client certificate is provided! If RabbitMQ is configured to verify peer
364
- certificate, connection upgrade will fail!
469
+ Using TLS but no client certificate is provided. If RabbitMQ is configured to require & verify peer
470
+ certificate, connection will be rejected. Learn more at https://www.rabbitmq.com/ssl.html
365
471
  MSG
366
472
  end
367
473
  if @tls_certificate && !@tls_key
368
474
  @logger.warn "Using TLS but no client private key is provided!"
369
475
  end
370
476
 
371
- # setting TLS/SSL version only works correctly when done
372
- # vis set_params. MK.
373
- ctx.set_params(:ssl_version => @opts.fetch(:tls_protocol, DEFAULT_TLS_PROTOCOL))
374
-
375
477
  verify_mode = if @verify_peer
376
478
  OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
377
479
  else
378
480
  OpenSSL::SSL::VERIFY_NONE
379
481
  end
482
+ @logger.debug { "Will use peer verification mode #{verify_mode}" }
483
+ ctx.verify_mode = verify_mode
484
+
485
+ if !@verify_peer
486
+ @logger.warn <<-MSG
487
+ Using TLS but peer hostname verification is disabled. This is convenient for local development
488
+ but prone to man-in-the-middle attacks. Please set verify_peer: true in production. Learn more at https://www.rabbitmq.com/ssl.html
489
+ MSG
490
+ end
380
491
 
381
- ctx.set_params(:verify_mode => verify_mode)
492
+ ssl_version = opts[:tls_protocol] || opts[:ssl_version]
493
+ ctx.ssl_version = ssl_version if ssl_version
382
494
 
383
495
  ctx
384
496
  end
385
497
 
386
- def default_tls_certificates
387
- if defined?(JRUBY_VERSION)
388
- # see https://github.com/jruby/jruby/issues/1055. MK.
389
- []
390
- else
391
- default_ca_file = ENV[OpenSSL::X509::DEFAULT_CERT_FILE_ENV] || OpenSSL::X509::DEFAULT_CERT_FILE
392
- default_ca_path = ENV[OpenSSL::X509::DEFAULT_CERT_DIR_ENV] || OpenSSL::X509::DEFAULT_CERT_DIR
393
-
394
- [
395
- default_ca_file,
396
- File.join(default_ca_path, 'ca-certificates.crt'), # Ubuntu/Debian
397
- File.join(default_ca_path, 'ca-bundle.crt'), # Amazon Linux & Fedora/RHEL
398
- File.join(default_ca_path, 'ca-bundle.pem') # OpenSUSE
399
- ].uniq
400
- end
401
- end
402
-
403
498
  def initialize_tls_certificate_store(certs)
404
- certs = certs.select { |path| File.readable? path }
405
- @logger.debug "Using CA certificates at #{certs.join(', ')}"
406
- if certs.empty?
407
- @logger.error "No CA certificates found, add one with :tls_ca_certificates"
499
+ cert_files = []
500
+ cert_inlines = []
501
+ certs.each do |cert|
502
+ # if it starts with / or C:/ then it's a file path that may or may not
503
+ # exist (e.g. a default OpenSSL path). MK.
504
+ if File.readable?(cert) || cert =~ /^([a-z]:?)?\//i
505
+ cert_files.push(cert)
506
+ else
507
+ cert_inlines.push(cert)
508
+ end
408
509
  end
510
+ @logger.debug { "Using CA certificates at #{cert_files.join(', ')}" }
511
+ @logger.debug { "Using #{cert_inlines.count} inline CA certificates" }
409
512
  OpenSSL::X509::Store.new.tap do |store|
410
- certs.each { |path| store.add_file(path) }
513
+ store.set_default_paths
514
+ cert_files.select { |path| File.readable?(path) }.
515
+ each { |path| store.add_file(path) }
516
+ cert_inlines.
517
+ each { |cert| store.add_cert(OpenSSL::X509::Certificate.new(cert)) }
411
518
  end
412
519
  end
413
520