DefV-juggernaut 0.5.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
@@ -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.
@@ -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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'juggernaut')
4
+ Juggernaut::Runner.run
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'juggernaut'
3
+ s.authors = ['Alex MacCaw']
4
+ s.email = 'info@eribium.org'
5
+ s.version = "0.5.7.2"
6
+ s.date = "2008-12-24"
7
+ s.summary = "Juggernaut"
8
+ s.bindir = "bin"
9
+ s.files = [
10
+ "README.txt",
11
+ "Manifest.txt",
12
+ "Rakefile",
13
+ "juggernaut.gemspec",
14
+ "lib/juggernaut.rb",
15
+ "lib/juggernaut/client.rb",
16
+ "lib/juggernaut/message.rb",
17
+ "lib/juggernaut/miscel.rb",
18
+ "lib/juggernaut/runner.rb",
19
+ "lib/juggernaut/server.rb",
20
+ "lib/juggernaut/utils.rb",
21
+ "bin/juggernaut"
22
+ ]
23
+ s.executables << 'juggernaut'
24
+ s.add_dependency("eventmachine", ['>=0.10.0'])
25
+ s.add_dependency("json", [">=1.1.2"])
26
+ end
@@ -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.7'
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,185 @@
1
+ require 'timeout'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require 'openssl'
5
+ require 'base64'
6
+ require 'cgi'
7
+
8
+ module Juggernaut
9
+ class Client
10
+ include Juggernaut::Miscel
11
+
12
+ class UnverifiedClient < Juggernaut::JuggernautError #:nodoc:
13
+ end
14
+
15
+ attr_reader :id
16
+ attr_reader :connections
17
+ @@clients = []
18
+
19
+ class << self
20
+ # Actually does a find_or_create_by_id
21
+ def find_or_create(subscriber, request)
22
+ if client = find_by_id(request[:client_id])
23
+ client.add_new_connection(subscriber)
24
+ client.authenticate(request)
25
+ client
26
+ else
27
+ self.new(subscriber, request)
28
+ end
29
+ end
30
+
31
+
32
+ def add_client(client)
33
+ @@clients << client unless @@clients.include?(client)
34
+ end
35
+
36
+ # Client find methods
37
+
38
+ def find_all
39
+ @@clients
40
+ end
41
+
42
+ def find(&block)
43
+ @@clients.select(&block).uniq
44
+ end
45
+
46
+ def find_by_id(id)
47
+ find {|client| client.id == id }.first
48
+ end
49
+
50
+ def find_by_signature(signature)
51
+ # signature should be unique
52
+ find {|client|
53
+ client.connections.select {|connection| connection.signature == signature }.any?
54
+ }.first
55
+ end
56
+
57
+ def find_by_channels(channels)
58
+ find {|client|
59
+ client.has_channels?(channels)
60
+ }
61
+ end
62
+
63
+ def find_by_id_and_channels(id, channels)
64
+ find {|client|
65
+ client.has_channels?(channels) && client.id == id
66
+ }.first
67
+ end
68
+
69
+ def send_logouts_after_timeout
70
+ @@clients.each do |client|
71
+ if !client.alive? and client.give_up?
72
+ client.logout_request
73
+ @@clients.delete(client)
74
+ end
75
+ end
76
+ end
77
+
78
+ # Called when the server is shutting down
79
+ def send_logouts_to_all_clients
80
+ @@clients.each do |client|
81
+ client.logout_request
82
+ end
83
+ end
84
+ end
85
+
86
+ def initialize(subscriber, request)
87
+ @connections = []
88
+ @id = request[:client_id]
89
+ self.authenticate(request)
90
+ add_new_connection(subscriber)
91
+ end
92
+
93
+ def authenticate(request)
94
+ time = request[:time]
95
+ nonce = request[:nonce]
96
+ hmac = Base64.decode64(CGI.unescape(request[:hash]))
97
+ calculated_hmac = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, @id, "#{options[:client_secret_key]}:#{time}:#{nonce}:#{@id}")
98
+ raise UnverifiedClient unless hmac == calculated_hmac
99
+ end
100
+
101
+ def to_json
102
+ {
103
+ :client_id => @id,
104
+ }.to_json
105
+ end
106
+
107
+ def add_new_connection(subscriber)
108
+ @connections << subscriber
109
+ end
110
+
111
+ def subscription_request(channels)
112
+ return true unless options[:subscription_url]
113
+ post_request(options[:subscription_url], channels)
114
+ end
115
+
116
+ def logout_connection_request(channels)
117
+ return true unless options[:logout_connection_url]
118
+ post_request(options[:logout_connection_url], channels)
119
+ end
120
+
121
+ def logout_request
122
+ return true unless options[:logout_url]
123
+ post_request(options[:logout_url])
124
+ end
125
+
126
+ def send_message(msg, channels = nil)
127
+ @connections.each do |em|
128
+ em.broadcast(msg) if !channels or channels.empty? or em.has_channels?(channels)
129
+ end
130
+ end
131
+
132
+ def has_channels?(channels)
133
+ @connections.each do |em|
134
+ return true if em.has_channels?(channels)
135
+ end
136
+ false
137
+ end
138
+
139
+ def remove_channels!(channels)
140
+ @connections.each do |em|
141
+ em.remove_channels!(channels)
142
+ end
143
+ end
144
+
145
+ def alive?
146
+ @connections.select{|em| em.alive? }.any?
147
+ end
148
+
149
+ def give_up?
150
+ @connections.select {|em|
151
+ em.logout_timeout and Time.now > em.logout_timeout
152
+ }.any?
153
+ end
154
+
155
+ private
156
+
157
+ def post_request(url, channels = [])
158
+ uri = URI.parse(url)
159
+ uri.path = '/' if uri.path == ''
160
+ params = []
161
+ params << "client_id=#{id}" if id
162
+ channels.each {|chan| params << "channels[]=#{chan}" }
163
+ headers = {"User-Agent" => "Ruby/#{RUBY_VERSION}"}
164
+ begin
165
+ http = Net::HTTP.new(uri.host, uri.port)
166
+ if uri.scheme == 'https'
167
+ http.use_ssl = true
168
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
169
+ end
170
+ http.read_timeout = 5
171
+ resp, data = http.post(uri.path, params.join('&'), headers)
172
+ unless resp.is_a?(Net::HTTPOK)
173
+ return false
174
+ end
175
+ rescue => e
176
+ logger.debug("Bad request #{url.to_s}: #{e}")
177
+ return false
178
+ rescue Timeout::Error
179
+ logger.debug("#{url.to_s} timeout")
180
+ return false
181
+ end
182
+ true
183
+ end
184
+ end
185
+ 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 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,371 @@
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
+ @client.logout_connection_request(@channels) if @client # todo - should be called after timeout?
139
+ logger.debug "Lost client: #{@client.id}" if @client
140
+ mark_dead('Unbind called')
141
+ end
142
+
143
+ # As far as I'm aware, send_data
144
+ # never throws an exception
145
+ def publish(msg)
146
+ logger.debug "Sending msg: #{msg.to_s}"
147
+ logger.debug "To client: #{@client.id}" if @client
148
+ send_data(msg.to_s + CR)
149
+ end
150
+
151
+ # Connection methods
152
+
153
+ def broadcast(bdy)
154
+ msg = Juggernaut::Message.new(@current_msg_id += 1, bdy, self.signature)
155
+ @messages << msg if options[:store_messages]
156
+ publish(msg)
157
+ end
158
+
159
+ def mark_dead(reason = "Unknown error")
160
+ # Once dead, a client never recovers since a reconnection
161
+ # attempt would hook onto a new em instance. A client
162
+ # usually dies through an unbind
163
+ @connected = false
164
+ @logout_timeout = Time::now + (options[:timeout] || 5)
165
+ @status = "DEAD: %s: Could potentially logout at %s" %
166
+ [ reason, @logout_timeout ]
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
+ def broadcast_all_messages_from(msg_id, signature_id)
206
+ return unless msg_id or signature_id
207
+ client = Juggernaut::Client.find_by_signature(signature)
208
+ return if !client
209
+ msg_id = Integer(msg_id)
210
+ return if msg_id >= client.connections.select {|c| c == self }.current_msg_id
211
+ client.messages.select {|msg|
212
+ (msg_id..client.connections.select {|c| c == self }).include?(msg.id)
213
+ }.each {|msg| publish(msg) }
214
+ end
215
+
216
+ # todo - how should this be called - if at all?
217
+ def clean_up_old_messages(how_many_to_keep = 1000)
218
+ while @messages.length > how_many_to_keep
219
+ # We need to shift, as we want to remove the oldest first
220
+ @messages.shift
221
+ end
222
+ end
223
+
224
+ protected
225
+
226
+ # Commands
227
+
228
+ def broadcast_command
229
+ raise MalformedBroadcast, @request unless @request[:type]
230
+
231
+ raise UnauthorisedBroadcast, @request unless authenticate_broadcast_or_query
232
+
233
+ case @request[:type].to_sym
234
+ when :to_channels
235
+ # if channels is a blank array, sends to everybody!
236
+ broadcast_to_channels(@request[:body], @request[:channels])
237
+ when :to_clients
238
+ broadcast_needs :client_ids
239
+ @request[:client_ids].each do |client_id|
240
+ # if channels aren't empty, scopes broadcast to clients on those channels
241
+ broadcast_to_client(@request[:body], client_id, @request[:channels])
242
+ end
243
+ else
244
+ raise MalformedBroadcast, @request
245
+ end
246
+ end
247
+
248
+ def query_command
249
+ raise MalformedQuery, @request unless @request[:type]
250
+
251
+ raise UnauthorisedQuery, @request unless authenticate_broadcast_or_query
252
+
253
+ case @request[:type].to_sym
254
+ when :remove_channels_from_all_clients
255
+ query_needs :channels
256
+ clients = Juggernaut::Client.find_all
257
+ clients.each {|client| client.remove_channels!(@request[:channels]) }
258
+ when :remove_channels_from_client
259
+ query_needs :client_ids, :channels
260
+ @request[:client_ids].each do |client_id|
261
+ client = Juggernaut::Client.find_by_id(client_id)
262
+ client.remove_channels!(@request[:channels]) if client
263
+ end
264
+ when :show_clients
265
+ if @request[:client_ids] and @request[:client_ids].any?
266
+ clients = @request[:client_ids].collect{ |client_id| Client.find_by_id(client_id) }.compact.uniq
267
+ else
268
+ clients = Juggernaut::Client.find_all
269
+ end
270
+ publish clients.to_json
271
+ when :show_client
272
+ query_needs :client_id
273
+ publish Juggernaut::Client.find_by_id(@request[:client_id]).to_json
274
+ when :show_clients_for_channels
275
+ query_needs :channels
276
+ publish Juggernaut::Client.find_by_channels(@request[:channels]).to_json
277
+ else
278
+ raise MalformedQuery, @request
279
+ end
280
+ end
281
+
282
+ def subscribe_command
283
+ if channels = @request[:channels]
284
+ add_channels(channels)
285
+ end
286
+
287
+ @client = Juggernaut::Client.find_or_create(self, @request)
288
+
289
+ if !@client.subscription_request(@channels)
290
+ raise UnauthorisedSubscription, @client
291
+ end
292
+
293
+ Juggernaut::Client.add_client(@client)
294
+
295
+ if options[:store_messages]
296
+ broadcast_all_messages_from(@request[:last_msg_id], @request[:signature])
297
+ end
298
+ end
299
+
300
+ private
301
+
302
+ # Different broadcast types
303
+
304
+ def broadcast_to_channels(msg, channels = [])
305
+ Juggernaut::Client.find_all.each {|client| client.send_message(msg, channels) }
306
+ end
307
+
308
+ def broadcast_to_client(body, client_id, channels)
309
+ client = Juggernaut::Client.find_by_id(client_id)
310
+ client.send_message(body, channels) if client
311
+ end
312
+
313
+ # Helper methods
314
+
315
+ def broadcast_needs(*args)
316
+ args.each do |arg|
317
+ raise MalformedBroadcast, @request unless @request.has_key?(arg)
318
+ end
319
+ end
320
+
321
+ def subscribe_needs(*args)
322
+ args.each do |arg|
323
+ raise MalformedSubscribe, @request unless @request.has_key?(arg)
324
+ end
325
+ end
326
+
327
+ def query_needs(*args)
328
+ args.each do |arg|
329
+ raise MalformedQuery, @request unless @request.has_key?(arg)
330
+ end
331
+ end
332
+
333
+ def authenticate_broadcast_or_query
334
+ if options[:allowed_ips]
335
+ return true if options[:allowed_ips].include?(client_ip)
336
+ elsif !@request[:secret_key]
337
+ return true if broadcast_query_request
338
+ elsif options[:secret_key]
339
+ return true if @request[:secret_key] == options[:secret_key]
340
+ end
341
+ if !options[:allowed_ips] and !options[:secret_key] and !options[:broadcast_query_login_url]
342
+ return true
343
+ end
344
+ false
345
+ end
346
+
347
+ def broadcast_query_request
348
+ return false unless options[:broadcast_query_login_url]
349
+ url = URI.parse(options[:broadcast_query_login_url])
350
+ params = []
351
+ params << "client_id=#{@request[:client_id]}" if @request[:client_id]
352
+ params << "session_id=#{URI.escape(@request[:session_id])}" if @request[:session_id]
353
+ params << "type=#{@request[:type]}"
354
+ params << "command=#{@request[:command]}"
355
+ (@request[:channels] || []).each {|chan| params << "channels[]=#{chan}" }
356
+ url.query = params.join('&')
357
+ begin
358
+ open(url.to_s, "User-Agent" => "Ruby/#{RUBY_VERSION}")
359
+ rescue Timeout::Error
360
+ return false
361
+ rescue
362
+ return false
363
+ end
364
+ true
365
+ end
366
+
367
+ def client_ip
368
+ Socket.unpack_sockaddr_in(get_peername)[1]
369
+ end
370
+ end
371
+ 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,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: DefV-juggernaut
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.7.2
5
+ platform: ruby
6
+ authors:
7
+ - Alex MacCaw
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-24 00:00:00 -08:00
13
+ default_executable:
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
+ description:
34
+ email: info@eribium.org
35
+ executables:
36
+ - juggernaut
37
+ extensions: []
38
+
39
+ extra_rdoc_files: []
40
+
41
+ files:
42
+ - README.txt
43
+ - Manifest.txt
44
+ - Rakefile
45
+ - juggernaut.gemspec
46
+ - lib/juggernaut.rb
47
+ - lib/juggernaut/client.rb
48
+ - lib/juggernaut/message.rb
49
+ - lib/juggernaut/miscel.rb
50
+ - lib/juggernaut/runner.rb
51
+ - lib/juggernaut/server.rb
52
+ - lib/juggernaut/utils.rb
53
+ - bin/juggernaut
54
+ has_rdoc: false
55
+ homepage:
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ requirements: []
74
+
75
+ rubyforge_project:
76
+ rubygems_version: 1.2.0
77
+ signing_key:
78
+ specification_version: 2
79
+ summary: Juggernaut
80
+ test_files: []
81
+