M4D-juggernaut 0.5.9

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/Manifest.txt ADDED
@@ -0,0 +1,11 @@
1
+ Manifest.txt
2
+ README.txt
3
+ Rakefile
4
+ bin/juggernaut
5
+ lib/juggernaut.rb
6
+ lib/juggernaut/client.rb
7
+ lib/juggernaut/message.rb
8
+ lib/juggernaut/miscel.rb
9
+ lib/juggernaut/runner.rb
10
+ lib/juggernaut/server.rb
11
+ lib/juggernaut/utils.rb
data/README.txt ADDED
@@ -0,0 +1,32 @@
1
+ Juggernaut
2
+ by Alex MacCaw http://www.eribium.org
3
+ minor fixes and unit tests by Ripta Pasay
4
+
5
+ == DESCRIPTION:
6
+ See Plugin README:
7
+ http://juggernaut.rubyforge.org/svn/trunk/juggernaut/README
8
+
9
+ == LICENSE:
10
+
11
+ (The MIT License)
12
+
13
+ Copyright (c) 2008 Alex MacCaw
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining
16
+ a copy of this software and associated documentation files (the
17
+ 'Software'), to deal in the Software without restriction, including
18
+ without limitation the rights to use, copy, modify, merge, publish,
19
+ distribute, sublicense, and/or sell copies of the Software, and to
20
+ permit persons to whom the Software is furnished to do so, subject to
21
+ the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be
24
+ included in all copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
27
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
29
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
30
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
31
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
32
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/juggernaut.rb'
6
+
7
+ Hoe.new('juggernaut', Juggernaut::VERSION) do |p|
8
+ p.rubyforge_name = 'juggernaut'
9
+ p.author = 'Alex MacCaw'
10
+ p.email = 'info@eribium.org'
11
+ p.url = 'http://juggernaut.rubyforge.org'
12
+ p.extra_deps << ['eventmachine', '>=0.10.0']
13
+ p.extra_deps << ['json', '>=1.1.2']
14
+ end
15
+
16
+ # vim: syntax=Ruby
data/bin/juggernaut ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'juggernaut')
4
+ Juggernaut::Runner.run
data/lib/juggernaut.rb ADDED
@@ -0,0 +1,158 @@
1
+ require 'rubygems'
2
+ require 'logger'
3
+ require 'eventmachine'
4
+ require 'json'
5
+ $:.unshift(File.dirname(__FILE__))
6
+
7
+ module Juggernaut
8
+ VERSION = '0.5.8'
9
+
10
+ class JuggernautError < StandardError #:nodoc:
11
+ end
12
+
13
+ @@options = {}
14
+
15
+ DEFAULT_CONFIG_FILE = <<-EOF
16
+ # ======================
17
+ # Juggernaut Options
18
+ # ======================
19
+
20
+ # === Subscription authentication ===
21
+ # Leave all subscription options uncommented to allow anyone to subscribe.
22
+
23
+ # If specified, subscription_url is called everytime a client subscribes.
24
+ # Parameters passed are: session_id, client_id and an array of channels.
25
+ #
26
+ # The server should check that the session_id matches up to the client_id
27
+ # and that the client is allowed to access the specified channels.
28
+ #
29
+ # If a status code other than 200 is encountered, the subscription_request fails
30
+ # and the client is disconnected.
31
+ #
32
+ # :subscription_url: http://localhost:3000/sessions/juggernaut_subscription
33
+
34
+ # === Broadcast and query authentication ===
35
+ # Leave all broadcast/query options uncommented to allow anyone to broadcast/query.
36
+ #
37
+ # Broadcast authentication in a production environment is very importantant since broadcasters
38
+ # can execute JavaScript on subscribed clients, leaving you vulnerable to cross site scripting
39
+ # attacks if broadcasters aren't authenticated.
40
+
41
+ # 1) Via IP address
42
+ #
43
+ # If specified, if a client has an ip that is specified in allowed_ips, than it is automatically
44
+ # authenticated, even if a secret_key isn't provided.
45
+ #
46
+ # This is the recommended method for broadcast authentication.
47
+ #
48
+ :allowed_ips:
49
+ - 127.0.0.1
50
+ # - 192.168.0.1
51
+
52
+ # 2) Via HTTP request
53
+ #
54
+ # If specified, if a client attempts a broadcast/query, without a secret_key or using an IP
55
+ # no included in allowed_ips, then broadcast_query_login_url will be called.
56
+ # Parameters passed, if given, are: session_id, client_id, channels and type.
57
+ #
58
+ # The server should check that the session_id matches up to the client id, and the client
59
+ # is allowed to perform that particular type of broadcast/query.
60
+ #
61
+ # If a status code other than 200 is encountered, the broadcast_query_login_url fails
62
+ # and the client is disconnected.
63
+ #
64
+ # :broadcast_query_login_url: http://localhost:3000/sessions/juggernaut_broadcast
65
+
66
+ # 3) Via shared secret key
67
+ #
68
+ # This secret key must be sent with any query/broadcast commands.
69
+ # It must be the same as the one in the Rails config file.
70
+ #
71
+ # You shouldn't authenticate broadcasts from subscribed clients using this method
72
+ # since the secret_key will be easily visible in the page (and not so secret any more)!
73
+ #
74
+ # :secret_key: your_secret_key_here
75
+
76
+ # == Subscription Logout ==
77
+
78
+ # If specified, logout_connection_url is called everytime a specific connection from a subscribed client disconnects.
79
+ # Parameters passed are session_id, client_id and an array of channels specific to that connection.
80
+ #
81
+ # :logout_connection_url: http://localhost:3000/sessions/juggernaut_connection_logout
82
+
83
+ # Logout url is called when all connections from a subscribed client are closed.
84
+ # Parameters passed are session_id and client_id.
85
+ #
86
+ # :logout_url: http://localhost:3000/sessions/juggernaut_logout
87
+
88
+ # === Miscellaneous ===
89
+
90
+ # timeout defaults to 10. A timeout is the time between when a client closes a connection
91
+ # and a logout_request or logout_connection_request is made. The reason for this is that a client
92
+ # may only temporarily be disconnected, and may attempt a reconnect very soon.
93
+ #
94
+ # :timeout: 10
95
+
96
+ # store_messages defaults to false. If this option is true, messages send to connections will be stored.
97
+ # This is useful since a client can then receive broadcasted message that it has missed (perhaps it was disconnected).
98
+ #
99
+ # :store_messages: false
100
+
101
+ # === Server ===
102
+
103
+ # Host defaults to "0.0.0.0". You shouldn't need to change this.
104
+ # :host: 0.0.0.0
105
+
106
+ # Port is mandatory
107
+ :port: 5001
108
+
109
+ # Defaults to value of :port. If you are doing port forwarding you'll need to configure this to the same
110
+ # value as :public_port in the juggernaut_hosts.yml file
111
+ # :public_port: 5001
112
+
113
+ EOF
114
+
115
+ class << self
116
+ def options
117
+ @@options
118
+ end
119
+
120
+ def options=(val)
121
+ @@options = val
122
+ end
123
+
124
+ def logger
125
+ return @@logger if defined?(@@logger) && !@@logger.nil?
126
+ FileUtils.mkdir_p(File.dirname(log_path))
127
+ @@logger = Logger.new(log_path)
128
+ @@logger.level = Logger::INFO if options[:debug] == false
129
+ @@logger
130
+ rescue
131
+ @@logger = Logger.new(STDOUT)
132
+ end
133
+
134
+ def logger=(logger)
135
+ @@logger = logger
136
+ end
137
+
138
+ def log_path
139
+ options[:log_path] || File.join(%w( / var run juggernaut.log ))
140
+ end
141
+
142
+ def pid_path
143
+ options[:pid_path] || File.join(%w( / var run ), "juggernaut.#{options[:port]}.pid" )
144
+ end
145
+
146
+ def config_path
147
+ options[:config_path] || File.join(%w( / var run juggernaut.yml ))
148
+ end
149
+
150
+ end
151
+ end
152
+
153
+ require 'juggernaut/utils'
154
+ require 'juggernaut/miscel'
155
+ require 'juggernaut/message'
156
+ require 'juggernaut/client'
157
+ require 'juggernaut/server'
158
+ require 'juggernaut/runner'
@@ -0,0 +1,270 @@
1
+ require 'timeout'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'uri'
5
+
6
+ module Juggernaut
7
+ class Client
8
+ include Juggernaut::Miscel
9
+
10
+ @@clients = [ ]
11
+
12
+ attr_reader :id
13
+ attr_accessor :session_id
14
+ attr_reader :connections
15
+
16
+ class << self
17
+ # Actually does a find_or_create_by_id
18
+ def find_or_create(subscriber, request)
19
+ if client = find_by_id(request[:client_id])
20
+ client.session_id = request[:session_id]
21
+ client.add_new_connection(subscriber)
22
+ client
23
+ else
24
+ self.new(subscriber, request)
25
+ end
26
+ end
27
+
28
+ # Client find methods
29
+ def find_all
30
+ @@clients
31
+ end
32
+
33
+ def find(&block)
34
+ @@clients.select(&block).uniq
35
+ end
36
+
37
+ def find_by_id(id)
38
+ find { |client| client.id == id }.first
39
+ end
40
+
41
+ def find_by_signature(signature)
42
+ # signature should be unique
43
+ find do |client|
44
+ client.connections.select { |connection| connection.signature == signature }.any?
45
+ end.first
46
+ end
47
+
48
+ def find_by_channels(channels)
49
+ find do |client|
50
+ client.has_channels?(channels)
51
+ end
52
+ end
53
+
54
+ def find_by_id_and_channels(id, channels)
55
+ find do |client|
56
+ client.has_channels?(channels) && client.id == id
57
+ end.first
58
+ end
59
+
60
+ def send_logouts_after_timeout
61
+ @@clients.each do |client|
62
+ if client.give_up?
63
+ client.logout_request
64
+ end
65
+ end
66
+ end
67
+
68
+ # Called when the server is shutting down
69
+ def send_logouts_to_all_clients
70
+ @@clients.each do |client|
71
+ client.logout_request
72
+ end
73
+ end
74
+
75
+ def reset!
76
+ @@clients.clear
77
+ end
78
+
79
+ def register_client(client)
80
+ @@clients << client unless @@clients.include?(client)
81
+ end
82
+
83
+ def client_registered?(client)
84
+ @@clients.include?(client)
85
+ end
86
+
87
+ def unregister_client(client)
88
+ @@clients.delete(client)
89
+ end
90
+ end
91
+
92
+ def initialize(subscriber, request)
93
+ @connections = []
94
+ @id = request[:client_id]
95
+ @session_id = request[:session_id]
96
+ @messages = []
97
+ @logout_timeout = 0
98
+ self.register
99
+ add_new_connection(subscriber)
100
+ end
101
+
102
+ def to_json
103
+ {
104
+ :client_id => @id,
105
+ :num_connections => @connections.size,
106
+ :session_id => @session_id,
107
+ :channels => self.channels
108
+ }.to_json
109
+ end
110
+
111
+ def add_new_connection(subscriber)
112
+ @connections << subscriber
113
+ end
114
+
115
+ def friendly_id
116
+ if self.id
117
+ "with ID #{self.id}"
118
+ else
119
+ "session #{self.session_id}"
120
+ end
121
+ end
122
+
123
+ def subscription_request(channels)
124
+ return true unless options[:subscription_url]
125
+ post_request(options[:subscription_url], channels, :timeout => options[:post_request_timeout] || 5)
126
+ end
127
+
128
+ def logout_connection_request(channels)
129
+ return true unless options[:logout_connection_url]
130
+ post_request(options[:logout_connection_url], channels, :timeout => options[:post_request_timeout] || 5)
131
+ end
132
+
133
+ def logout_request
134
+ self.unregister
135
+ logger.debug("Timed out client #{friendly_id}")
136
+ return true unless options[:logout_url]
137
+ post_request(options[:logout_url], [ ], :timeout => options[:post_request_timeout] || 5)
138
+ end
139
+
140
+ def remove_connection(connection)
141
+ @connections.delete(connection)
142
+ self.reset_logout_timeout!
143
+ self.logout_request if self.give_up?
144
+ end
145
+
146
+ def send_message(msg, channels = nil)
147
+ store_message(msg, channels) if options[:store_messages]
148
+ send_message_to_connections(msg, channels)
149
+ end
150
+
151
+ # Send messages that are queued up for this particular client.
152
+ # Messages are only queued for previously-connected clients.
153
+ def send_queued_messages(connection)
154
+ self.expire_queued_messages!
155
+
156
+ # Weird looping because we don't want to send messages that get
157
+ # added to the array after we start iterating (since those will
158
+ # get sent to the client anyway).
159
+ @length = @messages.length
160
+
161
+ logger.debug("Sending #{@length} queued message(s) to client #{friendly_id}")
162
+
163
+ @length.times do |id|
164
+ message = @messages[id]
165
+ send_message_to_connection(connection, message[:message], message[:channels])
166
+ end
167
+ end
168
+
169
+ def channels
170
+ @connections.collect { |em| em.channels }.flatten.uniq
171
+ end
172
+
173
+ def has_channels?(channels)
174
+ @connections.each do |em|
175
+ return true if em.has_channels?(channels)
176
+ end
177
+ false
178
+ end
179
+
180
+ def add_channels(channels)
181
+ @connections.each do |em|
182
+ em.add_channels(channels)
183
+ end
184
+ end
185
+
186
+ def remove_channels!(channels)
187
+ @connections.each do |em|
188
+ em.remove_channels!(channels)
189
+ end
190
+ end
191
+
192
+ def alive?
193
+ @connections.select { |em| em.alive? }.any?
194
+ end
195
+
196
+ # This client is only dead if there are no connections and we are
197
+ # past the timeout (if we are within the timeout, the user could
198
+ # just be doing a page reload or going to a new page)
199
+ def give_up?
200
+ !alive? and (Time.now > @logout_timeout)
201
+ end
202
+
203
+ protected
204
+
205
+ def register
206
+ self.class.register_client(self)
207
+ end
208
+
209
+ def registered?
210
+ self.class.client_registered?(self)
211
+ end
212
+
213
+ def unregister
214
+ self.class.unregister_client(self)
215
+ end
216
+
217
+ def post_request(url, channels = [ ], options = { })
218
+ uri = URI.parse(url)
219
+ uri.path = '/' if uri.path == ''
220
+ params = []
221
+ params << "client_id=#{id}" if id
222
+ params << "session_id=#{session_id}" if session_id
223
+ channels.each {|chan| params << "channels[]=#{chan}" }
224
+ headers = {"User-Agent" => "Ruby/#{RUBY_VERSION}"}
225
+ begin
226
+ http = Net::HTTP.new(uri.host, uri.port)
227
+ if uri.scheme == 'https'
228
+ http.use_ssl = true
229
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
230
+ end
231
+ http.read_timeout = options[:timeout] || 5
232
+ resp, data = http.post(uri.path, params.join('&'), headers)
233
+ return resp.is_a?(Net::HTTPOK)
234
+ rescue => e
235
+ logger.error("Bad request #{url.to_s} (#{e.class}: #{e.message})")
236
+ return false
237
+ rescue Timeout::Error
238
+ logger.error("#{url.to_s} timeout")
239
+ return false
240
+ end
241
+ end
242
+
243
+ def send_message_to_connections(msg, channels)
244
+ @connections.each do |connection|
245
+ send_message_to_connection(connection, msg, channels)
246
+ end
247
+ end
248
+
249
+ def send_message_to_connection(connection, msg, channels)
250
+ connection.broadcast(msg) if !channels or channels.empty? or connection.has_channels?(channels)
251
+ end
252
+
253
+ # Queued messages are stored until a timeout is reached which is the
254
+ # same as the connection timeout. This takes care of messages that
255
+ # come in between page loads or ones that come in right when you are
256
+ # clicking off one page and loading the next one.
257
+ def store_message(msg, channels)
258
+ self.expire_queued_messages!
259
+ @messages << { :channels => channels, :message => msg, :timeout => Time.now + options[:timeout] }
260
+ end
261
+
262
+ def expire_queued_messages!
263
+ @messages.reject! { |message| Time.now > message[:timeout] }
264
+ end
265
+
266
+ def reset_logout_timeout!
267
+ @logout_timeout = Time.now + options[:timeout]
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,19 @@
1
+ module Juggernaut
2
+ class Message
3
+ attr_accessor :id
4
+ attr_accessor :signature
5
+ attr_accessor :body
6
+ attr_reader :created_at
7
+
8
+ def initialize(id, body, signature)
9
+ @id = id
10
+ @body = body
11
+ @signature = signature
12
+ @created_at = Time.now
13
+ end
14
+
15
+ def to_s
16
+ { :id => @id.to_s, :body => @body, :signature => @signature }.to_json
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ module Juggernaut
2
+ module Miscel
3
+ def options
4
+ Juggernaut.options
5
+ end
6
+
7
+ def options=(ob)
8
+ Juggernaut.options = ob
9
+ end
10
+
11
+ def log_path
12
+ Juggernaut.log_path
13
+ end
14
+
15
+ def pid_path
16
+ Juggernaut.pid_path
17
+ end
18
+
19
+ def config_path
20
+ Juggernaut.config_path
21
+ end
22
+
23
+ def logger
24
+ Juggernaut.logger
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,224 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+ require 'erb'
4
+ require 'resolv'
5
+
6
+ module Juggernaut
7
+ class Runner
8
+ include Juggernaut::Miscel
9
+
10
+ class << self
11
+ def run(argv = ARGV)
12
+ self.new(argv)
13
+ end
14
+ end
15
+
16
+ def initialize(argv = ARGV)
17
+ self.options = {
18
+ :host => "0.0.0.0",
19
+ :port => 5001,
20
+ :debug => false,
21
+ :cleanup_timer => 2,
22
+ :timeout => 10,
23
+ :store_messages => false
24
+ }
25
+
26
+ self.options.merge!({
27
+ :pid_path => pid_path,
28
+ :log_path => log_path,
29
+ :config_path => config_path
30
+ })
31
+
32
+ parse_options(argv)
33
+
34
+ if !File.exists?(config_path)
35
+ puts "You must generate a config file (juggernaut -g filename.yml)"
36
+ exit
37
+ end
38
+
39
+ options.merge!(YAML::load(ERB.new(IO.read(config_path)).result))
40
+
41
+ if options.include?(:kill)
42
+ kill_pid(options[:kill] || '*')
43
+ end
44
+
45
+ if options[:allowed_ips]
46
+ options[:allowed_ips] = options[:allowed_ips].collect { |ip| Resolv.getaddress(ip) }
47
+ end
48
+
49
+ Process.euid = options[:user] if options[:user]
50
+ Process.egid = options[:group] if options[:group]
51
+
52
+ if !options[:daemonize]
53
+ start
54
+ else
55
+ daemonize
56
+ end
57
+ end
58
+
59
+ def start
60
+ puts "Starting Juggernaut server #{Juggernaut::VERSION} on port: #{options[:port]}..."
61
+
62
+ trap("INT") {
63
+ stop
64
+ exit
65
+ }
66
+ trap("TERM"){
67
+ stop
68
+ exit
69
+ }
70
+
71
+ if options[:descriptor_table_size]
72
+ EM.epoll
73
+ new_size = EM.set_descriptor_table_size( options[:descriptor_table_size] )
74
+ logger.debug "New descriptor-table size is #{new_size}"
75
+ end
76
+
77
+ EventMachine::run {
78
+ EventMachine::add_periodic_timer( options[:cleanup_timer] || 2 ) { Juggernaut::Client.send_logouts_after_timeout }
79
+ EventMachine::start_server(options[:host], options[:port].to_i, Juggernaut::Server)
80
+ EM.set_effective_user( options[:user] ) if options[:user]
81
+ }
82
+ end
83
+
84
+ def stop
85
+ puts "Stopping Juggernaut server"
86
+ Juggernaut::Client.send_logouts_to_all_clients
87
+ EventMachine::stop
88
+ end
89
+
90
+ def parse_options(argv)
91
+ OptionParser.new do |opts|
92
+ opts.summary_width = 25
93
+ opts.banner = "Juggernaut (#{VERSION})\n\n",
94
+ "Usage: juggernaut [-h host] [-p port] [-P file]\n",
95
+ " [-d] [-k port] [-l file] [-e]\n",
96
+ " juggernaut --help\n",
97
+ " juggernaut --version\n"
98
+
99
+ opts.separator ""
100
+ opts.separator ""; opts.separator "Configuration:"
101
+
102
+ opts.on("-g", "--generate FILE", String, "Generate config file", "(default: #{options[:config_path]})") do |v|
103
+ options[:config_path] = File.expand_path(v) if v
104
+ generate_config_file
105
+ end
106
+
107
+ opts.on("-c", "--config FILE", String, "Path to configuration file.", "(default: #{options[:config_path]})") do |v|
108
+ options[:config_path] = File.expand_path(v)
109
+ end
110
+
111
+ opts.separator ""; opts.separator "Network:"
112
+
113
+ opts.on("-h", "--host HOST", String, "Specify host", "(default: #{options[:host]})") do |v|
114
+ options[:host] = v
115
+ end
116
+
117
+ opts.on("-p", "--port PORT", Integer, "Specify port", "(default: #{options[:port]})") do |v|
118
+ options[:port] = v
119
+ end
120
+
121
+ opts.on("-s", "--fdsize SIZE", Integer, "Set the file descriptor size an user epoll() on Linux", "(default: use select() which is limited to 1024 clients)") do |v|
122
+ options[:descriptor_table_size] = v
123
+ end
124
+
125
+ opts.separator ""; opts.separator "Daemonization:"
126
+
127
+ opts.on("-P", "--pid FILE", String, "save PID in FILE when using -d option.", "(default: #{options[:pid_path]})") do |v|
128
+ options[:pid_path] = File.expand_path(v)
129
+ end
130
+
131
+ opts.on("-d", "--daemon", "Daemonize mode") do |v|
132
+ options[:daemonize] = v
133
+ end
134
+
135
+ opts.on("-k", "--kill PORT", String, :OPTIONAL, "Kill specified running daemons - leave blank to kill all.") do |v|
136
+ options[:kill] = v
137
+ end
138
+
139
+ opts.separator ""; opts.separator "Logging:"
140
+
141
+ opts.on("-l", "--log [FILE]", String, "Path to print debugging information.", "(default: #{options[:log_path]})") do |v|
142
+ options[:log_path] = File.expand_path(v)
143
+ end
144
+
145
+ opts.on("-e", "--debug", "Run in debug mode", "(default: #{options[:debug]})") do |v|
146
+ options[:debug] = v
147
+ end
148
+
149
+ opts.separator ""; opts.separator "Permissions:"
150
+
151
+ opts.on("-u", "--user USER", Integer, "User to run as") do |user|
152
+ options[:user] = user
153
+ end
154
+
155
+ opts.on("-G", "--group GROUP", String, "Group to run as") do |group|
156
+ options[:group] = group
157
+ end
158
+
159
+ opts.separator ""; opts.separator "Miscellaneous:"
160
+
161
+ opts.on_tail("-?", "--help", "Display this usage information.") do
162
+ puts "#{opts}\n"
163
+ exit
164
+ end
165
+
166
+ opts.on_tail("-v", "--version", "Display version") do |v|
167
+ puts "Juggernaut #{VERSION}"
168
+ exit
169
+ end
170
+ end.parse!(argv)
171
+ options
172
+ end
173
+
174
+ private
175
+
176
+ def generate_config_file
177
+ if File.exists?(config_path)
178
+ puts "Config file already exists. You must remove it before generating a new one."
179
+ exit
180
+ end
181
+ puts "Generating config file...."
182
+ File.open(config_path, 'w+') do |file|
183
+ file.write DEFAULT_CONFIG_FILE.gsub('your_secret_key_here', Digest::SHA1.hexdigest("--#{Time.now.to_s.split(//).sort_by {rand}.join}--"))
184
+ end
185
+ puts "Config file generated at #{config_path}"
186
+ exit
187
+ end
188
+
189
+ def store_pid(pid)
190
+ FileUtils.mkdir_p(File.dirname(pid_path))
191
+ File.open(pid_path, 'w'){|f| f.write("#{pid}\n")}
192
+ end
193
+
194
+ def kill_pid(k)
195
+ Dir[options[:pid_path]||File.join(File.dirname(pid_dir), "juggernaut.#{k}.pid")].each do |f|
196
+ begin
197
+ puts f
198
+ pid = IO.read(f).chomp.to_i
199
+ FileUtils.rm f
200
+ Process.kill(9, pid)
201
+ puts "killed PID: #{pid}"
202
+ rescue => e
203
+ puts "Failed to kill! #{k}: #{e}"
204
+ end
205
+ end
206
+ exit
207
+ end
208
+
209
+ def daemonize
210
+ fork do
211
+ Process.setsid
212
+ exit if fork
213
+ store_pid(Process.pid)
214
+ # Dir.chdir "/" # Mucks up logs
215
+ File.umask 0000
216
+ STDIN.reopen "/dev/null"
217
+ STDOUT.reopen "/dev/null", "a"
218
+ STDERR.reopen STDOUT
219
+ start
220
+ end
221
+ end
222
+
223
+ end
224
+ end
@@ -0,0 +1,374 @@
1
+ require 'eventmachine'
2
+ require 'socket'
3
+ require 'json'
4
+ require 'open-uri'
5
+ require 'fileutils'
6
+ require 'digest/sha1'
7
+
8
+ module Juggernaut
9
+ class Server < EventMachine::Connection
10
+ include Juggernaut::Miscel
11
+
12
+ class InvalidRequest < Juggernaut::JuggernautError #:nodoc:
13
+ end
14
+
15
+ class InvalidCommand < Juggernaut::JuggernautError #:nodoc:
16
+ end
17
+
18
+ class CorruptJSON < Juggernaut::JuggernautError #:nodoc:
19
+ end
20
+
21
+ class MalformedBroadcast < Juggernaut::JuggernautError #:nodoc:
22
+ end
23
+
24
+ class MalformedSubscribe < Juggernaut::JuggernautError #:nodoc:
25
+ end
26
+
27
+ class UnauthorisedSubscription < Juggernaut::JuggernautError #:nodoc:
28
+ end
29
+
30
+ class MalformedQuery < Juggernaut::JuggernautError #:nodoc:
31
+ end
32
+
33
+ class UnauthorisedBroadcast < Juggernaut::JuggernautError #:nodoc:
34
+ end
35
+
36
+ class UnauthorisedQuery < Juggernaut::JuggernautError #:nodoc:
37
+ end
38
+
39
+ POLICY_FILE = <<-EOF
40
+ <cross-domain-policy>
41
+ <allow-access-from domain="*" to-ports="PORT" />
42
+ </cross-domain-policy>
43
+ EOF
44
+
45
+ POLICY_REQUEST = "<policy-file-request/>"
46
+
47
+ CR = "\0"
48
+
49
+ attr_reader :current_msg_id
50
+ attr_reader :messages
51
+ attr_reader :connected
52
+ attr_reader :logout_timeout
53
+ attr_reader :status
54
+ attr_reader :channels
55
+ attr_reader :client
56
+
57
+ # EM methods
58
+
59
+ def post_init
60
+ logger.debug "New client [#{client_ip}]"
61
+ @client = nil
62
+ @channels = []
63
+ @current_msg_id = 0
64
+ @connected = true
65
+ @logout_timeout = nil
66
+ @buffer = ''
67
+ end
68
+
69
+ # Juggernaut packets are terminated with "\0"
70
+ # so we need to buffer the data until we find the
71
+ # terminating "\0"
72
+ def receive_data(data)
73
+ @buffer << data
74
+ @buffer = process_whole_messages(@buffer)
75
+ end
76
+
77
+ # process any whole messages in the buffer,
78
+ # and return the new contents of the buffer
79
+ def process_whole_messages(data)
80
+ return data if data !~ /\0/ # only process if data contains a \0 char
81
+ messages = data.split("\0")
82
+ if data =~ /\0$/
83
+ data = ''
84
+ else
85
+ # remove the last message from the list (because it is incomplete) before processing
86
+ data = messages.pop
87
+ end
88
+ messages.each {|message| process_message(message.strip)}
89
+ return data
90
+ end
91
+
92
+ def process_message(ln)
93
+ logger.debug "Processing message: #{ln}"
94
+ @request = nil
95
+
96
+ if ln == POLICY_REQUEST
97
+ logger.debug "Sending crossdomain file"
98
+ send_data POLICY_FILE.gsub('PORT', (options[:public_port]||options[:port]).to_s)
99
+ close_connection_after_writing
100
+ return
101
+ end
102
+
103
+ begin
104
+ @request = JSON.parse(ln) unless ln.empty?
105
+ rescue
106
+ raise CorruptJSON, ln
107
+ end
108
+
109
+ raise InvalidRequest, ln if !@request
110
+
111
+ @request.symbolize_keys!
112
+
113
+ # For debugging
114
+ @request[:ip] = client_ip
115
+
116
+ @request[:channels] = (@request[:channels] || []).compact.select {|c| !!c && c != '' }.uniq
117
+
118
+ if @request[:client_ids]
119
+ @request[:client_ids] = @request[:client_ids].to_a.compact.select {|c| !!c && c != '' }.uniq
120
+ end
121
+
122
+ case @request[:command].to_sym
123
+ when :broadcast
124
+ broadcast_command
125
+ when :subscribe
126
+ subscribe_command
127
+ when :query
128
+ query_command
129
+ when :noop
130
+ noop_command
131
+ else
132
+ raise InvalidCommand, @request
133
+ end
134
+
135
+ rescue JuggernautError => e
136
+ logger.error("#{e} - #{e.message.inspect}")
137
+ close_connection
138
+ # So as to stop em quitting
139
+ rescue => e
140
+ logger ? logger.error(e) : puts(e)
141
+ end
142
+
143
+ def unbind
144
+ if @client
145
+ # todo - should be called after timeout?
146
+ @client.logout_connection_request(@channels)
147
+ logger.debug "Lost client #{@client.friendly_id}"
148
+ end
149
+ mark_dead('Unbind called')
150
+ end
151
+
152
+ # As far as I'm aware, send_data
153
+ # never throws an exception
154
+ def publish(msg)
155
+ logger.debug "Sending msg: #{msg.to_s} to client #{@request[:client_id]} (session #{@request[:session_id]})"
156
+ send_data(msg.to_s + CR)
157
+ end
158
+
159
+ # Connection methods
160
+
161
+ def broadcast(bdy)
162
+ msg = Juggernaut::Message.new(@current_msg_id += 1, bdy, self.signature)
163
+ publish(msg)
164
+ end
165
+
166
+ def mark_dead(reason = "Unknown error")
167
+ # Once dead, a client never recovers since a reconnection
168
+ # attempt would hook onto a new em instance. A client
169
+ # usually dies through an unbind
170
+ @connected = false
171
+ @client.remove_connection(self) if @client
172
+ end
173
+
174
+ def alive?
175
+ @connected == true
176
+ end
177
+
178
+ def has_channels?(channels)
179
+ channels.each {|channel|
180
+ return true if has_channel?(channel)
181
+ }
182
+ false
183
+ end
184
+
185
+ def has_channel?(channel)
186
+ @channels.include?(channel)
187
+ end
188
+
189
+ def add_channel(chan_name)
190
+ return if !chan_name or chan_name == ''
191
+ @channels << chan_name unless has_channel?(chan_name)
192
+ end
193
+
194
+ def add_channels(chan_names)
195
+ chan_names.to_a.each do |chan_name|
196
+ add_channel(chan_name)
197
+ end
198
+ end
199
+
200
+ def remove_channel!(chan_name)
201
+ @channels.delete(chan_name)
202
+ end
203
+
204
+ def remove_channels!(chan_names)
205
+ chan_names.to_a.each do |chan_name|
206
+ remove_channel!(chan_name)
207
+ end
208
+ end
209
+
210
+ protected
211
+
212
+ # Commands
213
+
214
+ def broadcast_command
215
+ raise MalformedBroadcast, @request unless @request[:type]
216
+
217
+ raise UnauthorisedBroadcast, @request unless authenticate_broadcast_or_query
218
+
219
+ case @request[:type].to_sym
220
+ when :to_channels
221
+ # if channels is a blank array, sends to everybody!
222
+ broadcast_to_channels(@request[:body], @request[:channels])
223
+ when :to_clients
224
+ broadcast_needs :client_ids
225
+ @request[:client_ids].each do |client_id|
226
+ # if channels aren't empty, scopes broadcast to clients on those channels
227
+ broadcast_to_client(@request[:body], client_id, @request[:channels])
228
+ end
229
+ else
230
+ raise MalformedBroadcast, @request
231
+ end
232
+ end
233
+
234
+ def query_command
235
+ raise MalformedQuery, @request unless @request[:type]
236
+
237
+ raise UnauthorisedQuery, @request unless authenticate_broadcast_or_query
238
+
239
+ case @request[:type].to_sym
240
+ when :remove_channels_from_all_clients
241
+ query_needs :channels
242
+ clients = Juggernaut::Client.find_all
243
+ clients.each {|client| client.remove_channels!(@request[:channels]) }
244
+ when :remove_channels_from_client
245
+ query_needs :client_ids, :channels
246
+ @request[:client_ids].each do |client_id|
247
+ client = Juggernaut::Client.find_by_id(client_id)
248
+ client.remove_channels!(@request[:channels]) if client
249
+ end
250
+ when :add_channels_to_clients
251
+ query_needs :client_ids, :channels
252
+ @request[:client_ids].each do |client_id|
253
+ client = Juggernaut::Client.find_by_id(client_id)
254
+ client.add_channels(@request[:channels]) if client
255
+ end
256
+ when :show_channels_for_client
257
+ query_needs :client_id
258
+ if client = Juggernaut::Client.find_by_id(@request[:client_id])
259
+ publish client.channels.to_json
260
+ else
261
+ publish nil.to_json
262
+ end
263
+ when :show_clients
264
+ if @request[:client_ids] and @request[:client_ids].any?
265
+ clients = @request[:client_ids].collect{ |client_id| Client.find_by_id(client_id) }.compact.uniq
266
+ else
267
+ clients = Juggernaut::Client.find_all
268
+ end
269
+ publish clients.to_json
270
+ when :show_client
271
+ query_needs :client_id
272
+ publish Juggernaut::Client.find_by_id(@request[:client_id]).to_json
273
+ when :show_clients_for_channels
274
+ query_needs :channels
275
+ publish Juggernaut::Client.find_by_channels(@request[:channels]).to_json
276
+ else
277
+ raise MalformedQuery, @request
278
+ end
279
+ end
280
+
281
+ def noop_command
282
+ logger.debug "NOOP"
283
+ end
284
+
285
+ def subscribe_command
286
+ logger.debug "SUBSCRIBE: #{@request.inspect}"
287
+
288
+ if channels = @request[:channels]
289
+ add_channels(channels)
290
+ end
291
+
292
+ @client = Juggernaut::Client.find_or_create(self, @request)
293
+
294
+ if !@client.subscription_request(@channels)
295
+ raise UnauthorisedSubscription, @client
296
+ end
297
+
298
+ if options[:store_messages]
299
+ @client.send_queued_messages(self)
300
+ end
301
+ end
302
+
303
+ private
304
+
305
+ # Different broadcast types
306
+
307
+ def broadcast_to_channels(msg, channels = [])
308
+ Juggernaut::Client.find_all.each {|client| client.send_message(msg, channels) }
309
+ end
310
+
311
+ def broadcast_to_client(body, client_id, channels)
312
+ client = Juggernaut::Client.find_by_id(client_id)
313
+ client.send_message(body, channels) if client
314
+ end
315
+
316
+ # Helper methods
317
+
318
+ def broadcast_needs(*args)
319
+ args.each do |arg|
320
+ raise MalformedBroadcast, @request unless @request.has_key?(arg)
321
+ end
322
+ end
323
+
324
+ def subscribe_needs(*args)
325
+ args.each do |arg|
326
+ raise MalformedSubscribe, @request unless @request.has_key?(arg)
327
+ end
328
+ end
329
+
330
+ def query_needs(*args)
331
+ args.each do |arg|
332
+ raise MalformedQuery, @request unless @request.has_key?(arg)
333
+ end
334
+ end
335
+
336
+ def authenticate_broadcast_or_query
337
+ if options[:allowed_ips]
338
+ return true if options[:allowed_ips].include?(client_ip)
339
+ elsif !@request[:secret_key]
340
+ return true if broadcast_query_request
341
+ elsif options[:secret_key]
342
+ return true if @request[:secret_key] == options[:secret_key]
343
+ end
344
+ if !options[:allowed_ips] and !options[:secret_key] and !options[:broadcast_query_login_url]
345
+ return true
346
+ end
347
+ false
348
+ end
349
+
350
+ def broadcast_query_request
351
+ return false unless options[:broadcast_query_login_url]
352
+ url = URI.parse(options[:broadcast_query_login_url])
353
+ params = []
354
+ params << "client_id=#{@request[:client_id]}" if @request[:client_id]
355
+ params << "session_id=#{URI.escape(@request[:session_id])}" if @request[:session_id]
356
+ params << "type=#{@request[:type]}"
357
+ params << "command=#{@request[:command]}"
358
+ (@request[:channels] || []).each {|chan| params << "channels[]=#{chan}" }
359
+ url.query = params.join('&')
360
+ begin
361
+ open(url.to_s, "User-Agent" => "Ruby/#{RUBY_VERSION}")
362
+ rescue Timeout::Error
363
+ return false
364
+ rescue
365
+ return false
366
+ end
367
+ true
368
+ end
369
+
370
+ def client_ip
371
+ Socket.unpack_sockaddr_in(get_peername)[1] rescue nil
372
+ end
373
+ end
374
+ end
@@ -0,0 +1,11 @@
1
+ class Hash
2
+ def symbolize_keys!
3
+ keys.each do |key|
4
+ unless key.is_a?(Symbol) || (new_key = key.to_sym).nil?
5
+ self[new_key] = self[key]
6
+ delete(key)
7
+ end
8
+ end
9
+ self
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: M4D-juggernaut
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 5
9
+ - 9
10
+ version: 0.5.9
11
+ platform: ruby
12
+ authors:
13
+ - Alex MacCaw
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2008-12-06 00:00:00 +01:00
19
+ default_executable: juggernaut
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: eventmachine
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 55
30
+ segments:
31
+ - 0
32
+ - 10
33
+ - 0
34
+ version: 0.10.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: json
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 23
46
+ segments:
47
+ - 1
48
+ - 1
49
+ - 2
50
+ version: 1.1.2
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: hoe
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 11
62
+ segments:
63
+ - 1
64
+ - 7
65
+ - 0
66
+ version: 1.7.0
67
+ type: :development
68
+ version_requirements: *id003
69
+ description: "See Plugin README: http://juggernaut.rubyforge.org/svn/trunk/juggernaut/README"
70
+ email: info@eribium.org
71
+ executables:
72
+ - juggernaut
73
+ extensions: []
74
+
75
+ extra_rdoc_files:
76
+ - Manifest.txt
77
+ - README.txt
78
+ files:
79
+ - Manifest.txt
80
+ - README.txt
81
+ - Rakefile
82
+ - bin/juggernaut
83
+ - lib/juggernaut.rb
84
+ - lib/juggernaut/client.rb
85
+ - lib/juggernaut/message.rb
86
+ - lib/juggernaut/miscel.rb
87
+ - lib/juggernaut/runner.rb
88
+ - lib/juggernaut/server.rb
89
+ - lib/juggernaut/utils.rb
90
+ has_rdoc: true
91
+ homepage: http://juggernaut.rubyforge.org
92
+ licenses: []
93
+
94
+ post_install_message:
95
+ rdoc_options:
96
+ - --main
97
+ - README.txt
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ hash: 3
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ hash: 3
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ requirements: []
119
+
120
+ rubyforge_project: juggernaut
121
+ rubygems_version: 1.3.7
122
+ signing_key:
123
+ specification_version: 2
124
+ summary: "See Plugin README: http://juggernaut.rubyforge.org/svn/trunk/juggernaut/README"
125
+ test_files: []
126
+