stomper 1.0.0 → 2.0.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.
- data/.gitignore +5 -0
- data/{spec/spec.opts → .rspec} +0 -2
- data/Gemfile +4 -0
- data/LICENSE +201 -201
- data/README.md +130 -0
- data/Rakefile +5 -0
- data/examples/basic.rb +38 -0
- data/examples/events.rb +54 -0
- data/features/acking_messages.feature +147 -0
- data/features/disconnecting.feature +12 -0
- data/features/establish_connection.feature +44 -0
- data/features/protocol_version_negotiation.feature +61 -0
- data/features/receipts.feature +72 -0
- data/features/scopes.feature +32 -0
- data/features/secure_connections.feature +38 -0
- data/features/send_and_message.feature +28 -0
- data/features/steps/acking_messages_steps.rb +39 -0
- data/features/steps/disconnecting_steps.rb +8 -0
- data/features/steps/establish_connection_steps.rb +74 -0
- data/features/steps/frame_transmission_steps.rb +35 -0
- data/features/steps/protocol_version_negotiation_steps.rb +15 -0
- data/features/steps/receipts_steps.rb +79 -0
- data/features/steps/scopes_steps.rb +52 -0
- data/features/steps/secure_connections_steps.rb +41 -0
- data/features/steps/send_and_message_steps.rb +35 -0
- data/features/steps/subscribing_steps.rb +36 -0
- data/features/steps/threaded_receiver_steps.rb +8 -0
- data/features/steps/transactions_steps.rb +0 -0
- data/features/subscribing.feature +151 -0
- data/features/support/env.rb +11 -0
- data/features/support/header_helpers.rb +12 -0
- data/features/support/ssl/README +6 -0
- data/features/support/ssl/broker_cert.csr +17 -0
- data/features/support/ssl/broker_cert.pem +72 -0
- data/features/support/ssl/broker_key.pem +27 -0
- data/features/support/ssl/client_cert.csr +17 -0
- data/features/support/ssl/client_cert.pem +72 -0
- data/features/support/ssl/client_key.pem +27 -0
- data/features/support/ssl/demoCA/cacert.pem +17 -0
- data/features/support/ssl/demoCA/index.txt +2 -0
- data/features/support/ssl/demoCA/index.txt.attr +1 -0
- data/features/support/ssl/demoCA/index.txt.attr.old +1 -0
- data/features/support/ssl/demoCA/index.txt.old +1 -0
- data/features/support/ssl/demoCA/newcerts/01.pem +72 -0
- data/features/support/ssl/demoCA/newcerts/02.pem +72 -0
- data/features/support/ssl/demoCA/private/cakey.pem +17 -0
- data/features/support/ssl/demoCA/serial +1 -0
- data/features/support/ssl/demoCA/serial.old +1 -0
- data/features/support/test_stomp_server.rb +150 -0
- data/features/threaded_receiver.feature +11 -0
- data/features/transactions.feature +66 -0
- data/lib/stomper.rb +30 -20
- data/lib/stomper/connection.rb +442 -102
- data/lib/stomper/errors.rb +59 -0
- data/lib/stomper/extensions.rb +10 -0
- data/lib/stomper/extensions/common.rb +258 -0
- data/lib/stomper/extensions/events.rb +213 -0
- data/lib/stomper/extensions/heartbeat.rb +101 -0
- data/lib/stomper/extensions/scoping.rb +56 -0
- data/lib/stomper/frame.rb +54 -0
- data/lib/stomper/frame_serializer.rb +217 -0
- data/lib/stomper/headers.rb +15 -0
- data/lib/stomper/receipt_manager.rb +36 -0
- data/lib/stomper/receivers.rb +7 -0
- data/lib/stomper/receivers/threaded.rb +71 -0
- data/lib/stomper/scopes.rb +9 -0
- data/lib/stomper/scopes/header_scope.rb +49 -0
- data/lib/stomper/scopes/receipt_scope.rb +44 -0
- data/lib/stomper/scopes/transaction_scope.rb +109 -0
- data/lib/stomper/sockets.rb +66 -28
- data/lib/stomper/subscription_manager.rb +79 -0
- data/lib/stomper/support.rb +68 -0
- data/lib/stomper/support/1.8/frame_serializer.rb +53 -0
- data/lib/stomper/support/1.8/headers.rb +183 -0
- data/lib/stomper/support/1.9/frame_serializer.rb +64 -0
- data/lib/stomper/support/1.9/headers.rb +172 -0
- data/lib/stomper/support/ruby.rb +13 -0
- data/lib/stomper/uris.rb +49 -0
- data/lib/stomper/version.rb +7 -0
- data/spec/spec_helper.rb +13 -9
- data/spec/stomper/connection_spec.rb +712 -0
- data/spec/stomper/extensions/common_spec.rb +187 -0
- data/spec/stomper/extensions/events_spec.rb +78 -0
- data/spec/stomper/extensions/heartbeat_spec.rb +103 -0
- data/spec/stomper/extensions/scoping_spec.rb +21 -0
- data/spec/stomper/frame_serializer_1.8_spec.rb +318 -0
- data/spec/stomper/frame_serializer_spec.rb +316 -0
- data/spec/stomper/frame_spec.rb +36 -0
- data/spec/stomper/headers_spec.rb +224 -0
- data/spec/stomper/receipt_manager_spec.rb +91 -0
- data/spec/stomper/receivers/threaded_spec.rb +116 -0
- data/spec/stomper/scopes/header_scope_spec.rb +42 -0
- data/spec/stomper/scopes/receipt_scope_spec.rb +51 -0
- data/spec/stomper/scopes/transaction_scope_spec.rb +183 -0
- data/spec/stomper/sockets_spec.rb +113 -0
- data/spec/stomper/subscription_manager_spec.rb +107 -0
- data/spec/stomper/support_spec.rb +69 -0
- data/spec/stomper/uris_spec.rb +54 -0
- data/spec/stomper_spec.rb +9 -0
- data/spec/support/custom_argument_matchers.rb +57 -0
- data/spec/support/existential_frame_matchers.rb +19 -0
- data/spec/support/frame_header_matchers.rb +10 -0
- data/stomper.gemspec +30 -0
- metadata +272 -97
- data/AUTHORS +0 -21
- data/CHANGELOG +0 -20
- data/README.rdoc +0 -120
- data/lib/stomper/client.rb +0 -34
- data/lib/stomper/frame_reader.rb +0 -73
- data/lib/stomper/frame_writer.rb +0 -21
- data/lib/stomper/frames.rb +0 -39
- data/lib/stomper/frames/abort.rb +0 -10
- data/lib/stomper/frames/ack.rb +0 -25
- data/lib/stomper/frames/begin.rb +0 -11
- data/lib/stomper/frames/client_frame.rb +0 -89
- data/lib/stomper/frames/commit.rb +0 -10
- data/lib/stomper/frames/connect.rb +0 -10
- data/lib/stomper/frames/connected.rb +0 -30
- data/lib/stomper/frames/disconnect.rb +0 -10
- data/lib/stomper/frames/error.rb +0 -21
- data/lib/stomper/frames/message.rb +0 -48
- data/lib/stomper/frames/receipt.rb +0 -19
- data/lib/stomper/frames/send.rb +0 -10
- data/lib/stomper/frames/server_frame.rb +0 -38
- data/lib/stomper/frames/subscribe.rb +0 -42
- data/lib/stomper/frames/unsubscribe.rb +0 -19
- data/lib/stomper/open_uri_interface.rb +0 -41
- data/lib/stomper/receipt_handlers.rb +0 -23
- data/lib/stomper/receiptor.rb +0 -38
- data/lib/stomper/subscriber.rb +0 -76
- data/lib/stomper/subscription.rb +0 -128
- data/lib/stomper/subscriptions.rb +0 -95
- data/lib/stomper/threaded_receiver.rb +0 -59
- data/lib/stomper/transaction.rb +0 -185
- data/lib/stomper/transactor.rb +0 -50
- data/lib/stomper/uri.rb +0 -55
- data/spec/client_spec.rb +0 -29
- data/spec/connection_spec.rb +0 -22
- data/spec/frame_reader_spec.rb +0 -37
- data/spec/frame_writer_spec.rb +0 -27
- data/spec/frames/client_frame_spec.rb +0 -66
- data/spec/frames/indirect_frame_spec.rb +0 -45
- data/spec/frames/server_frame_spec.rb +0 -85
- data/spec/open_uri_interface_spec.rb +0 -132
- data/spec/receiptor_spec.rb +0 -35
- data/spec/shared_connection_examples.rb +0 -79
- data/spec/subscriber_spec.rb +0 -77
- data/spec/subscription_spec.rb +0 -157
- data/spec/subscriptions_spec.rb +0 -145
- data/spec/threaded_receiver_spec.rb +0 -33
- data/spec/transaction_spec.rb +0 -139
- data/spec/transactor_spec.rb +0 -46
@@ -0,0 +1,101 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Provides the heart-beating interface for a {Stomper::Connection} object.
|
4
|
+
module Stomper::Extensions::Heartbeat
|
5
|
+
# Extends an object with any additional modules that are appropriate
|
6
|
+
# for the Stomp protocol being used.
|
7
|
+
def self.extend_by_protocol_version(instance, version)
|
8
|
+
if EXTEND_BY_VERSION[version]
|
9
|
+
EXTEND_BY_VERSION[version].each do |mod|
|
10
|
+
instance.extend mod
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# By default, this method does nothing. If the established connection
|
16
|
+
# utilizes the Stomp 1.1 protocol, this method will be overridden by
|
17
|
+
# {Stomper::Protocols::V1_1::Heartbeating#beat}.
|
18
|
+
def beat; end
|
19
|
+
|
20
|
+
# By default, a connection is alive if it is connected.
|
21
|
+
# If the established connection utilizes the Stomp 1.1 protocol, this
|
22
|
+
# method will be overridden by {Stomper::Protocols::V1_1::Heartbeating#alive?}.
|
23
|
+
# @return [true,false]
|
24
|
+
# @see #dead?
|
25
|
+
def alive?
|
26
|
+
connected?
|
27
|
+
end
|
28
|
+
|
29
|
+
# A {Stomper::Connection connection} is dead if it is not +alive?+
|
30
|
+
# @return [true, false]
|
31
|
+
# @see #alive?
|
32
|
+
def dead?
|
33
|
+
!alive?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Stomp Protocol 1.1 extensions to the heart-beating interface
|
37
|
+
module V1_1
|
38
|
+
# Send a heartbeat to the broker
|
39
|
+
def beat
|
40
|
+
transmit ::Stomper::Frame.new
|
41
|
+
end
|
42
|
+
|
43
|
+
# Stomp 1.1 {Stomper::Connection connections} are alive if they are
|
44
|
+
# +connected?+ and are meeting their negotiated heart-beating obligations.
|
45
|
+
# @return [true, false]
|
46
|
+
# @see #dead?
|
47
|
+
def alive?
|
48
|
+
connected? && client_alive? && broker_alive?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Maximum number of milliseconds that can pass between frame / heartbeat
|
52
|
+
# transmissions before we consider the client to be dead.
|
53
|
+
# @return [Fixnum]
|
54
|
+
def heartbeat_client_limit
|
55
|
+
unless defined?(@heartbeat_client_limit)
|
56
|
+
@heartbeat_client_limit = heartbeating[0] > 0 ? (1.1 * heartbeating[0]) : 0
|
57
|
+
end
|
58
|
+
@heartbeat_client_limit
|
59
|
+
end
|
60
|
+
|
61
|
+
# Maximum number of milliseconds that can pass between frames / heartbeats
|
62
|
+
# received before we consider the broker to be dead.
|
63
|
+
# @return [Fixnum]
|
64
|
+
def heartbeat_broker_limit
|
65
|
+
unless defined?(@heartbeat_broker_limit)
|
66
|
+
@heartbeat_broker_limit = heartbeating[1] > 0 ? (1.1 * heartbeating[1]) : 0
|
67
|
+
end
|
68
|
+
@heartbeat_broker_limit
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns true if the client is alive. Client is alive if client heartbeating
|
72
|
+
# is disabled, or the number of milliseconds that have passed since last
|
73
|
+
# transmission is less than or equal to {#heartbeat_client_limit client} limit
|
74
|
+
# @return [true,false]
|
75
|
+
# @see #heartbeat_client_limit
|
76
|
+
# @see #broker_alive?
|
77
|
+
def client_alive?
|
78
|
+
# Consider some benchmarking to determine if this is faster than
|
79
|
+
# re-writing the method after its first invocation.
|
80
|
+
heartbeat_client_limit == 0 ||
|
81
|
+
duration_since_transmitted <= heartbeat_client_limit
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns true if the broker is alive. Broker is alive if broker heartbeating
|
85
|
+
# is disabled, or the number of milliseconds that have passed since last
|
86
|
+
# receiving is less than or equal to {#heartbeat_broker_limit broker} limit
|
87
|
+
# @return [true,false]
|
88
|
+
# @see #heartbeat_broker_limit
|
89
|
+
# @see #client_alive?
|
90
|
+
def broker_alive?
|
91
|
+
heartbeat_broker_limit == 0 ||
|
92
|
+
duration_since_received <= heartbeat_broker_limit
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# A mapping between protocol versions and modules to include
|
97
|
+
EXTEND_BY_VERSION = {
|
98
|
+
'1.0' => [ ],
|
99
|
+
'1.1' => [ ::Stomper::Extensions::Heartbeat::V1_1 ]
|
100
|
+
}
|
101
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Provides the scoping interface for a {Stomper::Connection} object.
|
4
|
+
module Stomper::Extensions::Scoping
|
5
|
+
# Creates a new {Stomper::Scopes::TransactionScope} to perform
|
6
|
+
# a transaction. If a block is provided, all SEND, ACK, NACK, COMMIT and
|
7
|
+
# ABORT frames generated within the block are bound to the same transaction.
|
8
|
+
# Further, if an exception is raised within the block, the transaction is
|
9
|
+
# rolled back through an ABORT frame, otherwise it is automatically committed
|
10
|
+
# through a COMMIT frame. If a block is not provided, the transaction must
|
11
|
+
# be manually aborted or committed through the returned
|
12
|
+
# {Stomper::Scopes::TransactionScope} object.
|
13
|
+
# @param [{Symbol => Object}] headers
|
14
|
+
# @yield [tx] block is evaluated as a transaction
|
15
|
+
# @yieldparam [Stomper::Scopes::TransactionScope] tx
|
16
|
+
# @return [Stomper::Scopes::TransactionScope]
|
17
|
+
# @example Gonna need an example or two
|
18
|
+
def with_transaction(headers={}, &block)
|
19
|
+
create_scope(::Stomper::Scopes::TransactionScope, headers, block)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Creates a new {Stomper::Scopes::ReceiptScope} using
|
23
|
+
# a supplied block as the receipt handler. If no block is provided, no
|
24
|
+
# receipt handler is created; however, all frames generated through this
|
25
|
+
# {Stomper::Scopes::ReceiptScope} will still request a RECEIPT
|
26
|
+
# from the broker.
|
27
|
+
# @param [{Symbol => Object}] headers
|
28
|
+
# @yield [receipt] callback invoked upon receiving the RECEIPT frame
|
29
|
+
# @yieldparam [Stomper::Frame] the received RECEIPT frame
|
30
|
+
# @return [Stomper::Scopes::ReceiptScope]
|
31
|
+
# @example Gonna need an example or two
|
32
|
+
# @see Stomper::Extensions::Events#on_receipt}
|
33
|
+
def with_receipt(headers={}, &block)
|
34
|
+
create_scope(::Stomper::Scopes::ReceiptScope, headers, block)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates a new {Stomper::Scopes::HeaderScope} from the
|
38
|
+
# supplied hash of headers. If a block is provided, it will be invoked with
|
39
|
+
# with this {Stomper::Scopes::HeaderScope} as its only parameter.
|
40
|
+
# @param [{Symbol => Object}] headers
|
41
|
+
# @yield [header_scope] block is evaluated applying the specified headers to
|
42
|
+
# all frames generated within the block.
|
43
|
+
# @yieldparam [Stomper::Scopes::HeaderScope] header_scope
|
44
|
+
# @return [Stomper::Scopes::HeaderScope]
|
45
|
+
# @example Gonna need an example or two
|
46
|
+
def with_headers(headers, &block)
|
47
|
+
create_scope(::Stomper::Scopes::HeaderScope, headers, block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_scope(klass, headers, callback)
|
51
|
+
klass.new(self, headers).tap do |scoped|
|
52
|
+
scoped.apply_to(callback)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
private :create_scope
|
56
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# A generic encapsulation of a frame as specified by the Stomp protocol.
|
4
|
+
class Stomper::Frame
|
5
|
+
# The command name of this frame (CONNECTED, SEND, RECEIPT, etc.)
|
6
|
+
# @return [String]
|
7
|
+
attr_accessor :command
|
8
|
+
|
9
|
+
# The body of this frame
|
10
|
+
# @return [String] if a body has been set
|
11
|
+
attr_accessor :body
|
12
|
+
|
13
|
+
# The headers associated with this frame
|
14
|
+
# @return [Stomper::Headers]
|
15
|
+
attr_reader :headers
|
16
|
+
|
17
|
+
# Creates a new frame. The frame will be initialized with the optional
|
18
|
+
# +command+ name, a {Stomper::Headers headers} collection initialized
|
19
|
+
# with the optional +headers+ hash, and an optional body.
|
20
|
+
def initialize(command=nil, headers={}, body=nil)
|
21
|
+
@command = command
|
22
|
+
@headers = ::Stomper::Headers.new(headers)
|
23
|
+
@body = body
|
24
|
+
end
|
25
|
+
|
26
|
+
# Gets the header value paired with the supplied name. This is a convenient
|
27
|
+
# shortcut for `frame.headers[name]`.
|
28
|
+
#
|
29
|
+
# @param [Object] name the header name associated with the desired value
|
30
|
+
# @return [String] the value associated with the requested header name
|
31
|
+
# @see Stomper::Headers#[]
|
32
|
+
# @example
|
33
|
+
# frame['content-type'] #=> 'text/plain'
|
34
|
+
def [](name); @headers[name]; end
|
35
|
+
|
36
|
+
# Sets the header value paired with the supplied name. This is a convenient
|
37
|
+
# shortcut for `frame.headers[name] = val`.
|
38
|
+
#
|
39
|
+
# @param [Object] name the header name to associate with the supplied value
|
40
|
+
# @param [Object] val the value to associate with the supplied header name
|
41
|
+
# @return [String] the supplied value as a string, or `nil` if `nil` was supplied as the value.
|
42
|
+
# @see Stomper::Headers#[]=
|
43
|
+
# @example
|
44
|
+
# frame['content-type'] = 'text/plain' #=> 'text/plain'
|
45
|
+
# frame['other header'] = 42 #=> '42'
|
46
|
+
def []=(name, val); @headers[name] = val; end
|
47
|
+
|
48
|
+
# A convenience method for getting the 'content-type' header without
|
49
|
+
# any parameters.
|
50
|
+
# @return [String]
|
51
|
+
def content_type
|
52
|
+
@headers[:'content-type'] && @headers[:'content-type'].split(';').first || ''
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# This class serializes Stomp frames to IO streams. Submodule mixins within
|
4
|
+
# this class are used to adjust the serialization behavior depending upon
|
5
|
+
# the Stomp Protocol version being used.
|
6
|
+
# @see Stomper::Support::Ruby1_8::FrameSerializer Implementation for Ruby 1.8.7
|
7
|
+
# @see Stomper::Support::Ruby1_9::FrameSerializer Implementation for Ruby 1.9
|
8
|
+
class Stomper::FrameSerializer
|
9
|
+
# The character that must be present at the end of every Stomp frame.
|
10
|
+
FRAME_TERMINATOR = "\000"
|
11
|
+
|
12
|
+
# Creates a new frame serializer that will read {Stomper::Frame frames} from
|
13
|
+
# and write {Stomper::Frame frames} to the supplied IO object (typically
|
14
|
+
# a TCP or SSL socket.)
|
15
|
+
# @param [IO] io IO stream to read from and write to
|
16
|
+
def initialize(io)
|
17
|
+
@io = io
|
18
|
+
@write_mutex = ::Mutex.new
|
19
|
+
@read_mutex = ::Mutex.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# Extends the serializer based on the version of the protocol being used.
|
23
|
+
# @param [String] version protocol version being used
|
24
|
+
# @return [self]
|
25
|
+
def extend_for_protocol(version)
|
26
|
+
if EXTEND_BY_VERSION[version]
|
27
|
+
EXTEND_BY_VERSION[version].each { |m| extend m }
|
28
|
+
end
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
# Serializes and writes a {Stomper::Frame} to the underlying IO stream. This
|
33
|
+
# includes setting the appropriate values for 'content-length' and
|
34
|
+
# 'content-type' headers, if applicable.
|
35
|
+
# @param [Stomper::Frame] frame
|
36
|
+
# @return [Stomper::Frame] the frame that was passed to the method
|
37
|
+
def write_frame(frame)
|
38
|
+
@write_mutex.synchronize { __write_frame__(frame) }
|
39
|
+
end
|
40
|
+
|
41
|
+
# Deserializes and returns a {Stomper::Frame} read from the underlying IO
|
42
|
+
# stream. If the IO stream produces data that violates the Stomp protocol
|
43
|
+
# specification, an instance of {Stomper::Errors::FatalProtocolError}, or
|
44
|
+
# one of its subclasses, will be raised.
|
45
|
+
# @return [Stomper::Frame]
|
46
|
+
# @raise [Stomper::Errors::MalformedFrameError] if the frame is not properly terminated
|
47
|
+
# @raise [Stomper::Errors::MalformedHeaderError] if a header is malformed (eg: contains no ':' separator)
|
48
|
+
def read_frame
|
49
|
+
@read_mutex.synchronize { __read_frame__ }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Converts the headers of the supplied frame into a single "\n" delimited
|
53
|
+
# string that's suitable for writing to io.
|
54
|
+
# @param [Stomper::Frame] frame the frame whose headers should be serialized
|
55
|
+
# @return [String]
|
56
|
+
def serialize_headers(frame)
|
57
|
+
serialized = frame.headers.inject('') do |head_str, (k, v)|
|
58
|
+
k = escape_header_name(k)
|
59
|
+
next head_str if k.empty? || ['content-type', 'content-length'].include?(k)
|
60
|
+
head_str << "#{k}:#{escape_header_value(v)}\n"
|
61
|
+
end
|
62
|
+
if frame.body
|
63
|
+
if ct = determine_content_type(frame)
|
64
|
+
serialized << "content-type:#{ct}\n"
|
65
|
+
end
|
66
|
+
if clen = determine_content_length(frame)
|
67
|
+
serialized << "content-length:#{clen}\n"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
serialized
|
71
|
+
end
|
72
|
+
|
73
|
+
# Escape a header name to comply with Stomp Protocol 1.0 specifications.
|
74
|
+
# All LF ("\n") and ":" characters are replaced with empty strings.
|
75
|
+
# @note If the connection is using the 1.1 protocol, this method will
|
76
|
+
# be overridden by {FrameSerializer::V1_1#escape_header_name}
|
77
|
+
# @param [String] str
|
78
|
+
# @return [String] escaped header name
|
79
|
+
def escape_header_name(str)
|
80
|
+
str.gsub(/[\n:]/, '')
|
81
|
+
end
|
82
|
+
|
83
|
+
# Escape a header value to comply with Stomp Protocol 1.0 specifications.
|
84
|
+
# All LF ("\n") characters are replaced with empty strings.
|
85
|
+
# @note If the connection is using the 1.1 protocol, this method will
|
86
|
+
# be overridden by {FrameSerializer::V1_1#escape_header_value}
|
87
|
+
# @param [String] str
|
88
|
+
# @return [String] escaped header value
|
89
|
+
def escape_header_value(str)
|
90
|
+
str.gsub(/\n/, '')
|
91
|
+
end
|
92
|
+
|
93
|
+
# Return the header name as it was passed. Stomp 1.0 does not provide
|
94
|
+
# any means for escaping special characters such as ":" and "\n"
|
95
|
+
# @note If the connection is using the 1.1 protocol, this method will
|
96
|
+
# be overridden by {FrameSerializer::V1_1#unescape_header_name}
|
97
|
+
# @param [String] str
|
98
|
+
# @return [String]
|
99
|
+
def unescape_header_name(str); str; end
|
100
|
+
alias :unescape_header_value :unescape_header_name
|
101
|
+
|
102
|
+
private
|
103
|
+
# These are the un-synchronized methods that do the real reading/writing
|
104
|
+
# of frames. This approach facilitates easier testing of Thread safety
|
105
|
+
def __write_frame__(frame)
|
106
|
+
if frame.command
|
107
|
+
@io.write [frame.command, "\n", serialize_headers(frame),
|
108
|
+
"\n", frame.body, FRAME_TERMINATOR].compact.join
|
109
|
+
else
|
110
|
+
@io.write "\n"
|
111
|
+
end
|
112
|
+
frame
|
113
|
+
end
|
114
|
+
def __read_frame__
|
115
|
+
command = @io.gets
|
116
|
+
return nil if command.nil?
|
117
|
+
command.chomp!
|
118
|
+
frame = Stomper::Frame.new
|
119
|
+
unless command.nil? || command.empty?
|
120
|
+
frame.command = command
|
121
|
+
|
122
|
+
while (header_line = get_header_line.chomp).length > 0
|
123
|
+
raise ::Stomper::Errors::MalformedHeaderError,
|
124
|
+
"unterminated header: '#{header_line}'" unless header_line.include? ':'
|
125
|
+
k, v = header_line.split(':', 2)
|
126
|
+
frame.headers.append(unescape_header_name(k), unescape_header_value(v))
|
127
|
+
end
|
128
|
+
|
129
|
+
body = nil
|
130
|
+
if frame[:'content-length'] && (len = frame[:'content-length'].to_i) > 0
|
131
|
+
body = @io.read len
|
132
|
+
raise ::Stomper::Errors::MalformedFrameError, "frame was not properly terminated" if get_body_byte
|
133
|
+
else
|
134
|
+
while (c = get_body_byte)
|
135
|
+
body ||= ""
|
136
|
+
body << c
|
137
|
+
end
|
138
|
+
end
|
139
|
+
frame.body = body && encode_body(body, frame[:'content-type'])
|
140
|
+
end
|
141
|
+
frame
|
142
|
+
end
|
143
|
+
|
144
|
+
# Stomp Protocol 1.1 specific frame writing / reading methods.
|
145
|
+
module V1_1
|
146
|
+
# Mapping of characters to their appropriate escape sequences. This
|
147
|
+
# is used when escaping headers for frames being written to the stream.
|
148
|
+
CHARACTER_ESCAPES = {
|
149
|
+
':' => "\\c",
|
150
|
+
"\n" => "\\n",
|
151
|
+
"\\" => "\\\\"
|
152
|
+
}
|
153
|
+
|
154
|
+
# Mapping of escape sequences to their appropriate characters. This
|
155
|
+
# is used when unescaping headers being read from the stream.
|
156
|
+
ESCAPE_SEQUENCES = {
|
157
|
+
'c' => ':',
|
158
|
+
'\\' => "\\",
|
159
|
+
'n' => "\n"
|
160
|
+
}
|
161
|
+
|
162
|
+
# Escape a header name to comply with Stomp Protocol 1.1 specifications.
|
163
|
+
# All special characters (the keys of {CHARACTER_ESCAPES}) are replaced
|
164
|
+
# by their corresponding escape sequences.
|
165
|
+
# @param [String] str
|
166
|
+
# @return [String] escaped header name
|
167
|
+
def escape_header_name(hdr)
|
168
|
+
hdr.each_char.inject('') do |esc, ch|
|
169
|
+
esc << (CHARACTER_ESCAPES[ch] || ch)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
alias :escape_header_value :escape_header_name
|
173
|
+
|
174
|
+
# Return the header name after known escape sequences have been
|
175
|
+
# translated to their respective values. The keys of {ESCAPE_SEQUENCES},
|
176
|
+
# prefixed with a '\' character, denote the allowed escape sequences.
|
177
|
+
# If an unknown escape sequence is encountered, an error is raised.
|
178
|
+
# @param [String] str header string to unescape
|
179
|
+
# @return [String] unescaped header string
|
180
|
+
# @raise [Stomper::Errors::InvalidHeaderEscapeSequenceError] if an
|
181
|
+
# unknown escape sequence is encountered within the string or if
|
182
|
+
# an escape sequence is not properly completed (the last character of
|
183
|
+
# +str+ is '\')
|
184
|
+
def unescape_header_name(str)
|
185
|
+
state = :read
|
186
|
+
str.each_char.inject('') do |unesc, ch|
|
187
|
+
case state
|
188
|
+
when :read
|
189
|
+
if ch == '\\'
|
190
|
+
state = :unescape
|
191
|
+
else
|
192
|
+
unesc << ch
|
193
|
+
end
|
194
|
+
when :unescape
|
195
|
+
state = :read
|
196
|
+
if ESCAPE_SEQUENCES[ch]
|
197
|
+
unesc << ESCAPE_SEQUENCES[ch]
|
198
|
+
else
|
199
|
+
raise ::Stomper::Errors::InvalidHeaderEscapeSequenceError,
|
200
|
+
"invalid header escape sequence encountered '\\#{ch}'"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
unesc
|
204
|
+
end.tap do
|
205
|
+
raise ::Stomper::Errors::InvalidHeaderEscapeSequenceError,
|
206
|
+
"incomplete escape sequence encountered in '#{str}'" if state != :read
|
207
|
+
end
|
208
|
+
end
|
209
|
+
alias :unescape_header_value :unescape_header_name
|
210
|
+
end
|
211
|
+
|
212
|
+
# The modules to mix-in to {FrameSerializer} instances depending upon which
|
213
|
+
# protocol version is being used.
|
214
|
+
EXTEND_BY_VERSION = {
|
215
|
+
'1.1' => [ ::Stomper::FrameSerializer::V1_1 ]
|
216
|
+
}
|
217
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# A specialized container for storing header name / value pairs for a Stomp
|
4
|
+
# {Stomper::Frame Frame}. This container behaves much like a +Hash+, but
|
5
|
+
# is specialized for the Stomp protocol. Header names are always converted
|
6
|
+
# into +String+s through the use of +to_s+ and may have more than one value
|
7
|
+
# associated with them.
|
8
|
+
#
|
9
|
+
# @note Header names are case sensitive, therefore the names 'header'
|
10
|
+
# and 'Header' will not refer to the same values.
|
11
|
+
# @see Stomper::Support::Ruby1_8::Headers Implementation for Ruby 1.8.7
|
12
|
+
# @see Stomper::Support::Ruby1_9::Headers Implementation for Ruby 1.9
|
13
|
+
class Stomper::Headers
|
14
|
+
include ::Enumerable
|
15
|
+
end
|