juggernaut 0.5

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,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: