M4D-juggernaut 0.5.9

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