stomper 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (152) hide show
  1. data/.gitignore +5 -0
  2. data/{spec/spec.opts → .rspec} +0 -2
  3. data/Gemfile +4 -0
  4. data/LICENSE +201 -201
  5. data/README.md +130 -0
  6. data/Rakefile +5 -0
  7. data/examples/basic.rb +38 -0
  8. data/examples/events.rb +54 -0
  9. data/features/acking_messages.feature +147 -0
  10. data/features/disconnecting.feature +12 -0
  11. data/features/establish_connection.feature +44 -0
  12. data/features/protocol_version_negotiation.feature +61 -0
  13. data/features/receipts.feature +72 -0
  14. data/features/scopes.feature +32 -0
  15. data/features/secure_connections.feature +38 -0
  16. data/features/send_and_message.feature +28 -0
  17. data/features/steps/acking_messages_steps.rb +39 -0
  18. data/features/steps/disconnecting_steps.rb +8 -0
  19. data/features/steps/establish_connection_steps.rb +74 -0
  20. data/features/steps/frame_transmission_steps.rb +35 -0
  21. data/features/steps/protocol_version_negotiation_steps.rb +15 -0
  22. data/features/steps/receipts_steps.rb +79 -0
  23. data/features/steps/scopes_steps.rb +52 -0
  24. data/features/steps/secure_connections_steps.rb +41 -0
  25. data/features/steps/send_and_message_steps.rb +35 -0
  26. data/features/steps/subscribing_steps.rb +36 -0
  27. data/features/steps/threaded_receiver_steps.rb +8 -0
  28. data/features/steps/transactions_steps.rb +0 -0
  29. data/features/subscribing.feature +151 -0
  30. data/features/support/env.rb +11 -0
  31. data/features/support/header_helpers.rb +12 -0
  32. data/features/support/ssl/README +6 -0
  33. data/features/support/ssl/broker_cert.csr +17 -0
  34. data/features/support/ssl/broker_cert.pem +72 -0
  35. data/features/support/ssl/broker_key.pem +27 -0
  36. data/features/support/ssl/client_cert.csr +17 -0
  37. data/features/support/ssl/client_cert.pem +72 -0
  38. data/features/support/ssl/client_key.pem +27 -0
  39. data/features/support/ssl/demoCA/cacert.pem +17 -0
  40. data/features/support/ssl/demoCA/index.txt +2 -0
  41. data/features/support/ssl/demoCA/index.txt.attr +1 -0
  42. data/features/support/ssl/demoCA/index.txt.attr.old +1 -0
  43. data/features/support/ssl/demoCA/index.txt.old +1 -0
  44. data/features/support/ssl/demoCA/newcerts/01.pem +72 -0
  45. data/features/support/ssl/demoCA/newcerts/02.pem +72 -0
  46. data/features/support/ssl/demoCA/private/cakey.pem +17 -0
  47. data/features/support/ssl/demoCA/serial +1 -0
  48. data/features/support/ssl/demoCA/serial.old +1 -0
  49. data/features/support/test_stomp_server.rb +150 -0
  50. data/features/threaded_receiver.feature +11 -0
  51. data/features/transactions.feature +66 -0
  52. data/lib/stomper.rb +30 -20
  53. data/lib/stomper/connection.rb +442 -102
  54. data/lib/stomper/errors.rb +59 -0
  55. data/lib/stomper/extensions.rb +10 -0
  56. data/lib/stomper/extensions/common.rb +258 -0
  57. data/lib/stomper/extensions/events.rb +213 -0
  58. data/lib/stomper/extensions/heartbeat.rb +101 -0
  59. data/lib/stomper/extensions/scoping.rb +56 -0
  60. data/lib/stomper/frame.rb +54 -0
  61. data/lib/stomper/frame_serializer.rb +217 -0
  62. data/lib/stomper/headers.rb +15 -0
  63. data/lib/stomper/receipt_manager.rb +36 -0
  64. data/lib/stomper/receivers.rb +7 -0
  65. data/lib/stomper/receivers/threaded.rb +71 -0
  66. data/lib/stomper/scopes.rb +9 -0
  67. data/lib/stomper/scopes/header_scope.rb +49 -0
  68. data/lib/stomper/scopes/receipt_scope.rb +44 -0
  69. data/lib/stomper/scopes/transaction_scope.rb +109 -0
  70. data/lib/stomper/sockets.rb +66 -28
  71. data/lib/stomper/subscription_manager.rb +79 -0
  72. data/lib/stomper/support.rb +68 -0
  73. data/lib/stomper/support/1.8/frame_serializer.rb +53 -0
  74. data/lib/stomper/support/1.8/headers.rb +183 -0
  75. data/lib/stomper/support/1.9/frame_serializer.rb +64 -0
  76. data/lib/stomper/support/1.9/headers.rb +172 -0
  77. data/lib/stomper/support/ruby.rb +13 -0
  78. data/lib/stomper/uris.rb +49 -0
  79. data/lib/stomper/version.rb +7 -0
  80. data/spec/spec_helper.rb +13 -9
  81. data/spec/stomper/connection_spec.rb +712 -0
  82. data/spec/stomper/extensions/common_spec.rb +187 -0
  83. data/spec/stomper/extensions/events_spec.rb +78 -0
  84. data/spec/stomper/extensions/heartbeat_spec.rb +103 -0
  85. data/spec/stomper/extensions/scoping_spec.rb +21 -0
  86. data/spec/stomper/frame_serializer_1.8_spec.rb +318 -0
  87. data/spec/stomper/frame_serializer_spec.rb +316 -0
  88. data/spec/stomper/frame_spec.rb +36 -0
  89. data/spec/stomper/headers_spec.rb +224 -0
  90. data/spec/stomper/receipt_manager_spec.rb +91 -0
  91. data/spec/stomper/receivers/threaded_spec.rb +116 -0
  92. data/spec/stomper/scopes/header_scope_spec.rb +42 -0
  93. data/spec/stomper/scopes/receipt_scope_spec.rb +51 -0
  94. data/spec/stomper/scopes/transaction_scope_spec.rb +183 -0
  95. data/spec/stomper/sockets_spec.rb +113 -0
  96. data/spec/stomper/subscription_manager_spec.rb +107 -0
  97. data/spec/stomper/support_spec.rb +69 -0
  98. data/spec/stomper/uris_spec.rb +54 -0
  99. data/spec/stomper_spec.rb +9 -0
  100. data/spec/support/custom_argument_matchers.rb +57 -0
  101. data/spec/support/existential_frame_matchers.rb +19 -0
  102. data/spec/support/frame_header_matchers.rb +10 -0
  103. data/stomper.gemspec +30 -0
  104. metadata +272 -97
  105. data/AUTHORS +0 -21
  106. data/CHANGELOG +0 -20
  107. data/README.rdoc +0 -120
  108. data/lib/stomper/client.rb +0 -34
  109. data/lib/stomper/frame_reader.rb +0 -73
  110. data/lib/stomper/frame_writer.rb +0 -21
  111. data/lib/stomper/frames.rb +0 -39
  112. data/lib/stomper/frames/abort.rb +0 -10
  113. data/lib/stomper/frames/ack.rb +0 -25
  114. data/lib/stomper/frames/begin.rb +0 -11
  115. data/lib/stomper/frames/client_frame.rb +0 -89
  116. data/lib/stomper/frames/commit.rb +0 -10
  117. data/lib/stomper/frames/connect.rb +0 -10
  118. data/lib/stomper/frames/connected.rb +0 -30
  119. data/lib/stomper/frames/disconnect.rb +0 -10
  120. data/lib/stomper/frames/error.rb +0 -21
  121. data/lib/stomper/frames/message.rb +0 -48
  122. data/lib/stomper/frames/receipt.rb +0 -19
  123. data/lib/stomper/frames/send.rb +0 -10
  124. data/lib/stomper/frames/server_frame.rb +0 -38
  125. data/lib/stomper/frames/subscribe.rb +0 -42
  126. data/lib/stomper/frames/unsubscribe.rb +0 -19
  127. data/lib/stomper/open_uri_interface.rb +0 -41
  128. data/lib/stomper/receipt_handlers.rb +0 -23
  129. data/lib/stomper/receiptor.rb +0 -38
  130. data/lib/stomper/subscriber.rb +0 -76
  131. data/lib/stomper/subscription.rb +0 -128
  132. data/lib/stomper/subscriptions.rb +0 -95
  133. data/lib/stomper/threaded_receiver.rb +0 -59
  134. data/lib/stomper/transaction.rb +0 -185
  135. data/lib/stomper/transactor.rb +0 -50
  136. data/lib/stomper/uri.rb +0 -55
  137. data/spec/client_spec.rb +0 -29
  138. data/spec/connection_spec.rb +0 -22
  139. data/spec/frame_reader_spec.rb +0 -37
  140. data/spec/frame_writer_spec.rb +0 -27
  141. data/spec/frames/client_frame_spec.rb +0 -66
  142. data/spec/frames/indirect_frame_spec.rb +0 -45
  143. data/spec/frames/server_frame_spec.rb +0 -85
  144. data/spec/open_uri_interface_spec.rb +0 -132
  145. data/spec/receiptor_spec.rb +0 -35
  146. data/spec/shared_connection_examples.rb +0 -79
  147. data/spec/subscriber_spec.rb +0 -77
  148. data/spec/subscription_spec.rb +0 -157
  149. data/spec/subscriptions_spec.rb +0 -145
  150. data/spec/threaded_receiver_spec.rb +0 -33
  151. data/spec/transaction_spec.rb +0 -139
  152. data/spec/transactor_spec.rb +0 -46
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Manages receipt handling for a {Stomper::Connection}
4
+ class Stomper::ReceiptManager
5
+ # Creates a new receipt handler for the supplied {Stomper::Connection connection}
6
+ # @param [Stomper::Connection] connection
7
+ def initialize(connection)
8
+ @mon = ::Monitor.new
9
+ @callbacks = {}
10
+ connection.before_disconnect do |d, con|
11
+ @close_on = d[:receipt] if d[:receipt]
12
+ end
13
+ connection.on_receipt do |r, con|
14
+ dispatch(r)
15
+ connection.close if r[:'receipt-id'] == @close_on
16
+ end
17
+ end
18
+
19
+ # Adds a callback handler for a RECEIPT frame that matches the supplied
20
+ # receipt ID.
21
+ # @param [String] r_id ID of the receipt to match
22
+ # @param [Proc] callback Proc to invoke when a matching RECEIPT frame is
23
+ # received from the broker.
24
+ # @return [self]
25
+ def add(r_id, callback)
26
+ @mon.synchronize { @callbacks[r_id] = callback }
27
+ self
28
+ end
29
+
30
+ private
31
+ def dispatch(receipt)
32
+ cb = @mon.synchronize { @callbacks.delete(receipt[:'receipt-id']) }
33
+ cb && cb.call(receipt)
34
+ self
35
+ end
36
+ end
@@ -0,0 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Namespace for {Stomper::Connection} receivers
4
+ module Stomper::Receivers
5
+ end
6
+
7
+ require 'stomper/receivers/threaded'
@@ -0,0 +1,71 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Basic threaded receiver
4
+ class Stomper::Receivers::Threaded
5
+ # Returns true if the receiver is currently running, false otherwise.
6
+ # If the polling thread is terminated due to a raised exception, this
7
+ # attribute will be false.
8
+ # @return [true,false]
9
+ attr_reader :running
10
+ alias :running? :running
11
+
12
+ # Creates a new threaded receiver for the supplied {Stomper::Connection}.
13
+ # Invoking {#start} on this receiver will create a new thread that will
14
+ # continually call {Stomper::Connection#receive receive} on the
15
+ # {Stomper::Connection connection}. Stopping this receiver with {#stop}
16
+ # will terminate the thread.
17
+ # @param [Stomper::Connection] connection
18
+ def initialize(connection)
19
+ @connection = connection
20
+ @running = false
21
+ @run_mutex = ::Mutex.new
22
+ @run_thread = nil
23
+ @raised_while_running = nil
24
+ end
25
+
26
+ # Starts the receiver by creating a new thread to continually poll the
27
+ # {Stomper::Connection connection} for new Stomp frames. If an error is
28
+ # raised while calling {Stomper::Connection#receive}, the polling thread
29
+ # will terminate, and {#running?} will return false.
30
+ # @return [self]
31
+ def start
32
+ is_starting = @run_mutex.synchronize { @running = true unless @running }
33
+ if is_starting
34
+ @run_thread = Thread.new do
35
+ while @running
36
+ begin
37
+ @running = false if @connection.receive.nil?
38
+ rescue Exception => ex
39
+ @running = false
40
+ raise ex
41
+ end
42
+ end
43
+ end
44
+ end
45
+ self
46
+ end
47
+
48
+ # Stops the receiver by shutting down the polling thread. If an error was
49
+ # raised within the thread, this method will generally re-raise the error.
50
+ # The one exception to this behavior is if the error raised was an instance
51
+ # of +IOError+ and a call to {Stomper::Connection#connected?} returns false,
52
+ # in which case the error is ignored. The reason for this is that performing
53
+ # a read operation on a closed stream will raise an +IOError+. It is likely
54
+ # that when shutting down a connection and its receiver, the polling thread
55
+ # may be blocked on reading from the stream and raise such an error.
56
+ # @return [self]
57
+ # @raise [Exception]
58
+ def stop
59
+ stopped = @run_mutex.synchronize { @run_thread.nil? }
60
+ unless stopped
61
+ @running = false
62
+ begin
63
+ @run_thread.join
64
+ rescue IOError
65
+ raise if @connection.connected?
66
+ end
67
+ @run_thread = nil
68
+ end
69
+ self
70
+ end
71
+ end
@@ -0,0 +1,9 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Module containing connection scopes.
4
+ module Stomper::Scopes
5
+ end
6
+
7
+ require 'stomper/scopes/header_scope'
8
+ require 'stomper/scopes/receipt_scope'
9
+ require 'stomper/scopes/transaction_scope'
@@ -0,0 +1,49 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # A "connection scope" that provides much of the same interface as
4
+ # {Stomper::Connection}, but automatically applies header name/value pairs
5
+ # to all frames generated on the scope.
6
+ class ::Stomper::Scopes::HeaderScope
7
+ include ::Stomper::Extensions::Common
8
+
9
+ # The underlying {Stomper::Connection connection} to transmit frames through.
10
+ # @return [Stomper::Connection]
11
+ attr_reader :connection
12
+ # The headers to apply to all frames generated on this scope.
13
+ # @return [{Symbol => String}]
14
+ attr_reader :headers
15
+
16
+ # Creates a new {Stomper::Scopes::HeaderScope}. The supplied +headers+ hash will have
17
+ # all of its keys converted to symbols and its values converted to strings,
18
+ # so the key/value pairs must support this transformation (through +to_sym+
19
+ # and +to_s+, respectively.)
20
+ # @param [Stomper::Connection] connection
21
+ # @param [{Object => String}] headers
22
+ def initialize(connection, headers)
23
+ @headers = ::Stomper::Support.keys_to_sym(headers)
24
+ @connection = connection
25
+ ::Stomper::Extensions::Common.extend_by_protocol_version(self, @connection.version)
26
+ end
27
+
28
+ # Applies this scope to a block.
29
+ def apply_to(callback)
30
+ callback.call(self) if callback
31
+ end
32
+
33
+ # Transmits a frame, applying the set headers. After merging its headers
34
+ # into the frame, the frame is passed to the underlying connection for
35
+ # transmission.
36
+ # @param [Stomper::Frame] frame
37
+ def transmit(frame)
38
+ frame.headers.reverse_merge!(@headers)
39
+ @connection.transmit frame
40
+ end
41
+
42
+ # Returns the connection's {Stomper::ReceiptManager}
43
+ # @return [Stomper::ReceiptManager]
44
+ def receipt_manager; @connection.receipt_manager; end
45
+
46
+ # Returns the connection's {Stomper::SubscriptionManager}
47
+ # @return [Stomper::SubscriptionManager]
48
+ def subscription_manager; @connection.subscription_manager; end
49
+ end
@@ -0,0 +1,44 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Automatically generates "receipt" headers, if none are present and
4
+ # applies a supplied callback to every receipt received for frames generated
5
+ # through it. As instances of this class rely on event callbacks attached
6
+ # to the underlying {Stomper::Connection connection}, it is entirely possible
7
+ # for those events to be triggered on +Thread+ other than main. It is for
8
+ # this reason that synchronization is used to ensure the integrity of
9
+ # the internal list of receipt IDs that have not yet been processed through
10
+ # the callback.
11
+ class Stomper::Scopes::ReceiptScope < ::Stomper::Scopes::HeaderScope
12
+ # A list of frames that support being receipted.
13
+ # @return [Array<String>]
14
+ FRAME_COMMANDS = %w(SEND SUBSCRIBE UNSUBSCRIBE
15
+ BEGIN COMMIT ABORT ACK NACK DISCONNECT)
16
+
17
+ # Create a new receipt scope. All receiptable frames transmitted through
18
+ # this instance will use the same callback for handling the RECEIPT frame
19
+ # sent by the broker.
20
+ def initialize(connection, headers)
21
+ super
22
+ @receipt_handler = nil
23
+ end
24
+
25
+ # Takes a block as a callback to invoke when a receipt is received.
26
+ def apply_to(callback)
27
+ @receipt_handler = callback
28
+ end
29
+
30
+ # Transmits a frame. This method will add an auto-generated +receipt+ header
31
+ # to the frame if one has not been set, and then set up a handler for the
32
+ # +receipt+ value, invoking the callback set through {#apply_to} when
33
+ # the corresponding RECEIPT frame is received from the broker.
34
+ # @param [Stomper::Frame] frame
35
+ def transmit(frame)
36
+ if @receipt_handler && FRAME_COMMANDS.include?(frame.command)
37
+ r_id = frame[:receipt]
38
+ r_id = ::Stomper::Support.next_serial if r_id.nil? || r_id.empty?
39
+ receipt_manager.add(r_id, @receipt_handler)
40
+ frame[:receipt] = r_id
41
+ end
42
+ super(frame)
43
+ end
44
+ end
@@ -0,0 +1,109 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # A "connection scope" that provides a convenient interface for handling
4
+ # transactions. In addition to the behaviors of {HeaderScope}, for frames
5
+ # that the Stomp protocol allows to be enclosed within a transaction, this
6
+ # scope automatically attaches a +transaction+ header.
7
+ class Stomper::Scopes::TransactionScope < ::Stomper::Scopes::HeaderScope
8
+ # A list of frames that support being part of a transaction.
9
+ # @return [Array<String>]
10
+ FRAME_COMMANDS = %w(SEND BEGIN COMMIT ABORT ACK NACK)
11
+ # The value assigned to the +transaction+ header.
12
+ # @return [String]
13
+ attr_reader :transaction
14
+
15
+ def initialize(connection, headers)
16
+ super
17
+ @headers[:transaction] ||= ::Stomper::Support.next_serial
18
+ @transaction = self.headers[:transaction]
19
+ @transaction_state = :pending
20
+ end
21
+
22
+ # Overrides the standard {Stomper::Extensions::Commmon#begin} behavior
23
+ # to start the transaction encapsulated by this {TransactionScope transaction}.
24
+ def begin_with_transaction(headers={})
25
+ if transaction_pending?
26
+ @transaction_state = :starting
27
+ else
28
+ raise ::Stomper::Errors::TransactionStartedError unless transaction_pending?
29
+ end
30
+ begin_without_transaction(@transaction, headers).tap do |f|
31
+ @transaction_state = :started
32
+ end
33
+ end
34
+ alias :begin_without_transaction :begin
35
+ alias :begin :begin_with_transaction
36
+
37
+ # Overrides the standard {Stomper::Extensions::Commmon#abort} behavior
38
+ # to abort the transaction encapsulated by this {TransactionScope transaction}.
39
+ def abort_with_transaction(headers={})
40
+ abort_without_transaction(@transaction, headers).tap do |f|
41
+ @transaction_state = :aborted
42
+ end
43
+ end
44
+ alias :abort_without_transaction :abort
45
+ alias :abort :abort_with_transaction
46
+
47
+ # Overrides the standard {Stomper::Extensions::Commmon#commit} behavior
48
+ # to commit the transaction encapsulated by this {TransactionScope transaction}.
49
+ def commit_with_transaction(headers={})
50
+ commit_without_transaction(@transaction, headers).tap do |f|
51
+ @transaction_state = :committed
52
+ end
53
+ end
54
+ alias :commit_without_transaction :commit
55
+ alias :commit :commit_with_transaction
56
+
57
+ # Transmits a frame, but only applies the +transaction+ header if the
58
+ # frame command is amongst those commands that can be included in a
59
+ # transaction.
60
+ # @param [Stomper::Frame] frame
61
+ def transmit(frame)
62
+ self.begin if transaction_pending?
63
+ if FRAME_COMMANDS.include? frame.command
64
+ if frame.command != 'BEGIN' && transaction_finalized?
65
+ raise ::Stomper::Errors::TransactionFinalizedError
66
+ end
67
+ super(frame)
68
+ else
69
+ @connection.transmit frame
70
+ end
71
+ end
72
+
73
+ # Applies this transaction to a block. Before any transactionable frame
74
+ # is transmitted within the block, a BEGIN frame will be generated. If
75
+ # the block completes without raising an error, a COMMIT frame will be
76
+ # transmitted to complete the transaction, otherwise an ABORT frame will
77
+ # be transmitted signalling that the transaction should be rolled-back by
78
+ # the broker.
79
+ def apply_to(callback)
80
+ begin
81
+ super
82
+ self.commit if transaction_started?
83
+ rescue Exception => err
84
+ self.abort if transaction_started?
85
+ raise err
86
+ end
87
+ end
88
+
89
+ # Returns true if a BEGIN frame has not yet been transmitted for this
90
+ # transaction, false otherwise.
91
+ # @return [true, false]
92
+ def transaction_pending?; @transaction_state == :pending; end
93
+ # Returns true if a BEGIN frame has been transmitted for this
94
+ # transaction but neither COMMIT nor ABORT have been sent, false otherwise.
95
+ # @return [true, false]
96
+ def transaction_started?; @transaction_state == :started; end
97
+ # Returns true if a COMMIT frame has been transmitted for this
98
+ # transaction, false otherwise.
99
+ # @return [true, false]
100
+ def transaction_committed?; @transaction_state == :committed; end
101
+ # Returns true if an ABORT frame has been transmitted for this
102
+ # transaction, false otherwise.
103
+ # @return [true, false]
104
+ def transaction_aborted?; @transaction_state == :aborted; end
105
+ # Returns true if a COMMIT or ABORT frame has been transmitted for this
106
+ # transaction, false otherwise.
107
+ # @return [true, false]
108
+ def transaction_finalized?; transaction_aborted? || transaction_committed?; end
109
+ end
@@ -1,37 +1,75 @@
1
- module Stomper
2
- module Sockets
3
- class SSL < DelegateClass(OpenSSL::SSL::SSLSocket)
4
- include FrameReader
5
- include FrameWriter
1
+ # -*- encoding: utf-8 -*-
6
2
 
7
- def initialize(host, port, *args)
8
- @context = OpenSSL::SSL::SSLContext.new
9
- @context.verify_mode = OpenSSL::SSL::VERIFY_NONE
3
+ # Socket helpers.
4
+ module Stomper::Sockets
5
+
6
+ # A wrapper for an SSL Socket that tidies up the SSL specifics so
7
+ # our Connection library isn't troubled by them.
8
+ class SSL < DelegateClass(::OpenSSL::SSL::SSLSocket)
9
+ # Default SSL options to use with new {SSL} connections.
10
+ # @return {Symbol => Object}
11
+ DEFAULT_SSL_OPTIONS = {
12
+ :verify_mode => ::OpenSSL::SSL::VERIFY_PEER |
13
+ ::OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT,
14
+ :ca_file => nil,
15
+ :ca_path => nil,
16
+ :cert => nil,
17
+ :key => nil,
18
+ :post_connection_check => true
19
+ }
20
+
21
+ # Create a new {SSL} connection to +host+ on +port+.
22
+ # @param [String] host hostname or IP address to connect to
23
+ # @param [Fixnum] port port number to establish the connection on
24
+ # @option opts [Object] :verify_mode (OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT)
25
+ # the methodology to use when verifying SSL certificates
26
+ # @option opts [String] :ca_file (nil) A single file containing all known
27
+ # certificates for each certificate authority (CA)
28
+ # @option opts [String] :ca_path (nil) An openssl hashed directory of
29
+ # individual certificate files for each CA
30
+ # @option opts [OpenSSL::X509::Certificate] :cert (nil) Client's
31
+ # certificate. This is needed when server requires client to validate
32
+ # itself with a certificate.
33
+ # @option opts [OpenSSL::PKey::PKey] :key (nil) Client's private key.
34
+ # This is needed when server requires client to validate itself with
35
+ # a certificate.
36
+ def initialize(host, port, ssl_opts={})
37
+ ssl_opts = DEFAULT_SSL_OPTIONS.merge(ssl_opts)
10
38
 
11
- tcp_sock = TCPSocket.new(host, port)
12
- @socket = OpenSSL::SSL::SSLSocket.new(tcp_sock, @context)
13
- @socket.sync_close = true
14
- @socket.connect
15
- super(@socket)
16
- end
17
-
18
- def ready?
19
- @socket.io.ready?
20
- end
39
+ @context = ::OpenSSL::SSL::SSLContext.new
40
+ post_check = ssl_opts.delete(:post_connection_check)
41
+ post_check_host = (post_check == true) ? host : post_check
21
42
 
22
- def shutdown(mode=2)
23
- @socket.io.shutdown(mode)
43
+ DEFAULT_SSL_OPTIONS.keys.each do |k|
44
+ @context.__send__(:"#{k}=", ssl_opts[k]) if ssl_opts.key?(k)
24
45
  end
25
- end
26
46
 
27
- class TCP < DelegateClass(TCPSocket)
28
- include FrameReader
29
- include FrameWriter
30
-
31
- def initialize(host, port, *args)
32
- @socket = TCPSocket.new(host, port)
33
- super(@socket)
47
+ tcp_sock = ::TCPSocket.new(host, port)
48
+ @socket = ::OpenSSL::SSL::SSLSocket.new(tcp_sock, @context)
49
+ @socket.sync_close = true
50
+ @socket.connect
51
+ if post_check_host
52
+ @socket.post_connection_check(post_check_host)
34
53
  end
54
+ super(@socket)
55
+ end
56
+
57
+ # Passes the :ready? message on to the socket's underlying io object.
58
+ def ready?; @socket.io.ready?; end
59
+
60
+ # Passes the :shutdown message on to the socket's underlying io object.
61
+ def shutdown(mode=2); @socket.io.shutdown(mode); end
62
+ end
63
+
64
+ # A wrapper for an TCP Socket that tidies up the specifics so
65
+ # our Connection library isn't troubled by them.
66
+ class TCP < DelegateClass(::TCPSocket)
67
+ # Create a new {TCP} connection to +host+ on +port+.
68
+ # @param [String] host hostname or IP address to connect to
69
+ # @param [Fixnum] port port number to establish the connection on
70
+ def initialize(host, port)
71
+ @socket = TCPSocket.new(host, port)
72
+ super(@socket)
35
73
  end
36
74
  end
37
75
  end
@@ -0,0 +1,79 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Manages subscriptions for a {Stomper::Connection}
4
+ class Stomper::SubscriptionManager
5
+ # Creates a new subscription handler for the supplied {Stomper::Connection connection}
6
+ # @param [Stomper::Connection] connection
7
+ def initialize(connection)
8
+ @mon = ::Monitor.new
9
+ @callbacks = {}
10
+ @dests_to_ids = {}
11
+ connection.on_message { |m, con| dispatch(m) }
12
+ connection.on_unsubscribe { |u, con| remove(u) }
13
+ end
14
+
15
+ # Adds a callback handler for a MESSAGE frame that is sent via the subscription
16
+ # associated with the supplied SUBSCRIBE frame.
17
+ # @param [Stomper::Frame] subscribe SUBSCRIBE frame for the subscription
18
+ # @param [Proc] callback Proc to invoke when a matching MESSAGE frame is
19
+ # received from the broker.
20
+ # @return [self]
21
+ def add(subscribe, callback)
22
+ s_id = subscribe[:id]
23
+ dest = subscribe[:destination]
24
+ @mon.synchronize do
25
+ @callbacks[s_id] = callback
26
+ @dests_to_ids[dest] ||= []
27
+ @dests_to_ids[dest] << s_id
28
+ end
29
+ end
30
+
31
+ # Returns true if the subscription ID is registered
32
+ # @param [String] id
33
+ # @return [true,false]
34
+ def subscribed_id?(id)
35
+ @mon.synchronize { @callbacks.key? id }
36
+ end
37
+
38
+ # Returns true if the subscription destination is registered
39
+ # @param [String] destination
40
+ # @return [true,false]
41
+ def subscribed_destination?(destination)
42
+ @mon.synchronize { @dests_to_ids.key? destination }
43
+ end
44
+
45
+ # Returns an array of subscription IDs that correspond to
46
+ # the given subscription destination. If the destination is unknown,
47
+ # returns +nil+.
48
+ # @param [String] destination
49
+ # @return [Array<String>, nil]
50
+ def ids_for_destination(destination)
51
+ @mon.synchronize { @dests_to_ids[destination] && @dests_to_ids[destination].dup }
52
+ end
53
+
54
+ private
55
+ def remove(unsub)
56
+ s_id = unsub[:id]
57
+ @mon.synchronize do
58
+ @dests_to_ids.each do |dest, ids|
59
+ ids.delete s_id
60
+ @dests_to_ids.delete dest if ids.empty?
61
+ end
62
+ @callbacks.delete(s_id)
63
+ end
64
+ end
65
+
66
+ def dispatch(message)
67
+ s_id = message[:subscription]
68
+ dest = message[:destination]
69
+ if s_id.nil? || s_id.empty?
70
+ cbs = @mon.synchronize do
71
+ @dests_to_ids[dest] && @dests_to_ids[dest].map { |id| @callbacks[id] }
72
+ end
73
+ cbs && cbs.each { |cb| cb.call(message) }
74
+ else
75
+ cb = @mon.synchronize { @callbacks[s_id] }
76
+ cb && cb.call(message)
77
+ end
78
+ end
79
+ end