stomp_out 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|