faye-huboard 1.0.4

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,127 @@
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 'multi_json'
10
+ require 'rack'
11
+ require 'securerandom'
12
+ require 'set'
13
+ require 'time'
14
+ require 'uri'
15
+
16
+ module Faye
17
+ VERSION = '1.0.3'
18
+
19
+ ROOT = File.expand_path(File.dirname(__FILE__))
20
+
21
+ autoload :Deferrable, File.join(ROOT, 'faye', 'mixins', 'deferrable')
22
+ autoload :Logging, File.join(ROOT, 'faye', 'mixins', 'logging')
23
+ autoload :Publisher, File.join(ROOT, 'faye', 'mixins', 'publisher')
24
+ autoload :Timeouts, File.join(ROOT, 'faye', 'mixins', 'timeouts')
25
+
26
+ autoload :Namespace, File.join(ROOT, 'faye', 'util', 'namespace')
27
+
28
+ autoload :Engine, File.join(ROOT, 'faye', 'engines', 'proxy')
29
+
30
+ autoload :Channel, File.join(ROOT, 'faye', 'protocol', 'channel')
31
+ autoload :Client, File.join(ROOT, 'faye', 'protocol', 'client')
32
+ autoload :Dispatcher, File.join(ROOT, 'faye', 'protocol', 'dispatcher')
33
+ autoload :Extensible, File.join(ROOT, 'faye', 'protocol', 'extensible')
34
+ autoload :Grammar, File.join(ROOT, 'faye', 'protocol', 'grammar')
35
+ autoload :Publication, File.join(ROOT, 'faye', 'protocol', 'publication')
36
+ autoload :Server, File.join(ROOT, 'faye', 'protocol', 'server')
37
+ autoload :Subscription, File.join(ROOT, 'faye', 'protocol', 'subscription')
38
+
39
+ autoload :Error, File.join(ROOT, 'faye', 'error')
40
+ autoload :Transport, File.join(ROOT, 'faye', 'transport', 'transport')
41
+
42
+ autoload :RackAdapter, File.join(ROOT, 'faye', 'adapters', 'rack_adapter')
43
+ autoload :StaticServer, File.join(ROOT, 'faye', 'adapters', 'static_server')
44
+
45
+ BAYEUX_VERSION = '1.0'
46
+ JSONP_CALLBACK = 'jsonpcallback'
47
+ CONNECTION_TYPES = %w[long-polling cross-origin-long-polling callback-polling websocket eventsource in-process]
48
+
49
+ MANDATORY_CONNECTION_TYPES = %w[long-polling callback-polling in-process]
50
+
51
+ class << self
52
+ attr_accessor :logger
53
+ end
54
+
55
+ def self.ensure_reactor_running!
56
+ Engine.ensure_reactor_running!
57
+ end
58
+
59
+ def self.random(*args)
60
+ Engine.random(*args)
61
+ end
62
+
63
+ def self.client_id_from_messages(messages)
64
+ first = [messages].flatten.find { |m| m['channel'] == '/meta/connect' }
65
+ first && first['clientId']
66
+ end
67
+
68
+ def self.copy_object(object)
69
+ case object
70
+ when Hash
71
+ clone = {}
72
+ object.each { |k,v| clone[k] = copy_object(v) }
73
+ clone
74
+ when Array
75
+ clone = []
76
+ object.each { |v| clone << copy_object(v) }
77
+ clone
78
+ else
79
+ object
80
+ end
81
+ end
82
+
83
+ def self.parse_url(url)
84
+ String === url ? URI.parse(url) : url
85
+ end
86
+
87
+ def self.to_json(value)
88
+ case value
89
+ when Hash, Array then MultiJson.dump(value)
90
+ when String, NilClass then value.inspect
91
+ else value.to_s
92
+ end
93
+ end
94
+
95
+ def self.async_each(list, iterator, callback)
96
+ n = list.size
97
+ i = -1
98
+ calls = 0
99
+ looping = false
100
+
101
+ loop, resume = nil, nil
102
+
103
+ iterate = lambda do
104
+ calls -= 1
105
+ i += 1
106
+ if i == n
107
+ callback.call if callback
108
+ else
109
+ iterator.call(list[i], resume)
110
+ end
111
+ end
112
+
113
+ loop = lambda do
114
+ unless looping
115
+ looping = true
116
+ iterate.call while calls > 0
117
+ looping = false
118
+ end
119
+ end
120
+
121
+ resume = lambda do
122
+ calls += 1
123
+ loop.call
124
+ end
125
+ resume.call
126
+ end
127
+ end
@@ -0,0 +1,248 @@
1
+ module Faye
2
+ class RackAdapter
3
+
4
+ include Logging
5
+
6
+ extend Forwardable
7
+ def_delegators '@server.engine', *Faye::Publisher.instance_methods
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
+ VALID_JSONP_CALLBACK = /^[a-z_\$][a-z0-9_\$]*(\.[a-z_\$][a-z0-9_\$]*)*$/i
19
+
20
+ # This header is passed by Rack::Proxy during testing. Rack::Proxy seems to
21
+ # set content-length for you, and setting it in here really slows the tests
22
+ # down. Better suggestions welcome.
23
+ HTTP_X_NO_CONTENT_LENGTH = 'HTTP_X_NO_CONTENT_LENGTH'
24
+
25
+ def initialize(app = nil, options = nil, &block)
26
+ @app = app if app.respond_to?(:call)
27
+ @options = [app, options].grep(Hash).first || {}
28
+
29
+ @endpoint = @options[:mount] || DEFAULT_ENDPOINT
30
+ @endpoint_re = Regexp.new('^' + @endpoint.gsub(/\/$/, '') + '(/[^/]*)*(\\.[^\\.]+)?$')
31
+ @server = Server.new(@options)
32
+
33
+ @static = StaticServer.new(ROOT, /\.(?:js|map)$/)
34
+ @static.map(File.basename(@endpoint) + '.js', SCRIPT_PATH)
35
+ @static.map('client.js', SCRIPT_PATH)
36
+
37
+ if extensions = @options[:extensions]
38
+ [*extensions].each { |extension| add_extension(extension) }
39
+ end
40
+
41
+ block.call(self) if block
42
+ end
43
+
44
+ def listen(*args)
45
+ raise 'The listen() method is deprecated - see https://github.com/faye/faye-websocket-ruby#running-your-socket-application for information on running your Faye server'
46
+ end
47
+
48
+ def add_extension(extension)
49
+ @server.add_extension(extension)
50
+ end
51
+
52
+ def remove_extension(extension)
53
+ @server.remove_extension(extension)
54
+ end
55
+
56
+ def close
57
+ @server.close
58
+ end
59
+
60
+ def get_client
61
+ @client ||= Client.new(@server)
62
+ end
63
+
64
+ def call(env)
65
+ Faye.ensure_reactor_running!
66
+ request = Rack::Request.new(env)
67
+
68
+ unless request.path_info =~ @endpoint_re
69
+ env['faye.client'] = get_client
70
+ return @app ? @app.call(env) :
71
+ [404, TYPE_TEXT, ["Sure you're not looking for #{@endpoint} ?"]]
72
+ end
73
+
74
+ return @static.call(env) if @static =~ request.path_info
75
+
76
+ # http://groups.google.com/group/faye-users/browse_thread/thread/4a01bb7d25d3636a
77
+ if env['REQUEST_METHOD'] == 'OPTIONS' or env['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] == 'POST'
78
+ return handle_options
79
+ end
80
+
81
+ return handle_websocket(request) if Faye::WebSocket.websocket?(env)
82
+ return handle_eventsource(request) if Faye::EventSource.eventsource?(env)
83
+
84
+ handle_request(request)
85
+ end
86
+
87
+ private
88
+
89
+ def handle_request(request)
90
+ unless json_msg = message_from_request(request)
91
+ error 'Received request with no message: ?', format_request(request)
92
+ return [400, TYPE_TEXT, ['Bad request']]
93
+ end
94
+
95
+ unless json_msg.force_encoding('UTF-8').valid_encoding?
96
+ error 'Received request with invalid encoding: ?', format_request(request)
97
+ return [400, TYPE_TEXT, ['Bad request']]
98
+ end
99
+
100
+ debug("Received message via HTTP #{request.request_method}: ?", json_msg)
101
+
102
+ message = MultiJson.load(json_msg)
103
+ jsonp = request.params['jsonp'] || JSONP_CALLBACK
104
+ headers = request.get? ? TYPE_SCRIPT.dup : TYPE_JSON.dup
105
+ origin = request.env['HTTP_ORIGIN']
106
+ callback = request.env['async.callback']
107
+
108
+ if jsonp !~ VALID_JSONP_CALLBACK
109
+ error 'Invalid JSON-P callback: ?', jsonp
110
+ return [400, TYPE_TEXT, ['Bad request']]
111
+ end
112
+
113
+ headers['Access-Control-Allow-Origin'] = origin if origin
114
+ headers['Cache-Control'] = 'no-cache, no-store'
115
+ headers['X-Content-Type-Options'] = 'nosniff'
116
+
117
+ request.env['rack.hijack'].call if request.env['rack.hijack']
118
+ hijack = request.env['rack.hijack_io']
119
+
120
+ EventMachine.next_tick do
121
+ @server.process(message, request) do |replies|
122
+ response = Faye.to_json(replies)
123
+
124
+ if request.get?
125
+ response = "/**/#{ jsonp }(#{ jsonp_escape(response) });"
126
+ headers['Content-Disposition'] = 'attachment; filename=f.txt'
127
+ end
128
+
129
+ headers['Content-Length'] = response.bytesize.to_s unless request.env[HTTP_X_NO_CONTENT_LENGTH]
130
+ headers['Connection'] = 'close'
131
+ debug('HTTP response: ?', response)
132
+ send_response([200, headers, [response]], hijack, callback)
133
+ end
134
+ end
135
+
136
+ ASYNC_RESPONSE
137
+ rescue => e
138
+ error "#{e.message}\nBacktrace:\n#{e.backtrace * "\n"}"
139
+ [400, TYPE_TEXT, ['Bad request']]
140
+ end
141
+
142
+ def message_from_request(request)
143
+ message = request.params['message']
144
+ return message if message
145
+
146
+ # Some clients do not send a content-type, e.g.
147
+ # Internet Explorer when using cross-origin-long-polling
148
+ # Some use application/xml when using CORS
149
+ content_type = request.env['CONTENT_TYPE'] || ''
150
+
151
+ if content_type.split(';').first == 'application/json'
152
+ request.body.read
153
+ else
154
+ CGI.parse(request.body.read)['message'][0]
155
+ end
156
+ end
157
+
158
+ def jsonp_escape(json)
159
+ json.gsub(/\u2028/, '\u2028').gsub(/\u2029/, '\u2029')
160
+ end
161
+
162
+ def send_response(response, hijack, callback)
163
+ return callback.call(response) if callback
164
+
165
+ buffer = "HTTP/1.1 #{response[0]} OK\r\n"
166
+ response[1].each do |name, value|
167
+ buffer << "#{name}: #{value}\r\n"
168
+ end
169
+ buffer << "\r\n"
170
+ response[2].each do |chunk|
171
+ buffer << chunk
172
+ end
173
+
174
+ hijack.write(buffer)
175
+ hijack.flush
176
+ hijack.close_write
177
+ end
178
+
179
+ def handle_websocket(request)
180
+ ws = Faye::WebSocket.new(request.env, nil, :ping => @options[:ping])
181
+ client_id = nil
182
+
183
+ ws.onmessage = lambda do |event|
184
+ begin
185
+ debug("Received message via WebSocket[#{ws.version}]: ?", event.data)
186
+
187
+ message = MultiJson.load(event.data)
188
+ cid = Faye.client_id_from_messages(message)
189
+
190
+ @server.close_socket(client_id, false) if client_id and cid and cid != client_id
191
+ @server.open_socket(cid, ws, request)
192
+ client_id = cid
193
+
194
+ @server.process(message, request) do |replies|
195
+ ws.send(Faye.to_json(replies)) if ws
196
+ end
197
+ rescue => e
198
+ error "#{e.message}\nBacktrace:\n#{e.backtrace * "\n"}"
199
+ end
200
+ end
201
+
202
+ ws.onclose = lambda do |event|
203
+ @server.close_socket(client_id)
204
+ ws = nil
205
+ end
206
+
207
+ ws.rack_response
208
+ end
209
+
210
+ def handle_eventsource(request)
211
+ es = Faye::EventSource.new(request.env, :ping => @options[:ping])
212
+ client_id = es.url.split('/').pop
213
+
214
+ debug('Opened EventSource connection for ?', client_id)
215
+ @server.open_socket(client_id, es, request)
216
+
217
+ es.onclose = lambda do |event|
218
+ @server.close_socket(client_id)
219
+ es = nil
220
+ end
221
+
222
+ es.rack_response
223
+ end
224
+
225
+ def handle_options
226
+ headers = {
227
+ 'Access-Control-Allow-Credentials' => 'false',
228
+ 'Access-Control-Allow-Headers' => 'Accept, Content-Type, Pragma, X-Requested-With',
229
+ 'Access-Control-Allow-Methods' => 'POST, GET, PUT, DELETE, OPTIONS',
230
+ 'Access-Control-Allow-Origin' => '*',
231
+ 'Access-Control-Max-Age' => '86400'
232
+ }
233
+ [200, headers, []]
234
+ end
235
+
236
+ def format_request(request)
237
+ request.body.rewind
238
+ string = "curl -X #{request.request_method.upcase}"
239
+ string << " '#{request.url}'"
240
+ if request.post?
241
+ string << " -H 'Content-Type: #{request.env['CONTENT_TYPE']}'"
242
+ string << " -d '#{request.body.read}'"
243
+ end
244
+ string
245
+ end
246
+
247
+ end
248
+ end
@@ -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,58 @@
1
+ module Faye
2
+ module Engine
3
+
4
+ class Connection
5
+ include 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
+ message.delete('clientId')
19
+ return @socket.send(message) if @socket
20
+ return unless @inbox.add?(message)
21
+ begin_delivery_timeout
22
+ end
23
+
24
+ def connect(options, &block)
25
+ options = options || {}
26
+ timeout = options['timeout'] ? options['timeout'] / 1000.0 : @engine.timeout
27
+
28
+ set_deferred_status(:unknown)
29
+ callback(&block)
30
+
31
+ begin_delivery_timeout
32
+ begin_connection_timeout(timeout)
33
+ end
34
+
35
+ def flush
36
+ remove_timeout(:connection)
37
+ remove_timeout(:delivery)
38
+
39
+ set_deferred_status(:succeeded, @inbox.entries)
40
+ @inbox = []
41
+
42
+ @engine.close_connection(@id) unless @socket
43
+ end
44
+
45
+ private
46
+
47
+ def begin_delivery_timeout
48
+ return if @inbox.empty?
49
+ add_timeout(:delivery, MAX_DELAY) { flush }
50
+ end
51
+
52
+ def begin_connection_timeout(timeout)
53
+ add_timeout(:connection, timeout) { flush }
54
+ end
55
+ end
56
+
57
+ end
58
+ end