amqp-client 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,409 +3,490 @@
3
3
  require "socket"
4
4
  require "uri"
5
5
  require "openssl"
6
- require_relative "./frames"
6
+ require_relative "./frame_bytes"
7
7
  require_relative "./channel"
8
8
  require_relative "./errors"
9
9
 
10
10
  module AMQP
11
- # Represents a single AMQP connection
12
- class Connection
13
- def self.connect(uri, read_loop_thread: true, **options)
14
- uri = URI.parse(uri)
15
- tls = uri.scheme == "amqps"
16
- port = port_from_env || uri.port || (tls ? 5671 : 5672)
17
- host = uri.host || "localhost"
18
- user = uri.user || "guest"
19
- password = uri.password || "guest"
20
- vhost = URI.decode_www_form_component(uri.path[1..-1] || "/")
21
- options = URI.decode_www_form(uri.query || "").map! { |k, v| [k.to_sym, v] }.to_h.merge(options)
22
-
23
- socket = Socket.tcp host, port, connect_timeout: 20, resolv_timeout: 5
24
- enable_tcp_keepalive(socket)
25
- if tls
26
- cert_store = OpenSSL::X509::Store.new
27
- cert_store.set_default_paths
28
- context = OpenSSL::SSL::SSLContext.new
29
- context.cert_store = cert_store
30
- context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless [false, "false", "none"].include? options[:verify_peer]
31
- socket = OpenSSL::SSL::SSLSocket.new(socket, context)
32
- socket.sync_close = true # closing the TLS socket also closes the TCP socket
33
- socket.hostname = host # SNI host
34
- socket.connect
35
- socket.post_connection_check(host) || raise(AMQP::Client::Error, "TLS certificate hostname doesn't match requested")
11
+ class Client
12
+ # Represents a single established AMQP connection
13
+ class Connection
14
+ # Establish a connection to an AMQP broker
15
+ # @param uri [String] URL on the format amqp://username:password@hostname/vhost, use amqps:// for encrypted connection
16
+ # @param read_loop_thread [Boolean] If true run {#read_loop} in a background thread,
17
+ # otherwise the user have to run it explicitly, without {#read_loop} the connection won't function
18
+ # @option options [Boolean] connection_name (PROGRAM_NAME) Set a name for the connection to be able to identify
19
+ # the client from the broker
20
+ # @option options [Boolean] verify_peer (true) Verify broker's TLS certificate, set to false for self-signed certs
21
+ # @option options [Integer] connect_timeout (30) TCP connection timeout
22
+ # @option options [Integer] heartbeat (0) Heartbeat timeout, defaults to 0 and relies on TCP keepalive instead
23
+ # @option options [Integer] frame_max (131_072) Maximum frame size,
24
+ # the smallest of the client's and the broker's values will be used
25
+ # @option options [Integer] channel_max (2048) Maxium number of channels the client will be allowed to have open.
26
+ # Maxium allowed is 65_536. The smallest of the client's and the broker's value will be used.
27
+ # @return [Connection]
28
+ def initialize(uri = "", read_loop_thread: true, **options)
29
+ uri = URI.parse(uri)
30
+ tls = uri.scheme == "amqps"
31
+ port = port_from_env || uri.port || (tls ? 5671 : 5672)
32
+ host = uri.host || "localhost"
33
+ user = uri.user || "guest"
34
+ password = uri.password || "guest"
35
+ vhost = URI.decode_www_form_component(uri.path[1..] || "/")
36
+ options = URI.decode_www_form(uri.query || "").map! { |k, v| [k.to_sym, v] }.to_h.merge(options)
37
+
38
+ socket = open_socket(host, port, tls, options)
39
+ channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, options)
40
+
41
+ @socket = socket
42
+ @channel_max = channel_max.zero? ? 65_536 : channel_max
43
+ @frame_max = frame_max
44
+ @heartbeat = heartbeat
45
+ @channels = {}
46
+ @closed = nil
47
+ @replies = ::Queue.new
48
+ @write_lock = Mutex.new
49
+ @blocked = nil
50
+ Thread.new { read_loop } if read_loop_thread
36
51
  end
37
- channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, **options)
38
- Connection.new(socket, channel_max, frame_max, heartbeat, read_loop_thread: read_loop_thread)
39
- end
40
52
 
41
- def initialize(socket, channel_max, frame_max, heartbeat, read_loop_thread: true)
42
- @socket = socket
43
- @channel_max = channel_max.zero? ? 65_536 : channel_max
44
- @frame_max = frame_max
45
- @heartbeat = heartbeat
46
- @channels = {}
47
- @closed = false
48
- @replies = Queue.new
49
- @write_lock = Mutex.new
50
- Thread.new { read_loop } if read_loop_thread
51
- end
53
+ # Alias for {#initialize}
54
+ # @see #initialize
55
+ # @deprecated
56
+ def self.connect(uri, read_loop_thread: true, **options)
57
+ new(uri, read_loop_thread: read_loop_thread, **options)
58
+ end
52
59
 
53
- attr_reader :frame_max
60
+ # The max frame size negotiated between the client and the broker
61
+ # @return [Integer]
62
+ attr_reader :frame_max
54
63
 
55
- def inspect
56
- "#<#{self.class} @closed=#{@closed} channel_count=#{@channels.size}>"
57
- end
64
+ # Custom inspect
65
+ # @return [String]
66
+ # @api private
67
+ def inspect
68
+ "#<#{self.class} @closed=#{@closed} channel_count=#{@channels.size}>"
69
+ end
58
70
 
59
- def channel(id = nil)
60
- raise ArgumentError, "Channel ID cannot be 0" if id&.zero?
61
- raise ArgumentError, "Channel ID higher than connection's channel max #{@channel_max}" if id && id > @channel_max
71
+ # Open an AMQP channel
72
+ # @param id [Integer, nil] If nil a new channel will be opened, otherwise an already open channel might be reused
73
+ # @return [Channel]
74
+ def channel(id = nil)
75
+ raise ArgumentError, "Channel ID cannot be 0" if id&.zero?
76
+ raise ArgumentError, "Channel ID higher than connection's channel max #{@channel_max}" if id && id > @channel_max
62
77
 
63
- if id
64
- ch = @channels[id] ||= Channel.new(self, id)
65
- else
66
- id = nil
67
- 1.upto(@channel_max) do |i|
68
- break id = i unless @channels.key? i
69
- end
70
- raise AMQP::Client::Error, "Max channels reached" if id.nil?
78
+ if id
79
+ ch = @channels[id] ||= Channel.new(self, id)
80
+ else
81
+ 1.upto(@channel_max) do |i|
82
+ break id = i unless @channels.key? i
83
+ end
84
+ raise Error, "Max channels reached" if id.nil?
71
85
 
72
- ch = @channels[id] = Channel.new(self, id)
86
+ ch = @channels[id] = Channel.new(self, id)
87
+ end
88
+ ch.open
73
89
  end
74
- ch.open
75
- end
76
90
 
77
- # Declare a new channel, yield, and then close the channel
78
- def with_channel
79
- ch = channel
80
- begin
81
- yield ch
82
- ensure
83
- ch.close
91
+ # Declare a new channel, yield, and then close the channel
92
+ # @yield [Channel]
93
+ # @return [Object] Whatever was returned by the block
94
+ def with_channel
95
+ ch = channel
96
+ begin
97
+ yield ch
98
+ ensure
99
+ ch.close
100
+ end
84
101
  end
85
- end
86
102
 
87
- def close(reason = "", code = 200)
88
- return if @closed
103
+ # Gracefully close a connection
104
+ # @param reason [String] A reason to close the connection can be logged by the broker
105
+ # @param code [Integer]
106
+ # @return [nil]
107
+ def close(reason: "", code: 200)
108
+ return if @closed
89
109
 
90
- @closed = true
91
- write_bytes FrameBytes.connection_close(code, reason)
92
- @channels.each_value { |ch| ch.closed!(code, reason, 0, 0) }
93
- expect(:close_ok)
94
- end
110
+ @closed = [code, reason]
111
+ @channels.each_value { |ch| ch.closed!(:connection, code, reason, 0, 0) }
112
+ if @blocked
113
+ @socket.close
114
+ else
115
+ write_bytes FrameBytes.connection_close(code, reason)
116
+ expect(:close_ok)
117
+ end
118
+ nil
119
+ end
95
120
 
96
- def closed?
97
- @closed
98
- end
121
+ # True if the connection is closed
122
+ # @return [Boolean]
123
+ def closed?
124
+ !@closed.nil?
125
+ end
99
126
 
100
- def write_bytes(*bytes)
101
- if @socket.is_a? OpenSSL::SSL::SSLSocket
127
+ # Write byte array(s) directly to the socket (thread-safe)
128
+ # @param bytes [String] One or more byte arrays
129
+ # @return [Integer] number of bytes written
130
+ # @api private
131
+ def write_bytes(*bytes)
132
+ blocked = @blocked
133
+ warn "AMQP-Client blocked by broker: #{blocked}" if blocked
102
134
  @write_lock.synchronize do
135
+ warn "AMQP-Client unblocked by broker" if blocked
103
136
  @socket.write(*bytes)
104
137
  end
105
- else
106
- @socket.write(*bytes)
138
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
139
+ raise Error::ConnectionClosed.new(*@closed) if @closed
140
+
141
+ raise Error, "Could not write to socket, #{e.message}"
107
142
  end
108
- rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
109
- raise AMQP::Client::Error, "Could not write to socket, #{e.message}"
110
- end
111
143
 
112
- # Reads from the socket, required for any kind of progress. Blocks until the connection is closed
113
- def read_loop
114
- # read more often than write so that channel errors crop up early
115
- Thread.current.priority += 1
116
- socket = @socket
117
- frame_max = @frame_max
118
- frame_start = String.new(capacity: 7)
119
- frame_buffer = String.new(capacity: frame_max)
120
- loop do
121
- socket.read(7, frame_start)
122
- type, channel_id, frame_size = frame_start.unpack("C S> L>")
123
- if frame_size > frame_max
124
- raise AMQP::Client::Error, "Frame size #{frame_size} is larger than negotiated max frame size #{frame_max}"
125
- end
144
+ # Reads from the socket, required for any kind of progress.
145
+ # Blocks until the connection is closed. Normally run as a background thread automatically.
146
+ # @return [nil]
147
+ def read_loop
148
+ # read more often than write so that channel errors crop up early
149
+ Thread.current.priority += 1
150
+ socket = @socket
151
+ frame_max = @frame_max
152
+ frame_start = String.new(capacity: 7)
153
+ frame_buffer = String.new(capacity: frame_max)
154
+ loop do
155
+ socket.read(7, frame_start)
156
+ type, channel_id, frame_size = frame_start.unpack("C S> L>")
157
+ frame_max >= frame_size || raise(Error, "Frame size #{frame_size} larger than negotiated max frame size #{frame_max}")
126
158
 
127
- # read the frame content
128
- socket.read(frame_size, frame_buffer)
159
+ # read the frame content
160
+ socket.read(frame_size, frame_buffer)
129
161
 
130
- # make sure that the frame end is correct
131
- frame_end = socket.readchar.ord
132
- raise AMQP::Client::UnexpectedFrameEnd, frame_end if frame_end != 206
162
+ # make sure that the frame end is correct
163
+ frame_end = socket.readchar.ord
164
+ raise UnexpectedFrameEnd, frame_end if frame_end != 206
133
165
 
134
- # parse the frame, will return false if a close frame was received
135
- parse_frame(type, channel_id, frame_size, frame_buffer) || return
136
- end
137
- rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
138
- warn "AMQP-Client read error: #{e.inspect}"
139
- nil # ignore read errors
140
- ensure
141
- @closed = true
142
- @replies.close
143
- begin
144
- @socket.close
145
- rescue IOError, OpenSSL::OpenSSLError, SystemCallError
166
+ # parse the frame, will return false if a close frame was received
167
+ parse_frame(type, channel_id, frame_buffer) || return
168
+ end
146
169
  nil
147
- end
148
- end
149
-
150
- private
151
-
152
- def parse_frame(type, channel_id, frame_size, buf)
153
- case type
154
- when 1 # method frame
155
- class_id, method_id = buf.unpack("S> S>")
156
- case class_id
157
- when 10 # connection
158
- raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
159
-
160
- case method_id
161
- when 50 # connection#close
162
- @closed = true
163
- code, text_len = buf.unpack("@4 S> C")
164
- text = buf.byteslice(7, text_len).force_encoding("utf-8")
165
- error_class_id, error_method_id = buf.byteslice(7 + text_len, 4).unpack("S> S>")
166
- @channels.each_value { |ch| ch.closed!(code, text, error_class_id, error_method_id) }
167
- begin
168
- write_bytes FrameBytes.connection_close_ok
169
- rescue AMQP::Client::Error
170
- nil # rabbitmq closes the socket after sending Connection::Close, so ignore write errors
171
- end
172
- return false
173
- when 51 # connection#close-ok
174
- @closed = true
175
- @replies.push [:close_ok]
176
- return false
177
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
178
- end
179
- when 20 # channel
180
- case method_id
181
- when 11 # channel#open-ok
182
- @channels[channel_id].reply [:channel_open_ok]
183
- when 40 # channel#close
184
- reply_code, reply_text_len = buf.unpack("@4 S> C")
185
- reply_text = buf.byteslice(7, reply_text_len).force_encoding("utf-8")
186
- classid, methodid = buf.byteslice(7 + reply_text_len, 4).unpack("S> S>")
187
- channel = @channels.delete(channel_id)
188
- channel.closed!(reply_code, reply_text, classid, methodid)
189
- write_bytes FrameBytes.channel_close_ok(channel_id)
190
- when 41 # channel#close-ok
191
- channel = @channels.delete(channel_id)
192
- channel.reply [:channel_close_ok]
193
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
194
- end
195
- when 40 # exchange
196
- case method_id
197
- when 11 # declare-ok
198
- @channels[channel_id].reply [:exchange_declare_ok]
199
- when 21 # delete-ok
200
- @channels[channel_id].reply [:exchange_delete_ok]
201
- when 31 # bind-ok
202
- @channels[channel_id].reply [:exchange_bind_ok]
203
- when 51 # unbind-ok
204
- @channels[channel_id].reply [:exchange_unbind_ok]
205
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
206
- end
207
- when 50 # queue
208
- case method_id
209
- when 11 # declare-ok
210
- queue_name_len = buf.unpack1("@4 C")
211
- queue_name = buf.byteslice(5, queue_name_len).force_encoding("utf-8")
212
- message_count, consumer_count = buf.byteslice(5 + queue_name_len, 8).unpack("L> L>")
213
- @channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
214
- when 21 # bind-ok
215
- @channels[channel_id].reply [:queue_bind_ok]
216
- when 31 # purge-ok
217
- @channels[channel_id].reply [:queue_purge_ok]
218
- when 41 # delete-ok
219
- message_count = buf.unpack1("@4 L>")
220
- @channels[channel_id].reply [:queue_delete, message_count]
221
- when 51 # unbind-ok
222
- @channels[channel_id].reply [:queue_unbind_ok]
223
- else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
224
- end
225
- when 60 # basic
226
- case method_id
227
- when 11 # qos-ok
228
- @channels[channel_id].reply [:basic_qos_ok]
229
- when 21 # consume-ok
230
- tag_len = buf.unpack1("@4 C")
231
- tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
232
- @channels[channel_id].reply [:basic_consume_ok, tag]
233
- when 30 # cancel
234
- tag_len = buf.unpack1("@4 C")
235
- tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
236
- no_wait = buf[5 + tag_len].ord
237
- @channels[channel_id].close_consumer(tag)
238
- write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait == 1
239
- when 31 # cancel-ok
240
- tag_len = buf.unpack1("@4 C")
241
- tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
242
- @channels[channel_id].reply [:basic_cancel_ok, tag]
243
- when 50 # return
244
- reply_code, reply_text_len = buf.unpack("@4 S> C")
245
- pos = 7
246
- reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
247
- pos += reply_text_len
248
- exchange_len = buf[pos].ord
249
- pos += 1
250
- exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
251
- pos += exchange_len
252
- routing_key_len = buf[pos].ord
253
- pos += 1
254
- routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
255
- @channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
256
- when 60 # deliver
257
- ctag_len = buf[4].ord
258
- consumer_tag = buf.byteslice(5, ctag_len).force_encoding("utf-8")
259
- pos = 5 + ctag_len
260
- delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
261
- pos += 8 + 1 + 1
262
- exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
263
- pos += exchange_len
264
- rk_len = buf[pos].ord
265
- pos += 1
266
- routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
267
- @channels[channel_id].message_delivered(consumer_tag, delivery_tag, redelivered == 1, exchange, routing_key)
268
- when 71 # get-ok
269
- delivery_tag, redelivered, exchange_len = buf.unpack("@4 Q> C C")
270
- pos = 14
271
- exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
272
- pos += exchange_len
273
- routing_key_len = buf[pos].ord
274
- pos += 1
275
- routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
276
- pos += routing_key_len
277
- _message_count = buf.byteslice(pos, 4).unpack1("L>")
278
- @channels[channel_id].message_delivered(nil, delivery_tag, redelivered == 1, exchange, routing_key)
279
- when 72 # get-empty
280
- @channels[channel_id].basic_get_empty
281
- when 80 # ack
282
- delivery_tag, multiple = buf.unpack("@4 Q> C")
283
- @channels[channel_id].confirm [:ack, delivery_tag, multiple == 1]
284
- when 111 # recover-ok
285
- @channels[channel_id].reply [:basic_recover_ok]
286
- when 120 # nack
287
- delivery_tag, multiple, requeue = buf.unpack("@4 Q> C C")
288
- @channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
289
- else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
290
- end
291
- when 85 # confirm
292
- case method_id
293
- when 11 # select-ok
294
- @channels[channel_id].reply [:confirm_select_ok]
295
- else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
296
- end
297
- when 90 # tx
298
- case method_id
299
- when 11 # select-ok
300
- @channels[channel_id].reply [:tx_select_ok]
301
- when 21 # commit-ok
302
- @channels[channel_id].reply [:tx_commit_ok]
303
- when 31 # rollback-ok
304
- @channels[channel_id].reply [:tx_rollback_ok]
305
- else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
170
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
171
+ @closed ||= [400, "read error: #{e.message}"]
172
+ nil # ignore read errors
173
+ ensure
174
+ @closed ||= [400, "unknown"]
175
+ @replies.close
176
+ begin
177
+ @write_lock.synchronize do
178
+ @socket.close
306
179
  end
307
- else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
180
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError
181
+ nil
308
182
  end
309
- when 2 # header
310
- body_size = buf.unpack1("@4 Q>")
311
- properties = Properties.decode(buf.byteslice(12, buf.bytesize - 12))
312
- @channels[channel_id].header_delivered body_size, properties
313
- when 3 # body
314
- @channels[channel_id].body_delivered buf
315
- else raise AMQP::Client::UnsupportedFrameType, type
316
183
  end
317
- true
318
- end
319
-
320
- def expect(expected_frame_type)
321
- frame_type, args = @replies.pop
322
- frame_type == expected_frame_type || raise(AMQP::Client::UnexpectedFrame.new(expected_frame_type, frame_type))
323
- args
324
- end
325
184
 
326
- def self.establish(socket, user, password, vhost, **options)
327
- channel_max, frame_max, heartbeat = nil
328
- socket.write "AMQP\x00\x00\x09\x01"
329
- buf = String.new(capacity: 4096)
330
- loop do
331
- begin
332
- socket.readpartial(4096, buf)
333
- rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
334
- raise AMQP::Client::Error, "Could not establish AMQP connection: #{e.message}"
335
- end
336
-
337
- type, channel_id, frame_size = buf.unpack("C S> L>")
338
- frame_end = buf[frame_size + 7].ord
339
- raise UnexpectedFrameEndError, frame_end if frame_end != 206
185
+ private
340
186
 
187
+ def parse_frame(type, channel_id, buf)
341
188
  case type
342
189
  when 1 # method frame
343
- class_id, method_id = buf.unpack("@7 S> S>")
190
+ class_id, method_id = buf.unpack("S> S>")
344
191
  case class_id
345
192
  when 10 # connection
346
- raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
193
+ raise Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
347
194
 
348
195
  case method_id
349
- when 10 # connection#start
350
- conn_name = options[:connection_name] || $PROGRAM_NAME
351
- properties = CLIENT_PROPERTIES.merge({ connection_name: conn_name })
352
- socket.write FrameBytes.connection_start_ok "\u0000#{user}\u0000#{password}", properties
353
- when 30 # connection#tune
354
- channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
355
- channel_max = [channel_max, 2048].min
356
- frame_max = [frame_max, 131_072].min
357
- heartbeat = [heartbeat, 0].min
358
- socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
359
- socket.write FrameBytes.connection_open(vhost)
360
- when 41 # connection#open-ok
361
- return [channel_max, frame_max, heartbeat]
362
196
  when 50 # connection#close
363
- code, text_len = buf.unpack("@11 S> C")
364
- text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
365
- socket.write FrameBytes.connection_close_ok
366
- raise AMQP::Client::Error, "Could not establish AMQP connection: #{code} #{text} #{error_class_id} #{error_method_id}"
367
- else raise AMQP::Client::Error, "Unexpected class/method: #{class_id} #{method_id}"
197
+ code, text_len = buf.unpack("@4 S> C")
198
+ text = buf.byteslice(7, text_len).force_encoding("utf-8")
199
+ error_class_id, error_method_id = buf.byteslice(7 + text_len, 4).unpack("S> S>")
200
+ @closed = [code, text, error_class_id, error_method_id]
201
+ @channels.each_value { |ch| ch.closed!(:connection, code, text, error_class_id, error_method_id) }
202
+ begin
203
+ write_bytes FrameBytes.connection_close_ok
204
+ rescue Error
205
+ nil # rabbitmq closes the socket after sending Connection::Close, so ignore write errors
206
+ end
207
+ return false
208
+ when 51 # connection#close-ok
209
+ @replies.push [:close_ok]
210
+ return false
211
+ when 60 # connection#blocked
212
+ reason_len = buf.getbyte(4)
213
+ reason = buf.byteslice(5, reason_len).force_encoding("utf-8")
214
+ @blocked = reason
215
+ @write_lock.lock
216
+ when 61 # connection#unblocked
217
+ @blocked = nil
218
+ @write_lock.unlock
219
+ else raise Error::UnsupportedMethodFrame, class_id, method_id
220
+ end
221
+ when 20 # channel
222
+ case method_id
223
+ when 11 # channel#open-ok
224
+ @channels[channel_id].reply [:channel_open_ok]
225
+ when 40 # channel#close
226
+ reply_code, reply_text_len = buf.unpack("@4 S> C")
227
+ reply_text = buf.byteslice(7, reply_text_len).force_encoding("utf-8")
228
+ classid, methodid = buf.byteslice(7 + reply_text_len, 4).unpack("S> S>")
229
+ channel = @channels.delete(channel_id)
230
+ channel.closed!(:channel, reply_code, reply_text, classid, methodid)
231
+ write_bytes FrameBytes.channel_close_ok(channel_id)
232
+ when 41 # channel#close-ok
233
+ channel = @channels.delete(channel_id)
234
+ channel.reply [:channel_close_ok]
235
+ else raise Error::UnsupportedMethodFrame, class_id, method_id
236
+ end
237
+ when 40 # exchange
238
+ case method_id
239
+ when 11 # declare-ok
240
+ @channels[channel_id].reply [:exchange_declare_ok]
241
+ when 21 # delete-ok
242
+ @channels[channel_id].reply [:exchange_delete_ok]
243
+ when 31 # bind-ok
244
+ @channels[channel_id].reply [:exchange_bind_ok]
245
+ when 51 # unbind-ok
246
+ @channels[channel_id].reply [:exchange_unbind_ok]
247
+ else raise Error::UnsupportedMethodFrame, class_id, method_id
248
+ end
249
+ when 50 # queue
250
+ case method_id
251
+ when 11 # declare-ok
252
+ queue_name_len = buf.getbyte(4)
253
+ queue_name = buf.byteslice(5, queue_name_len).force_encoding("utf-8")
254
+ message_count, consumer_count = buf.byteslice(5 + queue_name_len, 8).unpack("L> L>")
255
+ @channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
256
+ when 21 # bind-ok
257
+ @channels[channel_id].reply [:queue_bind_ok]
258
+ when 31 # purge-ok
259
+ @channels[channel_id].reply [:queue_purge_ok]
260
+ when 41 # delete-ok
261
+ message_count = buf.unpack1("@4 L>")
262
+ @channels[channel_id].reply [:queue_delete, message_count]
263
+ when 51 # unbind-ok
264
+ @channels[channel_id].reply [:queue_unbind_ok]
265
+ else raise Error::UnsupportedMethodFrame.new class_id, method_id
368
266
  end
369
- else raise AMQP::Client::Error, "Unexpected class/method: #{class_id} #{method_id}"
267
+ when 60 # basic
268
+ case method_id
269
+ when 11 # qos-ok
270
+ @channels[channel_id].reply [:basic_qos_ok]
271
+ when 21 # consume-ok
272
+ tag_len = buf.getbyte(4)
273
+ tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
274
+ @channels[channel_id].reply [:basic_consume_ok, tag]
275
+ when 30 # cancel
276
+ tag_len = buf.getbyte(4)
277
+ tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
278
+ no_wait = buf.getbyte(5 + tag_len) == 1
279
+ @channels[channel_id].close_consumer(tag)
280
+ write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait
281
+ when 31 # cancel-ok
282
+ tag_len = buf.getbyte(4)
283
+ tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
284
+ @channels[channel_id].reply [:basic_cancel_ok, tag]
285
+ when 50 # return
286
+ reply_code, reply_text_len = buf.unpack("@4 S> C")
287
+ pos = 7
288
+ reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
289
+ pos += reply_text_len
290
+ exchange_len = buf.getbyte(pos)
291
+ pos += 1
292
+ exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
293
+ pos += exchange_len
294
+ routing_key_len = buf.getbyte(pos)
295
+ pos += 1
296
+ routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
297
+ @channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
298
+ when 60 # deliver
299
+ ctag_len = buf.getbyte(4)
300
+ consumer_tag = buf.byteslice(5, ctag_len).force_encoding("utf-8")
301
+ pos = 5 + ctag_len
302
+ delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
303
+ pos += 8 + 1 + 1
304
+ exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
305
+ pos += exchange_len
306
+ rk_len = buf.getbyte(pos)
307
+ pos += 1
308
+ routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
309
+ @channels[channel_id].message_delivered(consumer_tag, delivery_tag, redelivered == 1, exchange, routing_key)
310
+ when 71 # get-ok
311
+ delivery_tag, redelivered, exchange_len = buf.unpack("@4 Q> C C")
312
+ pos = 14
313
+ exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
314
+ pos += exchange_len
315
+ routing_key_len = buf.getbyte(pos)
316
+ pos += 1
317
+ routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
318
+ # pos += routing_key_len
319
+ # message_count = buf.byteslice(pos, 4).unpack1("L>")
320
+ @channels[channel_id].message_delivered(nil, delivery_tag, redelivered == 1, exchange, routing_key)
321
+ when 72 # get-empty
322
+ @channels[channel_id].basic_get_empty
323
+ when 80 # ack
324
+ delivery_tag, multiple = buf.unpack("@4 Q> C")
325
+ @channels[channel_id].confirm [:ack, delivery_tag, multiple == 1]
326
+ when 111 # recover-ok
327
+ @channels[channel_id].reply [:basic_recover_ok]
328
+ when 120 # nack
329
+ delivery_tag, multiple, requeue = buf.unpack("@4 Q> C C")
330
+ @channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
331
+ else raise Error::UnsupportedMethodFrame.new class_id, method_id
332
+ end
333
+ when 85 # confirm
334
+ case method_id
335
+ when 11 # select-ok
336
+ @channels[channel_id].reply [:confirm_select_ok]
337
+ else raise Error::UnsupportedMethodFrame.new class_id, method_id
338
+ end
339
+ when 90 # tx
340
+ case method_id
341
+ when 11 # select-ok
342
+ @channels[channel_id].reply [:tx_select_ok]
343
+ when 21 # commit-ok
344
+ @channels[channel_id].reply [:tx_commit_ok]
345
+ when 31 # rollback-ok
346
+ @channels[channel_id].reply [:tx_rollback_ok]
347
+ else raise Error::UnsupportedMethodFrame.new class_id, method_id
348
+ end
349
+ else raise Error::UnsupportedMethodFrame.new class_id, method_id
370
350
  end
371
- else raise AMQP::Client::Error, "Unexpected frame type: #{type}"
351
+ when 2 # header
352
+ body_size = buf.unpack1("@4 Q>")
353
+ properties = Properties.decode(buf, 12)
354
+ @channels[channel_id].header_delivered body_size, properties
355
+ when 3 # body
356
+ @channels[channel_id].body_delivered buf
357
+ else raise Error::UnsupportedFrameType, type
372
358
  end
359
+ true
373
360
  end
374
- rescue StandardError
375
- socket.close rescue nil
376
- raise
377
- end
378
361
 
379
- def self.enable_tcp_keepalive(socket)
380
- socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
381
- socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 60)
382
- socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
383
- socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
384
- rescue StandardError => e
385
- warn "AMQP-Client could not enable TCP keepalive on socket. #{e.inspect}"
386
- end
362
+ def expect(expected_frame_type)
363
+ frame_type, args = @replies.pop
364
+ if frame_type.nil?
365
+ return if expected_frame_type == :close_ok
366
+
367
+ raise(Error::ConnectionClosed, "while waiting for #{expected_frame_type}")
368
+ end
369
+ frame_type == expected_frame_type || raise(Error::UnexpectedFrame.new(expected_frame_type, frame_type))
370
+ args
371
+ end
387
372
 
388
- def self.port_from_env
389
- return unless (port = ENV["AMQP_PORT"])
373
+ # Connect to the host/port, optionally establish a TLS connection
374
+ # @return [Socket]
375
+ # @return [OpenSSL::SSL::SSLSocket]
376
+ def open_socket(host, port, tls, options)
377
+ connect_timeout = options.fetch(:connect_timeout, 30).to_i
378
+ socket = Socket.tcp host, port, connect_timeout: connect_timeout
379
+ enable_tcp_keepalive(socket)
380
+ if tls
381
+ cert_store = OpenSSL::X509::Store.new
382
+ cert_store.set_default_paths
383
+ context = OpenSSL::SSL::SSLContext.new
384
+ context.cert_store = cert_store
385
+ verify_peer = [false, "false", "none"].include? options[:verify_peer]
386
+ context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless verify_peer
387
+ socket = OpenSSL::SSL::SSLSocket.new(socket, context)
388
+ socket.sync_close = true # closing the TLS socket also closes the TCP socket
389
+ socket.hostname = host # SNI host
390
+ socket.connect
391
+ socket.post_connection_check(host) || raise(Error, "TLS certificate hostname doesn't match requested")
392
+ end
393
+ socket
394
+ end
390
395
 
391
- port.to_i
392
- end
396
+ # Negotiate a connection
397
+ # @return [Array<Integer, Integer, Integer>] channel_max, frame_max, heartbeat
398
+ def establish(socket, user, password, vhost, options)
399
+ channel_max, frame_max, heartbeat = nil
400
+ socket.write "AMQP\x00\x00\x09\x01"
401
+ buf = String.new(capacity: 4096)
402
+ loop do
403
+ begin
404
+ socket.readpartial(4096, buf)
405
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
406
+ raise Error, "Could not establish AMQP connection: #{e.message}"
407
+ end
408
+
409
+ type, channel_id, frame_size = buf.unpack("C S> L>")
410
+ frame_end = buf.getbyte(frame_size + 7)
411
+ raise UnexpectedFrameEndError, frame_end if frame_end != 206
412
+
413
+ case type
414
+ when 1 # method frame
415
+ class_id, method_id = buf.unpack("@7 S> S>")
416
+ case class_id
417
+ when 10 # connection
418
+ raise Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
419
+
420
+ case method_id
421
+ when 10 # connection#start
422
+ conn_name = options[:connection_name] || $PROGRAM_NAME
423
+ properties = CLIENT_PROPERTIES.merge({ connection_name: conn_name })
424
+ socket.write FrameBytes.connection_start_ok "\u0000#{user}\u0000#{password}", properties
425
+ when 30 # connection#tune
426
+ channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
427
+ channel_max = 65_536 if channel_max.zero?
428
+ channel_max = [channel_max, options.fetch(:channel_max, 2048).to_i].min
429
+ frame_max = [frame_max, options.fetch(:frame_max, 131_072).to_i].min
430
+ heartbeat = [heartbeat, options.fetch(:heartbeat, 0).to_i].min
431
+ socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
432
+ socket.write FrameBytes.connection_open(vhost)
433
+ when 41 # connection#open-ok
434
+ return [channel_max, frame_max, heartbeat]
435
+ when 50 # connection#close
436
+ code, text_len = buf.unpack("@11 S> C")
437
+ text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
438
+ socket.write FrameBytes.connection_close_ok
439
+ raise Error, "Could not establish AMQP connection: #{code} #{text} #{error_class_id} #{error_method_id}"
440
+ else raise Error, "Unexpected class/method: #{class_id} #{method_id}"
441
+ end
442
+ else raise Error, "Unexpected class/method: #{class_id} #{method_id}"
443
+ end
444
+ else raise Error, "Unexpected frame type: #{type}"
445
+ end
446
+ end
447
+ rescue StandardError => e
448
+ begin
449
+ socket.close
450
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError
451
+ nil
452
+ end
453
+ raise e
454
+ end
455
+
456
+ # Enable TCP keepalive, which is prefered to heartbeats
457
+ # @return [void]
458
+ def enable_tcp_keepalive(socket)
459
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
460
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 60)
461
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
462
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
463
+ rescue StandardError => e
464
+ warn "AMQP-Client could not enable TCP keepalive on socket. #{e.inspect}"
465
+ end
466
+
467
+ # Fetch the AMQP port number from ENV
468
+ # @return [Integer] A port number
469
+ # @return [nil] When the environment variable AMQP_PORT isn't set
470
+ def port_from_env
471
+ return unless (port = ENV["AMQP_PORT"])
393
472
 
394
- private_class_method :establish, :enable_tcp_keepalive, :port_from_env
395
-
396
- CLIENT_PROPERTIES = {
397
- capabilities: {
398
- authentication_failure_close: true,
399
- publisher_confirms: true,
400
- consumer_cancel_notify: true,
401
- exchange_exchange_bindings: true,
402
- "basic.nack": true,
403
- "connection.blocked": true
404
- },
405
- product: "amqp-client.rb",
406
- platform: RUBY_DESCRIPTION,
407
- version: AMQP::Client::VERSION,
408
- information: "http://github.com/cloudamqp/amqp-client.rb"
409
- }.freeze
473
+ port.to_i
474
+ end
475
+
476
+ CLIENT_PROPERTIES = {
477
+ capabilities: {
478
+ authentication_failure_close: true,
479
+ publisher_confirms: true,
480
+ consumer_cancel_notify: true,
481
+ exchange_exchange_bindings: true,
482
+ "basic.nack": true,
483
+ "connection.blocked": true
484
+ },
485
+ product: "amqp-client.rb",
486
+ platform: RUBY_DESCRIPTION,
487
+ version: VERSION,
488
+ information: "http://github.com/cloudamqp/amqp-client.rb"
489
+ }.freeze
490
+ end
410
491
  end
411
492
  end