juggernaut 0.5

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