juggernaut_rails 0.5.9

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,95 @@
1
+ Juggernaut
2
+ by Alex MacCaw http://www.eribium.org
3
+ minor fixes and unit tests by Ripta Pasay
4
+ bundled by Karl Varga
5
+
6
+ == ABOUT
7
+
8
+ I've created this Gem to simplify the Juggernaut installation process and to improve its integration with Rails. Previously you needed to install both the Juggernaut Gem and the Rails plugin, and things like log paths just weren't following Rails conventions.
9
+
10
+ This Gem includes Rake tasks for starting and stopping the Jug server and allows Jug server configurations based on the Rails environment, as well as better support for jQuery. For instance when we deploy to production we use Capistrano so we want our production PID to be in the <tt>shared/pids/</tt> directory, while in development we stick with the default: <tt>tmp/pids/</tt>. Now your <tt>config/juggernaut.yml</tt> configuration can handle that.
11
+
12
+ At the time of writing (Oct 29 2009) this Gem is based on (the latest) version 0.5.8 of Alex MacCaw's Gem and includes the latest <tt>juggernaut_plugin</tt>.
13
+
14
+ == INSTALL
15
+
16
+ Install the gem
17
+
18
+ # This requires GemCutter. To get started with GemCutter:
19
+ # gem install gemcutter
20
+ # gem tumble
21
+ gem install juggernaut_rails
22
+
23
+ Now add the gem to your application and include Juggernaut's Rake tasks in your app's Rakefile:
24
+
25
+ # config/environment.rb
26
+ config.gem 'juggernaut_rails'
27
+
28
+ # Rakefile
29
+ begin
30
+ require 'juggernaut/rails/tasks'
31
+ rescue Exception => e
32
+ puts "Warning, couldn't load gem tasks: #{e.message}! Skipping..."
33
+ end
34
+
35
+ Copy the required media files into your application:
36
+
37
+ rake juggernaut:install
38
+
39
+ If you are using jQuery, you will need the jQuery JS files as well:
40
+
41
+ rake juggernaut:install:jquery
42
+
43
+ Include the Juggernaut scripts in your layout and create a simple Jug view:
44
+
45
+ # app/views/layout/application.html.erb
46
+ <%= javascript_include_tag :defaults, :juggernaut %>
47
+
48
+ # If you're using jQuery (note: the jQuery library is not included in Juggernaut's install)
49
+ # <%= javascript_include_tag 'jquery', :juggernaut_jquery %>
50
+
51
+ # app/views/your/view.html.erb
52
+ <%= juggernaut %>
53
+
54
+ Start your Juggernaut server. To control the server use these Rake tasks:
55
+
56
+ rake juggernaut:start
57
+ rake juggernaut:stop
58
+
59
+ Start your Rails application, open your browser and visit the view we created earlier.
60
+ Now in your <tt>script/console</tt> execute the following:
61
+
62
+ Juggernaut.send_to_all("alert('hi from juggernaut')")
63
+
64
+ You should get the alert message in your browser, or in the console log if you are using FireFox's FireBug.
65
+
66
+ Take a look at the configuration options in <tt>config/juggernaut.yml</tt> and <tt>config/juggernaut_hosts.yml</tt>. Look at your debugging log output in <tt>log/juggernaut.log</tt> and the PID of the running Jug server in <tt>tmp/pids/juggernaut.5001.pid</tt>. By default debugging is turned on when running in <tt>development</tt> mode.
67
+
68
+ == DESCRIPTION:
69
+ See Plugin README:
70
+ http://juggernaut.rubyforge.org/svn/trunk/juggernaut/README
71
+
72
+ == LICENSE:
73
+
74
+ (The MIT License)
75
+
76
+ Copyright (c) 2008 Alex MacCaw
77
+
78
+ Permission is hereby granted, free of charge, to any person obtaining
79
+ a copy of this software and associated documentation files (the
80
+ 'Software'), to deal in the Software without restriction, including
81
+ without limitation the rights to use, copy, modify, merge, publish,
82
+ distribute, sublicense, and/or sell copies of the Software, and to
83
+ permit persons to whom the Software is furnished to do so, subject to
84
+ the following conditions:
85
+
86
+ The above copyright notice and this permission notice shall be
87
+ included in all copies or substantial portions of the Software.
88
+
89
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
90
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
91
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
92
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
93
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
94
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
95
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gemspec|
7
+ gemspec.name = "juggernaut_rails"
8
+ gemspec.summary = "A push server written in Ruby with Rails integration."
9
+ gemspec.email = "info@eribium.org"
10
+ gemspec.homepage = "http://juggernaut.rubyforge.org"
11
+ gemspec.rubyforge_project = 'juggernaut'
12
+ gemspec.description = <<-END
13
+ The Juggernaut Gem for Ruby on Rails aims to revolutionize your Rails app by letting the server initiate a connection and push data to the client. In other words your app can have a real time connection to the server with the advantage of instant updates. Although the obvious use of this is for chat, the most exciting prospect for me is collaborative cms and wikis.
14
+
15
+ This Gem bundles Alex MacCaw's Juggernaut Gem and Rails plugin into one, and extends its Rails intergration for a simpler install and setup.
16
+ END
17
+ gemspec.authors = ["Alex MacCaw"]
18
+ gemspec.require_paths = ['lib', 'lib/juggernaut']
19
+ gemspec.files = FileList['[A-Z]*',
20
+ '{bin,lib,media,rails,test,rails}/**/*']
21
+ gemspec.add_dependency 'eventmachine', '>= 0.10.0'
22
+ gemspec.add_dependency 'json', '>= 1.1.2'
23
+ end
24
+ Jeweler::GemcutterTasks.new
25
+ rescue LoadError
26
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
27
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.9
data/bin/juggernaut ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'juggernaut')
4
+ Juggernaut::Runner.run
data/lib/juggernaut.rb ADDED
@@ -0,0 +1,158 @@
1
+ require 'rubygems'
2
+ require 'logger'
3
+ require 'eventmachine'
4
+ require 'json'
5
+ $:.unshift(File.dirname(__FILE__))
6
+
7
+ module Juggernaut
8
+ VERSION = '0.5.8'
9
+
10
+ class JuggernautError < StandardError #:nodoc:
11
+ end
12
+
13
+ @@options = {}
14
+
15
+ DEFAULT_CONFIG_FILE = <<-EOF
16
+ # ======================
17
+ # Juggernaut Options
18
+ # ======================
19
+
20
+ # === Subscription authentication ===
21
+ # Leave all subscription options uncommented to allow anyone to subscribe.
22
+
23
+ # If specified, subscription_url is called everytime a client subscribes.
24
+ # Parameters passed are: session_id, client_id and an array of channels.
25
+ #
26
+ # The server should check that the session_id matches up to the client_id
27
+ # and that the client is allowed to access the specified channels.
28
+ #
29
+ # If a status code other than 200 is encountered, the subscription_request fails
30
+ # and the client is disconnected.
31
+ #
32
+ # :subscription_url: http://localhost:3000/sessions/juggernaut_subscription
33
+
34
+ # === Broadcast and query authentication ===
35
+ # Leave all broadcast/query options uncommented to allow anyone to broadcast/query.
36
+ #
37
+ # Broadcast authentication in a production environment is very importantant since broadcasters
38
+ # can execute JavaScript on subscribed clients, leaving you vulnerable to cross site scripting
39
+ # attacks if broadcasters aren't authenticated.
40
+
41
+ # 1) Via IP address
42
+ #
43
+ # If specified, if a client has an ip that is specified in allowed_ips, than it is automatically
44
+ # authenticated, even if a secret_key isn't provided.
45
+ #
46
+ # This is the recommended method for broadcast authentication.
47
+ #
48
+ :allowed_ips:
49
+ - 127.0.0.1
50
+ # - 192.168.0.1
51
+
52
+ # 2) Via HTTP request
53
+ #
54
+ # If specified, if a client attempts a broadcast/query, without a secret_key or using an IP
55
+ # no included in allowed_ips, then broadcast_query_login_url will be called.
56
+ # Parameters passed, if given, are: session_id, client_id, channels and type.
57
+ #
58
+ # The server should check that the session_id matches up to the client id, and the client
59
+ # is allowed to perform that particular type of broadcast/query.
60
+ #
61
+ # If a status code other than 200 is encountered, the broadcast_query_login_url fails
62
+ # and the client is disconnected.
63
+ #
64
+ # :broadcast_query_login_url: http://localhost:3000/sessions/juggernaut_broadcast
65
+
66
+ # 3) Via shared secret key
67
+ #
68
+ # This secret key must be sent with any query/broadcast commands.
69
+ # It must be the same as the one in the Rails config file.
70
+ #
71
+ # You shouldn't authenticate broadcasts from subscribed clients using this method
72
+ # since the secret_key will be easily visible in the page (and not so secret any more)!
73
+ #
74
+ # :secret_key: your_secret_key_here
75
+
76
+ # == Subscription Logout ==
77
+
78
+ # If specified, logout_connection_url is called everytime a specific connection from a subscribed client disconnects.
79
+ # Parameters passed are session_id, client_id and an array of channels specific to that connection.
80
+ #
81
+ # :logout_connection_url: http://localhost:3000/sessions/juggernaut_connection_logout
82
+
83
+ # Logout url is called when all connections from a subscribed client are closed.
84
+ # Parameters passed are session_id and client_id.
85
+ #
86
+ # :logout_url: http://localhost:3000/sessions/juggernaut_logout
87
+
88
+ # === Miscellaneous ===
89
+
90
+ # timeout defaults to 10. A timeout is the time between when a client closes a connection
91
+ # and a logout_request or logout_connection_request is made. The reason for this is that a client
92
+ # may only temporarily be disconnected, and may attempt a reconnect very soon.
93
+ #
94
+ # :timeout: 10
95
+
96
+ # store_messages defaults to false. If this option is true, messages send to connections will be stored.
97
+ # This is useful since a client can then receive broadcasted message that it has missed (perhaps it was disconnected).
98
+ #
99
+ # :store_messages: false
100
+
101
+ # === Server ===
102
+
103
+ # Host defaults to "0.0.0.0". You shouldn't need to change this.
104
+ # :host: 0.0.0.0
105
+
106
+ # Port is mandatory
107
+ :port: 5001
108
+
109
+ # Defaults to value of :port. If you are doing port forwarding you'll need to configure this to the same
110
+ # value as :public_port in the juggernaut_hosts.yml file
111
+ # :public_port: 5001
112
+
113
+ EOF
114
+
115
+ class << self
116
+ def options
117
+ @@options
118
+ end
119
+
120
+ def options=(val)
121
+ @@options = val
122
+ end
123
+
124
+ def logger
125
+ return @@logger if defined?(@@logger) && !@@logger.nil?
126
+ FileUtils.mkdir_p(File.dirname(log_path))
127
+ @@logger = Logger.new(log_path)
128
+ @@logger.level = Logger::INFO if options[:debug] == false
129
+ @@logger
130
+ rescue
131
+ @@logger = Logger.new(STDOUT)
132
+ end
133
+
134
+ def logger=(logger)
135
+ @@logger = logger
136
+ end
137
+
138
+ def log_path
139
+ options[:log_path] || File.join(%w( / var run juggernaut.log ))
140
+ end
141
+
142
+ def pid_path
143
+ options[:pid_path] || File.join(%w( / var run ), "juggernaut.#{options[:port]}.pid" )
144
+ end
145
+
146
+ def config_path
147
+ options[:config_path] || File.join(%w( / var run juggernaut.yml ))
148
+ end
149
+
150
+ end
151
+ end
152
+
153
+ require 'juggernaut/utils'
154
+ require 'juggernaut/miscel'
155
+ require 'juggernaut/message'
156
+ require 'juggernaut/client'
157
+ require 'juggernaut/server'
158
+ require 'juggernaut/runner'
@@ -0,0 +1,263 @@
1
+ require 'timeout'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'uri'
5
+
6
+ module Juggernaut
7
+ class Client
8
+ include Juggernaut::Miscel
9
+
10
+ @@clients = [ ]
11
+
12
+ attr_reader :id
13
+ attr_accessor :session_id
14
+ attr_reader :connections
15
+
16
+ class << self
17
+ # Actually does a find_or_create_by_id
18
+ def find_or_create(subscriber, request)
19
+ if client = find_by_id(request[:client_id])
20
+ client.session_id = request[:session_id]
21
+ client.add_new_connection(subscriber)
22
+ client
23
+ else
24
+ self.new(subscriber, request)
25
+ end
26
+ end
27
+
28
+ # Client find methods
29
+ def find_all
30
+ @@clients
31
+ end
32
+
33
+ def find(&block)
34
+ @@clients.select(&block).uniq
35
+ end
36
+
37
+ def find_by_id(id)
38
+ find { |client| client.id == id }.first
39
+ end
40
+
41
+ def find_by_signature(signature)
42
+ # signature should be unique
43
+ find do |client|
44
+ client.connections.select { |connection| connection.signature == signature }.any?
45
+ end.first
46
+ end
47
+
48
+ def find_by_channels(channels)
49
+ find do |client|
50
+ client.has_channels?(channels)
51
+ end
52
+ end
53
+
54
+ def find_by_id_and_channels(id, channels)
55
+ find do |client|
56
+ client.has_channels?(channels) && client.id == id
57
+ end.first
58
+ end
59
+
60
+ def send_logouts_after_timeout
61
+ @@clients.each do |client|
62
+ if client.give_up?
63
+ client.logout_request
64
+ end
65
+ end
66
+ end
67
+
68
+ # Called when the server is shutting down
69
+ def send_logouts_to_all_clients
70
+ @@clients.each do |client|
71
+ client.logout_request
72
+ end
73
+ end
74
+
75
+ def reset!
76
+ @@clients.clear
77
+ end
78
+
79
+ def register_client(client)
80
+ @@clients << client unless @@clients.include?(client)
81
+ end
82
+
83
+ def client_registered?(client)
84
+ @@clients.include?(client)
85
+ end
86
+
87
+ def unregister_client(client)
88
+ @@clients.delete(client)
89
+ end
90
+ end
91
+
92
+ def initialize(subscriber, request)
93
+ @connections = []
94
+ @id = request[:client_id]
95
+ @session_id = request[:session_id]
96
+ @messages = []
97
+ @logout_timeout = 0
98
+ self.register
99
+ add_new_connection(subscriber)
100
+ end
101
+
102
+ def to_json
103
+ {
104
+ :client_id => @id,
105
+ :num_connections => @connections.size,
106
+ :session_id => @session_id
107
+ }.to_json
108
+ end
109
+
110
+ def add_new_connection(subscriber)
111
+ @connections << subscriber
112
+ end
113
+
114
+ def friendly_id
115
+ if self.id
116
+ "with ID #{self.id}"
117
+ else
118
+ "session #{self.session_id}"
119
+ end
120
+ end
121
+
122
+ def subscription_request(channels)
123
+ return true unless options[:subscription_url]
124
+ post_request(options[:subscription_url], channels, :timeout => options[:post_request_timeout] || 5)
125
+ end
126
+
127
+ def logout_connection_request(channels)
128
+ return true unless options[:logout_connection_url]
129
+ post_request(options[:logout_connection_url], channels, :timeout => options[:post_request_timeout] || 5)
130
+ end
131
+
132
+ def logout_request
133
+ self.unregister
134
+ logger.debug("Timed out client #{friendly_id}")
135
+ return true unless options[:logout_url]
136
+ post_request(options[:logout_url], [ ], :timeout => options[:post_request_timeout] || 5)
137
+ end
138
+
139
+ def remove_connection(connection)
140
+ @connections.delete(connection)
141
+ self.reset_logout_timeout!
142
+ self.logout_request if self.give_up?
143
+ end
144
+
145
+ def send_message(msg, channels = nil)
146
+ store_message(msg, channels) if options[:store_messages]
147
+ send_message_to_connections(msg, channels)
148
+ end
149
+
150
+ # Send messages that are queued up for this particular client.
151
+ # Messages are only queued for previously-connected clients.
152
+ def send_queued_messages(connection)
153
+ self.expire_queued_messages!
154
+
155
+ # Weird looping because we don't want to send messages that get
156
+ # added to the array after we start iterating (since those will
157
+ # get sent to the client anyway).
158
+ @length = @messages.length
159
+
160
+ logger.debug("Sending #{@length} queued message(s) to client #{friendly_id}")
161
+
162
+ @length.times do |id|
163
+ message = @messages[id]
164
+ send_message_to_connection(connection, message[:message], message[:channels])
165
+ end
166
+ end
167
+
168
+ def channels
169
+ @connections.collect { |em| em.channels }.flatten.uniq
170
+ end
171
+
172
+ def has_channels?(channels)
173
+ @connections.each do |em|
174
+ return true if em.has_channels?(channels)
175
+ end
176
+ false
177
+ end
178
+
179
+ def remove_channels!(channels)
180
+ @connections.each do |em|
181
+ em.remove_channels!(channels)
182
+ end
183
+ end
184
+
185
+ def alive?
186
+ @connections.select { |em| em.alive? }.any?
187
+ end
188
+
189
+ # This client is only dead if there are no connections and we are
190
+ # past the timeout (if we are within the timeout, the user could
191
+ # just be doing a page reload or going to a new page)
192
+ def give_up?
193
+ !alive? and (Time.now > @logout_timeout)
194
+ end
195
+
196
+ protected
197
+
198
+ def register
199
+ self.class.register_client(self)
200
+ end
201
+
202
+ def registered?
203
+ self.class.client_registered?(self)
204
+ end
205
+
206
+ def unregister
207
+ self.class.unregister_client(self)
208
+ end
209
+
210
+ def post_request(url, channels = [ ], options = { })
211
+ uri = URI.parse(url)
212
+ uri.path = '/' if uri.path == ''
213
+ params = []
214
+ params << "client_id=#{id}" if id
215
+ params << "session_id=#{session_id}" if session_id
216
+ channels.each {|chan| params << "channels[]=#{chan}" }
217
+ headers = {"User-Agent" => "Ruby/#{RUBY_VERSION}"}
218
+ begin
219
+ http = Net::HTTP.new(uri.host, uri.port)
220
+ if uri.scheme == 'https'
221
+ http.use_ssl = true
222
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
223
+ end
224
+ http.read_timeout = options[:timeout] || 5
225
+ resp, data = http.post(uri.path, params.join('&'), headers)
226
+ return resp.is_a?(Net::HTTPOK)
227
+ rescue => e
228
+ logger.error("Bad request #{url.to_s} (#{e.class}: #{e.message})")
229
+ return false
230
+ rescue Timeout::Error
231
+ logger.error("#{url.to_s} timeout")
232
+ return false
233
+ end
234
+ end
235
+
236
+ def send_message_to_connections(msg, channels)
237
+ @connections.each do |connection|
238
+ send_message_to_connection(connection, msg, channels)
239
+ end
240
+ end
241
+
242
+ def send_message_to_connection(connection, msg, channels)
243
+ connection.broadcast(msg) if !channels or channels.empty? or connection.has_channels?(channels)
244
+ end
245
+
246
+ # Queued messages are stored until a timeout is reached which is the
247
+ # same as the connection timeout. This takes care of messages that
248
+ # come in between page loads or ones that come in right when you are
249
+ # clicking off one page and loading the next one.
250
+ def store_message(msg, channels)
251
+ self.expire_queued_messages!
252
+ @messages << { :channels => channels, :message => msg, :timeout => Time.now + options[:timeout] }
253
+ end
254
+
255
+ def expire_queued_messages!
256
+ @messages.reject! { |message| Time.now > message[:timeout] }
257
+ end
258
+
259
+ def reset_logout_timeout!
260
+ @logout_timeout = Time.now + options[:timeout]
261
+ end
262
+ end
263
+ end