face-faye 0.8.9

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.
Files changed (73) hide show
  1. data/History.txt +304 -0
  2. data/README.rdoc +83 -0
  3. data/lib/faye-browser-min.js +2 -0
  4. data/lib/faye-browser-min.js.map +8 -0
  5. data/lib/faye-browser.js +2194 -0
  6. data/lib/faye.rb +122 -0
  7. data/lib/faye/adapters/rack_adapter.rb +216 -0
  8. data/lib/faye/adapters/static_server.rb +56 -0
  9. data/lib/faye/engines/connection.rb +60 -0
  10. data/lib/faye/engines/memory.rb +112 -0
  11. data/lib/faye/engines/proxy.rb +121 -0
  12. data/lib/faye/error.rb +49 -0
  13. data/lib/faye/mixins/logging.rb +47 -0
  14. data/lib/faye/mixins/publisher.rb +30 -0
  15. data/lib/faye/mixins/timeouts.rb +22 -0
  16. data/lib/faye/protocol/channel.rb +124 -0
  17. data/lib/faye/protocol/client.rb +376 -0
  18. data/lib/faye/protocol/extensible.rb +43 -0
  19. data/lib/faye/protocol/grammar.rb +58 -0
  20. data/lib/faye/protocol/publication.rb +5 -0
  21. data/lib/faye/protocol/server.rb +293 -0
  22. data/lib/faye/protocol/socket.rb +23 -0
  23. data/lib/faye/protocol/subscription.rb +24 -0
  24. data/lib/faye/transport/http.rb +76 -0
  25. data/lib/faye/transport/local.rb +22 -0
  26. data/lib/faye/transport/transport.rb +116 -0
  27. data/lib/faye/transport/web_socket.rb +92 -0
  28. data/lib/faye/util/namespace.rb +20 -0
  29. data/spec/browser.html +45 -0
  30. data/spec/encoding_helper.rb +7 -0
  31. data/spec/install.sh +78 -0
  32. data/spec/javascript/channel_spec.js +15 -0
  33. data/spec/javascript/client_spec.js +729 -0
  34. data/spec/javascript/engine/memory_spec.js +7 -0
  35. data/spec/javascript/engine_spec.js +417 -0
  36. data/spec/javascript/faye_spec.js +34 -0
  37. data/spec/javascript/grammar_spec.js +66 -0
  38. data/spec/javascript/node_adapter_spec.js +307 -0
  39. data/spec/javascript/publisher_spec.js +27 -0
  40. data/spec/javascript/server/connect_spec.js +168 -0
  41. data/spec/javascript/server/disconnect_spec.js +121 -0
  42. data/spec/javascript/server/extensions_spec.js +60 -0
  43. data/spec/javascript/server/handshake_spec.js +145 -0
  44. data/spec/javascript/server/integration_spec.js +131 -0
  45. data/spec/javascript/server/publish_spec.js +85 -0
  46. data/spec/javascript/server/subscribe_spec.js +247 -0
  47. data/spec/javascript/server/unsubscribe_spec.js +245 -0
  48. data/spec/javascript/server_spec.js +121 -0
  49. data/spec/javascript/transport_spec.js +135 -0
  50. data/spec/node.js +55 -0
  51. data/spec/phantom.js +17 -0
  52. data/spec/ruby/channel_spec.rb +17 -0
  53. data/spec/ruby/client_spec.rb +741 -0
  54. data/spec/ruby/engine/memory_spec.rb +7 -0
  55. data/spec/ruby/engine_examples.rb +427 -0
  56. data/spec/ruby/faye_spec.rb +30 -0
  57. data/spec/ruby/grammar_spec.rb +68 -0
  58. data/spec/ruby/publisher_spec.rb +27 -0
  59. data/spec/ruby/rack_adapter_spec.rb +236 -0
  60. data/spec/ruby/server/connect_spec.rb +170 -0
  61. data/spec/ruby/server/disconnect_spec.rb +120 -0
  62. data/spec/ruby/server/extensions_spec.rb +68 -0
  63. data/spec/ruby/server/handshake_spec.rb +143 -0
  64. data/spec/ruby/server/integration_spec.rb +133 -0
  65. data/spec/ruby/server/publish_spec.rb +81 -0
  66. data/spec/ruby/server/subscribe_spec.rb +247 -0
  67. data/spec/ruby/server/unsubscribe_spec.rb +247 -0
  68. data/spec/ruby/server_spec.rb +121 -0
  69. data/spec/ruby/transport_spec.rb +136 -0
  70. data/spec/spec_helper.rb +11 -0
  71. data/spec/testswarm +42 -0
  72. data/spec/thin_proxy.rb +37 -0
  73. metadata +441 -0
@@ -0,0 +1,122 @@
1
+ require 'cgi'
2
+ require 'cookiejar'
3
+ require 'digest/sha1'
4
+ require 'em-http'
5
+ require 'em-http/version'
6
+ require 'eventmachine'
7
+ require 'faye/websocket'
8
+ require 'forwardable'
9
+ require 'rack'
10
+ require 'set'
11
+ require 'time'
12
+ require 'uri'
13
+ require 'multi_json'
14
+
15
+ module Faye
16
+ VERSION = '0.8.9'
17
+
18
+ ROOT = File.expand_path(File.dirname(__FILE__))
19
+
20
+ autoload :Publisher, File.join(ROOT, 'faye', 'mixins', 'publisher')
21
+ autoload :Timeouts, File.join(ROOT, 'faye', 'mixins', 'timeouts')
22
+ autoload :Logging, File.join(ROOT, 'faye', 'mixins', 'logging')
23
+
24
+ autoload :Namespace, File.join(ROOT, 'faye', 'util', 'namespace')
25
+
26
+ autoload :Engine, File.join(ROOT, 'faye', 'engines', 'proxy')
27
+
28
+ autoload :Grammar, File.join(ROOT, 'faye', 'protocol', 'grammar')
29
+ autoload :Extensible, File.join(ROOT, 'faye', 'protocol', 'extensible')
30
+ autoload :Channel, File.join(ROOT, 'faye', 'protocol', 'channel')
31
+ autoload :Subscription, File.join(ROOT, 'faye', 'protocol', 'subscription')
32
+ autoload :Publication, File.join(ROOT, 'faye', 'protocol', 'publication')
33
+ autoload :Client, File.join(ROOT, 'faye', 'protocol', 'client')
34
+ autoload :Server, File.join(ROOT, 'faye', 'protocol', 'server')
35
+
36
+ autoload :Transport, File.join(ROOT, 'faye', 'transport', 'transport')
37
+ autoload :Error, File.join(ROOT, 'faye', 'error')
38
+
39
+ autoload :RackAdapter, File.join(ROOT, 'faye', 'adapters', 'rack_adapter')
40
+ autoload :StaticServer, File.join(ROOT, 'faye', 'adapters', 'static_server')
41
+
42
+ BAYEUX_VERSION = '1.0'
43
+ JSONP_CALLBACK = 'jsonpcallback'
44
+ CONNECTION_TYPES = %w[long-polling cross-origin-long-polling callback-polling websocket eventsource in-process]
45
+
46
+ MANDATORY_CONNECTION_TYPES = %w[long-polling callback-polling in-process]
47
+
48
+ class << self
49
+ attr_accessor :logger
50
+ end
51
+ self.logger = method(:puts)
52
+
53
+ def self.ensure_reactor_running!
54
+ Engine.ensure_reactor_running!
55
+ end
56
+
57
+ def self.random(*args)
58
+ Engine.random(*args)
59
+ end
60
+
61
+ def self.client_id_from_messages(messages)
62
+ first = [messages].flatten.first
63
+ first && first['clientId']
64
+ end
65
+
66
+ def self.copy_object(object)
67
+ case object
68
+ when Hash
69
+ clone = {}
70
+ object.each { |k,v| clone[k] = copy_object(v) }
71
+ clone
72
+ when Array
73
+ clone = []
74
+ object.each { |v| clone << copy_object(v) }
75
+ clone
76
+ else
77
+ object
78
+ end
79
+ end
80
+
81
+ def self.to_json(value)
82
+ case value
83
+ when Hash, Array then MultiJson.dump(value)
84
+ when String, NilClass then value.inspect
85
+ else value.to_s
86
+ end
87
+ end
88
+
89
+ def self.async_each(list, iterator, callback)
90
+ n = list.size
91
+ i = -1
92
+ calls = 0
93
+ looping = false
94
+
95
+ loop, resume = nil, nil
96
+
97
+ iterate = lambda do
98
+ calls -= 1
99
+ i += 1
100
+ if i == n
101
+ callback.call if callback
102
+ else
103
+ iterator.call(list[i], resume)
104
+ end
105
+ end
106
+
107
+ loop = lambda do
108
+ unless looping
109
+ looping = true
110
+ iterate.call while calls > 0
111
+ looping = false
112
+ end
113
+ end
114
+
115
+ resume = lambda do
116
+ calls += 1
117
+ loop.call
118
+ end
119
+ resume.call
120
+ end
121
+ end
122
+
@@ -0,0 +1,216 @@
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 = 'faye-browser-min.js'
13
+
14
+ TYPE_JSON = {'Content-Type' => 'application/json; charset=utf-8'}
15
+ TYPE_SCRIPT = {'Content-Type' => 'text/javascript; charset=utf-8'}
16
+ TYPE_TEXT = {'Content-Type' => 'text/plain; charset=utf-8'}
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.gsub(/\/$/, '') + '(/[^/]+)*(\\.[^\\.]+)?$')
29
+ @server = Server.new(@options)
30
+
31
+ @static = StaticServer.new(ROOT, /\.(?:js|map)$/)
32
+ @static.map(File.basename(@endpoint) + '.js', SCRIPT_PATH)
33
+ @static.map('client.js', SCRIPT_PATH)
34
+
35
+ return unless extensions = @options[:extensions]
36
+ [*extensions].each { |extension| add_extension(extension) }
37
+ end
38
+
39
+ def add_extension(extension)
40
+ @server.add_extension(extension)
41
+ end
42
+
43
+ def remove_extension(extension)
44
+ @server.remove_extension(extension)
45
+ end
46
+
47
+ def get_client
48
+ @client ||= Client.new(@server)
49
+ end
50
+
51
+ def listen(port, ssl_options = nil)
52
+ Faye::WebSocket.load_adapter('thin')
53
+ handler = Rack::Handler.get('thin')
54
+ handler.run(self, :Port => port) do |s|
55
+ if ssl_options
56
+ s.ssl = true
57
+ s.ssl_options = {
58
+ :private_key_file => ssl_options[:key],
59
+ :cert_chain_file => ssl_options[:cert]
60
+ }
61
+ end
62
+ @thin_server = s
63
+ end
64
+ end
65
+
66
+ def stop
67
+ return unless @thin_server
68
+ @thin_server.stop
69
+ @thin_server = nil
70
+ end
71
+
72
+ def call(env)
73
+ Faye.ensure_reactor_running!
74
+ request = Rack::Request.new(env)
75
+
76
+ unless request.path_info =~ @endpoint_re
77
+ env['faye.client'] = get_client
78
+ return @app ? @app.call(env) :
79
+ [404, TYPE_TEXT, ["Sure you're not looking for #{@endpoint} ?"]]
80
+ end
81
+
82
+ # http://groups.google.com/group/faye-users/browse_thread/thread/4a01bb7d25d3636a
83
+ if env['REQUEST_METHOD'] == 'OPTIONS' or env['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] == 'POST'
84
+ return handle_options(request)
85
+ end
86
+
87
+ return @static.call(env) if @static =~ request.path_info
88
+ return handle_websocket(env) if Faye::WebSocket.websocket?(env)
89
+ return handle_eventsource(env) if Faye::EventSource.eventsource?(env)
90
+
91
+ handle_request(request)
92
+ end
93
+
94
+ private
95
+
96
+ def handle_request(request)
97
+ unless json_msg = message_from_request(request)
98
+ error 'Received request with no message: ?', format_request(request)
99
+ return [400, TYPE_TEXT, ['Bad request']]
100
+ end
101
+
102
+ debug "Received message via HTTP #{request.request_method}: ?", json_msg
103
+
104
+ message = MultiJson.load(json_msg)
105
+ jsonp = request.params['jsonp'] || JSONP_CALLBACK
106
+ headers = request.get? ? TYPE_SCRIPT.dup : TYPE_JSON.dup
107
+ origin = request.env['HTTP_ORIGIN']
108
+ callback = request.env['async.callback']
109
+
110
+ @server.flush_connection(message) if request.get?
111
+
112
+ headers['Access-Control-Allow-Origin'] = origin if origin
113
+ headers['Cache-Control'] = 'no-cache, no-store'
114
+
115
+ @server.process(message, false) do |replies|
116
+ response = Faye.to_json(replies)
117
+ response = "#{ jsonp }(#{ response });" if request.get?
118
+ headers['Content-Length'] = response.bytesize.to_s unless request.env[HTTP_X_NO_CONTENT_LENGTH]
119
+ headers['Connection'] = 'close'
120
+ debug 'HTTP response: ?', response
121
+ callback.call [200, headers, [response]]
122
+ end
123
+
124
+ ASYNC_RESPONSE
125
+ rescue => e
126
+ error "#{e.message}\nBacktrace:\n#{e.backtrace * "\n"}"
127
+ [400, TYPE_TEXT, ['Bad request']]
128
+ end
129
+
130
+ def handle_websocket(env)
131
+ ws = Faye::WebSocket.new(env, nil, :ping => @options[:ping])
132
+ client_id = nil
133
+
134
+ ws.onmessage = lambda do |event|
135
+ begin
136
+ debug "Received message via WebSocket[#{ws.version}]: ?", event.data
137
+
138
+ message = MultiJson.load(event.data)
139
+ cid = Faye.client_id_from_messages(message)
140
+
141
+ @server.close_socket(client_id) if client_id and cid != client_id
142
+ @server.open_socket(cid, ws)
143
+ client_id = cid
144
+
145
+ @server.process(message, false) do |replies|
146
+ ws.send(Faye.to_json(replies)) if ws
147
+ end
148
+ rescue => e
149
+ error "#{e.message}\nBacktrace:\n#{e.backtrace * "\n"}"
150
+ end
151
+ end
152
+
153
+ ws.onclose = lambda do |event|
154
+ @server.close_socket(client_id)
155
+ ws = nil
156
+ end
157
+
158
+ ws.rack_response
159
+ end
160
+
161
+ def handle_eventsource(env)
162
+ es = Faye::EventSource.new(env, :ping => @options[:ping])
163
+ client_id = es.url.split('/').pop
164
+
165
+ debug 'Opened EventSource connection for ?', client_id
166
+ @server.open_socket(client_id, es)
167
+
168
+ es.onclose = lambda do |event|
169
+ @server.close_socket(client_id)
170
+ es = nil
171
+ end
172
+
173
+ es.rack_response
174
+ end
175
+
176
+ def message_from_request(request)
177
+ message = request.params['message']
178
+ return message if message
179
+
180
+ # Some clients do not send a content-type, e.g.
181
+ # Internet Explorer when using cross-origin-long-polling
182
+ # Some use application/xml when using CORS
183
+ content_type = request.env['CONTENT_TYPE'] || ''
184
+
185
+ if content_type.split(';').first == 'application/json'
186
+ request.body.read
187
+ else
188
+ CGI.parse(request.body.read)['message'][0]
189
+ end
190
+ end
191
+
192
+ def format_request(request)
193
+ request.body.rewind
194
+ string = "curl -X #{request.request_method.upcase}"
195
+ string << " '#{request.url}'"
196
+ if request.post?
197
+ string << " -H 'Content-Type: #{request.env['CONTENT_TYPE']}'"
198
+ string << " -d '#{request.body.read}'"
199
+ end
200
+ string
201
+ end
202
+
203
+ def handle_options(request)
204
+ headers = {
205
+ 'Access-Control-Allow-Origin' => '*',
206
+ 'Access-Control-Allow-Credentials' => 'false',
207
+ 'Access-Control-Max-Age' => '86400',
208
+ 'Access-Control-Allow-Methods' => 'POST, GET, PUT, DELETE, OPTIONS',
209
+ 'Access-Control-Allow-Headers' => 'Accept, Content-Type, Pragma, X-Requested-With'
210
+ }
211
+ [200, headers, ['']]
212
+ end
213
+
214
+ end
215
+ end
216
+
@@ -0,0 +1,56 @@
1
+ module Faye
2
+ class StaticServer
3
+
4
+ def initialize(directory, path_regex)
5
+ @directory = directory
6
+ @path_regex = path_regex
7
+ @path_map = {}
8
+ @index = {}
9
+ end
10
+
11
+ def map(request_path, filename)
12
+ @path_map[request_path] = filename
13
+ end
14
+
15
+ def =~(pathname)
16
+ @path_regex =~ pathname
17
+ end
18
+
19
+ def call(env)
20
+ filename = File.basename(env['PATH_INFO'])
21
+ filename = @path_map[filename] || filename
22
+
23
+ cache = @index[filename] ||= {}
24
+ fullpath = File.join(@directory, filename)
25
+
26
+ begin
27
+ cache[:content] ||= File.read(fullpath)
28
+ cache[:digest] ||= Digest::SHA1.hexdigest(cache[:content])
29
+ cache[:mtime] ||= File.mtime(fullpath)
30
+ rescue
31
+ return [404, {}, []]
32
+ end
33
+
34
+ type = /\.js$/ =~ fullpath ? RackAdapter::TYPE_SCRIPT : RackAdapter::TYPE_JSON
35
+ ims = env['HTTP_IF_MODIFIED_SINCE']
36
+
37
+ no_content_length = env[RackAdapter::HTTP_X_NO_CONTENT_LENGTH]
38
+
39
+ headers = {
40
+ 'ETag' => cache[:digest],
41
+ 'Last-Modified' => cache[:mtime].httpdate
42
+ }
43
+
44
+ if env['HTTP_IF_NONE_MATCH'] == cache[:digest]
45
+ [304, headers, ['']]
46
+ elsif ims and cache[:mtime] <= Time.httpdate(ims)
47
+ [304, headers, ['']]
48
+ else
49
+ headers['Content-Length'] = cache[:content].bytesize.to_s unless no_content_length
50
+ headers.update(type)
51
+ [200, headers, [cache[:content]]]
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -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(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
+