nats 0.3.12
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/COPYING +19 -0
- data/README.md +95 -0
- data/Rakefile +31 -0
- data/bin/nats-pub +20 -0
- data/bin/nats-server +5 -0
- data/bin/nats-sub +22 -0
- data/lib/nats/client.rb +424 -0
- data/lib/nats/ext/bytesize.rb +5 -0
- data/lib/nats/ext/em.rb +6 -0
- data/lib/nats/ext/json.rb +7 -0
- data/lib/nats/server.rb +313 -0
- data/lib/nats/server/const.rb +51 -0
- data/lib/nats/server/options.rb +96 -0
- data/lib/nats/server/sublist.rb +116 -0
- data/nats.gemspec +46 -0
- metadata +125 -0
data/lib/nats/ext/em.rb
ADDED
data/lib/nats/server.rb
ADDED
@@ -0,0 +1,313 @@
|
|
1
|
+
|
2
|
+
require File.dirname(__FILE__) + '/ext/em'
|
3
|
+
require File.dirname(__FILE__) + '/ext/bytesize'
|
4
|
+
require File.dirname(__FILE__) + '/ext/json'
|
5
|
+
require File.dirname(__FILE__) + '/server/sublist'
|
6
|
+
require File.dirname(__FILE__) + '/server/options'
|
7
|
+
require File.dirname(__FILE__) + '/server/const'
|
8
|
+
|
9
|
+
require 'socket'
|
10
|
+
require 'fileutils'
|
11
|
+
require 'pp'
|
12
|
+
|
13
|
+
|
14
|
+
module NATSD #:nodoc: all
|
15
|
+
|
16
|
+
# Subscriber
|
17
|
+
Subscriber = Struct.new(:conn, :subject, :sid)
|
18
|
+
|
19
|
+
class Server
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_reader :id, :info, :log_time, :auth_required, :debug_flag, :trace_flag
|
23
|
+
|
24
|
+
alias auth_required? :auth_required
|
25
|
+
alias debug_flag? :debug_flag
|
26
|
+
alias trace_flag? :trace_flag
|
27
|
+
|
28
|
+
def version; "nats server version #{NATSD::VERSION}" end
|
29
|
+
|
30
|
+
def host; @options[:addr] end
|
31
|
+
def port; @options[:port] end
|
32
|
+
def pid_file; @options[:pid_file] end
|
33
|
+
|
34
|
+
def setup(argv)
|
35
|
+
@options = {}
|
36
|
+
|
37
|
+
parser.parse!(argv)
|
38
|
+
read_config_file
|
39
|
+
finalize_options
|
40
|
+
|
41
|
+
@id, @cid = fast_uuid, 1
|
42
|
+
@sublist = Sublist.new
|
43
|
+
@info = {
|
44
|
+
:nats_server_id => Server.id,
|
45
|
+
:version => VERSION,
|
46
|
+
:auth_required => auth_required?,
|
47
|
+
:max_payload => MAX_PAYLOAD_SIZE
|
48
|
+
}
|
49
|
+
|
50
|
+
# Check for daemon flag
|
51
|
+
if @options[:daemonize]
|
52
|
+
require 'rubygems'
|
53
|
+
require 'daemons'
|
54
|
+
# These log messages visible to controlling TTY
|
55
|
+
log "Starting #{NATSD::APP_NAME} version #{NATSD::VERSION} on port #{NATSD::Server.port}"
|
56
|
+
log "Switching to daemon mode"
|
57
|
+
Daemons.daemonize(:app_name => APP_NAME, :mode => :exec)
|
58
|
+
end
|
59
|
+
|
60
|
+
setup_logs
|
61
|
+
|
62
|
+
# Setup optimized select versions
|
63
|
+
EM.epoll unless @options[:noepoll]
|
64
|
+
EM.kqueue unless @options[:nokqueue]
|
65
|
+
|
66
|
+
# Write pid file if need be.
|
67
|
+
File.open(@options[:pid_file], 'w') { |f| f.puts "#{Process.pid}" } if @options[:pid_file]
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
def subscribe(subscriber)
|
72
|
+
@sublist.insert(subscriber.subject, subscriber)
|
73
|
+
end
|
74
|
+
|
75
|
+
def unsubscribe(subscriber)
|
76
|
+
@sublist.remove(subscriber.subject, subscriber)
|
77
|
+
end
|
78
|
+
|
79
|
+
def route_to_subscribers(subject, reply, msg)
|
80
|
+
@sublist.match(subject).each do |subscriber|
|
81
|
+
# Skip anyone in the closing state
|
82
|
+
next if subscriber.conn.closing
|
83
|
+
|
84
|
+
trace "Matched subscriber", subscriber[:subject], subscriber[:sid], subscriber.conn.client_info
|
85
|
+
subscriber.conn.send_data("MSG #{subject} #{subscriber.sid} #{reply} #{msg.bytesize}#{CR_LF}")
|
86
|
+
subscriber.conn.send_data(msg)
|
87
|
+
subscriber.conn.send_data(CR_LF)
|
88
|
+
|
89
|
+
# Check the outbound queue here and react if need be..
|
90
|
+
if subscriber.conn.get_outbound_data_size > MAX_OUTBOUND_SIZE
|
91
|
+
subscriber.conn.error_close SLOW_CONSUMER
|
92
|
+
log "Slow consumer dropped", subscriber.conn.client_info
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def auth_ok?(user, pass)
|
98
|
+
user == @options[:user] && pass == @options[:pass]
|
99
|
+
end
|
100
|
+
|
101
|
+
def cid
|
102
|
+
@cid+=1
|
103
|
+
end
|
104
|
+
|
105
|
+
def info_string
|
106
|
+
@info.to_json
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
module Connection #:nodoc:
|
113
|
+
|
114
|
+
attr_reader :cid, :closing
|
115
|
+
|
116
|
+
def client_info
|
117
|
+
@client_info ||= Socket.unpack_sockaddr_in(get_peername)
|
118
|
+
end
|
119
|
+
|
120
|
+
def post_init
|
121
|
+
@cid = Server.cid
|
122
|
+
@subscriptions = {}
|
123
|
+
@verbose = @pedantic = true # suppressed by most clients, but allows friendly telnet
|
124
|
+
@receive_data_calls = 0
|
125
|
+
send_info
|
126
|
+
@auth_pending = EM.add_timer(AUTH_TIMEOUT) { connect_auth_timeout } if Server.auth_required?
|
127
|
+
debug "Client connection created", client_info, cid
|
128
|
+
end
|
129
|
+
|
130
|
+
def connect_auth_timeout
|
131
|
+
error_close AUTH_REQUIRED
|
132
|
+
debug "Connection timeout due to lack of auth credentials", cid
|
133
|
+
end
|
134
|
+
|
135
|
+
def receive_data(data)
|
136
|
+
@receive_data_calls += 1
|
137
|
+
(@buf ||= '') << data
|
138
|
+
close_connection and return if @buf =~ /(\006|\004)/ # ctrl+c or ctrl+d for telnet friendly
|
139
|
+
while (@buf && !@buf.empty? && !@closing)
|
140
|
+
# Waiting on msg payload
|
141
|
+
if @msg_size
|
142
|
+
if (@buf.bytesize >= (@msg_size + CR_LF_SIZE))
|
143
|
+
msg = @buf.slice(0, @msg_size)
|
144
|
+
process_msg(msg)
|
145
|
+
@buf = @buf.slice((msg.bytesize + CR_LF_SIZE), @buf.bytesize)
|
146
|
+
else # Waiting for additional msg data
|
147
|
+
return
|
148
|
+
end
|
149
|
+
# Waiting on control line
|
150
|
+
elsif @buf =~ /^(.*)\r\n/
|
151
|
+
@buf = $'
|
152
|
+
process_op($1)
|
153
|
+
else # Waiting for additional data for control line
|
154
|
+
# This is not normal. Close immediately
|
155
|
+
if @buf.bytesize > MAX_CONTROL_LINE_SIZE
|
156
|
+
debug "MAX_CONTROL_LINE exceeded, closing connection.."
|
157
|
+
@closing = true
|
158
|
+
close_connection
|
159
|
+
end
|
160
|
+
return
|
161
|
+
end
|
162
|
+
end
|
163
|
+
# Nothing should be here.
|
164
|
+
end
|
165
|
+
|
166
|
+
def process_op(op)
|
167
|
+
case op
|
168
|
+
when PUB_OP
|
169
|
+
ctrace 'PUB OP', op
|
170
|
+
return if @auth_pending
|
171
|
+
@pub_sub, @reply, @msg_size, = $1, $3, $4.to_i
|
172
|
+
send_data PAYLOAD_TOO_BIG and return if (@msg_size > MAX_PAYLOAD_SIZE)
|
173
|
+
send_data INVALID_SUBJECT and return if @pedantic && !(@pub_sub =~ SUB_NO_WC)
|
174
|
+
when SUB_OP
|
175
|
+
ctrace 'SUB OP', op
|
176
|
+
return if @auth_pending
|
177
|
+
sub, sid = $1, $2
|
178
|
+
send_data INVALID_SUBJECT and return if !($1 =~ SUB)
|
179
|
+
send_data INVALID_SID_TAKEN and return if @subscriptions[sid]
|
180
|
+
subscriber = Subscriber.new(self, sub, sid)
|
181
|
+
@subscriptions[sid] = subscriber
|
182
|
+
Server.subscribe(subscriber)
|
183
|
+
send_data OK if @verbose
|
184
|
+
when UNSUB_OP
|
185
|
+
ctrace 'UNSUB OP', op
|
186
|
+
return if @xsauth_pending
|
187
|
+
sid, subscriber = $1, @subscriptions[$1]
|
188
|
+
send_data INVALID_SID_NOEXIST and return unless subscriber
|
189
|
+
Server.unsubscribe(subscriber)
|
190
|
+
@subscriptions.delete(sid)
|
191
|
+
send_data OK if @verbose
|
192
|
+
when PING
|
193
|
+
ctrace 'PING OP', op
|
194
|
+
send_data PONG_RESPONSE
|
195
|
+
when CONNECT
|
196
|
+
ctrace 'CONNECT OP', op
|
197
|
+
begin
|
198
|
+
config = JSON.parse($1, :symbolize_keys => true)
|
199
|
+
process_connect_config(config)
|
200
|
+
rescue => e
|
201
|
+
send_data INVALID_CONFIG
|
202
|
+
log_error
|
203
|
+
end
|
204
|
+
when INFO
|
205
|
+
ctrace 'INFO OP', op
|
206
|
+
send_info
|
207
|
+
else
|
208
|
+
ctrace 'Unknown Op', op
|
209
|
+
send_data UNKNOWN_OP
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def send_info
|
214
|
+
send_data "INFO #{Server.info_string}#{CR_LF}"
|
215
|
+
end
|
216
|
+
|
217
|
+
def process_msg(body)
|
218
|
+
ctrace 'Processing msg', @pub_sub, @reply, body
|
219
|
+
send_data OK if @verbose
|
220
|
+
Server.route_to_subscribers(@pub_sub, @reply, body)
|
221
|
+
@pub_sub = @msg_size = @reply = nil
|
222
|
+
true
|
223
|
+
end
|
224
|
+
|
225
|
+
def process_connect_config(config)
|
226
|
+
@verbose = config[:verbose] if config[:verbose] != nil
|
227
|
+
@pedantic = config[:pedantic] if config[:pedantic] != nil
|
228
|
+
|
229
|
+
send_data OK and return unless Server.auth_required?
|
230
|
+
|
231
|
+
EM.cancel_timer(@auth_pending)
|
232
|
+
if Server.auth_ok?(config[:user], config[:pass])
|
233
|
+
send_data OK
|
234
|
+
@auth_pending = nil
|
235
|
+
else
|
236
|
+
error_close AUTH_FAILED
|
237
|
+
debug "Authorization failed for connection", cid
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def error_close(msg)
|
242
|
+
send_data msg
|
243
|
+
close_connection_after_writing
|
244
|
+
@closing = true
|
245
|
+
end
|
246
|
+
|
247
|
+
def unbind
|
248
|
+
debug "Client connection closed", client_info, cid
|
249
|
+
ctrace "Receive_Data called #{@receive_data_calls} times." if @receive_data_calls > 0
|
250
|
+
@subscriptions.each_value { |subscriber| Server.unsubscribe(subscriber) }
|
251
|
+
EM.cancel_timer(@auth_pending) if @auth_pending
|
252
|
+
@auth_pending = nil
|
253
|
+
end
|
254
|
+
|
255
|
+
def ctrace(*args)
|
256
|
+
trace(args, "c: #{cid}")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
def fast_uuid #:nodoc:
|
263
|
+
v = [rand(0x0010000),rand(0x0010000),rand(0x0010000),
|
264
|
+
rand(0x0010000),rand(0x0010000),rand(0x1000000)]
|
265
|
+
"%04x%04x%04x%04x%04x%06x" % v
|
266
|
+
end
|
267
|
+
|
268
|
+
def log(*args) #:nodoc:
|
269
|
+
args.unshift(Time.now) if NATSD::Server.log_time
|
270
|
+
pp args.compact
|
271
|
+
end
|
272
|
+
|
273
|
+
def debug(*args) #:nodoc:
|
274
|
+
log *args if NATSD::Server.debug_flag?
|
275
|
+
end
|
276
|
+
|
277
|
+
def trace(*args) #:nodoc:
|
278
|
+
log *args if NATSD::Server.trace_flag?
|
279
|
+
end
|
280
|
+
|
281
|
+
def log_error(e=$!) #:nodoc:
|
282
|
+
debug e, e.backtrace
|
283
|
+
end
|
284
|
+
|
285
|
+
def shutdown #:nodoc:
|
286
|
+
puts
|
287
|
+
log 'Server exiting..'
|
288
|
+
EM.stop
|
289
|
+
FileUtils.rm(NATSD::Server.pid_file) if NATSD::Server.pid_file
|
290
|
+
exit
|
291
|
+
end
|
292
|
+
|
293
|
+
['TERM','INT'].each { |s| trap(s) { shutdown } }
|
294
|
+
|
295
|
+
# Do setup
|
296
|
+
NATSD::Server.setup(ARGV.dup)
|
297
|
+
|
298
|
+
# Event Loop
|
299
|
+
|
300
|
+
EM.run {
|
301
|
+
|
302
|
+
log "Starting #{NATSD::APP_NAME} version #{NATSD::VERSION} on port #{NATSD::Server.port}"
|
303
|
+
|
304
|
+
begin
|
305
|
+
EM.set_descriptor_table_size(32768) # Requires Root privileges
|
306
|
+
EventMachine::start_server(NATSD::Server.host, NATSD::Server.port, NATSD::Connection)
|
307
|
+
rescue => e
|
308
|
+
log "Could not start server on port #{NATSD::Server.port}"
|
309
|
+
log_error
|
310
|
+
exit
|
311
|
+
end
|
312
|
+
|
313
|
+
}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module NATSD #:nodoc:
|
2
|
+
|
3
|
+
VERSION = '0.3.12'
|
4
|
+
APP_NAME = 'nats-server'
|
5
|
+
|
6
|
+
DEFAULT_PORT = 4222
|
7
|
+
|
8
|
+
# Ops
|
9
|
+
INFO = /^INFO$/i
|
10
|
+
PUB_OP = /^PUB\s+(\S+)\s+((\S+)\s+)?(\d+)$/i
|
11
|
+
SUB_OP = /^SUB\s+(\S+)\s+(\S+)$/i
|
12
|
+
UNSUB_OP = /^UNSUB\s+(\S+)$/i
|
13
|
+
PING = /^PING$/i
|
14
|
+
CONNECT = /^CONNECT\s+(.+)$/i
|
15
|
+
|
16
|
+
# 1k should be plenty since payloads sans connect are payload
|
17
|
+
MAX_CONTROL_LINE_SIZE = 1024
|
18
|
+
|
19
|
+
# Should be using something different if > 1MB payload
|
20
|
+
MAX_PAYLOAD_SIZE = (1024*1024)
|
21
|
+
|
22
|
+
# Maximum outbound size per client
|
23
|
+
MAX_OUTBOUND_SIZE = (10*1024*1024)
|
24
|
+
|
25
|
+
# RESPONSES
|
26
|
+
CR_LF = "\r\n".freeze
|
27
|
+
CR_LF_SIZE = CR_LF.bytesize
|
28
|
+
OK = "+OK #{CR_LF}".freeze
|
29
|
+
PONG_RESPONSE = "PONG#{CR_LF}".freeze
|
30
|
+
|
31
|
+
INFO_RESPONSE = "#{CR_LF}".freeze
|
32
|
+
|
33
|
+
# ERR responses
|
34
|
+
PAYLOAD_TOO_BIG = "-ERR 'Payload size exceeded, max is #{MAX_PAYLOAD_SIZE} bytes'#{CR_LF}".freeze
|
35
|
+
INVALID_SUBJECT = "-ERR 'Invalid Subject'#{CR_LF}".freeze
|
36
|
+
INVALID_SID_TAKEN = "-ERR 'Invalid Subject Identifier (sid), already taken'#{CR_LF}".freeze
|
37
|
+
INVALID_SID_NOEXIST = "-ERR 'Invalid Subject-Identifier (sid), no subscriber registered'#{CR_LF}".freeze
|
38
|
+
INVALID_CONFIG = "-ERR 'Invalid config, valid JSON required for connection configuration'#{CR_LF}".freeze
|
39
|
+
AUTH_REQUIRED = "-ERR 'Authorization is required'#{CR_LF}".freeze
|
40
|
+
AUTH_FAILED = "-ERR 'Authorization failed'#{CR_LF}".freeze
|
41
|
+
UNKNOWN_OP = "-ERR 'Unkown Protocol Operation'#{CR_LF}".freeze
|
42
|
+
SLOW_CONSUMER = "-ERR 'Slow consumer detected, connection dropped'#{CR_LF}".freeze
|
43
|
+
|
44
|
+
# Pedantic Mode
|
45
|
+
SUB = /^([^\.\*>\s]+|>$|\*)(\.([^\.\*>\s]+|>$|\*))*$/
|
46
|
+
SUB_NO_WC = /^([^\.\*>\s]+)(\.([^\.\*>\s]+))*$/
|
47
|
+
|
48
|
+
# Autorization wait time
|
49
|
+
AUTH_TIMEOUT = 5
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
|
2
|
+
require 'optparse'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module NATSD
|
6
|
+
class Server
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def parser
|
10
|
+
@parser ||= OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: nats [options]"
|
12
|
+
|
13
|
+
opts.separator ""
|
14
|
+
opts.separator "Server options:"
|
15
|
+
|
16
|
+
opts.on("-a", "--addr HOST", "Bind to HOST address " +
|
17
|
+
"(default: #{@options[:addr]})") { |host| @options[:address] = host }
|
18
|
+
opts.on("-p", "--port PORT", "Use PORT (default: #{@options[:port]})") { |port| @options[:port] = port.to_i }
|
19
|
+
|
20
|
+
opts.on("-d", "--daemonize", "Run daemonized in the background") { @options[:daemonize] = true }
|
21
|
+
opts.on("-l", "--log FILE", "File to redirect output " +
|
22
|
+
"(default: #{@options[:log_file]})") { |file| @options[:log_file] = file }
|
23
|
+
opts.on("-T", "--logtime", "Timestamp log entries") { @options[:log_time] = true }
|
24
|
+
|
25
|
+
opts.on("-P", "--pid FILE", "File to store PID " +
|
26
|
+
"(default: #{@options[:pid_file]})") { |file| @options[:pid_file] = file }
|
27
|
+
|
28
|
+
opts.on("-C", "--config FILE", "Configuration File " +
|
29
|
+
"(default: #{@options[:config_file]})") { |file| @options[:config_file] = file }
|
30
|
+
|
31
|
+
opts.separator ""
|
32
|
+
opts.separator "Authorization options: (Should be done in config file for production)"
|
33
|
+
|
34
|
+
opts.on("--user user", "User required for connections") { |user| @options[:user] = user }
|
35
|
+
|
36
|
+
opts.on("--pass password", "Password required for connections") { |pass| @options[:pass] = pass }
|
37
|
+
opts.on("--password password", "Password required for connections") { |pass| @options[:pass] = pass }
|
38
|
+
|
39
|
+
opts.on("--no_epoll", "Enable epoll (Linux)") { @options[:noepoll] = true }
|
40
|
+
opts.on("--kqueue", "Enable kqueue (MacOSX and BSD)") { @options[:nokqueue] = true }
|
41
|
+
|
42
|
+
opts.separator ""
|
43
|
+
opts.separator "Common options:"
|
44
|
+
|
45
|
+
opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
|
46
|
+
opts.on_tail('-v', '--version', "Show version") { puts NATSD::Server.version; exit }
|
47
|
+
opts.on_tail("-D", "--debug", "Set debugging on") { @options[:debug] = true }
|
48
|
+
opts.on_tail("-V", "--trace", "Set tracing on of raw protocol") { @options[:trace] = true }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_config_file
|
53
|
+
return unless config_file = @options[:config_file]
|
54
|
+
config = File.open(config_file) { |f| YAML.load(f) }
|
55
|
+
# Command lines args, parsed first, will override these.
|
56
|
+
[:addr, :port, :log_file, :pid_file, :user, :pass, :log_time, :debug].each do |p|
|
57
|
+
c = config[p.to_s]
|
58
|
+
@options[p] = c if c and not @options[p]
|
59
|
+
end
|
60
|
+
rescue => e
|
61
|
+
log "Could not read configuration file: #{e}"
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
|
65
|
+
def setup_logs
|
66
|
+
return unless @options[:log_file]
|
67
|
+
$stdout.reopen(@options[:log_file], "w")
|
68
|
+
$stdout.sync = true
|
69
|
+
$stderr.reopen($stdout)
|
70
|
+
end
|
71
|
+
|
72
|
+
def finalize_options
|
73
|
+
# Addr/Port
|
74
|
+
@options[:port] ||= DEFAULT_PORT
|
75
|
+
@options[:addr] ||= '0.0.0.0'
|
76
|
+
|
77
|
+
# Debug and Tracing
|
78
|
+
@debug_flag = @options[:debug]
|
79
|
+
@trace_flag = @options[:trace]
|
80
|
+
|
81
|
+
# Log timestamps
|
82
|
+
@log_time = @options[:log_time]
|
83
|
+
# setup_logs
|
84
|
+
|
85
|
+
debug @options # Block pass?
|
86
|
+
debug "DEBUG is on"
|
87
|
+
trace "TRACE is on"
|
88
|
+
|
89
|
+
# Auth
|
90
|
+
@auth_required = (@options[:user] != nil)
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|