faye-huboard 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +351 -0
- data/README.md +35 -0
- data/lib/faye-browser-min.js +3 -0
- data/lib/faye-browser-min.js.map +1 -0
- data/lib/faye-browser.js +2659 -0
- data/lib/faye.rb +127 -0
- data/lib/faye/adapters/rack_adapter.rb +248 -0
- data/lib/faye/adapters/static_server.rb +56 -0
- data/lib/faye/engines/connection.rb +58 -0
- data/lib/faye/engines/memory.rb +121 -0
- data/lib/faye/engines/proxy.rb +126 -0
- data/lib/faye/error.rb +48 -0
- data/lib/faye/mixins/deferrable.rb +14 -0
- data/lib/faye/mixins/logging.rb +35 -0
- data/lib/faye/mixins/publisher.rb +18 -0
- data/lib/faye/mixins/timeouts.rb +26 -0
- data/lib/faye/protocol/channel.rb +123 -0
- data/lib/faye/protocol/client.rb +334 -0
- data/lib/faye/protocol/dispatcher.rb +146 -0
- data/lib/faye/protocol/extensible.rb +45 -0
- data/lib/faye/protocol/grammar.rb +57 -0
- data/lib/faye/protocol/publication.rb +5 -0
- data/lib/faye/protocol/server.rb +291 -0
- data/lib/faye/protocol/socket.rb +24 -0
- data/lib/faye/protocol/subscription.rb +23 -0
- data/lib/faye/transport/http.rb +69 -0
- data/lib/faye/transport/local.rb +21 -0
- data/lib/faye/transport/transport.rb +155 -0
- data/lib/faye/transport/web_socket.rb +134 -0
- data/lib/faye/util/namespace.rb +19 -0
- metadata +400 -0
data/lib/faye.rb
ADDED
@@ -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
|