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.
- 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
|