juggernaut 0.5.8 → 2.0.0.beta1

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.
@@ -1,368 +0,0 @@
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
- attr_reader :client
56
-
57
- # EM methods
58
-
59
- def post_init
60
- logger.debug "New client [#{client_ip}]"
61
- @client = nil
62
- @channels = []
63
- @current_msg_id = 0
64
- @connected = true
65
- @logout_timeout = nil
66
- @buffer = ''
67
- end
68
-
69
- # Juggernaut packets are terminated with "\0"
70
- # so we need to buffer the data until we find the
71
- # terminating "\0"
72
- def receive_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[:public_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
- # For debugging
114
- @request[:ip] = client_ip
115
-
116
- @request[:channels] = (@request[:channels] || []).compact.select {|c| !!c && c != '' }.uniq
117
-
118
- if @request[:client_ids]
119
- @request[:client_ids] = @request[:client_ids].to_a.compact.select {|c| !!c && c != '' }.uniq
120
- end
121
-
122
- case @request[:command].to_sym
123
- when :broadcast
124
- broadcast_command
125
- when :subscribe
126
- subscribe_command
127
- when :query
128
- query_command
129
- when :noop
130
- noop_command
131
- else
132
- raise InvalidCommand, @request
133
- end
134
-
135
- rescue JuggernautError => e
136
- logger.error("#{e} - #{e.message.inspect}")
137
- close_connection
138
- # So as to stop em quitting
139
- rescue => e
140
- logger ? logger.error(e) : puts(e)
141
- end
142
-
143
- def unbind
144
- if @client
145
- # todo - should be called after timeout?
146
- @client.logout_connection_request(@channels)
147
- logger.debug "Lost client #{@client.friendly_id}"
148
- end
149
- mark_dead('Unbind called')
150
- end
151
-
152
- # As far as I'm aware, send_data
153
- # never throws an exception
154
- def publish(msg)
155
- logger.debug "Sending msg: #{msg.to_s} to client #{@request[:client_id]} (session #{@request[:session_id]})"
156
- send_data(msg.to_s + CR)
157
- end
158
-
159
- # Connection methods
160
-
161
- def broadcast(bdy)
162
- msg = Juggernaut::Message.new(@current_msg_id += 1, bdy, self.signature)
163
- publish(msg)
164
- end
165
-
166
- def mark_dead(reason = "Unknown error")
167
- # Once dead, a client never recovers since a reconnection
168
- # attempt would hook onto a new em instance. A client
169
- # usually dies through an unbind
170
- @connected = false
171
- @client.remove_connection(self) if @client
172
- end
173
-
174
- def alive?
175
- @connected == true
176
- end
177
-
178
- def has_channels?(channels)
179
- channels.each {|channel|
180
- return true if has_channel?(channel)
181
- }
182
- false
183
- end
184
-
185
- def has_channel?(channel)
186
- @channels.include?(channel)
187
- end
188
-
189
- def add_channel(chan_name)
190
- return if !chan_name or chan_name == ''
191
- @channels << chan_name unless has_channel?(chan_name)
192
- end
193
-
194
- def add_channels(chan_names)
195
- chan_names.to_a.each do |chan_name|
196
- add_channel(chan_name)
197
- end
198
- end
199
-
200
- def remove_channel!(chan_name)
201
- @channels.delete(chan_name)
202
- end
203
-
204
- def remove_channels!(chan_names)
205
- chan_names.to_a.each do |chan_name|
206
- remove_channel!(chan_name)
207
- end
208
- end
209
-
210
- protected
211
-
212
- # Commands
213
-
214
- def broadcast_command
215
- raise MalformedBroadcast, @request unless @request[:type]
216
-
217
- raise UnauthorisedBroadcast, @request unless authenticate_broadcast_or_query
218
-
219
- case @request[:type].to_sym
220
- when :to_channels
221
- # if channels is a blank array, sends to everybody!
222
- broadcast_to_channels(@request[:body], @request[:channels])
223
- when :to_clients
224
- broadcast_needs :client_ids
225
- @request[:client_ids].each do |client_id|
226
- # if channels aren't empty, scopes broadcast to clients on those channels
227
- broadcast_to_client(@request[:body], client_id, @request[:channels])
228
- end
229
- else
230
- raise MalformedBroadcast, @request
231
- end
232
- end
233
-
234
- def query_command
235
- raise MalformedQuery, @request unless @request[:type]
236
-
237
- raise UnauthorisedQuery, @request unless authenticate_broadcast_or_query
238
-
239
- case @request[:type].to_sym
240
- when :remove_channels_from_all_clients
241
- query_needs :channels
242
- clients = Juggernaut::Client.find_all
243
- clients.each {|client| client.remove_channels!(@request[:channels]) }
244
- when :remove_channels_from_client
245
- query_needs :client_ids, :channels
246
- @request[:client_ids].each do |client_id|
247
- client = Juggernaut::Client.find_by_id(client_id)
248
- client.remove_channels!(@request[:channels]) if client
249
- end
250
- when :show_channels_for_client
251
- query_needs :client_id
252
- if client = Juggernaut::Client.find_by_id(@request[:client_id])
253
- publish client.channels.to_json
254
- else
255
- publish nil.to_json
256
- end
257
- when :show_clients
258
- if @request[:client_ids] and @request[:client_ids].any?
259
- clients = @request[:client_ids].collect{ |client_id| Client.find_by_id(client_id) }.compact.uniq
260
- else
261
- clients = Juggernaut::Client.find_all
262
- end
263
- publish clients.to_json
264
- when :show_client
265
- query_needs :client_id
266
- publish Juggernaut::Client.find_by_id(@request[:client_id]).to_json
267
- when :show_clients_for_channels
268
- query_needs :channels
269
- publish Juggernaut::Client.find_by_channels(@request[:channels]).to_json
270
- else
271
- raise MalformedQuery, @request
272
- end
273
- end
274
-
275
- def noop_command
276
- logger.debug "NOOP"
277
- end
278
-
279
- def subscribe_command
280
- logger.debug "SUBSCRIBE: #{@request.inspect}"
281
-
282
- if channels = @request[:channels]
283
- add_channels(channels)
284
- end
285
-
286
- @client = Juggernaut::Client.find_or_create(self, @request)
287
-
288
- if !@client.subscription_request(@channels)
289
- raise UnauthorisedSubscription, @client
290
- end
291
-
292
- if options[:store_messages]
293
- @client.send_queued_messages(self)
294
- end
295
- end
296
-
297
- private
298
-
299
- # Different broadcast types
300
-
301
- def broadcast_to_channels(msg, channels = [])
302
- Juggernaut::Client.find_all.each {|client| client.send_message(msg, channels) }
303
- end
304
-
305
- def broadcast_to_client(body, client_id, channels)
306
- client = Juggernaut::Client.find_by_id(client_id)
307
- client.send_message(body, channels) if client
308
- end
309
-
310
- # Helper methods
311
-
312
- def broadcast_needs(*args)
313
- args.each do |arg|
314
- raise MalformedBroadcast, @request unless @request.has_key?(arg)
315
- end
316
- end
317
-
318
- def subscribe_needs(*args)
319
- args.each do |arg|
320
- raise MalformedSubscribe, @request unless @request.has_key?(arg)
321
- end
322
- end
323
-
324
- def query_needs(*args)
325
- args.each do |arg|
326
- raise MalformedQuery, @request unless @request.has_key?(arg)
327
- end
328
- end
329
-
330
- def authenticate_broadcast_or_query
331
- if options[:allowed_ips]
332
- return true if options[:allowed_ips].include?(client_ip)
333
- elsif !@request[:secret_key]
334
- return true if broadcast_query_request
335
- elsif options[:secret_key]
336
- return true if @request[:secret_key] == options[:secret_key]
337
- end
338
- if !options[:allowed_ips] and !options[:secret_key] and !options[:broadcast_query_login_url]
339
- return true
340
- end
341
- false
342
- end
343
-
344
- def broadcast_query_request
345
- return false unless options[:broadcast_query_login_url]
346
- url = URI.parse(options[:broadcast_query_login_url])
347
- params = []
348
- params << "client_id=#{@request[:client_id]}" if @request[:client_id]
349
- params << "session_id=#{URI.escape(@request[:session_id])}" if @request[:session_id]
350
- params << "type=#{@request[:type]}"
351
- params << "command=#{@request[:command]}"
352
- (@request[:channels] || []).each {|chan| params << "channels[]=#{chan}" }
353
- url.query = params.join('&')
354
- begin
355
- open(url.to_s, "User-Agent" => "Ruby/#{RUBY_VERSION}")
356
- rescue Timeout::Error
357
- return false
358
- rescue
359
- return false
360
- end
361
- true
362
- end
363
-
364
- def client_ip
365
- Socket.unpack_sockaddr_in(get_peername)[1] rescue nil
366
- end
367
- end
368
- end
@@ -1,11 +0,0 @@
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
@@ -1,176 +0,0 @@
1
-
2
- $:.unshift "../lib"
3
- require "juggernaut"
4
- require "test/unit"
5
- require "shoulda"
6
- require "mocha"
7
-
8
- class TestClient < Test::Unit::TestCase
9
-
10
- CONFIG = File.join(File.dirname(__FILE__), "files", "juggernaut.yml")
11
-
12
- class DummySubscriber; end
13
-
14
- EXAMPLE_URL = "http://localhost:5000/callbacks/example"
15
- SECURE_URL = "https://localhost:5000/callbacks/example"
16
-
17
- context "Client" do
18
-
19
- setup do
20
- Juggernaut.options = {
21
- :logout_connection_url => "http://localhost:5000/callbacks/logout_connection",
22
- :logout_url => "http://localhost:5000/callbacks/logout",
23
- :subscription_url => "http://localhost:5000/callbacks/subscription"
24
- }
25
- @s1 = DummySubscriber.new
26
- @request = {
27
- :client_id => "jonny",
28
- :session_id => rand(1_000_000).to_s(16)
29
- }
30
- @client = Juggernaut::Client.find_or_create(@s1, @request)
31
- end
32
-
33
- teardown do
34
- Juggernaut::Client.reset!
35
- end
36
-
37
- should "have correct JSON representation" do
38
- assert_nothing_raised do
39
- json = {
40
- "client_id" => "jonny",
41
- "num_connections" => 1,
42
- "session_id" => @request[:session_id]
43
- }
44
- assert_equal json, JSON.parse(@client.to_json)
45
- end
46
- end
47
-
48
- should "return the client based on subscriber's signature" do
49
- @s1.stubs(:signature).returns("012345")
50
- assert_equal @client, Juggernaut::Client.find_by_signature("012345")
51
- end
52
-
53
- should "return the client based on client ID and channel list" do
54
- @client.stubs(:has_channels?).with(%w(a couple of channels)).returns(true)
55
- assert_equal @client, Juggernaut::Client.find_by_id_and_channels("jonny", %w(a couple of channels))
56
- assert_nil Juggernaut::Client.find_by_id_and_channels("peter", %w(a couple of channels))
57
- @client.stubs(:has_channels?).with(%w(something else)).returns(false)
58
- assert_nil Juggernaut::Client.find_by_id_and_channels("jonny", %w(something else))
59
- end
60
-
61
- should "automatically be registered, and can unregister" do
62
- assert @client.send(:registered?)
63
- assert_equal @client, @client.send(:unregister)
64
- assert_equal false, @client.send(:registered?)
65
- end
66
-
67
- should "be alive if at least one subscriber is alive" do
68
- @s1.stubs(:alive?).returns(true)
69
- @s2 = DummySubscriber.new
70
- @client.add_new_connection(@s2)
71
- @s2.stubs(:alive?).returns(false)
72
- assert @client.alive?
73
- end
74
-
75
- should "not be alive if no subscriber is alive" do
76
- @s1.stubs(:alive?).returns(false)
77
- @s2 = DummySubscriber.new
78
- @client.add_new_connection(@s2)
79
- @s2.stubs(:alive?).returns(false)
80
- assert_equal false, @client.alive?
81
- end
82
-
83
- should "not give up if within the timeout period" do
84
- Juggernaut.options[:timeout] = 10
85
- @s1.stubs(:alive?).returns(false)
86
- @client.send(:reset_logout_timeout!)
87
- assert_equal false, @client.give_up?
88
- end
89
-
90
- should "not give up if at least one subscriber is alive" do
91
- Juggernaut.options[:timeout] = 0
92
- @s1.stubs(:alive?).returns(true)
93
- @client.send(:reset_logout_timeout!)
94
- assert_equal false, @client.give_up?
95
- end
96
-
97
- should "send logouts after timeout" do
98
- Juggernaut.options[:timeout] = 0
99
- @s1.stubs(:alive?).returns(false)
100
- @client.send(:reset_logout_timeout!)
101
- @client.expects(:logout_request).once
102
- Juggernaut::Client.send_logouts_after_timeout
103
- end
104
-
105
- %w(subscription logout_connection).each do |type|
106
-
107
- context "#{type} request" do
108
-
109
- should "post to the correct URL" do
110
- @client.expects(:post_request).with(Juggernaut.options[:"#{type}_url"], %w(master), :timeout => 5).returns(true)
111
- assert_equal true, @client.send("#{type}_request", %w(master))
112
- end
113
-
114
- should "not raise exceptions if posting raises an exception" do
115
- @client.expects(:post_request).with(Juggernaut.options[:"#{type}_url"], %w(master), :timeout => 5).returns(false)
116
- assert_nothing_raised {
117
- assert_equal false, @client.send("#{type}_request", %w(master))
118
- }
119
- end
120
-
121
- end
122
-
123
- end
124
-
125
- context "post to URL" do
126
-
127
- should "return true when successful" do
128
- Net::HTTP.any_instance.
129
- expects(:post).
130
- with("/callbacks/example", "client_id=jonny&session_id=#{@request[:session_id]}&channels[]=master&channels[]=slave", {"User-Agent" => "Ruby/#{RUBY_VERSION}"}).
131
- returns([Net::HTTPOK.new('1.1', '200', 'OK'), ''])
132
- assert_equal true, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
133
- end
134
-
135
- should "return false on an internal server error" do
136
- Net::HTTP.any_instance.expects(:post).returns([Net::HTTPInternalServerError.new('1.1', '500', 'Internal Server Error'), ''])
137
- assert_equal false, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
138
- end
139
-
140
- should "return false when a runtime error is caught" do
141
- Net::HTTP.any_instance.expects(:post).raises(RuntimeError)
142
- assert_equal false, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
143
- end
144
-
145
- should "return false when callback times out" do
146
- Net::HTTP.any_instance.expects(:post).raises(Timeout::Error)
147
- assert_equal false, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
148
- end
149
-
150
- context "using a secure URL" do
151
-
152
- should "return true when successful" do
153
- Net::HTTP.any_instance.expects(:post).returns([Net::HTTPOK.new('1.1', '200', 'OK'), ''])
154
- Net::HTTP.any_instance.expects(:use_ssl=).with(true).returns(true)
155
- assert_equal true, @client.send(:post_request, SECURE_URL, %w(master slave))
156
- end
157
-
158
- end
159
-
160
- end
161
-
162
- context "channel list" do
163
-
164
- should "be the unique list of all channels in the subscribers" do
165
- @s1.stubs(:channels).returns(%w(master slave1))
166
- @s2 = DummySubscriber.new
167
- @s2.stubs(:channels).returns(%w(master slave2))
168
- @client.add_new_connection(@s2)
169
- assert_same_elements %w(master slave1 slave2), @client.channels
170
- end
171
-
172
- end
173
-
174
- end
175
-
176
- end