faye-huboard 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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