stomp_out 0.1.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/.travis.yml +10 -0
- data/CHANGELOG.rdoc +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +91 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/examples/config.ru +13 -0
- data/examples/websocket_client.rb +153 -0
- data/examples/websocket_server.rb +120 -0
- data/lib/stomp_out/client.rb +580 -0
- data/lib/stomp_out/errors.rb +67 -0
- data/lib/stomp_out/frame.rb +71 -0
- data/lib/stomp_out/heartbeat.rb +151 -0
- data/lib/stomp_out/parser.rb +134 -0
- data/lib/stomp_out/server.rb +667 -0
- data/lib/stomp_out.rb +29 -0
- data/stomp_out.gemspec +95 -0
- metadata +293 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
# Copyright (c) 2015 RightScale Inc
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module StompOut
|
23
|
+
|
24
|
+
# Container for STOMP frame
|
25
|
+
class Frame
|
26
|
+
|
27
|
+
attr_accessor :command, :headers, :body
|
28
|
+
|
29
|
+
# Create Stomp frame
|
30
|
+
#
|
31
|
+
# @param [String, NilClass] command name in upper case
|
32
|
+
# @param [Hash, NilClass] headers with string header name as key
|
33
|
+
# @param [String] body
|
34
|
+
def initialize(command = nil, headers = nil, body = nil)
|
35
|
+
@command = command
|
36
|
+
@headers = headers || {}
|
37
|
+
@body = body || ""
|
38
|
+
end
|
39
|
+
|
40
|
+
# Serialize frame for transmission on wire
|
41
|
+
#
|
42
|
+
# @return [String] serialized frame
|
43
|
+
def to_s
|
44
|
+
@headers["content-length"] = @body.size.to_s if @body.include?(NULL)
|
45
|
+
@headers.keys.sort.inject("#{@command}\n") { |r, key| r << "#{key}:#{@headers[key]}\n" } + "\n#{@body}#{NULL}\n"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Verify that required headers are present and then return their values
|
49
|
+
#
|
50
|
+
# @param [String] version of STOMP in use
|
51
|
+
# @param [Hash] required headers with name as key and list of STOMP versions
|
52
|
+
# to be excluded from the verification as value
|
53
|
+
#
|
54
|
+
# @return [Array, Object] values of selected headers in header name sorted order,
|
55
|
+
# or individual header value if only one header required
|
56
|
+
#
|
57
|
+
# @raise [ProtocolError] missing header
|
58
|
+
def require(version, required)
|
59
|
+
values = []
|
60
|
+
required.keys.sort.each do |header|
|
61
|
+
exclude = required[header]
|
62
|
+
value = @headers[header]
|
63
|
+
raise ProtocolError.new("Missing '#{header}' header", self) if value.nil? && !exclude.include?(version)
|
64
|
+
values << value
|
65
|
+
end
|
66
|
+
values.size > 1 ? values : values.first
|
67
|
+
end
|
68
|
+
|
69
|
+
end # Frame
|
70
|
+
|
71
|
+
end # StompOut
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# Copyright (c) 2015 RightScale Inc
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module StompOut
|
23
|
+
|
24
|
+
class Heartbeat
|
25
|
+
|
26
|
+
# EOL that acts as STOMP heartbeat
|
27
|
+
HEARTBEAT = "\n"
|
28
|
+
|
29
|
+
# Error margin for receiving heartbeats
|
30
|
+
ERROR_MARGIN_FACTOR = 1.5
|
31
|
+
|
32
|
+
attr_reader :incoming_rate, :outgoing_rate
|
33
|
+
|
34
|
+
# Determine whether heartbeat is usable based on whether eventmachine is available
|
35
|
+
#
|
36
|
+
# @return [Boolean] whether usable
|
37
|
+
def self.usable?
|
38
|
+
require 'eventmachine'
|
39
|
+
true
|
40
|
+
rescue LoadError => e
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
# Analyze heartbeat request and if there is an agreeable rate start generating
|
45
|
+
# heartbeats and/or monitoring incoming heartbeats
|
46
|
+
#
|
47
|
+
# @param [Client, Server] stomp client/server requesting heartbeat service that responds
|
48
|
+
# to the following callbacks:
|
49
|
+
# send_data(data) - send data over connection
|
50
|
+
# report_error(error) - report error to user
|
51
|
+
# @param [String, NilClass] rate_requested requested with two positive integers separated
|
52
|
+
# by comma; first integer indicates far end's support for sending heartbeats with 0 meaning
|
53
|
+
# cannot send and any other value indicating number of milliseconds between heartbeats
|
54
|
+
# it can guarantee; second integer indicates the heartbeats the far end would like
|
55
|
+
# to receive with 0 meaning none and any other value indicating the desired number
|
56
|
+
# of milliseconds between heartbeats
|
57
|
+
# @param [Integer] min_send_interval in msec that near end is willing to guarantee
|
58
|
+
# @param [Integer] desired_receive_interval in msec for far end to send heartbeats
|
59
|
+
#
|
60
|
+
# @raise [ProtocolError] invalid heartbeat setting
|
61
|
+
def initialize(stomp, rate_requested, min_send_interval = 0, desired_receive_interval = 0)
|
62
|
+
@stomp = stomp
|
63
|
+
@received_data = @sent_data = false
|
64
|
+
@incoming_rate = @outgoing_rate = 0
|
65
|
+
if rate_requested
|
66
|
+
@incoming_rate, @outgoing_rate = rate_requested.split(",").map do |h|
|
67
|
+
raise StompOut::ProtocolError, "Invalid 'heart-beat' header" if h.nil? || h !~ /^\d+$/
|
68
|
+
h.to_i
|
69
|
+
end
|
70
|
+
raise StompOut::ProtocolError, "Invalid 'heart-beat' header" if @outgoing_rate.nil? || @incoming_rate.nil?
|
71
|
+
@incoming_rate = [@incoming_rate, min_send_interval].max if @incoming_rate > 0
|
72
|
+
@outgoing_rate = [@outgoing_rate, desired_receive_interval].max if @outgoing_rate > 0
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Start heartbeat service
|
77
|
+
#
|
78
|
+
# @return [TrueClass] always true
|
79
|
+
def start
|
80
|
+
monitor_incoming if @incoming_rate > 0
|
81
|
+
generate_outgoing if @outgoing_rate > 0
|
82
|
+
end
|
83
|
+
|
84
|
+
# Stop heartbeat service
|
85
|
+
#
|
86
|
+
# @return [TrueClass] always true
|
87
|
+
def stop
|
88
|
+
if @incoming_timer
|
89
|
+
@incoming_timer.cancel
|
90
|
+
@incoming_timer = nil
|
91
|
+
end
|
92
|
+
if @outgoing_timer
|
93
|
+
@outgoing_timer.cancel
|
94
|
+
@outgoing_timer = nil
|
95
|
+
end
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
# Record that data has been sent to far end
|
100
|
+
#
|
101
|
+
# @return [TrueClass] always true
|
102
|
+
def sent_data
|
103
|
+
@sent_data = true
|
104
|
+
end
|
105
|
+
|
106
|
+
# Record that data has been received from far end
|
107
|
+
#
|
108
|
+
# @return [TrueClass] always true
|
109
|
+
def received_data
|
110
|
+
@received_data = true
|
111
|
+
end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
# Monitor incoming heartbeats
|
116
|
+
# Report failure and stop heartbeat if miss heartbeat by more than
|
117
|
+
# specified margin
|
118
|
+
#
|
119
|
+
# @return [TrueClass] always true
|
120
|
+
def monitor_incoming
|
121
|
+
interval = (@incoming_rate * ERROR_MARGIN_FACTOR) / 1000.0
|
122
|
+
@incoming_timer = EM::PeriodicTimer.new(interval) do
|
123
|
+
if @received_data
|
124
|
+
@received_data = false
|
125
|
+
else
|
126
|
+
stop
|
127
|
+
@stomp.report_error("heartbeat failure")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
true
|
131
|
+
end
|
132
|
+
|
133
|
+
# Generate outgoing heartbeats whenever there is not any other
|
134
|
+
# send activity for given heartbeat interval
|
135
|
+
#
|
136
|
+
# @return [TrueClass] always true
|
137
|
+
def generate_outgoing
|
138
|
+
interval = @outgoing_rate / 1000.0
|
139
|
+
@outgoing_timer = EM::PeriodicTimer.new(interval) do
|
140
|
+
if @sent_data
|
141
|
+
@sent_data = false
|
142
|
+
else
|
143
|
+
@stomp.send_data(HEARTBEAT)
|
144
|
+
@sent_data = true
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end # Heartbeat
|
150
|
+
|
151
|
+
end # StompOut
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# Copyright (c) 2015 RightScale Inc
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module StompOut
|
23
|
+
|
24
|
+
# Null terminator for frame
|
25
|
+
NULL = "\000"
|
26
|
+
|
27
|
+
# Parser for converting stream of data from connection into STOMP frames
|
28
|
+
class Parser
|
29
|
+
|
30
|
+
# Create frame parser
|
31
|
+
def initialize
|
32
|
+
@buffer = ""
|
33
|
+
@body_length = nil
|
34
|
+
@frame = Frame.new
|
35
|
+
@frames = []
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add data received from connection to end of buffer
|
39
|
+
#
|
40
|
+
# @return [TrueClass] always true
|
41
|
+
def <<(buf)
|
42
|
+
@buffer << buf
|
43
|
+
parse
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get next frame
|
47
|
+
#
|
48
|
+
# @return [Frame, NilClass] frame or nil if none available
|
49
|
+
def next
|
50
|
+
@frames.shift
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
# Parse the contents of the buffer
|
56
|
+
#
|
57
|
+
# @return [TrueClass] always true
|
58
|
+
def parse
|
59
|
+
parse_command_and_headers if @frame.command.nil?
|
60
|
+
success = if @frame.command
|
61
|
+
if @body_length
|
62
|
+
parse_binary_body
|
63
|
+
else
|
64
|
+
parse_text_body
|
65
|
+
end
|
66
|
+
elsif (match = @buffer.match(/\A\n|\A\r|\A\r\n/))
|
67
|
+
# Ignore heartbeat
|
68
|
+
@buffer = match.post_match
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
# Keep parsing if making progress and there is more data
|
73
|
+
parse if success && !@buffer.empty?
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Parse next command and headers at beginning of buffer
|
78
|
+
#
|
79
|
+
# @return [TrueClass] always true
|
80
|
+
def parse_command_and_headers
|
81
|
+
if (match = @buffer.match(/\A\s*(\S+)\r?\n((?:[ \t]*.*?[ \t]*:[ \t]*.*?[ \t]*$\r?\n)*)\r?\n/))
|
82
|
+
@frame.command, headers = match.captures
|
83
|
+
@buffer = match.post_match
|
84
|
+
headers.split(/\r?\n/).each do |data|
|
85
|
+
if data.match(/^\s*(\S+)\s*:\s*(.*?\s*)$/)
|
86
|
+
@frame.headers[$1] = $2 unless @frame.headers.has_key?($1)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
@body_length = (length = @frame.headers["content-length"]) && length.to_i
|
90
|
+
elsif @buffer.rindex(NULL)
|
91
|
+
raise ProtocolError, "Invalid frame (malformed headers)"
|
92
|
+
end
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
# Parse binary body at beginning of buffer
|
97
|
+
#
|
98
|
+
# @return [Frame, NilClass] frame created or nil if need more data
|
99
|
+
#
|
100
|
+
# @raise [ProtocolError] missing frame null terminator
|
101
|
+
def parse_binary_body
|
102
|
+
if @buffer.size > @body_length
|
103
|
+
# Also test for 0 here to be compatible with Ruby 1.8 string handling
|
104
|
+
unless [NULL, 0].include?(@buffer[@body_length])
|
105
|
+
raise ProtocolError, "Invalid frame (missing null terminator)"
|
106
|
+
end
|
107
|
+
parse_body(@body_length)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Parse text body at beginning of buffer
|
112
|
+
#
|
113
|
+
# @return [Frame, NilClass] frame created or nil if need more data
|
114
|
+
def parse_text_body
|
115
|
+
if (length = @buffer.index(NULL))
|
116
|
+
parse_body(length)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Parse body at beginning of buffer to complete frame
|
121
|
+
#
|
122
|
+
# @param [Integer] length of body
|
123
|
+
#
|
124
|
+
# @return [Frame] new frame
|
125
|
+
def parse_body(length)
|
126
|
+
@frame.body = @buffer[0...length]
|
127
|
+
@buffer = @buffer[length+1..-1]
|
128
|
+
@frames << @frame
|
129
|
+
@frame = Frame.new
|
130
|
+
end
|
131
|
+
|
132
|
+
end # Parser
|
133
|
+
|
134
|
+
end # StompOut
|