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.
@@ -0,0 +1,5 @@
1
+ if RUBY_VERSION <= "1.8.6"
2
+ class String #:nodoc:
3
+ def bytesize; self.size; end
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ begin
2
+ require 'eventmachine'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ require 'eventmachine'
6
+ end
@@ -0,0 +1,7 @@
1
+ begin
2
+ require 'yajl'
3
+ require 'yajl/json_gem'
4
+ rescue LoadError
5
+ require 'rubygems'
6
+ require 'json'
7
+ end
@@ -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