maccman-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,259 @@
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
+ }.to_json
108
+ end
109
+
110
+ def add_new_connection(subscriber)
111
+ @connections << subscriber
112
+ end
113
+
114
+ def friendly_id
115
+ if self.id
116
+ "with ID #{self.id}"
117
+ else
118
+ "session #{self.session_id}"
119
+ end
120
+ end
121
+
122
+ def subscription_request(channels)
123
+ return true unless options[:subscription_url]
124
+ post_request(options[:subscription_url], channels, :timeout => options[:post_request_timeout] || 5)
125
+ end
126
+
127
+ def logout_connection_request(channels)
128
+ return true unless options[:logout_connection_url]
129
+ post_request(options[:logout_connection_url], channels, :timeout => options[:post_request_timeout] || 5)
130
+ end
131
+
132
+ def logout_request
133
+ self.unregister
134
+ logger.debug("Timed out client #{friendly_id}")
135
+ return true unless options[:logout_url]
136
+ post_request(options[:logout_url], [ ], :timeout => options[:post_request_timeout] || 5)
137
+ end
138
+
139
+ def remove_connection(connection)
140
+ @connections.delete(connection)
141
+ self.reset_logout_timeout!
142
+ self.logout_request if self.give_up?
143
+ end
144
+
145
+ def send_message(msg, channels = nil)
146
+ store_message(msg, channels) if options[:store_messages]
147
+ send_message_to_connections(msg, channels)
148
+ end
149
+
150
+ # Send messages that are queued up for this particular client.
151
+ # Messages are only queued for previously-connected clients.
152
+ def send_queued_messages
153
+ self.expire_queued_messages!
154
+
155
+ # Weird looping because we don't want to send messages that get
156
+ # added to the array after we start iterating (since those will
157
+ # get sent to the client anyway).
158
+ @length = @messages.length
159
+
160
+ logger.debug("Sending #{@length} queued message(s) to client #{friendly_id}")
161
+
162
+ @length.times do |id|
163
+ message = @messages[id]
164
+ send_message_to_connections(message[:message], message[:channels])
165
+ end
166
+ end
167
+
168
+ def channels
169
+ @connections.collect { |em| em.channels }.flatten.uniq
170
+ end
171
+
172
+ def has_channels?(channels)
173
+ @connections.each do |em|
174
+ return true if em.has_channels?(channels)
175
+ end
176
+ false
177
+ end
178
+
179
+ def remove_channels!(channels)
180
+ @connections.each do |em|
181
+ em.remove_channels!(channels)
182
+ end
183
+ end
184
+
185
+ def alive?
186
+ @connections.select { |em| em.alive? }.any?
187
+ end
188
+
189
+ # This client is only dead if there are no connections and we are
190
+ # past the timeout (if we are within the timeout, the user could
191
+ # just be doing a page reload or going to a new page)
192
+ def give_up?
193
+ !alive? and (Time.now > @logout_timeout)
194
+ end
195
+
196
+ protected
197
+
198
+ def register
199
+ self.class.register_client(self)
200
+ end
201
+
202
+ def registered?
203
+ self.class.client_registered?(self)
204
+ end
205
+
206
+ def unregister
207
+ self.class.unregister_client(self)
208
+ end
209
+
210
+ def post_request(url, channels = [ ], options = { })
211
+ uri = URI.parse(url)
212
+ uri.path = '/' if uri.path == ''
213
+ params = []
214
+ params << "client_id=#{id}" if id
215
+ params << "session_id=#{session_id}" if session_id
216
+ channels.each {|chan| params << "channels[]=#{chan}" }
217
+ headers = {"User-Agent" => "Ruby/#{RUBY_VERSION}"}
218
+ begin
219
+ http = Net::HTTP.new(uri.host, uri.port)
220
+ if uri.scheme == 'https'
221
+ http.use_ssl = true
222
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
223
+ end
224
+ http.read_timeout = options[:timeout] || 5
225
+ resp, data = http.post(uri.path, params.join('&'), headers)
226
+ return resp.is_a?(Net::HTTPOK)
227
+ rescue => e
228
+ logger.error("Bad request #{url.to_s} (#{e.class}: #{e.message})")
229
+ return false
230
+ rescue Timeout::Error
231
+ logger.error("#{url.to_s} timeout")
232
+ return false
233
+ end
234
+ end
235
+
236
+ def send_message_to_connections(msg, channels)
237
+ @connections.each do |em|
238
+ em.broadcast(msg) if !channels or channels.empty? or em.has_channels?(channels)
239
+ end
240
+ end
241
+
242
+ # Queued messages are stored until a timeout is reached which is the
243
+ # same as the connection timeout. This takes care of messages that
244
+ # come in between page loads or ones that come in right when you are
245
+ # clicking off one page and loading the next one.
246
+ def store_message(msg, channels)
247
+ self.expire_queued_messages!
248
+ @messages << { :channels => channels, :message => msg, :timeout => Time.now + options[:timeout] }
249
+ end
250
+
251
+ def expire_queued_messages!
252
+ @messages.reject! { |message| Time.now > message[:timeout] }
253
+ end
254
+
255
+ def reset_logout_timeout!
256
+ @logout_timeout = Time.now + options[:timeout]
257
+ end
258
+ end
259
+ 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,219 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+ require 'erb'
4
+
5
+ module Juggernaut
6
+ class Runner
7
+ include Juggernaut::Miscel
8
+
9
+ class << self
10
+ def run(argv = ARGV)
11
+ self.new(argv)
12
+ end
13
+ end
14
+
15
+ def initialize(argv = ARGV)
16
+ self.options = {
17
+ :host => "0.0.0.0",
18
+ :port => 5001,
19
+ :debug => false,
20
+ :cleanup_timer => 2,
21
+ :timeout => 10,
22
+ :store_messages => false
23
+ }
24
+
25
+ self.options.merge!({
26
+ :pid_path => pid_path,
27
+ :log_path => log_path,
28
+ :config_path => config_path
29
+ })
30
+
31
+ parse_options(argv)
32
+
33
+ if !File.exists?(config_path)
34
+ puts "You must generate a config file (juggernaut -g filename.yml)"
35
+ exit
36
+ end
37
+
38
+ options.merge!(YAML::load(ERB.new(IO.read(config_path)).result))
39
+
40
+ if options.include?(:kill)
41
+ kill_pid(options[:kill] || '*')
42
+ end
43
+
44
+ Process.euid = options[:user] if options[:user]
45
+ Process.egid = options[:group] if options[:group]
46
+
47
+ if !options[:daemonize]
48
+ start
49
+ else
50
+ daemonize
51
+ end
52
+ end
53
+
54
+ def start
55
+ puts "Starting Juggernaut server #{Juggernaut::VERSION} on port: #{options[:port]}..."
56
+
57
+ trap("INT") {
58
+ stop
59
+ exit
60
+ }
61
+ trap("TERM"){
62
+ stop
63
+ exit
64
+ }
65
+
66
+ if options[:descriptor_table_size]
67
+ EM.epoll
68
+ new_size = EM.set_descriptor_table_size( options[:descriptor_table_size] )
69
+ logger.debug "New descriptor-table size is #{new_size}"
70
+ end
71
+
72
+ EventMachine::run {
73
+ EventMachine::add_periodic_timer( options[:cleanup_timer] || 2 ) { Juggernaut::Client.send_logouts_after_timeout }
74
+ EventMachine::start_server(options[:host], options[:port].to_i, Juggernaut::Server)
75
+ EM.set_effective_user( options[:user] ) if options[:user]
76
+ }
77
+ end
78
+
79
+ def stop
80
+ puts "Stopping Juggernaut server"
81
+ Juggernaut::Client.send_logouts_to_all_clients
82
+ EventMachine::stop
83
+ end
84
+
85
+ def parse_options(argv)
86
+ OptionParser.new do |opts|
87
+ opts.summary_width = 25
88
+ opts.banner = "Juggernaut (#{VERSION})\n\n",
89
+ "Usage: juggernaut [-h host] [-p port] [-P file]\n",
90
+ " [-d] [-k port] [-l file] [-e]\n",
91
+ " juggernaut --help\n",
92
+ " juggernaut --version\n"
93
+
94
+ opts.separator ""
95
+ opts.separator ""; opts.separator "Configuration:"
96
+
97
+ opts.on("-g", "--generate FILE", String, "Generate config file", "(default: #{options[:config_path]})") do |v|
98
+ options[:config_path] = File.expand_path(v) if v
99
+ generate_config_file
100
+ end
101
+
102
+ opts.on("-c", "--config FILE", String, "Path to configuration file.", "(default: #{options[:config_path]})") do |v|
103
+ options[:config_path] = File.expand_path(v)
104
+ end
105
+
106
+ opts.separator ""; opts.separator "Network:"
107
+
108
+ opts.on("-h", "--host HOST", String, "Specify host", "(default: #{options[:host]})") do |v|
109
+ options[:host] = v
110
+ end
111
+
112
+ opts.on("-p", "--port PORT", Integer, "Specify port", "(default: #{options[:port]})") do |v|
113
+ options[:port] = v
114
+ end
115
+
116
+ 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|
117
+ options[:descriptor_table_size] = v
118
+ end
119
+
120
+ opts.separator ""; opts.separator "Daemonization:"
121
+
122
+ opts.on("-P", "--pid FILE", String, "save PID in FILE when using -d option.", "(default: #{options[:pid_path]})") do |v|
123
+ options[:pid_path] = File.expand_path(v)
124
+ end
125
+
126
+ opts.on("-d", "--daemon", "Daemonize mode") do |v|
127
+ options[:daemonize] = v
128
+ end
129
+
130
+ opts.on("-k", "--kill PORT", String, :OPTIONAL, "Kill specified running daemons - leave blank to kill all.") do |v|
131
+ options[:kill] = v
132
+ end
133
+
134
+ opts.separator ""; opts.separator "Logging:"
135
+
136
+ opts.on("-l", "--log [FILE]", String, "Path to print debugging information.", "(default: #{options[:log_path]})") do |v|
137
+ options[:log_path] = File.expand_path(v)
138
+ end
139
+
140
+ opts.on("-e", "--debug", "Run in debug mode", "(default: #{options[:debug]})") do |v|
141
+ options[:debug] = v
142
+ end
143
+
144
+ opts.separator ""; opts.separator "Permissions:"
145
+
146
+ opts.on("-u", "--user USER", Integer, "User to run as") do |user|
147
+ options[:user] = user
148
+ end
149
+
150
+ opts.on("-G", "--group GROUP", String, "Group to run as") do |group|
151
+ options[:group] = group
152
+ end
153
+
154
+ opts.separator ""; opts.separator "Miscellaneous:"
155
+
156
+ opts.on_tail("-?", "--help", "Display this usage information.") do
157
+ puts "#{opts}\n"
158
+ exit
159
+ end
160
+
161
+ opts.on_tail("-v", "--version", "Display version") do |v|
162
+ puts "Juggernaut #{VERSION}"
163
+ exit
164
+ end
165
+ end.parse!(argv)
166
+ options
167
+ end
168
+
169
+ private
170
+
171
+ def generate_config_file
172
+ if File.exists?(config_path)
173
+ puts "Config file already exists. You must remove it before generating a new one."
174
+ exit
175
+ end
176
+ puts "Generating config file...."
177
+ File.open(config_path, 'w+') do |file|
178
+ file.write DEFAULT_CONFIG_FILE.gsub('your_secret_key_here', Digest::SHA1.hexdigest("--#{Time.now.to_s.split(//).sort_by {rand}.join}--"))
179
+ end
180
+ puts "Config file generated at #{config_path}"
181
+ exit
182
+ end
183
+
184
+ def store_pid(pid)
185
+ FileUtils.mkdir_p(File.dirname(pid_path))
186
+ File.open(pid_path, 'w'){|f| f.write("#{pid}\n")}
187
+ end
188
+
189
+ def kill_pid(k)
190
+ Dir[options[:pid_path]||File.join(File.dirname(pid_dir), "juggernaut.#{k}.pid")].each do |f|
191
+ begin
192
+ puts f
193
+ pid = IO.read(f).chomp.to_i
194
+ FileUtils.rm f
195
+ Process.kill(9, pid)
196
+ puts "killed PID: #{pid}"
197
+ rescue => e
198
+ puts "Failed to kill! #{k}: #{e}"
199
+ end
200
+ end
201
+ exit
202
+ end
203
+
204
+ def daemonize
205
+ fork do
206
+ Process.setsid
207
+ exit if fork
208
+ store_pid(Process.pid)
209
+ # Dir.chdir "/" # Mucks up logs
210
+ File.umask 0000
211
+ STDIN.reopen "/dev/null"
212
+ STDOUT.reopen "/dev/null", "a"
213
+ STDERR.reopen STDOUT
214
+ start
215
+ end
216
+ end
217
+
218
+ end
219
+ end
@@ -0,0 +1,363 @@
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
+
56
+ # EM methods
57
+
58
+ def post_init
59
+ logger.debug "New client [#{client_ip}]"
60
+ @client = nil
61
+ @channels = []
62
+ @current_msg_id = 0
63
+ @connected = true
64
+ @logout_timeout = nil
65
+ @buffer = ''
66
+ end
67
+
68
+ # Juggernaut packets are terminated with "\0"
69
+ # so we need to buffer the data until we find the
70
+ # terminating "\0"
71
+ def receive_data(data)
72
+ @buffer << data
73
+ @buffer = process_whole_messages(@buffer)
74
+ end
75
+
76
+ # process any whole messages in the buffer,
77
+ # and return the new contents of the buffer
78
+ def process_whole_messages(data)
79
+ return data if data !~ /\0/ # only process if data contains a \0 char
80
+ messages = data.split("\0")
81
+ if data =~ /\0$/
82
+ data = ''
83
+ else
84
+ # remove the last message from the list (because it is incomplete) before processing
85
+ data = messages.pop
86
+ end
87
+ messages.each {|message| process_message(message.strip)}
88
+ return data
89
+ end
90
+
91
+ def process_message(ln)
92
+ logger.debug "Processing message: #{ln}"
93
+ @request = nil
94
+
95
+ if ln == POLICY_REQUEST
96
+ logger.debug "Sending crossdomain file"
97
+ send_data POLICY_FILE.gsub('PORT', (options[:public_port]||options[:port]).to_s)
98
+ close_connection_after_writing
99
+ return
100
+ end
101
+
102
+ begin
103
+ @request = JSON.parse(ln) unless ln.empty?
104
+ rescue
105
+ raise CorruptJSON, ln
106
+ end
107
+
108
+ raise InvalidRequest, ln if !@request
109
+
110
+ @request.symbolize_keys!
111
+
112
+ # For debugging
113
+ @request[:ip] = client_ip
114
+
115
+ @request[:channels] = (@request[:channels] || []).compact.select {|c| !!c && c != '' }.uniq
116
+
117
+ if @request[:client_ids]
118
+ @request[:client_ids] = @request[:client_ids].to_a.compact.select {|c| !!c && c != '' }.uniq
119
+ end
120
+
121
+ case @request[:command].to_sym
122
+ when :broadcast: broadcast_command
123
+ when :subscribe: subscribe_command
124
+ when :query: query_command
125
+ when :noop: noop_command
126
+ else
127
+ raise InvalidCommand, @request
128
+ end
129
+
130
+ rescue JuggernautError => e
131
+ logger.error("#{e} - #{e.message.inspect}")
132
+ close_connection
133
+ # So as to stop em quitting
134
+ rescue => e
135
+ logger ? logger.error(e) : puts(e)
136
+ end
137
+
138
+ def unbind
139
+ if @client
140
+ # todo - should be called after timeout?
141
+ @client.logout_connection_request(@channels)
142
+ logger.debug "Lost client #{@client.friendly_id}"
143
+ end
144
+ mark_dead('Unbind called')
145
+ end
146
+
147
+ # As far as I'm aware, send_data
148
+ # never throws an exception
149
+ def publish(msg)
150
+ logger.debug "Sending msg: #{msg.to_s} to client #{@request[:client_id]} (session #{@request[:session_id]})"
151
+ send_data(msg.to_s + CR)
152
+ end
153
+
154
+ # Connection methods
155
+
156
+ def broadcast(bdy)
157
+ msg = Juggernaut::Message.new(@current_msg_id += 1, bdy, self.signature)
158
+ publish(msg)
159
+ end
160
+
161
+ def mark_dead(reason = "Unknown error")
162
+ # Once dead, a client never recovers since a reconnection
163
+ # attempt would hook onto a new em instance. A client
164
+ # usually dies through an unbind
165
+ @connected = false
166
+ @client.remove_connection(self) if @client
167
+ end
168
+
169
+ def alive?
170
+ @connected == true
171
+ end
172
+
173
+ def has_channels?(channels)
174
+ channels.each {|channel|
175
+ return true if has_channel?(channel)
176
+ }
177
+ false
178
+ end
179
+
180
+ def has_channel?(channel)
181
+ @channels.include?(channel)
182
+ end
183
+
184
+ def add_channel(chan_name)
185
+ return if !chan_name or chan_name == ''
186
+ @channels << chan_name unless has_channel?(chan_name)
187
+ end
188
+
189
+ def add_channels(chan_names)
190
+ chan_names.to_a.each do |chan_name|
191
+ add_channel(chan_name)
192
+ end
193
+ end
194
+
195
+ def remove_channel!(chan_name)
196
+ @channels.delete(chan_name)
197
+ end
198
+
199
+ def remove_channels!(chan_names)
200
+ chan_names.to_a.each do |chan_name|
201
+ remove_channel!(chan_name)
202
+ end
203
+ end
204
+
205
+ protected
206
+
207
+ # Commands
208
+
209
+ def broadcast_command
210
+ raise MalformedBroadcast, @request unless @request[:type]
211
+
212
+ raise UnauthorisedBroadcast, @request unless authenticate_broadcast_or_query
213
+
214
+ case @request[:type].to_sym
215
+ when :to_channels
216
+ # if channels is a blank array, sends to everybody!
217
+ broadcast_to_channels(@request[:body], @request[:channels])
218
+ when :to_clients
219
+ broadcast_needs :client_ids
220
+ @request[:client_ids].each do |client_id|
221
+ # if channels aren't empty, scopes broadcast to clients on those channels
222
+ broadcast_to_client(@request[:body], client_id, @request[:channels])
223
+ end
224
+ else
225
+ raise MalformedBroadcast, @request
226
+ end
227
+ end
228
+
229
+ def query_command
230
+ raise MalformedQuery, @request unless @request[:type]
231
+
232
+ raise UnauthorisedQuery, @request unless authenticate_broadcast_or_query
233
+
234
+ case @request[:type].to_sym
235
+ when :remove_channels_from_all_clients
236
+ query_needs :channels
237
+ clients = Juggernaut::Client.find_all
238
+ clients.each {|client| client.remove_channels!(@request[:channels]) }
239
+ when :remove_channels_from_client
240
+ query_needs :client_ids, :channels
241
+ @request[:client_ids].each do |client_id|
242
+ client = Juggernaut::Client.find_by_id(client_id)
243
+ client.remove_channels!(@request[:channels]) if client
244
+ end
245
+ when :show_channels_for_client
246
+ query_needs :client_id
247
+ if client = Juggernaut::Client.find_by_id(@request[:client_id])
248
+ publish client.channels.to_json
249
+ else
250
+ publish nil.to_json
251
+ end
252
+ when :show_clients
253
+ if @request[:client_ids] and @request[:client_ids].any?
254
+ clients = @request[:client_ids].collect{ |client_id| Client.find_by_id(client_id) }.compact.uniq
255
+ else
256
+ clients = Juggernaut::Client.find_all
257
+ end
258
+ publish clients.to_json
259
+ when :show_client
260
+ query_needs :client_id
261
+ publish Juggernaut::Client.find_by_id(@request[:client_id]).to_json
262
+ when :show_clients_for_channels
263
+ query_needs :channels
264
+ publish Juggernaut::Client.find_by_channels(@request[:channels]).to_json
265
+ else
266
+ raise MalformedQuery, @request
267
+ end
268
+ end
269
+
270
+ def noop_command
271
+ logger.debug "NOOP"
272
+ end
273
+
274
+ def subscribe_command
275
+ logger.debug "SUBSCRIBE: #{@request.inspect}"
276
+
277
+ if channels = @request[:channels]
278
+ add_channels(channels)
279
+ end
280
+
281
+ @client = Juggernaut::Client.find_or_create(self, @request)
282
+
283
+ if !@client.subscription_request(@channels)
284
+ raise UnauthorisedSubscription, @client
285
+ end
286
+
287
+ if options[:store_messages]
288
+ @client.send_queued_messages
289
+ end
290
+ end
291
+
292
+ private
293
+
294
+ # Different broadcast types
295
+
296
+ def broadcast_to_channels(msg, channels = [])
297
+ Juggernaut::Client.find_all.each {|client| client.send_message(msg, channels) }
298
+ end
299
+
300
+ def broadcast_to_client(body, client_id, channels)
301
+ client = Juggernaut::Client.find_by_id(client_id)
302
+ client.send_message(body, channels) if client
303
+ end
304
+
305
+ # Helper methods
306
+
307
+ def broadcast_needs(*args)
308
+ args.each do |arg|
309
+ raise MalformedBroadcast, @request unless @request.has_key?(arg)
310
+ end
311
+ end
312
+
313
+ def subscribe_needs(*args)
314
+ args.each do |arg|
315
+ raise MalformedSubscribe, @request unless @request.has_key?(arg)
316
+ end
317
+ end
318
+
319
+ def query_needs(*args)
320
+ args.each do |arg|
321
+ raise MalformedQuery, @request unless @request.has_key?(arg)
322
+ end
323
+ end
324
+
325
+ def authenticate_broadcast_or_query
326
+ if options[:allowed_ips]
327
+ return true if options[:allowed_ips].include?(client_ip)
328
+ elsif !@request[:secret_key]
329
+ return true if broadcast_query_request
330
+ elsif options[:secret_key]
331
+ return true if @request[:secret_key] == options[:secret_key]
332
+ end
333
+ if !options[:allowed_ips] and !options[:secret_key] and !options[:broadcast_query_login_url]
334
+ return true
335
+ end
336
+ false
337
+ end
338
+
339
+ def broadcast_query_request
340
+ return false unless options[:broadcast_query_login_url]
341
+ url = URI.parse(options[:broadcast_query_login_url])
342
+ params = []
343
+ params << "client_id=#{@request[:client_id]}" if @request[:client_id]
344
+ params << "session_id=#{URI.escape(@request[:session_id])}" if @request[:session_id]
345
+ params << "type=#{@request[:type]}"
346
+ params << "command=#{@request[:command]}"
347
+ (@request[:channels] || []).each {|chan| params << "channels[]=#{chan}" }
348
+ url.query = params.join('&')
349
+ begin
350
+ open(url.to_s, "User-Agent" => "Ruby/#{RUBY_VERSION}")
351
+ rescue Timeout::Error
352
+ return false
353
+ rescue
354
+ return false
355
+ end
356
+ true
357
+ end
358
+
359
+ def client_ip
360
+ Socket.unpack_sockaddr_in(get_peername)[1]
361
+ end
362
+ end
363
+ 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,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: maccman-juggernaut
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.9
5
+ platform: ruby
6
+ authors:
7
+ - Alex MacCaw
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-06 00:00:00 -08:00
13
+ default_executable: juggernaut
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.10.0
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: json
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 1.1.2
32
+ version:
33
+ - !ruby/object:Gem::Dependency
34
+ name: hoe
35
+ version_requirement:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.7.0
41
+ version:
42
+ description: "See Plugin README: http://juggernaut.rubyforge.org/svn/trunk/juggernaut/README"
43
+ email: info@eribium.org
44
+ executables:
45
+ - juggernaut
46
+ extensions: []
47
+
48
+ extra_rdoc_files:
49
+ - Manifest.txt
50
+ - README.txt
51
+ files:
52
+ - Manifest.txt
53
+ - README.txt
54
+ - Rakefile
55
+ - bin/juggernaut
56
+ - lib/juggernaut.rb
57
+ - lib/juggernaut/client.rb
58
+ - lib/juggernaut/message.rb
59
+ - lib/juggernaut/miscel.rb
60
+ - lib/juggernaut/runner.rb
61
+ - lib/juggernaut/server.rb
62
+ - lib/juggernaut/utils.rb
63
+ has_rdoc: true
64
+ homepage: http://juggernaut.rubyforge.org
65
+ post_install_message:
66
+ rdoc_options:
67
+ - --main
68
+ - README.txt
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: "0"
82
+ version:
83
+ requirements: []
84
+
85
+ rubyforge_project: juggernaut
86
+ rubygems_version: 1.2.0
87
+ signing_key:
88
+ specification_version: 2
89
+ summary: "See Plugin README: http://juggernaut.rubyforge.org/svn/trunk/juggernaut/README"
90
+ test_files: []
91
+