stomper 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|