stomp 1.1.10 → 1.2.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.
@@ -0,0 +1,45 @@
1
+ #
2
+ # Common Stomp 1.1 code.
3
+ #
4
+ require "rubygems" if RUBY_VERSION < "1.9"
5
+ require "stomp"
6
+ #
7
+ module Stomp11Common
8
+ #
9
+ def login()
10
+ ENV['STOMP_USER'] || 'guest'
11
+ end
12
+ #
13
+ def passcode()
14
+ ENV['STOMP_PASSCODE'] || 'guest'
15
+ end
16
+ #
17
+ def host()
18
+ ENV['STOMP_HOST'] || "localhost" # The connect host name
19
+ end
20
+ #
21
+ def port()
22
+ (ENV['STOMP_PORT'] || 62613).to_i # !! The author runs Apollo listening here
23
+ end
24
+ #
25
+ def virt_host()
26
+ ENV['STOMP_VHOST'] || "localhost" # The 1.1 virtual host name
27
+ end
28
+ #
29
+ def get_connection()
30
+ conn_hdrs = {"accept-version" => "1.1", # 1.1 only
31
+ "host" => virt_host, # the vhost
32
+ }
33
+ conn_hash = { :hosts => [
34
+ {:login => login, :passcode => passcode, :host => host, :port => port},
35
+ ],
36
+ :connect_headers => conn_hdrs,
37
+ }
38
+ conn = Stomp::Connection.new(conn_hash)
39
+ end
40
+ #
41
+ def nmsgs()
42
+ (ENV['STOMP_NMSGS'] || 1).to_i # Number of messages
43
+ end
44
+ end
45
+
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'rubygems'
4
+ require 'stomp'
5
+
6
+ client = Stomp::Client.new("failover://(stomp://:@localhost:61613,stomp://:@remotehost:61613)?initialReconnectDelay=5000&randomize=false&useExponentialBackOff=false")
7
+
8
+ puts "Subscribing to /topic/ronaldo"
9
+
10
+ client.subscribe("/topic/ronaldo") do |msg|
11
+ puts msg.to_s
12
+ puts "----------------"
13
+ end
14
+
15
+ loop do
16
+ sleep(1)
17
+ puts "."
18
+ end
19
+
@@ -0,0 +1,15 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'rubygems'
4
+ require 'stomp'
5
+
6
+ client = Stomp::Client.new("failover://(stomp://:@localhost:61613,stomp://:@remotehost:61613)?initialReconnectDelay=5000&randomize=false&useExponentialBackOff=false")
7
+ message = "ronaldo #{ARGV[0]}"
8
+
9
+ for i in (1..300)
10
+ puts "Sending message"
11
+ client.publish("/topic/ronaldo", "#{i}: #{message}")
12
+ puts "(#{Time.now})Message sent: #{i}"
13
+ sleep 1
14
+ end
15
+
data/lib/stomp.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  # Copyright 2005-2006 Brian McCallister
2
4
  # Copyright 2006 LogicBlaze Inc.
3
5
  #
@@ -19,6 +21,8 @@ require 'stomp/client'
19
21
  require 'stomp/message'
20
22
  require 'stomp/version'
21
23
  require 'stomp/errors'
24
+ require 'stomp/constants'
25
+ require 'stomp/codec'
22
26
 
23
27
  module Stomp
24
28
  end
data/lib/stomp/client.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  require 'thread'
2
4
  require 'digest/sha1'
3
5
 
@@ -143,7 +145,7 @@ module Stomp
143
145
  def subscribe(destination, headers = {})
144
146
  raise "No listener given" unless block_given?
145
147
  # use subscription id to correlate messages to subscription. As described in
146
- # the SUBSCRIPTION section of the protocol: http://stomp.codehaus.org/Protocol.
148
+ # the SUBSCRIPTION section of the protocol: http://stomp.github.com/.
147
149
  # If no subscription id is provided, generate one.
148
150
  set_subscription_id_if_missing(destination, headers)
149
151
  if @listeners[headers[:id]]
@@ -180,7 +182,12 @@ module Stomp
180
182
  end
181
183
  @connection.ack message.headers['message-id'], headers
182
184
  end
183
-
185
+
186
+ # Stomp 1.1+ NACK
187
+ def nack(message_id, headers = {})
188
+ @connection.nack message_id, headers
189
+ end
190
+
184
191
  # Unreceive a message, sending it back to its queue or to the DLQ
185
192
  #
186
193
  def unreceive(message, options = {})
@@ -238,11 +245,36 @@ module Stomp
238
245
  @listener_thread && !!@listener_thread.status
239
246
  end
240
247
 
248
+ # Convenience method
249
+ def set_logger(logger)
250
+ @connection.set_logger(logger)
251
+ end
252
+
253
+ # Convenience method
254
+ def protocol()
255
+ @connection.protocol
256
+ end
257
+
258
+ # Convenience method
259
+ def valid_utf8?(s)
260
+ @connection.valid_utf8?(s)
261
+ end
262
+
263
+ # Convenience method for clients
264
+ def sha1(data)
265
+ @connection.sha1(data)
266
+ end
267
+
268
+ # Convenience method for clients
269
+ def uuid()
270
+ @connection.uuid()
271
+ end
272
+
241
273
  private
242
274
  # Set a subscription id in the headers hash if one does not already exist.
243
275
  # For simplicities sake, all subscriptions have a subscription ID.
244
276
  # setting an id in the SUBSCRIPTION header is described in the stomp protocol docs:
245
- # http://stomp.codehaus.org/Protocol
277
+ # http://stomp.github.com/
246
278
  def set_subscription_id_if_missing(destination, headers)
247
279
  headers[:id] = headers[:id] ? headers[:id] : headers['id']
248
280
  if headers[:id] == nil
@@ -309,7 +341,7 @@ module Stomp
309
341
  # For backward compatibility, some messages may already exist with no
310
342
  # subscription id, in which case we can attempt to synthesize one.
311
343
  set_subscription_id_if_missing(message.headers['destination'], message.headers)
312
- subscription_id = message.headers['id']
344
+ subscription_id = message.headers[:id]
313
345
  end
314
346
  @listeners[subscription_id]
315
347
  end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module Stomp
4
+ #
5
+ # == Purpose
6
+ #
7
+ # A general CODEC for STOMP 1.1 header keys and values.
8
+ #
9
+ # See:
10
+ #
11
+ # * http://stomp.github.com/index.html
12
+ #
13
+ # for encode/decode rules.
14
+ #
15
+ class HeaderCodec
16
+
17
+ # Encode header data per STOMP 1.1 specification
18
+ def self.encode(in_string = nil)
19
+ return in_string unless in_string
20
+ ev = Stomp::ENCODE_VALUES # avoid typing below
21
+ os = in_string + ""
22
+ 0.step(ev.length-2,2) do |i|
23
+ os.gsub!(ev[i], ev[i+1])
24
+ end
25
+ os
26
+ end
27
+
28
+ # Decode header data per STOMP 1.1 specification
29
+ def self.decode(in_string = nil)
30
+ return in_string unless in_string
31
+ ev = Stomp::DECODE_VALUES # avoid typing below
32
+ os = in_string + ""
33
+ 0.step(ev.length-2,2) do |i|
34
+ os.gsub!(ev[i+1], ev[i])
35
+ end
36
+ os
37
+ end
38
+
39
+ end # of class HeaderCodec
40
+ end # of module Stomp
41
+
@@ -1,6 +1,9 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  require 'socket'
2
4
  require 'timeout'
3
5
  require 'io/wait'
6
+ require 'digest/sha1'
4
7
 
5
8
  module Stomp
6
9
 
@@ -9,6 +12,10 @@ module Stomp
9
12
  class Connection
10
13
  attr_reader :connection_frame
11
14
  attr_reader :disconnect_receipt
15
+ attr_reader :protocol
16
+ attr_reader :session
17
+ attr_reader :hb_received # Heartbeat received on time
18
+ attr_reader :hb_sent # Heartbeat sent successfully
12
19
  #alias :obj_send :send
13
20
 
14
21
  def self.default_port(ssl)
@@ -33,6 +40,7 @@ module Stomp
33
40
  # {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false},
34
41
  # {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false}
35
42
  # ],
43
+ # :reliable => true,
36
44
  # :initial_reconnect_delay => 0.01,
37
45
  # :max_reconnect_delay => 30.0,
38
46
  # :use_exponential_back_off => true,
@@ -40,7 +48,7 @@ module Stomp
40
48
  # :max_reconnect_attempts => 0,
41
49
  # :randomize => false,
42
50
  # :backup => false,
43
- # :timeout => -1,
51
+ # :connect_timeout => 0,
44
52
  # :connect_headers => {},
45
53
  # :parse_timeout => 5,
46
54
  # :logger => nil,
@@ -59,6 +67,10 @@ module Stomp
59
67
  #
60
68
  def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
61
69
  @received_messages = []
70
+ @protocol = Stomp::SPL_10 # Assumed at first
71
+ @hb_received = true # Assumed at first
72
+ @hb_sent = true # Assumed at first
73
+ @hbs = @hbr = false # Sending/Receiving heartbeats. Assume no for now.
62
74
 
63
75
  if login.is_a?(Hash)
64
76
  hashed_initialize(login)
@@ -73,6 +85,7 @@ module Stomp
73
85
  @ssl = false
74
86
  @parameters = nil
75
87
  @parse_timeout = 5 # To override, use hashed parameters
88
+ @connect_timeout = 0 # To override, use hashed parameters
76
89
  @logger = nil # To override, use hashed parameters
77
90
  end
78
91
 
@@ -92,10 +105,11 @@ module Stomp
92
105
  def hashed_initialize(params)
93
106
 
94
107
  @parameters = refine_params(params)
95
- @reliable = true
108
+ @reliable = @parameters[:reliable]
96
109
  @reconnect_delay = @parameters[:initial_reconnect_delay]
97
110
  @connect_headers = @parameters[:connect_headers]
98
111
  @parse_timeout = @parameters[:parse_timeout]
112
+ @connect_timeout = @parameters[:connect_timeout]
99
113
  @logger = @parameters[:logger]
100
114
  #sets the first host to connect
101
115
  change_host
@@ -155,6 +169,7 @@ module Stomp
155
169
 
156
170
  default_params = {
157
171
  :connect_headers => {},
172
+ :reliable => true,
158
173
  # Failover parameters
159
174
  :initial_reconnect_delay => 0.01,
160
175
  :max_reconnect_delay => 30.0,
@@ -163,7 +178,7 @@ module Stomp
163
178
  :max_reconnect_attempts => 0,
164
179
  :randomize => false,
165
180
  :backup => false,
166
- :timeout => -1,
181
+ :connect_timeout => 0,
167
182
  # Parse Timeout
168
183
  :parse_timeout => 5
169
184
  }
@@ -211,8 +226,11 @@ module Stomp
211
226
 
212
227
  # Begin a transaction, requires a name for the transaction
213
228
  def begin(name, headers = {})
229
+ raise Stomp::Error::NoCurrentConnection if closed?
230
+ headers = headers.symbolize_keys
214
231
  headers[:transaction] = name
215
- transmit("BEGIN", headers)
232
+ _headerCheck(headers)
233
+ transmit(Stomp::CMD_BEGIN, headers)
216
234
  end
217
235
 
218
236
  # Acknowledge a message, used when a subscription has specified
@@ -220,40 +238,83 @@ module Stomp
220
238
  #
221
239
  # Accepts a transaction header ( :transaction => 'some_transaction_id' )
222
240
  def ack(message_id, headers = {})
223
- headers['message-id'] = message_id
224
- transmit("ACK", headers)
241
+ raise Stomp::Error::NoCurrentConnection if closed?
242
+ raise Stomp::Error::MessageIDRequiredError if message_id.nil? || message_id == ""
243
+ headers = headers.symbolize_keys
244
+ headers[:'message-id'] = message_id
245
+ if @protocol >= Stomp::SPL_11
246
+ raise Stomp::Error::SubscriptionRequiredError unless headers[:subscription]
247
+ end
248
+ _headerCheck(headers)
249
+ transmit(Stomp::CMD_ACK, headers)
250
+ end
251
+
252
+ # STOMP 1.1+ NACK
253
+ def nack(message_id, headers = {})
254
+ raise Stomp::Error::NoCurrentConnection if closed?
255
+ raise Stomp::Error::UnsupportedProtocolError if @protocol == Stomp::SPL_10
256
+ raise Stomp::Error::MessageIDRequiredError if message_id.nil? || message_id == ""
257
+ headers = headers.symbolize_keys
258
+ headers[:'message-id'] = message_id
259
+ raise Stomp::Error::SubscriptionRequiredError unless headers[:subscription]
260
+ _headerCheck(headers)
261
+ transmit(Stomp::CMD_NACK, headers)
225
262
  end
226
263
 
227
264
  # Commit a transaction by name
228
265
  def commit(name, headers = {})
266
+ raise Stomp::Error::NoCurrentConnection if closed?
267
+ headers = headers.symbolize_keys
229
268
  headers[:transaction] = name
230
- transmit("COMMIT", headers)
269
+ _headerCheck(headers)
270
+ transmit(Stomp::CMD_COMMIT, headers)
231
271
  end
232
272
 
233
273
  # Abort a transaction by name
234
274
  def abort(name, headers = {})
275
+ raise Stomp::Error::NoCurrentConnection if closed?
276
+ headers = headers.symbolize_keys
235
277
  headers[:transaction] = name
236
- transmit("ABORT", headers)
278
+ _headerCheck(headers)
279
+ transmit(Stomp::CMD_ABORT, headers)
237
280
  end
238
281
 
239
282
  # Subscribe to a destination, must specify a name
240
283
  def subscribe(name, headers = {}, subId = nil)
284
+ raise Stomp::Error::NoCurrentConnection if closed?
285
+ headers = headers.symbolize_keys
241
286
  headers[:destination] = name
242
- transmit("SUBSCRIBE", headers)
287
+ if @protocol >= Stomp::SPL_11
288
+ raise Stomp::Error::SubscriptionRequiredError if (headers[:id].nil? && subId.nil?)
289
+ headers[:id] = subId if headers[:id].nil?
290
+ end
291
+ _headerCheck(headers)
292
+ if @logger && @logger.respond_to?(:on_subscribe)
293
+ @logger.on_subscribe(log_params, headers)
294
+ end
243
295
 
244
296
  # Store the sub so that we can replay if we reconnect.
245
297
  if @reliable
246
298
  subId = name if subId.nil?
299
+ raise Stomp::Error::DuplicateSubscription if @subscriptions[subId]
247
300
  @subscriptions[subId] = headers
248
301
  end
302
+
303
+ transmit(Stomp::CMD_SUBSCRIBE, headers)
249
304
  end
250
305
 
251
- # Unsubscribe from a destination, must specify a name
252
- def unsubscribe(name, headers = {}, subId = nil)
253
- headers[:destination] = name
254
- transmit("UNSUBSCRIBE", headers)
306
+ # Unsubscribe from a destination, which must be specified
307
+ def unsubscribe(dest, headers = {}, subId = nil)
308
+ raise Stomp::Error::NoCurrentConnection if closed?
309
+ headers = headers.symbolize_keys
310
+ headers[:destination] = dest
311
+ if @protocol >= Stomp::SPL_11
312
+ raise Stomp::Error::SubscriptionRequiredError if (headers[:id].nil? && subId.nil?)
313
+ end
314
+ _headerCheck(headers)
315
+ transmit(Stomp::CMD_UNSUBSCRIBE, headers)
255
316
  if @reliable
256
- subId = name if subId.nil?
317
+ subId = dest if subId.nil?
257
318
  @subscriptions.delete(subId)
258
319
  end
259
320
  end
@@ -263,8 +324,14 @@ module Stomp
263
324
  # To disable content length header ( :suppress_content_length => true )
264
325
  # Accepts a transaction header ( :transaction => 'some_transaction_id' )
265
326
  def publish(destination, message, headers = {})
327
+ raise Stomp::Error::NoCurrentConnection if closed?
328
+ headers = headers.symbolize_keys
266
329
  headers[:destination] = destination
267
- transmit("SEND", headers, message)
330
+ _headerCheck(headers)
331
+ if @logger && @logger.respond_to?(:on_publish)
332
+ @logger.on_publish(log_params, message, headers)
333
+ end
334
+ transmit(Stomp::CMD_SEND, headers, message)
268
335
  end
269
336
 
270
337
  def obj_send(*args)
@@ -282,6 +349,7 @@ module Stomp
282
349
  # Accepts a limit number of redeliveries option ( :max_redeliveries => 6 )
283
350
  # Accepts a force client acknowledgement option (:force_client_ack => true)
284
351
  def unreceive(message, options = {})
352
+ raise Stomp::Error::NoCurrentConnection if closed?
285
353
  options = { :dead_letter_queue => "/queue/DLQ", :max_redeliveries => 6 }.merge options
286
354
  # Lets make sure all keys are symbols
287
355
  message.headers = message.headers.symbolize_keys
@@ -318,8 +386,14 @@ module Stomp
318
386
 
319
387
  # Close this connection
320
388
  def disconnect(headers = {})
321
- transmit("DISCONNECT", headers)
389
+ raise Stomp::Error::NoCurrentConnection if closed?
322
390
  headers = headers.symbolize_keys
391
+ _headerCheck(headers)
392
+ if @protocol >= Stomp::SPL_11
393
+ @st.kill if @st # Kill ticker thread if any
394
+ @rt.kill if @rt # Kill ticker thread if any
395
+ end
396
+ transmit(Stomp::CMD_DISCONNECT, headers)
323
397
  @disconnect_receipt = receive if headers[:receipt]
324
398
  if @logger && @logger.respond_to?(:on_disconnect)
325
399
  @logger.on_disconnect(log_params)
@@ -330,6 +404,7 @@ module Stomp
330
404
  # Return a pending message if one is available, otherwise
331
405
  # return nil
332
406
  def poll
407
+ raise Stomp::Error::NoCurrentConnection if closed?
333
408
  # No need for a read lock here. The receive method eventually fulfills
334
409
  # that requirement.
335
410
  return nil if @socket.nil? || !@socket.ready?
@@ -357,6 +432,7 @@ module Stomp
357
432
  end
358
433
 
359
434
  def receive
435
+ raise Stomp::Error::NoCurrentConnection if closed?
360
436
  super_result = __old_receive
361
437
  if super_result.nil? && @reliable && !closed?
362
438
  errstr = "connection.receive returning EOF as nil - resetting connection.\n"
@@ -368,18 +444,64 @@ module Stomp
368
444
  @socket = nil
369
445
  super_result = __old_receive
370
446
  end
447
+ #
448
+ if @logger && @logger.respond_to?(:on_receive)
449
+ @logger.on_receive(log_params, super_result)
450
+ end
371
451
  return super_result
372
452
  end
373
453
 
454
+ # Convenience method
455
+ def set_logger(logger)
456
+ @logger = logger
457
+ end
458
+
459
+ # Convenience method
460
+ def valid_utf8?(s)
461
+ case RUBY_VERSION
462
+ when /1\.8/
463
+ rv = _valid_utf8?(s)
464
+ else
465
+ rv = s.encoding.name != Stomp::UTF8 ? false : s.valid_encoding?
466
+ end
467
+ rv
468
+ end
469
+
470
+ # Convenience method for clients, return a SHA1 digest for arbitrary data
471
+ def sha1(data)
472
+ Digest::SHA1.hexdigest(data)
473
+ end
474
+
475
+ # Convenience method for clients, return a type 4 UUID.
476
+ def uuid()
477
+ b = []
478
+ 0.upto(15) do |i|
479
+ b << rand(255)
480
+ end
481
+ b[6] = (b[6] & 0x0F) | 0x40
482
+ b[8] = (b[8] & 0xbf) | 0x80
483
+ # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
484
+ rs = sprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x%02x%02x",
485
+ b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15])
486
+ rs
487
+ end
488
+
374
489
  private
375
490
 
376
491
  def _receive( read_socket )
377
492
  @read_semaphore.synchronize do
378
- # Throw away leading newlines, which are actually trailing
379
- # newlines from the preceding message.
493
+ # Throw away leading newlines, which are perhaps trailing
494
+ # newlines from the preceding message, or alterantely a 1.1+ server
495
+ # heartbeat.
380
496
  begin
381
497
  last_char = read_socket.getc
382
498
  return nil if last_char.nil?
499
+ if @protocol >= Stomp::SPL_11
500
+ plc = parse_char(last_char)
501
+ if plc == "\n" # Server Heartbeat
502
+ @lr = Time.now.to_f if @hbr
503
+ end
504
+ end
383
505
  end until parse_char(last_char) != "\n"
384
506
  read_socket.ungetc(last_char)
385
507
 
@@ -409,8 +531,17 @@ module Stomp
409
531
  message_body << char while (char = parse_char(read_socket.getc)) != "\0"
410
532
  end
411
533
 
534
+ if @protocol >= Stomp::SPL_11
535
+ @lr = Time.now.to_f if @hbr
536
+ end
537
+
412
538
  # Adds the excluded \n and \0 and tries to create a new message with it
413
- Message.new(message_header + "\n" + message_body + "\0")
539
+ msg = Message.new(message_header + "\n" + message_body + "\0", @protocol >= Stomp::SPL_11)
540
+ #
541
+ if @protocol >= Stomp::SPL_11 && msg.command != Stomp::CMD_CONNECTED
542
+ msg.headers = _decodeHeaders(msg.headers)
543
+ end
544
+ msg
414
545
  end
415
546
  end
416
547
  end
@@ -442,6 +573,9 @@ module Stomp
442
573
  end
443
574
 
444
575
  def _transmit(used_socket, command, headers = {}, body = '')
576
+ if @protocol >= Stomp::SPL_11 && command != Stomp::CMD_CONNECT
577
+ headers = _encodeHeaders(headers)
578
+ end
445
579
  @transmit_semaphore.synchronize do
446
580
  # Handle nil body
447
581
  body = '' if body.nil?
@@ -457,18 +591,33 @@ module Stomp
457
591
  # For more information refer to http://juretta.com/log/2009/05/24/activemq-jms-stomp/
458
592
  # Lets send this header in the message, so it can maintain state when using unreceive
459
593
  headers['content-length'] = "#{body_length_bytes}" unless headers[:suppress_content_length]
460
-
594
+ headers['content-type'] = "text/plain; charset=UTF-8" unless headers['content-type']
461
595
  used_socket.puts command
462
- headers.each {|k,v| used_socket.puts "#{k}:#{v}" }
463
- used_socket.puts "content-type: text/plain; charset=UTF-8"
596
+ headers.each do |k,v|
597
+ if v.is_a?(Array)
598
+ v.each do |e|
599
+ used_socket.puts "#{k}:#{e}"
600
+ end
601
+ else
602
+ used_socket.puts "#{k}:#{v}"
603
+ end
604
+ end
464
605
  used_socket.puts
465
606
  used_socket.write body
466
607
  used_socket.write "\0"
608
+
609
+ if @protocol >= Stomp::SPL_11
610
+ @ls = Time.now.to_f if @hbs
611
+ end
612
+
467
613
  end
468
614
  end
469
615
 
470
616
  def open_tcp_socket
471
- tcp_socket = TCPSocket.open @host, @port
617
+ tcp_socket = nil
618
+ Timeout::timeout(@connect_timeout, Stomp::Error::SocketOpenTimeout) do
619
+ tcp_socket = TCPSocket.open @host, @port
620
+ end
472
621
 
473
622
  tcp_socket
474
623
  end
@@ -489,8 +638,10 @@ module Stomp
489
638
  # ctx.cert_store = truststores
490
639
 
491
640
  ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
492
-
493
- ssl = OpenSSL::SSL::SSLSocket.new(open_tcp_socket, ctx)
641
+ ssl = nil
642
+ Timeout::timeout(@connect_timeout, Stomp::Error::SocketOpenTimeout) do
643
+ ssl = OpenSSL::SSL::SSLSocket.new(open_tcp_socket, ctx)
644
+ end
494
645
  def ssl.ready?
495
646
  ! @rbuffer.empty? || @io.ready?
496
647
  end
@@ -524,18 +675,23 @@ module Stomp
524
675
  end
525
676
 
526
677
  def connect(used_socket)
678
+ @connect_headers = {} unless @connect_headers # Caller said nil/false
527
679
  headers = @connect_headers.clone
528
680
  headers[:login] = @login
529
681
  headers[:passcode] = @passcode
682
+ _pre_connect
530
683
  _transmit(used_socket, "CONNECT", headers)
531
684
  @connection_frame = _receive(used_socket)
685
+ _post_connect
532
686
  @disconnect_receipt = nil
687
+ @session = @connection_frame.headers["session"] if @connection_frame
533
688
  # replay any subscriptions.
534
- @subscriptions.each { |k,v| _transmit(used_socket, "SUBSCRIBE", v) }
689
+ @subscriptions.each { |k,v| _transmit(used_socket, Stomp::CMD_SUBSCRIBE, v) }
535
690
  end
536
691
 
537
692
  def log_params
538
- lparms = @parameters.clone
693
+ lparms = @parameters.clone if @parameters
694
+ lparms = {} unless lparms
539
695
  lparms[:cur_host] = @host
540
696
  lparms[:cur_port] = @port
541
697
  lparms[:cur_login] = @login
@@ -547,7 +703,445 @@ module Stomp
547
703
  #
548
704
  lparms
549
705
  end
550
- end
551
706
 
552
- end
707
+ def _pre_connect
708
+ @connect_headers = @connect_headers.symbolize_keys
709
+ raise Stomp::Error::ProtocolErrorConnect if (@connect_headers[:"accept-version"] && !@connect_headers[:host])
710
+ raise Stomp::Error::ProtocolErrorConnect if (!@connect_headers[:"accept-version"] && @connect_headers[:host])
711
+ return unless (@connect_headers[:"accept-version"] && @connect_headers[:host]) # 1.0
712
+ # Try 1.1 or greater
713
+ okvers = []
714
+ avers = @connect_headers[:"accept-version"].split(",")
715
+ avers.each do |nver|
716
+ if Stomp::SUPPORTED.index(nver)
717
+ okvers << nver
718
+ end
719
+ end
720
+ raise Stomp::Error::UnsupportedProtocolError if okvers == []
721
+ @connect_headers[:"accept-version"] = okvers.join(",") # This goes to server
722
+ # Heartbeats - pre connect
723
+ return unless @connect_headers[:"heart-beat"]
724
+ _validate_hbheader()
725
+ end
726
+
727
+ def _post_connect
728
+ return unless (@connect_headers[:"accept-version"] && @connect_headers[:host])
729
+ return if @connection_frame.command == Stomp::CMD_ERROR
730
+ cfh = @connection_frame.headers.symbolize_keys
731
+ @protocol = cfh[:version]
732
+ # Should not happen, but check anyway
733
+ raise Stomp::Error::UnsupportedProtocolError unless Stomp::SUPPORTED.index(@protocol)
734
+ # Heartbeats
735
+ return unless @connect_headers[:"heart-beat"]
736
+ _init_heartbeats()
737
+ end
738
+
739
+ def _validate_hbheader()
740
+ return if @connect_headers[:"heart-beat"] == "0,0" # Caller does not want heartbeats. OK.
741
+ parts = @connect_headers[:"heart-beat"].split(",")
742
+ if (parts.size != 2) || (parts[0] != parts[0].to_i.to_s) || (parts[1] != parts[1].to_i.to_s)
743
+ raise Stomp::Error::InvalidHeartBeatHeaderError
744
+ end
745
+ end
746
+
747
+ def _init_heartbeats()
748
+ return if @connect_headers[:"heart-beat"] == "0,0" # Caller does not want heartbeats. OK.
749
+ #
750
+ @cx = @cy = @sx = @sy = 0, # Variable names as in spec
751
+ #
752
+ @sti = @rti = 0.0 # Send/Receive ticker interval.
753
+ #
754
+ @ls = @lr = -1.0 # Last send/receive time (from Time.now.to_f)
755
+ #
756
+ @st = @rt = nil # Send/receive ticker thread
757
+ #
758
+ cfh = @connection_frame.headers.symbolize_keys
759
+ return if cfh[:"heart-beat"] == "0,0" # Server does not want heartbeats
760
+ #
761
+ parts = @connect_headers[:"heart-beat"].split(",")
762
+ @cx = parts[0].to_i
763
+ @cy = parts[1].to_i
764
+ #
765
+ parts = cfh[:"heart-beat"].split(",")
766
+ @sx = parts[0].to_i
767
+ @sy = parts[1].to_i
768
+ # Catch odd situations like someone has used => heart-beat:000,00000
769
+ return if (@cx == 0 && @cy == 0) || (@sx == 0 && @sy == 0)
770
+ #
771
+ @hbs = @hbr = true # Sending/Receiving heartbeats. Assume yes at first.
772
+ # Check for sending
773
+ @hbs = false if @cx == 0 || @sy == 0
774
+ # Check for receiving
775
+ @hbr = false if @sx == 0 || @cy == 0
776
+ # Should not do heartbeats at all
777
+ return if (!@hbs && !@hbr)
778
+ # If sending
779
+ if @hbs
780
+ sm = @cx >= @sy ? @cx : @sy # ticker interval, ms
781
+ @sti = 1000.0 * sm # ticker interval, μs
782
+ @ls = Time.now.to_f # best guess at start
783
+ _start_send_ticker
784
+ end
785
+
786
+ # If receiving
787
+ if @hbr
788
+ rm = @sx >= @cy ? @sx : @cy # ticker interval, ms
789
+ @rti = 1000.0 * rm # ticker interval, μs
790
+ @lr = Time.now.to_f # best guess at start
791
+ _start_receive_ticker
792
+ end
793
+
794
+ end
795
+
796
+ def _start_send_ticker
797
+ sleeptime = @sti / 1000000.0 # Sleep time secs
798
+ @st = Thread.new {
799
+ while true do
800
+ sleep sleeptime
801
+ curt = Time.now.to_f
802
+ if @logger && @logger.respond_to?(:on_hbfire)
803
+ @logger.on_hbfire(log_params, "send_fire", curt)
804
+ end
805
+ delta = curt - @ls
806
+ if delta > (@sti - (@sti/5.0)) / 1000000.0 # Be tolerant (minus)
807
+ if @logger && @logger.respond_to?(:on_hbfire)
808
+ @logger.on_hbfire(log_params, "send_heartbeat", curt)
809
+ end
810
+ # Send a heartbeat
811
+ @transmit_semaphore.synchronize do
812
+ begin
813
+ @socket.puts
814
+ @ls = curt # Update last send
815
+ @hb_sent = true # Reset if necessary
816
+ rescue Exception => sendex
817
+ @hb_sent = false # Set the warning flag
818
+ if @logger && @logger.respond_to?(:on_hbwrite_fail)
819
+ @logger.on_hbwrite_fail(log_params, {"ticker_interval" => @sti,
820
+ "exception" => sendex})
821
+ end
822
+ raise # Re-raise. What else could be done here?
823
+ end
824
+ end
825
+ end
826
+ Thread.pass
827
+ end
828
+ }
829
+ end
830
+
831
+ def _start_receive_ticker
832
+ sleeptime = @rti / 1000000.0 # Sleep time secs
833
+ @rt = Thread.new {
834
+ while true do
835
+ sleep sleeptime
836
+ curt = Time.now.to_f
837
+ if @logger && @logger.respond_to?(:on_hbfire)
838
+ @logger.on_hbfire(log_params, "receive_fire", curt)
839
+ end
840
+ delta = curt - @lr
841
+ if delta > ((@rti + (@rti/5.0)) / 1000000.0) # Be tolerant (plus)
842
+ if @logger && @logger.respond_to?(:on_hbfire)
843
+ @logger.on_hbfire(log_params, "receive_heartbeat", curt)
844
+ end
845
+ # Client code could be off doing something else (that is, no reading of
846
+ # the socket has been requested by the caller). Try to handle that case.
847
+ lock = @read_semaphore.try_lock
848
+ if lock
849
+ last_char = @socket.getc
850
+ plc = parse_char(last_char)
851
+ if plc == "\n" # Server Heartbeat
852
+ @lr = Time.now.to_f
853
+ else
854
+ @socket.ungetc(last_char)
855
+ end
856
+ @read_semaphore.unlock
857
+ else
858
+ # Shrug. Have not received one. Just set warning flag.
859
+ @hb_received = false
860
+ if @logger && @logger.respond_to?(:on_hbread_fail)
861
+ @logger.on_hbread_fail(log_params, {"ticker_interval" => @rti})
862
+ end
863
+ end
864
+ else
865
+ @hb_received = true # Reset if necessary
866
+ end
867
+ Thread.pass
868
+ end
869
+ }
870
+ end
871
+
872
+ # Ref:
873
+ # http://unicode.org/mail-arch/unicode-ml/y2003-m02/att-0467/01-The_Algorithm_to_Valide_an_UTF-8_String
874
+ #
875
+ def _valid_utf8?(string)
876
+ case RUBY_VERSION
877
+ when /1\.8\.[56]/
878
+ bytes = []
879
+ 0.upto(string.length-1) {|i|
880
+ bytes << string[i]
881
+ }
882
+ else
883
+ bytes = string.bytes
884
+ end
885
+
886
+ #
887
+ valid = true
888
+ index = -1
889
+ nb_hex = nil
890
+ ni_hex = nil
891
+ state = "start"
892
+ next_byte_save = nil
893
+ #
894
+ bytes.each do |next_byte|
895
+ index += 1
896
+ next_byte_save = next_byte
897
+ ni_hex = sprintf "%x", index
898
+ nb_hex = sprintf "%x", next_byte
899
+ # puts "Top: #{next_byte}(0x#{nb_hex}), index: #{index}(0x#{ni_hex})" if DEBUG
900
+ case state
901
+
902
+ # State: 'start'
903
+ # The 'start' state:
904
+ # * handles all occurrences of valid single byte characters i.e., the ASCII character set
905
+ # * provides state transition logic for start bytes of valid characters with 2-4 bytes
906
+ # * signals a validation failure for all other single bytes
907
+ #
908
+ when "start"
909
+ # puts "state: start" if DEBUG
910
+ case next_byte
911
+
912
+ # ASCII
913
+ # * Input = 0x00-0x7F : change state to START
914
+ when (0x00..0x7f)
915
+ # puts "state: start 1" if DEBUG
916
+ state = "start"
917
+
918
+ # Start byte of two byte characters
919
+ # * Input = 0xC2-0xDF: change state to A
920
+ when (0xc2..0xdf)
921
+ # puts "state: start 2" if DEBUG
922
+ state = "a"
923
+
924
+ # Start byte of some three byte characters
925
+ # * Input = 0xE1-0xEC, 0xEE-0xEF: change state to B
926
+ when (0xe1..0xec)
927
+ # puts "state: start 3" if DEBUG
928
+ state = "b"
929
+ when (0xee..0xef)
930
+ # puts "state: start 4" if DEBUG
931
+ state = "b"
932
+
933
+ # Start byte of special three byte characters
934
+ # * Input = 0xE0: change state to C
935
+ when 0xe0
936
+ # puts "state: start 5" if DEBUG
937
+ state = "c"
938
+
939
+ # Start byte of the remaining three byte characters
940
+ # * Input = 0xED: change state to D
941
+ when 0xed
942
+ # puts "state: start 6" if DEBUG
943
+ state = "d"
944
+
945
+ # Start byte of some four byte characters
946
+ # * Input = 0xF1-0xF3:change state to E
947
+ when (0xf1..0xf3)
948
+ # puts "state: start 7" if DEBUG
949
+ state = "e"
950
+
951
+ # Start byte of special four byte characters
952
+ # * Input = 0xF0: change state to F
953
+ when 0xf0
954
+ # puts "state: start 8" if DEBUG
955
+ state = "f"
956
+
957
+ # Start byte of very special four byte characters
958
+ # * Input = 0xF4: change state to G
959
+ when 0xf4
960
+ # puts "state: start 9" if DEBUG
961
+ state = "g"
962
+
963
+ # All other single characters are invalid
964
+ # * Input = Others (0x80-0xBF,0xC0-0xC1, 0xF5-0xFF): ERROR
965
+ else
966
+ valid = false
967
+ break
968
+ end # of the inner case, the 'start' state
969
+
970
+ # The last continuation byte of a 2, 3, or 4 byte character
971
+ # State: 'a'
972
+ # o Input = 0x80-0xBF: change state to START
973
+ # o Others: ERROR
974
+ when "a"
975
+ # puts "state: a" if DEBUG
976
+ if (0x80..0xbf) === next_byte
977
+ state = "start"
978
+ else
979
+ valid = false
980
+ break
981
+ end
982
+
983
+ # The first continuation byte for most 3 byte characters
984
+ # (those with start bytes in: 0xe1-0xec or 0xee-0xef)
985
+ # State: 'b'
986
+ # o Input = 0x80-0xBF: change state to A
987
+ # o Others: ERROR
988
+ when "b"
989
+ # puts "state: b" if DEBUG
990
+ if (0x80..0xbf) === next_byte
991
+ state = "a"
992
+ else
993
+ valid = false
994
+ break
995
+ end
996
+
997
+ # The first continuation byte for some special 3 byte characters
998
+ # (those with start byte 0xe0)
999
+ # State: 'c'
1000
+ # o Input = 0xA0-0xBF: change state to A
1001
+ # o Others: ERROR
1002
+ when "c"
1003
+ # puts "state: c" if DEBUG
1004
+ if (0xa0..0xbf) === next_byte
1005
+ state = "a"
1006
+ else
1007
+ valid = false
1008
+ break
1009
+ end
1010
+
1011
+ # The first continuation byte for the remaining 3 byte characters
1012
+ # (those with start byte 0xed)
1013
+ # State: 'd'
1014
+ # o Input = 0x80-0x9F: change state to A
1015
+ # o Others: ERROR
1016
+ when "d"
1017
+ # puts "state: d" if DEBUG
1018
+ if (0x80..0x9f) === next_byte
1019
+ state = "a"
1020
+ else
1021
+ valid = false
1022
+ break
1023
+ end
1024
+
1025
+ # The first continuation byte for some 4 byte characters
1026
+ # (those with start bytes in: 0xf1-0xf3)
1027
+ # State: 'e'
1028
+ # o Input = 0x80-0xBF: change state to B
1029
+ # o Others: ERROR
1030
+ when "e"
1031
+ # puts "state: e" if DEBUG
1032
+ if (0x80..0xbf) === next_byte
1033
+ state = "b"
1034
+ else
1035
+ valid = false
1036
+ break
1037
+ end
1038
+
1039
+ # The first continuation byte for some special 4 byte characters
1040
+ # (those with start byte 0xf0)
1041
+ # State: 'f'
1042
+ # o Input = 0x90-0xBF: change state to B
1043
+ # o Others: ERROR
1044
+ when "f"
1045
+ # puts "state: f" if DEBUG
1046
+ if (0x90..0xbf) === next_byte
1047
+ state = "b"
1048
+ else
1049
+ valid = false
1050
+ break
1051
+ end
1052
+
1053
+ # The first continuation byte for the remaining 4 byte characters
1054
+ # (those with start byte 0xf4)
1055
+ # State: 'g'
1056
+ # o Input = 0x80-0x8F: change state to B
1057
+ # o Others: ERROR
1058
+ when "g"
1059
+ # puts "state: g" if DEBUG
1060
+ if (0x80..0x8f) === next_byte
1061
+ state = "b"
1062
+ else
1063
+ valid = false
1064
+ break
1065
+ end
1066
+
1067
+ #
1068
+ else
1069
+ raise RuntimeError, "state: default"
1070
+ end
1071
+ end
1072
+ #
1073
+ # puts "State at end: #{state}" if DEBUG
1074
+ # Catch truncation at end of string
1075
+ if valid and state != 'start'
1076
+ # puts "Resetting valid value" if DEBUG
1077
+ valid = false
1078
+ end
1079
+ #
1080
+ valid
1081
+ end # of _valid_utf8?
1082
+
1083
+ def _headerCheck(h)
1084
+ return if @protocol == Stomp::SPL_10 # Do nothing for this environment
1085
+ #
1086
+ h.each_pair do |k,v|
1087
+ # Keys here are symbolized
1088
+ ks = k.to_s
1089
+ ks.force_encoding(Stomp::UTF8) if ks.respond_to?(:force_encoding)
1090
+ raise Stomp::Error::UTF8ValidationError unless valid_utf8?(ks)
1091
+ #
1092
+ if v.is_a?(Array)
1093
+ v.each do |e|
1094
+ e.force_encoding(Stomp::UTF8) if e.respond_to?(:force_encoding)
1095
+ raise Stomp::Error::UTF8ValidationError unless valid_utf8?(e)
1096
+ end
1097
+ else
1098
+ vs = v.to_s # Values are usually Strings, but could be TrueClass or Symbol
1099
+ vs.force_encoding(Stomp::UTF8) if vs.respond_to?(:force_encoding)
1100
+ raise Stomp::Error::UTF8ValidationError unless valid_utf8?(vs)
1101
+ end
1102
+ end
1103
+ end
1104
+
1105
+ #
1106
+ def _encodeHeaders(h)
1107
+ eh = {}
1108
+ h.each_pair do |k,v|
1109
+ # Keys are symbolized
1110
+ ks = k.to_s
1111
+ if v.is_a?(Array)
1112
+ kenc = Stomp::HeaderCodec::encode(ks)
1113
+ eh[kenc] = []
1114
+ v.each do |e|
1115
+ eh[kenc] << Stomp::HeaderCodec::encode(e)
1116
+ end
1117
+ else
1118
+ vs = v.to_s
1119
+ eh[Stomp::HeaderCodec::encode(ks)] = Stomp::HeaderCodec::encode(vs)
1120
+ end
1121
+ end
1122
+ eh
1123
+ end
1124
+
1125
+ #
1126
+ def _decodeHeaders(h)
1127
+ dh = {}
1128
+ h.each_pair do |k,v|
1129
+ # Keys here are NOT! symbolized
1130
+ if v.is_a?(Array)
1131
+ kdec = Stomp::HeaderCodec::decode(k)
1132
+ dh[kdec] = []
1133
+ v.each do |e|
1134
+ dh[kdec] << Stomp::HeaderCodec::decode(e)
1135
+ end
1136
+ else
1137
+ vs = v.to_s
1138
+ dh[Stomp::HeaderCodec::decode(k)] = Stomp::HeaderCodec::decode(vs)
1139
+ end
1140
+ end
1141
+ dh
1142
+ end
1143
+
1144
+ end # class
1145
+
1146
+ end # module
553
1147