bushido-faye 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/History.txt +247 -0
  2. data/README.rdoc +92 -0
  3. data/lib/faye-browser-min.js +1 -0
  4. data/lib/faye.rb +121 -0
  5. data/lib/faye/adapters/rack_adapter.rb +209 -0
  6. data/lib/faye/engines/connection.rb +60 -0
  7. data/lib/faye/engines/memory.rb +112 -0
  8. data/lib/faye/engines/proxy.rb +111 -0
  9. data/lib/faye/error.rb +49 -0
  10. data/lib/faye/mixins/logging.rb +47 -0
  11. data/lib/faye/mixins/publisher.rb +30 -0
  12. data/lib/faye/mixins/timeouts.rb +22 -0
  13. data/lib/faye/protocol/channel.rb +124 -0
  14. data/lib/faye/protocol/client.rb +378 -0
  15. data/lib/faye/protocol/extensible.rb +43 -0
  16. data/lib/faye/protocol/grammar.rb +58 -0
  17. data/lib/faye/protocol/publication.rb +5 -0
  18. data/lib/faye/protocol/server.rb +282 -0
  19. data/lib/faye/protocol/subscription.rb +24 -0
  20. data/lib/faye/transport/http.rb +76 -0
  21. data/lib/faye/transport/local.rb +22 -0
  22. data/lib/faye/transport/transport.rb +115 -0
  23. data/lib/faye/transport/web_socket.rb +99 -0
  24. data/lib/faye/util/namespace.rb +20 -0
  25. data/spec/browser.html +45 -0
  26. data/spec/encoding_helper.rb +7 -0
  27. data/spec/install.sh +78 -0
  28. data/spec/javascript/channel_spec.js +15 -0
  29. data/spec/javascript/client_spec.js +714 -0
  30. data/spec/javascript/engine/memory_spec.js +7 -0
  31. data/spec/javascript/engine_spec.js +417 -0
  32. data/spec/javascript/faye_spec.js +15 -0
  33. data/spec/javascript/grammar_spec.js +66 -0
  34. data/spec/javascript/node_adapter_spec.js +307 -0
  35. data/spec/javascript/publisher_spec.js +27 -0
  36. data/spec/javascript/server/connect_spec.js +168 -0
  37. data/spec/javascript/server/disconnect_spec.js +121 -0
  38. data/spec/javascript/server/extensions_spec.js +60 -0
  39. data/spec/javascript/server/handshake_spec.js +145 -0
  40. data/spec/javascript/server/integration_spec.js +124 -0
  41. data/spec/javascript/server/publish_spec.js +85 -0
  42. data/spec/javascript/server/subscribe_spec.js +247 -0
  43. data/spec/javascript/server/unsubscribe_spec.js +245 -0
  44. data/spec/javascript/server_spec.js +110 -0
  45. data/spec/javascript/transport_spec.js +130 -0
  46. data/spec/node.js +55 -0
  47. data/spec/phantom.js +17 -0
  48. data/spec/ruby/channel_spec.rb +17 -0
  49. data/spec/ruby/client_spec.rb +724 -0
  50. data/spec/ruby/engine/memory_spec.rb +7 -0
  51. data/spec/ruby/engine_examples.rb +427 -0
  52. data/spec/ruby/faye_spec.rb +14 -0
  53. data/spec/ruby/grammar_spec.rb +68 -0
  54. data/spec/ruby/publisher_spec.rb +27 -0
  55. data/spec/ruby/rack_adapter_spec.rb +236 -0
  56. data/spec/ruby/server/connect_spec.rb +170 -0
  57. data/spec/ruby/server/disconnect_spec.rb +120 -0
  58. data/spec/ruby/server/extensions_spec.rb +68 -0
  59. data/spec/ruby/server/handshake_spec.rb +143 -0
  60. data/spec/ruby/server/integration_spec.rb +126 -0
  61. data/spec/ruby/server/publish_spec.rb +81 -0
  62. data/spec/ruby/server/subscribe_spec.rb +247 -0
  63. data/spec/ruby/server/unsubscribe_spec.rb +247 -0
  64. data/spec/ruby/server_spec.rb +110 -0
  65. data/spec/ruby/transport_spec.rb +134 -0
  66. data/spec/spec_helper.rb +11 -0
  67. data/spec/testswarm +29 -0
  68. data/spec/thin_proxy.rb +37 -0
  69. metadata +302 -0
@@ -0,0 +1,209 @@
1
+ module Faye
2
+ class RackAdapter
3
+
4
+ include Logging
5
+
6
+ extend Forwardable
7
+ def_delegators "@server.engine", :bind, :unbind
8
+
9
+ ASYNC_RESPONSE = [-1, {}, []].freeze
10
+
11
+ DEFAULT_ENDPOINT = '/bayeux'
12
+ SCRIPT_PATH = File.join(ROOT, 'faye-browser-min.js')
13
+
14
+ TYPE_JSON = {'Content-Type' => 'application/json'}
15
+ TYPE_SCRIPT = {'Content-Type' => 'text/javascript'}
16
+ TYPE_TEXT = {'Content-Type' => 'text/plain'}
17
+
18
+ # This header is passed by Rack::Proxy during testing. Rack::Proxy seems to
19
+ # set content-length for you, and setting it in here really slows the tests
20
+ # down. Better suggestions welcome.
21
+ HTTP_X_NO_CONTENT_LENGTH = 'HTTP_X_NO_CONTENT_LENGTH'
22
+
23
+ def initialize(app = nil, options = nil)
24
+ @app = app if app.respond_to?(:call)
25
+ @options = [app, options].grep(Hash).first || {}
26
+
27
+ @endpoint = @options[:mount] || DEFAULT_ENDPOINT
28
+ @endpoint_re = Regexp.new('^' + @endpoint + '(/[^/]*)*(\\.js)?$')
29
+ @server = Server.new(@options)
30
+
31
+ return unless extensions = @options[:extensions]
32
+ [*extensions].each { |extension| add_extension(extension) }
33
+ end
34
+
35
+ def add_extension(extension)
36
+ @server.add_extension(extension)
37
+ end
38
+
39
+ def remove_extension(extension)
40
+ @server.remove_extension(extension)
41
+ end
42
+
43
+ def get_client
44
+ @client ||= Client.new(@server)
45
+ end
46
+
47
+ def listen(port, ssl_options = nil)
48
+ Faye::WebSocket.load_adapter('thin')
49
+ handler = Rack::Handler.get('thin')
50
+ handler.run(self, :Port => port) do |s|
51
+ if ssl_options
52
+ s.ssl = true
53
+ s.ssl_options = {
54
+ :private_key_file => ssl_options[:key],
55
+ :cert_chain_file => ssl_options[:cert]
56
+ }
57
+ end
58
+ @thin_server = s
59
+ end
60
+ end
61
+
62
+ def stop
63
+ return unless @thin_server
64
+ @thin_server.stop
65
+ @thin_server = nil
66
+ end
67
+
68
+ def call(env)
69
+ Faye.ensure_reactor_running!
70
+ request = Rack::Request.new(env)
71
+
72
+ unless request.path_info =~ @endpoint_re
73
+ env['faye.client'] = get_client
74
+ return @app ? @app.call(env) :
75
+ [404, TYPE_TEXT, ["Sure you're not looking for #{@endpoint} ?"]]
76
+ end
77
+
78
+ return serve_client_script(env) if request.path_info =~ /\.js$/
79
+ return handle_options(request) if env['REQUEST_METHOD'] == 'OPTIONS'
80
+ return handle_websocket(env) if Faye::WebSocket.websocket?(env)
81
+ return handle_eventsource(env) if Faye::EventSource.eventsource?(env)
82
+
83
+ handle_request(request)
84
+ end
85
+
86
+ private
87
+
88
+ def serve_client_script(env)
89
+ @client_script ||= File.read(SCRIPT_PATH)
90
+ @client_digest ||= Digest::SHA1.hexdigest(@client_script)
91
+ @client_mtime ||= File.mtime(SCRIPT_PATH)
92
+
93
+ headers = TYPE_SCRIPT.dup
94
+ ims = env['HTTP_IF_MODIFIED_SINCE']
95
+
96
+ headers['Content-Length'] = @client_script.bytesize.to_s unless env[HTTP_X_NO_CONTENT_LENGTH]
97
+ headers['ETag'] = @client_digest
98
+ headers['Last-Modified'] = @client_mtime.httpdate
99
+
100
+ if env['HTTP_IF_NONE_MATCH'] == @client_digest
101
+ [304, headers, ['']]
102
+ elsif ims and @client_mtime <= Time.httpdate(ims)
103
+ [304, headers, ['']]
104
+ else
105
+ [200, headers, [@client_script]]
106
+ end
107
+ end
108
+
109
+ def handle_request(request)
110
+ json_msg = message_from_request(request)
111
+ message = Yajl::Parser.parse(json_msg)
112
+ jsonp = request.params['jsonp'] || JSONP_CALLBACK
113
+ headers = request.get? ? TYPE_SCRIPT.dup : TYPE_JSON.dup
114
+ origin = request.env['HTTP_ORIGIN']
115
+ callback = request.env['async.callback']
116
+
117
+ debug 'Received ?: ?', request.env['REQUEST_METHOD'], json_msg
118
+ @server.flush_connection(message) if request.get?
119
+
120
+ headers['Access-Control-Allow-Origin'] = origin if origin
121
+ headers['Cache-Control'] = 'no-cache, no-store' if request.get?
122
+
123
+ @server.process(message, false) do |replies|
124
+ response = Faye.to_json(replies)
125
+ response = "#{ jsonp }(#{ response });" if request.get?
126
+ headers['Content-Length'] = response.bytesize.to_s unless request.env[HTTP_X_NO_CONTENT_LENGTH]
127
+ debug 'Returning ?', response
128
+ callback.call [200, headers, [response]]
129
+ end
130
+
131
+ ASYNC_RESPONSE
132
+ rescue => e
133
+ error "#{e.message}\nBacktrace:\n#{e.backtrace * "\n"}"
134
+ [400, TYPE_TEXT, ['Bad request']]
135
+ end
136
+
137
+ def handle_websocket(env)
138
+ ws = Faye::WebSocket.new(env, nil, :ping => @options[:ping])
139
+ client_id = nil
140
+
141
+ ws.onmessage = lambda do |event|
142
+ begin
143
+ message = Yajl::Parser.parse(event.data)
144
+ client_id = Faye.client_id_from_messages(message)
145
+
146
+ debug "Received via WebSocket[#{ws.version}]: ?", message
147
+ @server.open_socket(client_id, ws)
148
+
149
+ @server.process(message, false) do |replies|
150
+ ws.send(Faye.to_json(replies)) if ws
151
+ end
152
+ rescue => e
153
+ error "#{e.message}\nBacktrace:\n#{e.backtrace * "\n"}"
154
+ end
155
+ end
156
+
157
+ ws.onclose = lambda do |event|
158
+ @server.close_socket(client_id)
159
+ ws = nil
160
+ end
161
+
162
+ ws.rack_response
163
+ end
164
+
165
+ def handle_eventsource(env)
166
+ es = Faye::EventSource.new(env, :ping => @options[:ping])
167
+ client_id = es.url.split('/').pop
168
+
169
+ debug 'Opened EventSource connection for ?', client_id
170
+ @server.open_socket(client_id, es)
171
+
172
+ es.onclose = lambda do |event|
173
+ @server.close_socket(client_id)
174
+ es = nil
175
+ end
176
+
177
+ es.rack_response
178
+ end
179
+
180
+ def message_from_request(request)
181
+ message = request.params['message']
182
+ return message if message
183
+
184
+ # Some clients do not send a content-type, e.g.
185
+ # Internet Explorer when using cross-origin-long-polling
186
+ # Some use application/xml when using CORS
187
+ content_type = request.env['CONTENT_TYPE'] || ''
188
+
189
+ if content_type.split(';').first == 'application/json'
190
+ request.body.read
191
+ else
192
+ CGI.parse(request.body.read)['message'][0]
193
+ end
194
+ end
195
+
196
+ def handle_options(request)
197
+ headers = {
198
+ 'Access-Control-Allow-Origin' => '*',
199
+ 'Access-Control-Allow-Credentials' => 'false',
200
+ 'Access-Control-Max-Age' => '86400',
201
+ 'Access-Control-Allow-Methods' => 'POST, GET, PUT, DELETE, OPTIONS',
202
+ 'Access-Control-Allow-Headers' => 'Accept, Content-Type, X-Requested-With'
203
+ }
204
+ [200, headers, ['']]
205
+ end
206
+
207
+ end
208
+ end
209
+
@@ -0,0 +1,60 @@
1
+ module Faye
2
+ module Engine
3
+
4
+ class Connection
5
+ include EventMachine::Deferrable
6
+ include Timeouts
7
+
8
+ attr_accessor :socket
9
+
10
+ def initialize(engine, id, options = {})
11
+ @engine = engine
12
+ @id = id
13
+ @options = options
14
+ @inbox = Set.new
15
+ end
16
+
17
+ def deliver(message)
18
+ return socket.send(Faye.to_json([message])) if socket
19
+ return unless @inbox.add?(message)
20
+ begin_delivery_timeout
21
+ end
22
+
23
+ def connect(options, &block)
24
+ options = options || {}
25
+ timeout = options['timeout'] ? options['timeout'] / 1000.0 : @engine.timeout
26
+
27
+ set_deferred_status(:deferred)
28
+ callback(&block)
29
+
30
+ begin_delivery_timeout
31
+ begin_connection_timeout(timeout)
32
+ end
33
+
34
+ def flush!(force = false)
35
+ release_connection!(force)
36
+ set_deferred_status(:succeeded, @inbox.entries)
37
+ @inbox = []
38
+ end
39
+
40
+ private
41
+
42
+ def release_connection!(force = false)
43
+ @engine.close_connection(@id) if force or socket.nil?
44
+ remove_timeout(:connection)
45
+ remove_timeout(:delivery)
46
+ end
47
+
48
+ def begin_delivery_timeout
49
+ return if @inbox.empty?
50
+ add_timeout(:delivery, MAX_DELAY) { flush! }
51
+ end
52
+
53
+ def begin_connection_timeout(timeout)
54
+ add_timeout(:connection, timeout) { flush! }
55
+ end
56
+ end
57
+
58
+ end
59
+ end
60
+
@@ -0,0 +1,112 @@
1
+ module Faye
2
+ module Engine
3
+
4
+ class Memory
5
+ include Timeouts
6
+
7
+ def self.create(server, options)
8
+ new(server, options)
9
+ end
10
+
11
+ def initialize(server, options)
12
+ @server = server
13
+ @options = options
14
+ @namespace = Namespace.new
15
+ @clients = {}
16
+ @channels = {}
17
+ @messages = {}
18
+ end
19
+
20
+ def create_client(&callback)
21
+ client_id = @namespace.generate
22
+ @server.debug 'Created new client ?', client_id
23
+ ping(client_id)
24
+ @server.trigger(:handshake, client_id)
25
+ callback.call(client_id)
26
+ end
27
+
28
+ def destroy_client(client_id, &callback)
29
+ return unless @namespace.exists?(client_id)
30
+
31
+ if @clients.has_key?(client_id)
32
+ @clients[client_id].each { |channel| unsubscribe(client_id, channel) }
33
+ end
34
+
35
+ remove_timeout(client_id)
36
+ @namespace.release(client_id)
37
+ @messages.delete(client_id)
38
+ @server.debug 'Destroyed client ?', client_id
39
+ @server.trigger(:disconnect, client_id)
40
+ callback.call if callback
41
+ end
42
+
43
+ def client_exists(client_id, &callback)
44
+ callback.call(@namespace.exists?(client_id))
45
+ end
46
+
47
+ def ping(client_id)
48
+ timeout = @server.timeout
49
+ return unless Numeric === timeout
50
+ @server.debug 'Ping ?, ?', client_id, timeout
51
+ remove_timeout(client_id)
52
+ add_timeout(client_id, 2 * timeout) { destroy_client(client_id) }
53
+ end
54
+
55
+ def subscribe(client_id, channel, &callback)
56
+ @clients[client_id] ||= Set.new
57
+ should_trigger = @clients[client_id].add?(channel)
58
+
59
+ @channels[channel] ||= Set.new
60
+ @channels[channel].add(client_id)
61
+
62
+ @server.debug 'Subscribed client ? to channel ?', client_id, channel
63
+ @server.trigger(:subscribe, client_id, channel) if should_trigger
64
+ callback.call(true) if callback
65
+ end
66
+
67
+ def unsubscribe(client_id, channel, &callback)
68
+ if @clients.has_key?(client_id)
69
+ should_trigger = @clients[client_id].delete?(channel)
70
+ @clients.delete(client_id) if @clients[client_id].empty?
71
+ end
72
+
73
+ if @channels.has_key?(channel)
74
+ @channels[channel].delete(client_id)
75
+ @channels.delete(channel) if @channels[channel].empty?
76
+ end
77
+
78
+ @server.debug 'Unsubscribed client ? from channel ?', client_id, channel
79
+ @server.trigger(:unsubscribe, client_id, channel) if should_trigger
80
+ callback.call(true) if callback
81
+ end
82
+
83
+ def publish(message, channels)
84
+ @server.debug 'Publishing message ?', message
85
+
86
+ clients = Set.new
87
+
88
+ channels.each do |channel|
89
+ next unless subs = @channels[channel]
90
+ subs.each(&clients.method(:add))
91
+ end
92
+
93
+ clients.each do |client_id|
94
+ @server.debug 'Queueing for client ?: ?', client_id, message
95
+ @messages[client_id] ||= []
96
+ @messages[client_id] << Faye.copy_object(message)
97
+ empty_queue(client_id)
98
+ end
99
+
100
+ @server.trigger(:publish, message['clientId'], message['channel'], message['data'])
101
+ end
102
+
103
+ def empty_queue(client_id)
104
+ return unless @server.has_connection?(client_id)
105
+ @server.deliver(client_id, @messages[client_id])
106
+ @messages.delete(client_id)
107
+ end
108
+ end
109
+
110
+ end
111
+ end
112
+
@@ -0,0 +1,111 @@
1
+ module Faye
2
+ module Engine
3
+
4
+ METHODS = %w[create_client client_exists destroy_client ping subscribe unsubscribe]
5
+ MAX_DELAY = 0.0
6
+ INTERVAL = 0.0
7
+ TIMEOUT = 60.0
8
+ ID_LENGTH = 128
9
+
10
+ autoload :Connection, File.expand_path('../connection', __FILE__)
11
+ autoload :Memory, File.expand_path('../memory', __FILE__)
12
+
13
+ def self.ensure_reactor_running!
14
+ Thread.new { EM.run } unless EM.reactor_running?
15
+ Thread.pass until EM.reactor_running?
16
+ end
17
+
18
+ def self.get(options)
19
+ Proxy.new(options)
20
+ end
21
+
22
+ def self.random(bitlength = ID_LENGTH)
23
+ limit = 2 ** bitlength - 1
24
+ max_size = limit.to_s(36).size
25
+ string = rand(limit).to_s(36)
26
+ string = '0' + string while string.size < max_size
27
+ string
28
+ end
29
+
30
+ class Proxy
31
+ include Publisher
32
+ include Logging
33
+
34
+ attr_reader :interval, :timeout
35
+
36
+ extend Forwardable
37
+ def_delegators :@engine, *METHODS
38
+
39
+ def initialize(options)
40
+ @options = options
41
+ @connections = {}
42
+ @interval = @options[:interval] || INTERVAL
43
+ @timeout = @options[:timeout] || TIMEOUT
44
+
45
+ engine_class = @options[:type] || Memory
46
+ @engine = engine_class.create(self, @options)
47
+
48
+ debug 'Created new engine: ?', @options
49
+ end
50
+
51
+ def connect(client_id, options = {}, &callback)
52
+ debug 'Accepting connection from ?', client_id
53
+ @engine.ping(client_id)
54
+ conn = connection(client_id, true)
55
+ conn.connect(options, &callback)
56
+ @engine.empty_queue(client_id)
57
+ end
58
+
59
+ def has_connection?(client_id)
60
+ @connections.has_key?(client_id)
61
+ end
62
+
63
+ def connection(client_id, create)
64
+ conn = @connections[client_id]
65
+ return conn if conn or not create
66
+ @connections[client_id] = Connection.new(self, client_id)
67
+ end
68
+
69
+ def close_connection(client_id)
70
+ debug 'Closing connection for ?', client_id
71
+ @connections.delete(client_id)
72
+ end
73
+
74
+ def open_socket(client_id, socket)
75
+ return unless client_id
76
+ conn = connection(client_id, true)
77
+ conn.socket = socket
78
+ end
79
+
80
+ def deliver(client_id, messages)
81
+ return if !messages || messages.empty?
82
+ conn = connection(client_id, false)
83
+ return false unless conn
84
+ messages.each(&conn.method(:deliver))
85
+ true
86
+ end
87
+
88
+ def generate_id
89
+ Engine.random
90
+ end
91
+
92
+ def flush(client_id)
93
+ return unless client_id
94
+ debug 'Flushing connection for ?', client_id
95
+ conn = connection(client_id, false)
96
+ conn.flush!(true) if conn
97
+ end
98
+
99
+ def disconnect
100
+ @engine.disconnect if @engine.respond_to?(:disconnect)
101
+ end
102
+
103
+ def publish(message)
104
+ channels = Channel.expand(message['channel'])
105
+ @engine.publish(message, channels)
106
+ end
107
+ end
108
+
109
+ end
110
+ end
111
+