ripta-juggernaut 0.5.8

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
3
+ http://www.eribium.org
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,153 @@
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?(@@loggger)
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 log_path
135
+ options[:log_path] || File.join(%w( / var run juggernaut.log ))
136
+ end
137
+
138
+ def pid_path
139
+ options[:pid_path] || File.join(%w( / var run ), "juggernaut.#{options[:port]}.pid" )
140
+ end
141
+
142
+ def config_path
143
+ options[:config_path] || File.join(%w( / var run juggernaut.yml ))
144
+ end
145
+ end
146
+ end
147
+
148
+ require 'juggernaut/utils'
149
+ require 'juggernaut/miscel'
150
+ require 'juggernaut/message'
151
+ require 'juggernaut/client'
152
+ require 'juggernaut/server'
153
+ require 'juggernaut/runner'
@@ -0,0 +1,209 @@
1
+ require 'timeout'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module Juggernaut
6
+ class Client
7
+ include Juggernaut::Miscel
8
+
9
+ @@clients = [ ]
10
+
11
+ attr_reader :id
12
+ attr_accessor :session_id
13
+ attr_reader :connections
14
+
15
+ class << self
16
+ # Actually does a find_or_create_by_id
17
+ def find_or_create(subscriber, request)
18
+ if client = find_by_id(request[:client_id])
19
+ client.session_id = request[:session_id]
20
+ client.add_new_connection(subscriber)
21
+ client
22
+ else
23
+ self.new(subscriber, request)
24
+ end
25
+ end
26
+
27
+ # Client find methods
28
+ def find_all
29
+ @@clients
30
+ end
31
+
32
+ def find(&block)
33
+ @@clients.select(&block).uniq
34
+ end
35
+
36
+ def find_by_id(id)
37
+ find { |client| client.id == id }.first
38
+ end
39
+
40
+ def find_by_signature(signature)
41
+ # signature should be unique
42
+ find do |client|
43
+ client.connections.select { |connection| connection.signature == signature }.any?
44
+ end.first
45
+ end
46
+
47
+ def find_by_channels(channels)
48
+ find do |client|
49
+ client.has_channels?(channels)
50
+ end
51
+ end
52
+
53
+ def find_by_id_and_channels(id, channels)
54
+ find do |client|
55
+ client.has_channels?(channels) && client.id == id
56
+ end.first
57
+ end
58
+
59
+ def send_logouts_after_timeout
60
+ @@clients.each do |client|
61
+ if !client.alive? and client.give_up?
62
+ client.logout_request
63
+ end
64
+ end
65
+ end
66
+
67
+ # Called when the server is shutting down
68
+ def send_logouts_to_all_clients
69
+ @@clients.each do |client|
70
+ client.logout_request
71
+ end
72
+ end
73
+
74
+ def register_client(client)
75
+ @@clients << client unless @@clients.include?(client)
76
+ end
77
+
78
+ def client_registered?(client)
79
+ @@clients.include?(client)
80
+ end
81
+
82
+ def unregister_client(client)
83
+ @@clients.delete(client)
84
+ end
85
+ end
86
+
87
+ def initialize(subscriber, request)
88
+ @connections = []
89
+ @id = request[:client_id]
90
+ @session_id = request[:session_id]
91
+ self.register
92
+ add_new_connection(subscriber)
93
+ end
94
+
95
+ def to_json
96
+ {
97
+ :client_id => @id,
98
+ :num_connections => @connections.size,
99
+ :session_id => @session_id
100
+ }.to_json
101
+ end
102
+
103
+ def add_new_connection(subscriber)
104
+ @connections << subscriber
105
+ end
106
+
107
+ def friendly_id
108
+ if self.id
109
+ "with ID #{self.id}"
110
+ else
111
+ "session #{self.session_id}"
112
+ end
113
+ end
114
+
115
+ def subscription_request(channels)
116
+ return true unless options[:subscription_url]
117
+ post_request(options[:subscription_url], channels)
118
+ end
119
+
120
+ def logout_connection_request(channels)
121
+ return true unless options[:logout_connection_url]
122
+ post_request(options[:logout_connection_url], channels)
123
+ end
124
+
125
+ def logout_request
126
+ self.unregister
127
+ return true unless options[:logout_url]
128
+ post_request(options[:logout_url])
129
+ end
130
+
131
+ def remove_connection(connection)
132
+ @connections.delete(connection)
133
+ self.unregister if @connections.empty?
134
+ end
135
+
136
+ def send_message(msg, channels = nil)
137
+ @connections.each do |em|
138
+ em.broadcast(msg) if !channels or channels.empty? or em.has_channels?(channels)
139
+ end
140
+ end
141
+
142
+ def has_channels?(channels)
143
+ @connections.each do |em|
144
+ return true if em.has_channels?(channels)
145
+ end
146
+ false
147
+ end
148
+
149
+ def remove_channels!(channels)
150
+ @connections.each do |em|
151
+ em.remove_channels!(channels)
152
+ end
153
+ end
154
+
155
+ def alive?
156
+ @connections.select{|em| em.alive? }.any?
157
+ end
158
+
159
+ def give_up?
160
+ @connections.select do |em|
161
+ em.logout_timeout and Time.now > em.logout_timeout
162
+ end.any?
163
+ end
164
+
165
+ protected
166
+
167
+ def register
168
+ self.class.register_client(self)
169
+ end
170
+
171
+ def registered?
172
+ self.class.client_registered?(self)
173
+ end
174
+
175
+ def unregister
176
+ self.class.unregister_client(self)
177
+ end
178
+
179
+ def post_request(url, channels = [])
180
+ uri = URI.parse(url)
181
+ uri.path = '/' if uri.path == ''
182
+ params = []
183
+ params << "client_id=#{id}" if id
184
+ params << "session_id=#{session_id}" if session_id
185
+ channels.each {|chan| params << "channels[]=#{chan}" }
186
+ headers = {"User-Agent" => "Ruby/#{RUBY_VERSION}"}
187
+ begin
188
+ http = Net::HTTP.new(uri.host, uri.port)
189
+ if uri.scheme == 'https'
190
+ http.use_ssl = true
191
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
192
+ end
193
+ http.read_timeout = 5
194
+ resp, data = http.post(uri.path, params.join('&'), headers)
195
+ unless resp.is_a?(Net::HTTPOK)
196
+ return false
197
+ end
198
+ rescue => e
199
+ logger.debug("Bad request #{url.to_s}: #{e}")
200
+ return false
201
+ rescue Timeout::Error
202
+ logger.debug("#{url.to_s} timeout")
203
+ return false
204
+ end
205
+ true
206
+ end
207
+
208
+ end
209
+ 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
11
+ self.new
12
+ end
13
+ end
14
+
15
+ def initialize
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
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
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!
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,372 @@
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
+ @channels = []
61
+ @messages = []
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
+ else
126
+ raise InvalidCommand, @request
127
+ end
128
+
129
+ rescue JuggernautError => e
130
+ logger.error("#{e} - #{e.message.inspect}")
131
+ close_connection
132
+ # So as to stop em quitting
133
+ rescue => e
134
+ logger ? logger.error(e) : puts(e)
135
+ end
136
+
137
+ def unbind
138
+ if @client
139
+ # todo - should be called after timeout?
140
+ @client.logout_connection_request(@channels)
141
+ logger.debug "Lost client #{@client.friendly_id}"
142
+ end
143
+ mark_dead('Unbind called')
144
+ end
145
+
146
+ # As far as I'm aware, send_data
147
+ # never throws an exception
148
+ def publish(msg)
149
+ logger.debug "Sending msg: #{msg.to_s}"
150
+ logger.debug "To client #{@client.friendly_id}" if @client
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
+ @messages << msg if options[:store_messages]
159
+ publish(msg)
160
+ end
161
+
162
+ def mark_dead(reason = "Unknown error")
163
+ # Once dead, a client never recovers since a reconnection
164
+ # attempt would hook onto a new em instance. A client
165
+ # usually dies through an unbind
166
+ @connected = false
167
+ @logout_timeout = Time::now + (options[:timeout] || 5)
168
+ @status = "DEAD: %s: Could potentially logout at %s" % [ reason, @logout_timeout ]
169
+ @client.remove_connection(self) if @client
170
+ end
171
+
172
+ def alive?
173
+ @connected == true
174
+ end
175
+
176
+ def has_channels?(channels)
177
+ channels.each {|channel|
178
+ return true if has_channel?(channel)
179
+ }
180
+ false
181
+ end
182
+
183
+ def has_channel?(channel)
184
+ @channels.include?(channel)
185
+ end
186
+
187
+ def add_channel(chan_name)
188
+ return if !chan_name or chan_name == ''
189
+ @channels << chan_name unless has_channel?(chan_name)
190
+ end
191
+
192
+ def add_channels(chan_names)
193
+ chan_names.to_a.each do |chan_name|
194
+ add_channel(chan_name)
195
+ end
196
+ end
197
+
198
+ def remove_channel!(chan_name)
199
+ @channels.delete(chan_name)
200
+ end
201
+
202
+ def remove_channels!(chan_names)
203
+ chan_names.to_a.each do |chan_name|
204
+ remove_channel!(chan_name)
205
+ end
206
+ end
207
+
208
+ def broadcast_all_messages_from(msg_id, signature_id)
209
+ return unless msg_id or signature_id
210
+ client = Juggernaut::Client.find_by_signature(signature)
211
+ return if !client
212
+ msg_id = Integer(msg_id)
213
+ return if msg_id >= client.connections.select {|c| c == self }.current_msg_id
214
+ client.messages.select {|msg|
215
+ (msg_id..client.connections.select {|c| c == self }).include?(msg.id)
216
+ }.each {|msg| publish(msg) }
217
+ end
218
+
219
+ # todo - how should this be called - if at all?
220
+ def clean_up_old_messages(how_many_to_keep = 1000)
221
+ while @messages.length > how_many_to_keep
222
+ # We need to shift, as we want to remove the oldest first
223
+ @messages.shift
224
+ end
225
+ end
226
+
227
+ protected
228
+
229
+ # Commands
230
+
231
+ def broadcast_command
232
+ raise MalformedBroadcast, @request unless @request[:type]
233
+
234
+ raise UnauthorisedBroadcast, @request unless authenticate_broadcast_or_query
235
+
236
+ case @request[:type].to_sym
237
+ when :to_channels
238
+ # if channels is a blank array, sends to everybody!
239
+ broadcast_to_channels(@request[:body], @request[:channels])
240
+ when :to_clients
241
+ broadcast_needs :client_ids
242
+ @request[:client_ids].each do |client_id|
243
+ # if channels aren't empty, scopes broadcast to clients on those channels
244
+ broadcast_to_client(@request[:body], client_id, @request[:channels])
245
+ end
246
+ else
247
+ raise MalformedBroadcast, @request
248
+ end
249
+ end
250
+
251
+ def query_command
252
+ raise MalformedQuery, @request unless @request[:type]
253
+
254
+ raise UnauthorisedQuery, @request unless authenticate_broadcast_or_query
255
+
256
+ case @request[:type].to_sym
257
+ when :remove_channels_from_all_clients
258
+ query_needs :channels
259
+ clients = Juggernaut::Client.find_all
260
+ clients.each {|client| client.remove_channels!(@request[:channels]) }
261
+ when :remove_channels_from_client
262
+ query_needs :client_ids, :channels
263
+ @request[:client_ids].each do |client_id|
264
+ client = Juggernaut::Client.find_by_id(client_id)
265
+ client.remove_channels!(@request[:channels]) if client
266
+ end
267
+ when :show_clients
268
+ if @request[:client_ids] and @request[:client_ids].any?
269
+ clients = @request[:client_ids].collect{ |client_id| Client.find_by_id(client_id) }.compact.uniq
270
+ else
271
+ clients = Juggernaut::Client.find_all
272
+ end
273
+ publish clients.to_json
274
+ when :show_client
275
+ query_needs :client_id
276
+ publish Juggernaut::Client.find_by_id(@request[:client_id]).to_json
277
+ when :show_clients_for_channels
278
+ query_needs :channels
279
+ publish Juggernaut::Client.find_by_channels(@request[:channels]).to_json
280
+ else
281
+ raise MalformedQuery, @request
282
+ end
283
+ end
284
+
285
+ def subscribe_command
286
+ if channels = @request[:channels]
287
+ add_channels(channels)
288
+ end
289
+
290
+ @client = Juggernaut::Client.find_or_create(self, @request)
291
+
292
+ if !@client.subscription_request(@channels)
293
+ raise UnauthorisedSubscription, @client
294
+ end
295
+
296
+ if options[:store_messages]
297
+ broadcast_all_messages_from(@request[:last_msg_id], @request[:signature])
298
+ end
299
+ end
300
+
301
+ private
302
+
303
+ # Different broadcast types
304
+
305
+ def broadcast_to_channels(msg, channels = [])
306
+ Juggernaut::Client.find_all.each {|client| client.send_message(msg, channels) }
307
+ end
308
+
309
+ def broadcast_to_client(body, client_id, channels)
310
+ client = Juggernaut::Client.find_by_id(client_id)
311
+ client.send_message(body, channels) if client
312
+ end
313
+
314
+ # Helper methods
315
+
316
+ def broadcast_needs(*args)
317
+ args.each do |arg|
318
+ raise MalformedBroadcast, @request unless @request.has_key?(arg)
319
+ end
320
+ end
321
+
322
+ def subscribe_needs(*args)
323
+ args.each do |arg|
324
+ raise MalformedSubscribe, @request unless @request.has_key?(arg)
325
+ end
326
+ end
327
+
328
+ def query_needs(*args)
329
+ args.each do |arg|
330
+ raise MalformedQuery, @request unless @request.has_key?(arg)
331
+ end
332
+ end
333
+
334
+ def authenticate_broadcast_or_query
335
+ if options[:allowed_ips]
336
+ return true if options[:allowed_ips].include?(client_ip)
337
+ elsif !@request[:secret_key]
338
+ return true if broadcast_query_request
339
+ elsif options[:secret_key]
340
+ return true if @request[:secret_key] == options[:secret_key]
341
+ end
342
+ if !options[:allowed_ips] and !options[:secret_key] and !options[:broadcast_query_login_url]
343
+ return true
344
+ end
345
+ false
346
+ end
347
+
348
+ def broadcast_query_request
349
+ return false unless options[:broadcast_query_login_url]
350
+ url = URI.parse(options[:broadcast_query_login_url])
351
+ params = []
352
+ params << "client_id=#{@request[:client_id]}" if @request[:client_id]
353
+ params << "session_id=#{URI.escape(@request[:session_id])}" if @request[:session_id]
354
+ params << "type=#{@request[:type]}"
355
+ params << "command=#{@request[:command]}"
356
+ (@request[:channels] || []).each {|chan| params << "channels[]=#{chan}" }
357
+ url.query = params.join('&')
358
+ begin
359
+ open(url.to_s, "User-Agent" => "Ruby/#{RUBY_VERSION}")
360
+ rescue Timeout::Error
361
+ return false
362
+ rescue
363
+ return false
364
+ end
365
+ true
366
+ end
367
+
368
+ def client_ip
369
+ Socket.unpack_sockaddr_in(get_peername)[1]
370
+ end
371
+ end
372
+ 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: ripta-juggernaut
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.8
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
+