├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.md ├── Makefile ├── README.md ├── Rakefile ├── examples ├── automate.rb ├── haproxy.conf ├── node │ ├── benchmark.js │ ├── client.js │ ├── pingpong.js │ ├── server.js │ └── ticker.js ├── public │ ├── index.html │ ├── jquery.js │ ├── json2.js │ ├── mootools.js │ ├── prototype.js │ ├── soapbox.js │ ├── style.css │ └── ticker.html ├── ruby │ ├── app.rb │ ├── benchmark.rb │ ├── client.rb │ ├── config.ru │ ├── pingpong.rb │ ├── rainbows.conf │ ├── server.rb │ └── ticker.rb ├── server.crt └── server.key ├── faye.gemspec ├── lib ├── faye.rb └── faye │ ├── adapters │ ├── rack_adapter.rb │ └── static_server.rb │ ├── engines │ ├── connection.rb │ ├── memory.rb │ └── proxy.rb │ ├── error.rb │ ├── mixins │ ├── deferrable.rb │ ├── logging.rb │ ├── publisher.rb │ └── timeouts.rb │ ├── protocol │ ├── channel.rb │ ├── client.rb │ ├── dispatcher.rb │ ├── extensible.rb │ ├── grammar.rb │ ├── publication.rb │ ├── scheduler.rb │ ├── server.rb │ ├── socket.rb │ └── subscription.rb │ ├── transport │ ├── http.rb │ ├── local.rb │ ├── transport.rb │ └── web_socket.rb │ └── util │ └── namespace.rb ├── package.json ├── site ├── config │ ├── compass.rb │ └── site.rb ├── site │ ├── images │ │ ├── aha.png │ │ ├── buster.png │ │ ├── chaxpert.png │ │ ├── cloudblocks.png │ │ ├── faye-cluster.png │ │ ├── faye-internals.png │ │ ├── faye-logo.gif │ │ ├── gitter.png │ │ ├── groupme.png │ │ ├── ineda.png │ │ ├── medeo.png │ │ ├── myspace.png │ │ ├── nokia_mix_party.png │ │ ├── pathient.png │ │ ├── podio.png │ │ └── xydo.png │ ├── javascripts │ │ ├── analytics.js │ │ └── prettify.js │ └── stylesheets │ │ └── github.css └── src │ ├── layouts │ └── default.haml │ ├── pages │ ├── architecture.haml │ ├── browser.haml │ ├── browser │ │ ├── dispatch.haml │ │ ├── extensions.haml │ │ ├── publishing.haml │ │ ├── subscribing.haml │ │ └── transport.haml │ ├── download.haml │ ├── index.haml │ ├── node.haml │ ├── node │ │ ├── clients.haml │ │ ├── engines.haml │ │ ├── extensions.haml │ │ ├── monitoring.haml │ │ └── websockets.haml │ ├── ruby.haml │ ├── ruby │ │ ├── clients.haml │ │ ├── engines.haml │ │ ├── extensions.haml │ │ ├── monitoring.haml │ │ └── websockets.haml │ ├── security.haml │ └── security │ │ ├── authentication.haml │ │ ├── csrf.haml │ │ ├── headers.haml │ │ ├── javascript.haml │ │ ├── publication.haml │ │ ├── push.haml │ │ ├── subscription.haml │ │ └── summary.haml │ ├── partials │ ├── browser_navigation.haml │ ├── node_navigation.haml │ ├── ruby_navigation.haml │ └── security_navigation.haml │ └── stylesheets │ └── screen.sass ├── spec ├── browser.js ├── index.html ├── javascript │ ├── channel_spec.js │ ├── client_spec.js │ ├── dispatcher_spec.js │ ├── engine │ │ └── memory_spec.js │ ├── engine_spec.js │ ├── grammar_spec.js │ ├── node_adapter_spec.js │ ├── publisher_spec.js │ ├── server │ │ ├── connect_spec.js │ │ ├── disconnect_spec.js │ │ ├── extensions_spec.js │ │ ├── handshake_spec.js │ │ ├── integration_spec.js │ │ ├── publish_spec.js │ │ ├── subscribe_spec.js │ │ └── unsubscribe_spec.js │ ├── server_spec.js │ ├── transport_spec.js │ ├── uri_spec.js │ └── util │ │ ├── copy_object_spec.js │ │ └── random_spec.js ├── phantom.js ├── ruby │ ├── channel_spec.rb │ ├── client_spec.rb │ ├── dispatcher_spec.rb │ ├── encoding_helper.rb │ ├── engine │ │ └── memory_spec.rb │ ├── engine_examples.rb │ ├── faye_spec.rb │ ├── grammar_spec.rb │ ├── publisher_spec.rb │ ├── rack_adapter_spec.rb │ ├── server │ │ ├── connect_spec.rb │ │ ├── disconnect_spec.rb │ │ ├── extensions_spec.rb │ │ ├── handshake_spec.rb │ │ ├── integration_spec.rb │ │ ├── publish_spec.rb │ │ ├── subscribe_spec.rb │ │ └── unsubscribe_spec.rb │ ├── server_proxy.rb │ ├── server_spec.rb │ └── transport_spec.rb └── spec_helper.rb ├── src ├── adapters │ ├── content_types.js │ ├── node_adapter.js │ └── static_server.js ├── engines │ ├── connection.js │ ├── memory.js │ └── proxy.js ├── faye_browser.js ├── faye_node.js ├── mixins │ ├── deferrable.js │ ├── logging.js │ ├── publisher.js │ └── timeouts.js ├── protocol │ ├── channel.js │ ├── client.js │ ├── dispatcher.js │ ├── error.js │ ├── extensible.js │ ├── grammar.js │ ├── publication.js │ ├── scheduler.js │ ├── server.js │ ├── socket.js │ └── subscription.js ├── transport │ ├── browser_transports.js │ ├── cors.js │ ├── event_source.js │ ├── jsonp.js │ ├── node_http.js │ ├── node_local.js │ ├── node_transports.js │ ├── package.json │ ├── transport.js │ ├── web_socket.js │ └── xhr.js └── util │ ├── array.js │ ├── assign.js │ ├── browser │ ├── event.js │ ├── node_shim.js │ └── package.json │ ├── class.js │ ├── constants.js │ ├── cookies │ ├── browser_cookies.js │ ├── node_cookies.js │ └── package.json │ ├── copy_object.js │ ├── event_emitter.js │ ├── id_from_messages.js │ ├── namespace.js │ ├── promise.js │ ├── random.js │ ├── set.js │ ├── to_json.js │ ├── uri.js │ ├── validate_options.js │ └── websocket │ ├── browser_websocket.js │ ├── node_websocket.js │ └── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | build 3 | Gemfile.lock 4 | lib/client 5 | node_modules 6 | package-lock.json 7 | spec/*_bundle.js 8 | spec/*.map 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All projects under the [Faye](https://github.com/faye) umbrella are covered by 4 | the [Code of Conduct](https://github.com/faye/code-of-conduct). 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Faye 2 | 3 | Faye is implemented in both JavaScript and Ruby. You should be able to hack on 4 | each implementation independently, although since both implementations include a 5 | build of the JS client, you will need Node if you want to build the Ruby gem. 6 | 7 | To get the code: 8 | 9 | git clone git://github.com/faye/faye.git 10 | cd faye 11 | 12 | ## Working on the JavaScript codebase 13 | 14 | To install the dependencies (you will need to do this if you need to build the 15 | Ruby gem as well): 16 | 17 | npm install 18 | 19 | To run the tests on Node: 20 | 21 | npm test 22 | 23 | To run the tests in the browser, you should run 24 | 25 | make test 26 | 27 | which starts a process to continuously rebuild the source code and tests as you 28 | edit them. Open `spec/index.html` to run the tests. 29 | 30 | To build the package that we release to npm, run: 31 | 32 | make 33 | 34 | ## Working on the Ruby codebase 35 | 36 | To install the dependencies: 37 | 38 | bundle install 39 | 40 | To run the tests: 41 | 42 | bundle exec rspec 43 | 44 | To build the gem (you will need to install the Node dependencies for this): 45 | 46 | make gem 47 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2009-2025 James Coglan 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PATH := node_modules/.bin:$(PATH) 2 | SHELL := /bin/bash 3 | 4 | source_files := $(shell find src -name '*.js') 5 | spec_files := $(shell find spec -name '*_spec.js') 6 | webpack_config := webpack.config.js 7 | 8 | name := faye-browser 9 | bundles := $(name).js $(name)-min.js 10 | 11 | client_dir := build/client 12 | client_bundles := $(bundles:%=$(client_dir)/%) 13 | top_files := CHANGELOG.md LICENSE.md README.md package.json src 14 | top_level := $(top_files:%=build/%) 15 | 16 | .PHONY: all gem clean 17 | 18 | all: $(client_bundles) $(top_level) 19 | 20 | gem: all 21 | gem build faye.gemspec 22 | 23 | clean: 24 | rm -rf build *.gem spec/*_bundle.js{,.map} 25 | 26 | $(client_dir)/$(name).js: $(webpack_config) $(source_files) 27 | webpack; 28 | 29 | $(client_dir)/$(name)-min.js: $(webpack_config) $(source_files) 30 | NODE_ENV=production webpack; 31 | 32 | build/src: $(source_files) build 33 | rsync -a src/ $@/ 34 | 35 | build/%: % build 36 | cp lt; $@ 37 | 38 | build: 39 | mkdir -p $@ 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Faye 2 | 3 | Faye is a set of tools for simple publish-subscribe messaging between web 4 | clients. It ships with easy-to-use message routing servers for Node.js and Rack 5 | applications, and clients that can be used on the server and in the browser. 6 | 7 | - Documentation: http://faye.jcoglan.com 8 | - Mailing list: http://groups.google.com/group/faye-users 9 | - Bug tracker: http://github.com/faye/faye/issues 10 | - Source code: http://github.com/faye/faye 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require './lib/faye' 3 | 4 | task :example, :port, :ssl do |t, args| 5 | exec "ruby examples/ruby/server.rb #{args[:port]} #{args[:ssl] && 'ssl'}" 6 | end 7 | 8 | task :handshake, :port, :n, :c do |t, args| 9 | require 'cgi' 10 | require 'json' 11 | 12 | message = {:channel => '/meta/handshake', 13 | :version => '1.0', 14 | :supportedConnectionTypes => ['long-polling']} 15 | 16 | message = CGI.escape(JSON.dump message) 17 | url = "http://127.0.0.1:#{args[:port]}/bayeux?jsonp=callback&message=#{message}" 18 | puts "Request URL:\n#{url}\n\n" 19 | 20 | exec "ab -n #{args[:n]} -c #{args[:c]} '#{url}'" 21 | end 22 | -------------------------------------------------------------------------------- /examples/automate.rb: -------------------------------------------------------------------------------- 1 | # Load and configure Capybara 2 | 3 | require 'capybara/dsl' 4 | require 'terminus' 5 | Capybara.current_driver = :terminus 6 | Capybara.app_host = 'http://localhost:9292' 7 | extend Capybara::DSL 8 | 9 | # Acquire some browsers and log into each with a username 10 | 11 | NAMES = %w[alice bob carol dan erica frank gemma harold ingrid james] 12 | BROWSERS = {} 13 | Terminus.ensure_browsers 5 14 | 15 | Terminus.browsers.each_with_index do |browser, i| 16 | name = NAMES[i] 17 | puts "#{ name } is using #{ browser }" 18 | BROWSERS[name] = browser 19 | Terminus.browser = browser 20 | visit '/' 21 | fill_in 'username', :with => name 22 | click_button 'Go' 23 | end 24 | 25 | # Send a message from each browser to every other browser, and check that it 26 | # arrived. If it doesn't arrive, send all the browsers back to the dock and 27 | # raise an exception 28 | 29 | BROWSERS.each do |name, sender| 30 | BROWSERS.each do |at, target| 31 | next if at == name 32 | 33 | Terminus.browser = sender 34 | fill_in 'message', :with => "@#{ at } Hello, world!" 35 | click_button 'Send' 36 | 37 | Terminus.browser = target 38 | unless page.has_content?("#{ name }: @#{ at } Hello, world!") 39 | Terminus.return_to_dock 40 | raise "Message did not make it from #{ sender } to #{ target }" 41 | end 42 | end 43 | end 44 | 45 | # Re-dock all the browsers when we're finished 46 | 47 | Terminus.return_to_dock 48 | -------------------------------------------------------------------------------- /examples/haproxy.conf: -------------------------------------------------------------------------------- 1 | listen stats 127.0.0.1:7000 2 | option httpchk 3 | mode http 4 | stats uri / 5 | 6 | listen soapbox 127.0.0.1:3000 7 | option httpchk GET / 8 | server soapbox1 127.0.0.1:7070 weight 1 maxconn 1000 check port 7070 9 | server soapbox2 127.0.0.1:8080 weight 1 maxconn 1000 check port 8080 10 | server soapbox3 127.0.0.1:9090 weight 1 maxconn 1000 check port 9090 11 | -------------------------------------------------------------------------------- /examples/node/benchmark.js: -------------------------------------------------------------------------------- 1 | var faye = require('../../build'), 2 | 3 | port = process.argv[2] || 8000, 4 | path = process.argv[3] || 'bayeux', 5 | scheme = process.argv[4] === 'tls' ? 'https' : 'http'; 6 | 7 | var A = new faye.Client(scheme + '://localhost:' + port + '/' + path), 8 | B = new faye.Client(scheme + '://localhost:' + port + '/' + path); 9 | 10 | A.connect(function() { 11 | B.connect(function() { 12 | 13 | var time = new Date().getTime(), 14 | MAX = 1000; 15 | 16 | var stop = function() { 17 | console.log(new Date().getTime() - time); 18 | process.exit(); 19 | }; 20 | 21 | var handle = function(client, channel) { 22 | return function(n) { 23 | if (n === MAX) return stop(); 24 | client.publish(channel, n + 1); 25 | }; 26 | }; 27 | 28 | var subA = A.subscribe('/chat/a', handle(A, '/chat/b')), 29 | subB = B.subscribe('/chat/b', handle(B, '/chat/a')); 30 | 31 | subA.callback(function() { 32 | subB.callback(function() { 33 | console.log('START'); 34 | A.publish('/chat/b', 0); 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /examples/node/client.js: -------------------------------------------------------------------------------- 1 | // This script demonstrates a logger for the chat app. First, start the chat 2 | // server in one terminal then run this in another: 3 | // 4 | // $ node examples/node/server.js 5 | // $ node examples/node/client.js 6 | // 7 | // The client connects to the chat server and logs all messages sent by all 8 | // connected users. 9 | 10 | var fs = require('fs'), 11 | deflate = require('permessage-deflate'), 12 | faye = require('../../build'), 13 | 14 | port = process.argv[2] || 8000, 15 | path = process.argv[3] || 'bayeux', 16 | scheme = process.argv[4] === 'tls' ? 'https' : 'http', 17 | endpoint = scheme + '://localhost:' + port + '/' + path, 18 | cert = fs.readFileSync(__dirname + '/../server.crt'), 19 | proxy = { headers: { 'User-Agent': 'Faye' }, tls: { ca: cert }}; 20 | 21 | console.log('Connecting to ' + endpoint); 22 | 23 | var client = new faye.Client(endpoint, { proxy: proxy, tls: { ca: cert }}); 24 | client.addWebsocketExtension(deflate); 25 | 26 | var subscription = client.subscribe('/chat/*', function(message) { 27 | var user = message.user; 28 | 29 | var publication = client.publish('/members/' + user, { 30 | user: 'node-logger', 31 | message: ' Got your message, ' + user + '!' 32 | }); 33 | publication.callback(function() { 34 | console.log('[PUBLISH SUCCEEDED]'); 35 | }); 36 | publication.errback(function(error) { 37 | console.log('[PUBLISH FAILED]', error); 38 | }); 39 | }); 40 | 41 | subscription.callback(function() { 42 | console.log('[SUBSCRIBE SUCCEEDED]'); 43 | }); 44 | subscription.errback(function(error) { 45 | console.log('[SUBSCRIBE FAILED]', error); 46 | }); 47 | 48 | client.bind('transport:down', function() { 49 | console.log('[CONNECTION DOWN]'); 50 | }); 51 | client.bind('transport:up', function() { 52 | console.log('[CONNECTION UP]'); 53 | }); 54 | -------------------------------------------------------------------------------- /examples/node/pingpong.js: -------------------------------------------------------------------------------- 1 | var faye = require('../../build'); 2 | 3 | ENDPOINT = 'http://localhost:8000/bayeux'; 4 | console.log('Connecting to ' + ENDPOINT); 5 | 6 | var ping = new faye.Client(ENDPOINT); 7 | ping.subscribe('/ping', function() { 8 | console.log('PING'); 9 | setTimeout(function() { ping.publish('/pong', {}) }, 1000); 10 | }); 11 | 12 | var pong = new faye.Client(ENDPOINT); 13 | pong.subscribe('/pong', function() { 14 | console.log('PONG'); 15 | setTimeout(function() { pong.publish('/ping', {}) }, 1000); 16 | }); 17 | 18 | setTimeout(function() { ping.publish('/pong', {}) }, 500); 19 | -------------------------------------------------------------------------------- /examples/node/server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | http = require('http'), 4 | https = require('https'), 5 | mime = require('mime'), 6 | deflate = require('permessage-deflate'), 7 | faye = require('../../build'); 8 | 9 | var SHARED_DIR = __dirname + '/..', 10 | PUBLIC_DIR = SHARED_DIR + '/public', 11 | 12 | bayeux = new faye.NodeAdapter({ mount: '/bayeux', timeout: 20 }), 13 | port = process.argv[2] || '8000', 14 | secure = process.argv[3] === 'tls', 15 | key = fs.readFileSync(SHARED_DIR + '/server.key'), 16 | cert = fs.readFileSync(SHARED_DIR + '/server.crt'); 17 | 18 | bayeux.addWebsocketExtension(deflate); 19 | 20 | var handleRequest = function(request, response) { 21 | var path = (request.url === '/') ? '/index.html' : request.url; 22 | 23 | fs.readFile(PUBLIC_DIR + path, function(err, content) { 24 | var status = err ? 404 : 200; 25 | try { 26 | response.writeHead(status, { 'Content-Type': mime.lookup(path) }); 27 | response.end(content || 'Not found'); 28 | } catch (e) {} 29 | }); 30 | }; 31 | 32 | var server = secure 33 | ? https.createServer({ cert: cert, key: key }, handleRequest) 34 | : http.createServer(handleRequest); 35 | 36 | bayeux.attach(server); 37 | server.listen(Number(port)); 38 | 39 | bayeux.getClient().subscribe('/chat/*', function(message) { 40 | console.log('[' + message.user + ']: ' + message.message); 41 | }); 42 | 43 | bayeux.on('subscribe', function(clientId, channel) { 44 | console.log('[ SUBSCRIBE] ' + clientId + ' -> ' + channel); 45 | }); 46 | 47 | bayeux.on('unsubscribe', function(clientId, channel) { 48 | console.log('[UNSUBSCRIBE] ' + clientId + ' -> ' + channel); 49 | }); 50 | 51 | bayeux.on('disconnect', function(clientId) { 52 | console.log('[ DISCONNECT] ' + clientId); 53 | }); 54 | 55 | console.log('Listening on ' + port + (secure? ' (https)' : '')); 56 | -------------------------------------------------------------------------------- /examples/node/ticker.js: -------------------------------------------------------------------------------- 1 | var faye = require('../../build'); 2 | 3 | var endpoint = process.argv[2] || 'http://localhost:8000/bayeux', 4 | client = new faye.Client(endpoint), 5 | n = 0; 6 | 7 | setInterval(function() { 8 | client.publish('/chat/tick', { n: ++n }); 9 | }, 1000); 10 | -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | 3 | <html> 4 | <head> 5 | <meta charset="utf-8"> 6 | <title>Faye demo: chat client</title> 7 | <link rel="stylesheet" href="/style.css"> 8 | <script src="/mootools.js"></script> 9 | <script src="/jquery.js"></script> 10 | <script src="/json2.js"></script> 11 | <script src="/bayeux/client.js"></script> 12 | <script src="/soapbox.js"></script> 13 | </head> 14 | <body> 15 | 16 | <div class="container"> 17 | <h1><em>Soapbox</em> | a Twitter-style chat app</h1> 18 | 19 | <form id="enterUsername"> 20 | <label for="username">Username</label> 21 | <input type="text" name="username" id="username"> 22 | <input type="submit" value="Go"> 23 | </form> 24 | 25 | <div id="app"> 26 | <form id="addFollowee"> 27 | <label for="followee">Follow</label> 28 | <input type="text" name="followee" id="followee"> 29 | <input type="submit" value="Follow"> 30 | </form> 31 | 32 | <form id="postMessage"> 33 | <label for="message">Post a message</label> <span id="transport"></span><br> 34 | <textarea name="message" id="message" rows="3" cols="40"></textarea> 35 | <input type="submit" value="Send"> 36 | </form> 37 | 38 | <ul id="stream"> 39 | </ul> 40 | </div> 41 | 42 | <script> 43 | var bayeux = new Faye.Client('/bayeux'); 44 | Faye.logger = window.console; 45 | Soapbox.init(bayeux); 46 | </script> 47 | </div> 48 | 49 | </body> 50 | </html> 51 | -------------------------------------------------------------------------------- /examples/public/soapbox.js: -------------------------------------------------------------------------------- 1 | Soapbox = { 2 | /** 3 | * Initializes the application, passing in the globally shared Bayeux client. 4 | * Apps on the same page should share a Bayeux client so that they may share 5 | * an open HTTP connection with the server. 6 | */ 7 | init: function(bayeux) { 8 | var self = this; 9 | this._bayeux = bayeux; 10 | 11 | this._login = $('#enterUsername'); 12 | this._app = $('#app'); 13 | this._follow = $('#addFollowee'); 14 | this._post = $('#postMessage'); 15 | this._stream = $('#stream'); 16 | 17 | this._app.hide(); 18 | 19 | // When the user enters a username, store it and start the app 20 | this._login.submit(function() { 21 | self._username = $('#username').val(); 22 | self.launch(); 23 | return false; 24 | }); 25 | 26 | this._bayeux.addExtension({ 27 | outgoing: function(message, callback) { 28 | var type = message.connectionType; 29 | if (type) $('#transport').html('(' + type + ')'); 30 | callback(message); 31 | } 32 | }); 33 | }, 34 | 35 | /** 36 | * Starts the application after a username has been entered. A subscription is 37 | * made to receive messages that mention this user, and forms are set up to 38 | * accept new followers and send messages. 39 | */ 40 | launch: function() { 41 | var self = this; 42 | this._bayeux.subscribe('/members/' + this._username, this.accept, this); 43 | 44 | // Hide login form, show main application UI 45 | this._login.fadeOut('slow', function() { 46 | self._app.fadeIn('slow'); 47 | }); 48 | 49 | // When we add a follower, subscribe to a channel to which the followed user 50 | // will publish messages 51 | this._follow.submit(function() { 52 | var follow = $('#followee'), 53 | name = follow.val(); 54 | 55 | self._bayeux.subscribe('/chat/' + name, self.accept, self); 56 | follow.val(''); 57 | return false; 58 | }); 59 | 60 | // When we enter a message, send it and clear the message field. 61 | this._post.submit(function() { 62 | var msg = $('#message'); 63 | self.post(msg.val()); 64 | msg.val(''); 65 | return false; 66 | }); 67 | 68 | // Detect network problems and disable the form when offline 69 | this._bayeux.bind('transport:down', function() { 70 | this._post.find('textarea,input').attr('disabled', true); 71 | }, this); 72 | this._bayeux.bind('transport:up', function() { 73 | this._post.find('textarea,input').attr('disabled', false); 74 | }, this); 75 | }, 76 | 77 | /** 78 | * Sends messages that the user has entered. The message is scanned for 79 | * @reply-style mentions of other users, and the message is sent to those 80 | * users' channels. 81 | */ 82 | post: function(message) { 83 | var mentions = [], 84 | words = message.split(/\s+/), 85 | self = this, 86 | pattern = /\@[a-z0-9]+/i; 87 | 88 | // Extract @replies from the message 89 | $.each(words, function(i, word) { 90 | if (!pattern.test(word)) return; 91 | word = word.replace(/[^a-z0-9]/ig, ''); 92 | if (word !== self._username) mentions.push(word); 93 | }); 94 | 95 | // Message object to transmit over Bayeux channels 96 | message = { user: this._username, message: message }; 97 | 98 | // Publish to this user's 'from' channel, and to channels for any @replies 99 | // found in the message 100 | this._bayeux.publish('/chat/' + this._username, message); 101 | $.each(mentions, function(i, name) { 102 | self._bayeux.publish('/members/' + name, message); 103 | }); 104 | }, 105 | 106 | /** 107 | * Handler for messages received over subscribed channels. Takes the message 108 | * object sent by the post() method and displays it in the user's message list. 109 | */ 110 | accept: function(message) { 111 | this._stream.prepend('<li><b>' + message.user + ':</b> ' + 112 | message.message + '</li>'); 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /examples/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font: 16px/1.5 FreeSans, Helvetica, Arial, sans-serif; 5 | text-align: center; 6 | } 7 | 8 | .container { 9 | text-align: left; 10 | width: 400px; 11 | margin: 0 auto; 12 | } 13 | 14 | h1, form, ul li { 15 | padding: 24px 0; 16 | border-bottom: 1px solid #c0c0c0; 17 | } 18 | 19 | h1 { 20 | font-size: 20px; 21 | } 22 | 23 | h1 em { 24 | font-style: normal; 25 | font-weight: normal; 26 | color: #444; 27 | text-transform: uppercase; 28 | letter-spacing: -0.06em; 29 | } 30 | 31 | label { 32 | color: #444; 33 | font-weight: bold; 34 | } 35 | 36 | ul, li { 37 | list-style: none; 38 | margin: 0; 39 | padding: 0; 40 | } 41 | 42 | ul li { 43 | padding: 12px 0; 44 | } 45 | -------------------------------------------------------------------------------- /examples/public/ticker.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | 3 | <html> 4 | <head> 5 | <meta charset="utf-8"> 6 | <title>Ticker</title> 7 | <script src="/bayeux/client.js"></script> 8 | </head> 9 | <body> 10 | 11 | <p id="transport"></p> 12 | <h1 id="ticker"></h1> 13 | 14 | <script> 15 | var client = new Faye.Client('/bayeux'), 16 | ticker = document.getElementById('ticker'), 17 | transport = document.getElementById('transport'); 18 | 19 | client.addExtension({ 20 | outgoing: function(message, callback) { 21 | if (message.channel === '/meta/connect') { 22 | transport.innerHTML = message.connectionType; 23 | } 24 | callback(message); 25 | } 26 | }); 27 | 28 | client.subscribe('/chat/tick', function(message) { 29 | ticker.innerHTML = message.n; 30 | }); 31 | </script> 32 | 33 | </body> 34 | </html> 35 | -------------------------------------------------------------------------------- /examples/ruby/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'faye' 3 | require 'permessage_deflate' 4 | 5 | ROOT_DIR = File.expand_path('../..', __FILE__) 6 | set :root, ROOT_DIR 7 | set :logging, false 8 | 9 | get '/' do 10 | File.read(ROOT_DIR + '/public/index.html') 11 | end 12 | 13 | get '/post' do 14 | env['faye.client'].publish('/mentioning/*', { 15 | :user => 'sinatra', 16 | :message => params[:message] 17 | }) 18 | params[:message] 19 | end 20 | 21 | App = Faye::RackAdapter.new(Sinatra::Application, 22 | :mount => '/bayeux', 23 | :timeout => 25 24 | ) 25 | 26 | App.add_websocket_extension(PermessageDeflate) 27 | 28 | def App.log(message) 29 | end 30 | 31 | App.on(:subscribe) do |client_id, channel| 32 | puts "[ SUBSCRIBE] #{ client_id } -> #{ channel }" 33 | end 34 | 35 | App.on(:unsubscribe) do |client_id, channel| 36 | puts "[UNSUBSCRIBE] #{ client_id } -> #{ channel }" 37 | end 38 | 39 | App.on(:disconnect) do |client_id| 40 | puts "[ DISCONNECT] #{ client_id }" 41 | end 42 | -------------------------------------------------------------------------------- /examples/ruby/benchmark.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'faye' 4 | 5 | port = ARGV[0] || 9292 6 | path = ARGV[1] || 'bayeux' 7 | scheme = ARGV[2] == 'tls' ? 'https' : 'http' 8 | 9 | EM.run { 10 | A = Faye::Client.new("#{ scheme }://0.0.0.0:#{ port }/#{ path }") 11 | B = Faye::Client.new("#{ scheme }://0.0.0.0:#{ port }/#{ path }") 12 | 13 | A.connect do 14 | B.connect do 15 | 16 | time = Time.now.to_f * 1000 17 | MAX = 1000 18 | 19 | stop = lambda do 20 | puts Time.now.to_f * 1000 - time 21 | EM.stop 22 | end 23 | 24 | handle = lambda do |client, channel| 25 | lambda do |n| 26 | if n == MAX 27 | stop.call 28 | else 29 | client.publish(channel, n + 1) 30 | end 31 | end 32 | end 33 | 34 | sub_a = A.subscribe('/chat/a', &handle.call(A, '/chat/b')) 35 | sub_b = B.subscribe('/chat/b', &handle.call(B, '/chat/a')) 36 | 37 | sub_a.callback do 38 | sub_b.callback do 39 | puts 'START' 40 | A.publish('/chat/b', 0) 41 | end 42 | end 43 | end 44 | end 45 | } 46 | -------------------------------------------------------------------------------- /examples/ruby/client.rb: -------------------------------------------------------------------------------- 1 | # This script demonstrates a logger for the chat app. First, start the chat 2 | # server in one terminal then run this in another: 3 | # 4 | # $ ruby examples/ruby/server.rb 5 | # $ ruby examples/ruby/client.rb 6 | # 7 | # The client connects to the chat server and logs all messages sent by all 8 | # connected users. 9 | 10 | require 'rubygems' 11 | require 'bundler/setup' 12 | require 'faye' 13 | require 'permessage_deflate' 14 | 15 | port = ARGV[0] || 9292 16 | path = ARGV[1] || 'bayeux' 17 | scheme = ARGV[2] == 'tls' ? 'https' : 'http' 18 | endpoint = "#{ scheme }://user:pass@0.0.0.0:#{ port }/#{ path }" 19 | proxy = { :headers => { 'User-Agent' => 'Faye' }} 20 | 21 | EM.run { 22 | puts "Connecting to #{ endpoint }" 23 | 24 | client = Faye::Client.new(endpoint, :proxy => proxy) 25 | client.add_websocket_extension(PermessageDeflate) 26 | 27 | subscription = client.subscribe '/chat/*' do |message| 28 | user = message['user'] 29 | 30 | publication = client.publish("/members/#{ user }", { 31 | "user" => "ruby-logger", 32 | "message" => "Got your message, #{ user }!" 33 | }) 34 | publication.callback do 35 | puts "[PUBLISH SUCCEEDED]" 36 | end 37 | publication.errback do |error| 38 | puts "[PUBLISH FAILED] #{ error.inspect }" 39 | end 40 | end 41 | 42 | subscription.callback do 43 | puts "[SUBSCRIBE SUCCEEDED]" 44 | end 45 | subscription.errback do |error| 46 | puts "[SUBSCRIBE FAILED] #{ error.inspect }" 47 | end 48 | 49 | client.bind 'transport:down' do 50 | puts "[CONNECTION DOWN]" 51 | end 52 | client.bind 'transport:up' do 53 | puts "[CONNECTION UP]" 54 | end 55 | } 56 | -------------------------------------------------------------------------------- /examples/ruby/config.ru: -------------------------------------------------------------------------------- 1 | # Run using your favourite async server: 2 | # 3 | # thin start -R examples/ruby/config.ru -p 9292 4 | # rainbows -c examples/ruby/rainbows.conf -E production examples/ruby/config.ru -p 9292 5 | # 6 | # If you run using one of these commands, the webserver is loaded before this 7 | # file, so Faye::WebSocket can figure out which adapter to load. If instead you 8 | # run using `rackup`, you need the `load_adapter` line below. 9 | # 10 | # rackup -E production -s thin examples/ruby/config.ru -p 9292 11 | 12 | require 'rubygems' 13 | require 'bundler/setup' 14 | require File.expand_path('../app', __FILE__) 15 | Faye::WebSocket.load_adapter('thin') 16 | 17 | run App 18 | -------------------------------------------------------------------------------- /examples/ruby/pingpong.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'faye' 4 | 5 | EM.run { 6 | ENDPOINT = 'http://0.0.0.0:9292/bayeux' 7 | puts 'Connecting to ' + ENDPOINT 8 | 9 | ping = Faye::Client.new(ENDPOINT) 10 | ping.subscribe('/ping') do 11 | puts 'PING' 12 | EM.add_timer(1) { ping.publish('/pong', {}) } 13 | end 14 | 15 | pong = Faye::Client.new(ENDPOINT) 16 | pong.subscribe('/pong') do 17 | puts 'PONG' 18 | EM.add_timer(1) { ping.publish('/ping', {}) } 19 | end 20 | 21 | EM.add_timer(0.5) { ping.publish('/pong', {}) } 22 | } 23 | -------------------------------------------------------------------------------- /examples/ruby/rainbows.conf: -------------------------------------------------------------------------------- 1 | Rainbows! { use :EventMachine } 2 | -------------------------------------------------------------------------------- /examples/ruby/server.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | port = ARGV[0] || 9292 5 | secure = ARGV[1] == 'tls' 6 | engine = ARGV[2] || 'thin' 7 | shared = File.expand_path('../..', __FILE__) 8 | 9 | require File.expand_path('../app', __FILE__) 10 | Faye::WebSocket.load_adapter(engine) 11 | 12 | case engine 13 | 14 | when 'goliath' 15 | class FayeServer < Goliath::API 16 | def response(env) 17 | App.call(env) 18 | end 19 | end 20 | 21 | when 'puma' 22 | require 'puma/events' 23 | events = Puma::Events.new($stdout, $stderr) 24 | 25 | require 'puma/binder' 26 | binder = Puma::Binder.new(events) 27 | binder.parse(["tcp://0.0.0.0:#{ port }"], App) 28 | 29 | server = Puma::Server.new(App, events) 30 | server.binder = binder 31 | server.run.join 32 | 33 | when 'rainbows' 34 | rackup = Unicorn::Configurator::RACKUP 35 | rackup[:port] = port 36 | rackup[:set_listener] = true 37 | options = rackup[:options] 38 | options[:config_file] = File.expand_path('../rainbows.conf', __FILE__) 39 | Rainbows::HttpServer.new(App, options).start.join 40 | 41 | when 'thin' 42 | thin = Rack::Handler.get('thin') 43 | thin.run(App, :Host => '0.0.0.0', :Port => port) do |server| 44 | if secure 45 | server.ssl_options = { 46 | :private_key_file => shared + '/server.key', 47 | :cert_chain_file => shared + '/server.crt' 48 | } 49 | server.ssl = true 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /examples/ruby/ticker.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'faye' 4 | 5 | EM.run { 6 | endpoint = ARGV.first || 'http://0.0.0.0:9292/bayeux' 7 | client = Faye::Client.new(endpoint) 8 | n = 0 9 | 10 | EM.add_periodic_timer 1 do 11 | n += 1 12 | client.publish('/chat/tick', 'n' => n) 13 | end 14 | } 15 | -------------------------------------------------------------------------------- /examples/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDETCCAfkCFFVA1RW+x/RHvxEUYndBOz6n0ZweMA0GCSqGSIb3DQEBCwUAMEUx 3 | CzAJBgNVBAYTAlVLMRMwEQYDVQQIDApTb21lLVN0YXRlMQ0wCwYDVQQKDARGYXll 4 | MRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMjQwNjA1MTYwOTQ2WhcNMjUwNjA1MTYw 5 | OTQ2WjBFMQswCQYDVQQGEwJVSzETMBEGA1UECAwKU29tZS1TdGF0ZTENMAsGA1UE 6 | CgwERmF5ZTESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOC 7 | AQ8AMIIBCgKCAQEAsZz0XWvSsgYabtZ7xipxUhCI64FtOUZ8QsM+naq7TDSCX/bp 8 | 89UJUFLVZIupI1UkIowkfADMAZ4KOJnuSfkucZfOW2Od0EUxZd2/leh7zJdm4dNx 9 | bIOGhGitnzHV3L/lx6Zx+ai1TWJRQuJ+Ik8qOHDIrWUgw5nLeI1RnsRHbq1un7Vf 10 | swzC3y1vSdl+h2CEg+TIe7HtiIgmt4+AR4JoVEuwmqTSeloFrk6M8jN2Cvmt9QdG 11 | qUKFgxde+UonaRzK/yL/YBVg5oJs2kqS2Yf8lT8PIZ4xiP+28Z/ySYphZjFj5sSE 12 | S3xcdJNaxKbzpcrxsvN+UIzJ1qvsNO7Rm473IwIDAQABMA0GCSqGSIb3DQEBCwUA 13 | A4IBAQAmuriySwIL2casMSzh4PPm3qLrNXOMQf472/jzftM4DqwNY7Zq0hxp89W0 14 | mG7SZAQiv3gqKbgX4g5+EhSTDAjrTSbQsalgueF50DXInaR/a8fETFZVpd+O8HWA 15 | OiL1DX70INef8zap2yt5RMkFZT/RrZt8V/0/4PxePSXCmvnT/DhyLx6xyeoE48HL 16 | S/sG8oSr0xENj+O28X/fAS7eL/iQIUwHUcjcS+NyRYxcLJM70ZJjvvOQhUVlFKjV 17 | Ic+GxmGXDNozopFfgaGT0S2C3ZeLCEbqaFuJg9XSnO6UKM91euc82dfEhgiWE2OU 18 | TKQfFmSxTDHoDeG4TqR6jC1xRqEu 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /examples/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCxnPRda9KyBhpu 3 | 1nvGKnFSEIjrgW05RnxCwz6dqrtMNIJf9unz1QlQUtVki6kjVSQijCR8AMwBngo4 4 | me5J+S5xl85bY53QRTFl3b+V6HvMl2bh03Fsg4aEaK2fMdXcv+XHpnH5qLVNYlFC 5 | 4n4iTyo4cMitZSDDmct4jVGexEdurW6ftV+zDMLfLW9J2X6HYISD5Mh7se2IiCa3 6 | j4BHgmhUS7CapNJ6WgWuTozyM3YK+a31B0apQoWDF175SidpHMr/Iv9gFWDmgmza 7 | SpLZh/yVPw8hnjGI/7bxn/JJimFmMWPmxIRLfFx0k1rEpvOlyvGy835QjMnWq+w0 8 | 7tGbjvcjAgMBAAECggEABgocwxp6ASS0/GTds5jY3p4CUePGR4bOjeSeufTGxqoY 9 | btPyE6EAXpNafz9CgpmQD36tdOwAA+QQW+lcEXbgLeuoEDJ8eMsJiXm3XI0ZvJS/ 10 | Yllyx2pXhiQbF0k2CPobgaT2xjMG6zk3IyuZd2gyutWW9VJ1gUE3CoPfrSLmfOxp 11 | JFOTrpmukcaBnsVUAKLj0fk1HZbHFCa/N+p0QgdgYavHFfReqgJ6zY1jY5czOBkI 12 | 5Hrzcizi2zDHMbgJ37hIB5dNUSep4WzbC9ZnlBUhXXqpBAvh9k1N4H9PieVYMFZ8 13 | 2ftnxbLFyt5UnPk7Ord0xZmum04kdxG0AKRjBuweAQKBgQD40kLP3gL9suDrbJXe 14 | 4OQ72awnHCpwCcM8FKQkU/xcQp1D9Uth48ggilzY6nXrUmo2/+OwqCEj2o5jzf02 15 | dqfd7L7nmeZ6NUFf+dw3y1c7AlvdptBFCGVoBolsb3jaGXuri+k1uUC8VJbl90kr 16 | rEYmVTUutIf6Vd0pPJ62J6uNIwKBgQC2vMPzPI59RBHErf+zabqNJXq7IO80GREm 17 | wUi7pP2kj5ELUdcK4TjyD2q7Nl6egLOmzS4puELdMrcAgt3J3Bh65nhvt13S3b3A 18 | O4hxK6HMtEGBsk0IVqSC+s3q3G/U4IjINhEngGwL61vrfbfR+Y4LDuL9auGDQkvL 19 | mnjtEJ6OAQKBgFbWSqrw+GpB+20uQD/AjOa2WPZtRgJD5fcZ3Q8woGoydWA6Q0yu 20 | ijGRGEY7zVuLL7ZyJ6yHgMlahUcfpLdVQdCZxyZc96q+20n7kXeHZ7IYaKc6iIUP 21 | IRTk8yD85lh3fEmqUoGFXapcey1W2Bp9zR2jryPVrX8YaE7z8Q/xWFWxAoGAQXx4 22 | RGzJK37/Vxp77hHPttFdoD33OxZYnSjbJdPEyfphIktb4xw/Sg/YUer0EZ1RxE73 23 | YiAUZizMhDRhwvtLEpARTQfLacvpOkCbbuMSAsf+SbpZ/Mj//6hdrvL8aK9mlUk6 24 | 8IsHLWZU9JmDDI6AJtpY4jQxSNazTu22tE4mZAECgYAg1/OFoMBJ+VfvWhxJGgsQ 25 | z1G6f//OXFk6wQsyMV+aEsegn9YvM+3sI9c2fn9GJR47Gg0xvYJkwhzutyadI8q2 26 | /cRHl9Im8ak5oWk+afrhFmF+eNsrPFEOVj9RVIRoviscWq6inSvA1+44xzYxcPbM 27 | X6pI3ThzDU/59HnWClMsMw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /faye.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'faye' 3 | s.version = '1.4.1' 4 | s.summary = 'Simple pub/sub messaging for the web' 5 | s.author = 'James Coglan' 6 | s.email = 'jcoglan@gmail.com' 7 | s.homepage = 'https://faye.jcoglan.com' 8 | s.license = 'Apache-2.0' 9 | 10 | s.extra_rdoc_files = %w[README.md] 11 | s.rdoc_options = %w[--main README.md --markup markdown] 12 | s.require_paths = %w[lib] 13 | 14 | # It is important that the JavaScript files listed here are not removed: they 15 | # contain the browser client and the gem should fail to build without them. 16 | # You should generate them by running `make` in the project root. 17 | client_suffix = %w[.js .js.map -min.js -min.js.map] 18 | client_files = client_suffix.map { |ext| "build/client/faye-browser#{ext}" } 19 | 20 | s.files = %w[CHANGELOG.md LICENSE.md README.md] + 21 | Dir.glob('lib/**/*.rb') + 22 | client_files 23 | 24 | s.add_dependency 'cookiejar', '>= 0.3.0' 25 | s.add_dependency 'em-http-request', '>= 1.1.6' 26 | s.add_dependency 'eventmachine', '>= 0.12.0' 27 | s.add_dependency 'faye-websocket', '>= 0.11.0' 28 | s.add_dependency 'multi_json', '>= 1.0.0' 29 | s.add_dependency 'rack', '>= 1.0.0' 30 | s.add_dependency 'websocket-driver', '>= 0.5.1' 31 | 32 | s.add_development_dependency 'compass', '~> 0.11.0' 33 | s.add_development_dependency 'haml', '~> 3.1.0' 34 | s.add_development_dependency 'permessage_deflate', '>= 0.1.0' 35 | s.add_development_dependency 'puma', '>= 2.0.0' 36 | s.add_development_dependency 'rack-proxy', '~> 0.4.0' 37 | s.add_development_dependency 'rack-test' 38 | s.add_development_dependency 'rake' 39 | s.add_development_dependency 'RedCloth', '~> 3.0.0' 40 | s.add_development_dependency 'rspec', '~> 2.99.0' 41 | s.add_development_dependency 'rspec-eventmachine', '>= 0.2.0' 42 | s.add_development_dependency 'sass', '~> 3.2.0' 43 | s.add_development_dependency 'sinatra' 44 | s.add_development_dependency 'staticmatic' 45 | 46 | jruby = RUBY_PLATFORM =~ /java/ 47 | rbx = defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /rbx/ 48 | 49 | unless jruby 50 | s.add_development_dependency 'rainbows', '~> 4.4.0' 51 | s.add_development_dependency 'thin', '>= 1.2.0' 52 | end 53 | 54 | unless rbx or RUBY_VERSION < '1.9' 55 | s.add_development_dependency 'goliath' 56 | end 57 | 58 | unless jruby or rbx 59 | s.add_development_dependency 'passenger', '>= 4.0.0' 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/faye.rb: -------------------------------------------------------------------------------- 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.4.1' 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 :Scheduler, File.join(ROOT, 'faye', 'protocol', 'scheduler') 34 | autoload :Extensible, File.join(ROOT, 'faye', 'protocol', 'extensible') 35 | autoload :Grammar, File.join(ROOT, 'faye', 'protocol', 'grammar') 36 | autoload :Publication, File.join(ROOT, 'faye', 'protocol', 'publication') 37 | autoload :Server, File.join(ROOT, 'faye', 'protocol', 'server') 38 | autoload :Subscription, File.join(ROOT, 'faye', 'protocol', 'subscription') 39 | 40 | autoload :Error, File.join(ROOT, 'faye', 'error') 41 | autoload :Transport, File.join(ROOT, 'faye', 'transport', 'transport') 42 | 43 | autoload :RackAdapter, File.join(ROOT, 'faye', 'adapters', 'rack_adapter') 44 | autoload :StaticServer, File.join(ROOT, 'faye', 'adapters', 'static_server') 45 | 46 | BAYEUX_VERSION = '1.0' 47 | JSONP_CALLBACK = 'jsonpcallback' 48 | CONNECTION_TYPES = %w[long-polling cross-origin-long-polling callback-polling websocket eventsource in-process] 49 | 50 | MANDATORY_CONNECTION_TYPES = %w[long-polling callback-polling in-process] 51 | 52 | class << self 53 | attr_accessor :logger 54 | end 55 | 56 | def self.ensure_reactor_running! 57 | Engine.ensure_reactor_running! 58 | end 59 | 60 | def self.random(*args) 61 | Engine.random(*args) 62 | end 63 | 64 | def self.client_id_from_messages(messages) 65 | first = [messages].flatten.find { |m| m['channel'] == '/meta/connect' } 66 | first && first['clientId'] 67 | end 68 | 69 | def self.copy_object(object) 70 | case object 71 | when Hash 72 | clone = {} 73 | object.each { |k,v| clone[k] = copy_object(v) } 74 | clone 75 | when Array 76 | clone = [] 77 | object.each { |v| clone << copy_object(v) } 78 | clone 79 | else 80 | object 81 | end 82 | end 83 | 84 | def self.to_json(value) 85 | case value 86 | when Hash, Array then MultiJson.dump(value) 87 | when String, NilClass then value.inspect 88 | else value.to_s 89 | end 90 | end 91 | 92 | def self.async_each(list, iterator, callback) 93 | n = list.size 94 | i = -1 95 | calls = 0 96 | looping = false 97 | 98 | loop, resume = nil, nil 99 | 100 | iterate = lambda do 101 | calls -= 1 102 | i += 1 103 | if i == n 104 | callback.call if callback 105 | else 106 | iterator.call(list[i], resume) 107 | end 108 | end 109 | 110 | loop = lambda do 111 | unless looping 112 | looping = true 113 | iterate.call while calls > 0 114 | looping = false 115 | end 116 | end 117 | 118 | resume = lambda do 119 | calls += 1 120 | loop.call 121 | end 122 | resume.call 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/faye/adapters/static_server.rb: -------------------------------------------------------------------------------- 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 57 | -------------------------------------------------------------------------------- /lib/faye/engines/connection.rb: -------------------------------------------------------------------------------- 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 59 | -------------------------------------------------------------------------------- /lib/faye/engines/memory.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | module Engine 3 | 4 | class Memory 5 | include Timeouts 6 | 7 | def self.create(server, options) 8 | new(server, options) 9 | end 10 | 11 | def initialize(server, options) 12 | @server = server 13 | @options = options 14 | reset 15 | end 16 | 17 | def disconnect 18 | reset 19 | remove_all_timeouts 20 | end 21 | 22 | def reset 23 | @namespace = Namespace.new 24 | @clients = {} 25 | @channels = {} 26 | @messages = {} 27 | end 28 | 29 | def create_client(&callback) 30 | client_id = @namespace.generate 31 | @server.debug('Created new client ?', client_id) 32 | ping(client_id) 33 | @server.trigger(:handshake, client_id) 34 | callback.call(client_id) 35 | end 36 | 37 | def destroy_client(client_id, &callback) 38 | return unless @namespace.exists?(client_id) 39 | 40 | if @clients.has_key?(client_id) 41 | @clients[client_id].each { |channel| unsubscribe(client_id, channel) } 42 | end 43 | 44 | remove_timeout(client_id) 45 | @namespace.release(client_id) 46 | @messages.delete(client_id) 47 | @server.debug('Destroyed client ?', client_id) 48 | @server.trigger(:disconnect, client_id) 49 | @server.trigger(:close, client_id) 50 | callback.call if callback 51 | end 52 | 53 | def client_exists(client_id, &callback) 54 | callback.call(@namespace.exists?(client_id)) 55 | end 56 | 57 | def ping(client_id) 58 | timeout = @server.timeout 59 | return unless Numeric === timeout 60 | @server.debug('Ping ?, ?', client_id, timeout) 61 | remove_timeout(client_id) 62 | add_timeout(client_id, 2 * timeout) { destroy_client(client_id) } 63 | end 64 | 65 | def subscribe(client_id, channel, &callback) 66 | @clients[client_id] ||= Set.new 67 | should_trigger = @clients[client_id].add?(channel) 68 | 69 | @channels[channel] ||= Set.new 70 | @channels[channel].add(client_id) 71 | 72 | @server.debug('Subscribed client ? to channel ?', client_id, channel) 73 | @server.trigger(:subscribe, client_id, channel) if should_trigger 74 | callback.call(true) if callback 75 | end 76 | 77 | def unsubscribe(client_id, channel, &callback) 78 | if @clients.has_key?(client_id) 79 | should_trigger = @clients[client_id].delete?(channel) 80 | @clients.delete(client_id) if @clients[client_id].empty? 81 | end 82 | 83 | if @channels.has_key?(channel) 84 | @channels[channel].delete(client_id) 85 | @channels.delete(channel) if @channels[channel].empty? 86 | end 87 | 88 | @server.debug('Unsubscribed client ? from channel ?', client_id, channel) 89 | @server.trigger(:unsubscribe, client_id, channel) if should_trigger 90 | callback.call(true) if callback 91 | end 92 | 93 | def publish(message, channels) 94 | @server.debug('Publishing message ?', message) 95 | 96 | clients = Set.new 97 | 98 | channels.each do |channel| 99 | next unless subs = @channels[channel] 100 | subs.each(&clients.method(:add)) 101 | end 102 | 103 | clients.each do |client_id| 104 | @server.debug('Queueing for client ?: ?', client_id, message) 105 | @messages[client_id] ||= [] 106 | @messages[client_id] << Faye.copy_object(message) 107 | empty_queue(client_id) 108 | end 109 | 110 | @server.trigger(:publish, message['clientId'], message['channel'], message['data']) 111 | end 112 | 113 | def empty_queue(client_id) 114 | return unless @server.has_connection?(client_id) 115 | @server.deliver(client_id, @messages[client_id]) 116 | @messages.delete(client_id) 117 | end 118 | end 119 | 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/faye/engines/proxy.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | module Engine 3 | 4 | METHODS = %w[create_client client_exists destroy_client ping subscribe unsubscribe] 5 | MAX_DELAY = 0.0 6 | INTERVAL = 0.0 7 | TIMEOUT = 60.0 8 | ID_LENGTH = 160 9 | 10 | autoload :Connection, File.expand_path('../connection', __FILE__) 11 | autoload :Memory, File.expand_path('../memory', __FILE__) 12 | 13 | def self.ensure_reactor_running! 14 | Thread.new { EventMachine.run } unless EventMachine.reactor_running? 15 | Thread.pass until EventMachine.reactor_running? 16 | end 17 | 18 | def self.get(options) 19 | Proxy.new(options) 20 | end 21 | 22 | def self.random(bitlength = ID_LENGTH) 23 | limit = 2 ** bitlength 24 | max_size = (bitlength * Math.log(2) / Math.log(36)).ceil 25 | string = SecureRandom.random_number(limit).to_s(36) 26 | string = '0' + string while string.size < max_size 27 | string 28 | end 29 | 30 | class Proxy 31 | include Publisher 32 | include Logging 33 | 34 | attr_reader :interval, :timeout 35 | 36 | extend Forwardable 37 | def_delegators :@engine, *METHODS 38 | 39 | def initialize(options) 40 | super() 41 | 42 | @options = options 43 | @connections = {} 44 | @interval = @options[:interval] || INTERVAL 45 | @timeout = @options[:timeout] || TIMEOUT 46 | 47 | engine_class = @options[:type] || Memory 48 | @engine = engine_class.create(self, @options) 49 | 50 | bind :close do |client_id| 51 | EventMachine.next_tick { flush_connection(client_id) } 52 | end 53 | 54 | debug('Created new engine: ?', @options) 55 | end 56 | 57 | def connect(client_id, options = {}, &callback) 58 | debug('Accepting connection from ?', client_id) 59 | @engine.ping(client_id) 60 | conn = connection(client_id, true) 61 | conn.connect(options, &callback) 62 | @engine.empty_queue(client_id) 63 | end 64 | 65 | def has_connection?(client_id) 66 | @connections.has_key?(client_id) 67 | end 68 | 69 | def connection(client_id, create) 70 | conn = @connections[client_id] 71 | return conn if conn or not create 72 | @connections[client_id] = Connection.new(self, client_id) 73 | trigger('connection:open', client_id) 74 | @connections[client_id] 75 | end 76 | 77 | def close_connection(client_id) 78 | debug('Closing connection for ?', client_id) 79 | return unless conn = @connections[client_id] 80 | conn.socket.close if conn.socket 81 | trigger('connection:close', client_id) 82 | @connections.delete(client_id) 83 | end 84 | 85 | def open_socket(client_id, socket) 86 | conn = connection(client_id, true) 87 | conn.socket = socket 88 | end 89 | 90 | def deliver(client_id, messages) 91 | return if !messages || messages.empty? 92 | return false unless conn = connection(client_id, false) 93 | messages.each(&conn.method(:deliver)) 94 | true 95 | end 96 | 97 | def generate_id 98 | Engine.random 99 | end 100 | 101 | def flush_connection(client_id, close = true) 102 | return unless client_id 103 | debug('Flushing connection for ?', client_id) 104 | return unless conn = connection(client_id, false) 105 | conn.socket = nil unless close 106 | conn.flush 107 | close_connection(client_id) 108 | end 109 | 110 | def close 111 | @connections.keys.each { |client_id| flush_connection(client_id) } 112 | @engine.disconnect 113 | end 114 | 115 | def disconnect 116 | @engine.disconnect if @engine.respond_to?(:disconnect) 117 | end 118 | 119 | def publish(message) 120 | channels = Channel.expand(message['channel']) 121 | @engine.publish(message, channels) 122 | end 123 | end 124 | 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/faye/error.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | class Error 3 | 4 | def self.method_missing(type, *args) 5 | code = const_get(type.to_s.upcase) 6 | new(code[0], args, code[1]).to_s 7 | end 8 | 9 | def self.parse(message) 10 | message ||= '' 11 | return new(nil, [], message) unless Grammar::ERROR =~ message 12 | 13 | parts = message.split(':') 14 | code = parts[0].to_i 15 | params = parts[1].split(',') 16 | message = parts[2] 17 | 18 | new(code, params, message) 19 | end 20 | 21 | attr_reader :code, :params, :message 22 | 23 | def initialize(code, params, message) 24 | @code = code 25 | @params = params 26 | @message = message 27 | end 28 | 29 | def to_s 30 | "#{ @code }:#{ @params * ',' }:#{ @message }" 31 | end 32 | 33 | # http://code.google.com/p/cometd/wiki/BayeuxCodes 34 | VERSION_MISMATCH = [300, 'Version mismatch'] 35 | CONNTYPE_MISMATCH = [301, 'Connection types not supported'] 36 | EXT_MISMATCH = [302, 'Extension mismatch'] 37 | BAD_REQUEST = [400, 'Bad request'] 38 | CLIENT_UNKNOWN = [401, 'Unknown client'] 39 | PARAMETER_MISSING = [402, 'Missing required parameter'] 40 | CHANNEL_FORBIDDEN = [403, 'Forbidden channel'] 41 | CHANNEL_UNKNOWN = [404, 'Unknown channel'] 42 | CHANNEL_INVALID = [405, 'Invalid channel'] 43 | EXT_UNKNOWN = [406, 'Unknown extension'] 44 | PUBLISH_FAILED = [407, 'Failed to publish'] 45 | SERVER_ERROR = [500, 'Internal server error'] 46 | 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/faye/mixins/deferrable.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | module Deferrable 3 | 4 | include EventMachine::Deferrable 5 | 6 | def set_deferred_status(status, *args) 7 | if status == :unknown 8 | @deferred_status = @deferred_args = @callbacks = @errbacks = nil 9 | end 10 | super 11 | end 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/faye/mixins/logging.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | module Logging 3 | 4 | LOG_LEVELS = { 5 | :fatal => 4, 6 | :error => 3, 7 | :warn => 2, 8 | :info => 1, 9 | :debug => 0 10 | } 11 | 12 | LOG_LEVELS.each do |level, value| 13 | define_method(level) { |*args| write_log(args, level) } 14 | end 15 | 16 | private 17 | 18 | def write_log(message_args, level) 19 | return unless Faye.logger 20 | 21 | message = message_args.shift.gsub(/\?/) do 22 | Faye.to_json(message_args.shift) 23 | end 24 | 25 | banner = "[#{ self.class.name }] " 26 | 27 | if Faye.logger.respond_to?(level) 28 | Faye.logger.__send__(level, banner + message) 29 | elsif Faye.logger.respond_to?(:call) 30 | Faye.logger.call(banner + message) 31 | end 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/faye/mixins/publisher.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | module Publisher 3 | 4 | include ::WebSocket::Driver::EventEmitter 5 | 6 | alias :bind :add_listener 7 | alias :trigger :emit 8 | 9 | def unbind(event, &listener) 10 | if listener 11 | remove_listener(event, &listener) 12 | else 13 | remove_all_listeners(event) 14 | end 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/faye/mixins/timeouts.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | module Timeouts 3 | def add_timeout(name, delay, &block) 4 | Engine.ensure_reactor_running! 5 | @timeouts ||= {} 6 | return if @timeouts.has_key?(name) 7 | @timeouts[name] = EventMachine.add_timer(delay) do 8 | @timeouts.delete(name) 9 | block.call 10 | end 11 | end 12 | 13 | def remove_timeout(name) 14 | @timeouts ||= {} 15 | timeout = @timeouts[name] 16 | return if timeout.nil? 17 | EventMachine.cancel_timer(timeout) 18 | @timeouts.delete(name) 19 | end 20 | 21 | def remove_all_timeouts 22 | @timeouts ||= {} 23 | @timeouts.keys.each { |name| remove_timeout(name) } 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/faye/protocol/channel.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | class Channel 3 | 4 | include Publisher 5 | attr_reader :name 6 | 7 | def initialize(name) 8 | super() 9 | @name = name 10 | end 11 | 12 | def <<(message) 13 | trigger(:message, message) 14 | end 15 | 16 | def unused? 17 | listener_count(:message).zero? 18 | end 19 | 20 | HANDSHAKE = '/meta/handshake' 21 | CONNECT = '/meta/connect' 22 | SUBSCRIBE = '/meta/subscribe' 23 | UNSUBSCRIBE = '/meta/unsubscribe' 24 | DISCONNECT = '/meta/disconnect' 25 | 26 | META = 'meta' 27 | SERVICE = 'service' 28 | 29 | class << self 30 | def expand(name) 31 | segments = parse(name) 32 | channels = ['/**', name] 33 | 34 | copy = segments.dup 35 | copy[copy.size - 1] = '*' 36 | channels << unparse(copy) 37 | 38 | 1.upto(segments.size - 1) do |i| 39 | copy = segments[0...i] 40 | copy << '**' 41 | channels << unparse(copy) 42 | end 43 | 44 | channels 45 | end 46 | 47 | def valid?(name) 48 | Grammar::CHANNEL_NAME =~ name or 49 | Grammar::CHANNEL_PATTERN =~ name 50 | end 51 | 52 | def parse(name) 53 | return nil unless valid?(name) 54 | name.split('/')[1..-1] 55 | end 56 | 57 | def unparse(segments) 58 | '/' + segments.join('/') 59 | end 60 | 61 | def meta?(name) 62 | segments = parse(name) 63 | segments ? (segments.first == META) : nil 64 | end 65 | 66 | def service?(name) 67 | segments = parse(name) 68 | segments ? (segments.first == SERVICE) : nil 69 | end 70 | 71 | def subscribable?(name) 72 | return nil unless valid?(name) 73 | not meta?(name) and not service?(name) 74 | end 75 | end 76 | 77 | class Set 78 | def initialize 79 | @channels = {} 80 | end 81 | 82 | def keys 83 | @channels.keys 84 | end 85 | 86 | def remove(name) 87 | @channels.delete(name) 88 | end 89 | 90 | def has_subscription?(name) 91 | @channels.has_key?(name) 92 | end 93 | 94 | def subscribe(names, subscription) 95 | names.each do |name| 96 | channel = @channels[name] ||= Channel.new(name) 97 | channel.bind(:message, &subscription) 98 | end 99 | end 100 | 101 | def unsubscribe(name, subscription) 102 | channel = @channels[name] 103 | return false unless channel 104 | channel.unbind(:message, &subscription) 105 | if channel.unused? 106 | remove(name) 107 | true 108 | else 109 | false 110 | end 111 | end 112 | 113 | def distribute_message(message) 114 | channels = Channel.expand(message['channel']) 115 | channels.each do |name| 116 | channel = @channels[name] 117 | channel.trigger(:message, message) if channel 118 | end 119 | end 120 | end 121 | 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/faye/protocol/extensible.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | module Extensible 3 | include Logging 4 | 5 | def add_extension(extension) 6 | @extensions ||= [] 7 | @extensions << extension 8 | extension.added(self) if extension.respond_to?(:added) 9 | end 10 | 11 | def remove_extension(extension) 12 | return unless @extensions 13 | @extensions.delete_if do |ext| 14 | next false unless ext == extension 15 | extension.removed(self) if extension.respond_to?(:removed) 16 | true 17 | end 18 | end 19 | 20 | def pipe_through_extensions(stage, message, env, &callback) 21 | debug('Passing through ? extensions: ?', stage, message) 22 | 23 | return callback.call(message) unless @extensions 24 | extensions = @extensions.dup 25 | 26 | pipe = lambda do |message| 27 | next callback.call(message) unless message 28 | 29 | extension = extensions.shift 30 | next callback.call(message) unless extension 31 | 32 | next pipe.call(message) unless extension.respond_to?(stage) 33 | 34 | arity = extension.method(stage).arity 35 | if arity >= 3 36 | extension.__send__(stage, message, env, pipe) 37 | else 38 | extension.__send__(stage, message, pipe) 39 | end 40 | end 41 | pipe.call(message) 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/faye/protocol/grammar.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | module Grammar 3 | 4 | def self.rule(&block) 5 | source = instance_eval(&block) 6 | %r{^#{string(source)}$} 7 | end 8 | 9 | def self.choice(*list) 10 | '(' + list.map(&method(:string)) * '|' + ')' 11 | end 12 | 13 | def self.repeat(*pattern) 14 | '(' + string(pattern) + ')*' 15 | end 16 | 17 | def self.oneormore(*pattern) 18 | '(' + string(pattern) + ')+' 19 | end 20 | 21 | def self.string(item) 22 | return item.map(&method(:string)) * '' if Array === item 23 | String === item ? item : item.source.gsub(/^\^/, '').gsub(/\$/, '') 24 | end 25 | 26 | LOWALPHA = rule {[ '[a-z]' ]} 27 | UPALPHA = rule {[ '[A-Z]' ]} 28 | ALPHA = rule {[ choice(LOWALPHA, UPALPHA) ]} 29 | DIGIT = rule {[ '[0-9]' ]} 30 | ALPHANUM = rule {[ choice(ALPHA, DIGIT) ]} 31 | MARK = rule {[ choice(*%w[\\- \\_ \\! \\~ \\( \\) \\$ \\@]) ]} 32 | STRING = rule {[ repeat(choice(ALPHANUM, MARK, ' ', '\\/', '\\*', '\\.')) ]} 33 | TOKEN = rule {[ oneormore(choice(ALPHANUM, MARK)) ]} 34 | INTEGER = rule {[ oneormore(DIGIT) ]} 35 | 36 | CHANNEL_SEGMENT = rule {[ TOKEN ]} 37 | CHANNEL_SEGMENTS = rule {[ CHANNEL_SEGMENT, repeat('\\/', CHANNEL_SEGMENT) ]} 38 | CHANNEL_NAME = rule {[ '\\/', CHANNEL_SEGMENTS ]} 39 | 40 | WILD_CARD = rule {[ '\\*{1,2}' ]} 41 | CHANNEL_PATTERN = rule {[ repeat('\\/', CHANNEL_SEGMENT), '\\/', WILD_CARD ]} 42 | 43 | VERSION_ELEMENT = rule {[ ALPHANUM, repeat(choice(ALPHANUM, '\\-', '\\_')) ]} 44 | VERSION = rule {[ INTEGER, repeat('\\.', VERSION_ELEMENT) ]} 45 | 46 | CLIENT_ID = rule {[ oneormore(ALPHANUM) ]} 47 | 48 | ID = rule {[ oneormore(ALPHANUM) ]} 49 | 50 | ERROR_MESSAGE = rule {[ STRING ]} 51 | ERROR_ARGS = rule {[ STRING, repeat(',', STRING) ]} 52 | ERROR_CODE = rule {[ DIGIT, DIGIT, DIGIT ]} 53 | ERROR = rule {[ choice(string([ERROR_CODE, ':', ERROR_ARGS, ':', ERROR_MESSAGE]), 54 | string([ERROR_CODE, ':', ':', ERROR_MESSAGE])) ]} 55 | 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/faye/protocol/publication.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | class Publication 3 | include Deferrable 4 | end 5 | end -------------------------------------------------------------------------------- /lib/faye/protocol/scheduler.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | class Scheduler 3 | 4 | def initialize(message, options) 5 | @message = message 6 | @options = options 7 | @attempts = 0 8 | end 9 | 10 | def interval 11 | @options[:interval] 12 | end 13 | 14 | def timeout 15 | @options[:timeout] 16 | end 17 | 18 | def deliverable? 19 | attempts = @options[:attempts] 20 | deadline = @options[:deadline] 21 | now = Time.now.to_f 22 | 23 | return false if attempts and @attempts >= attempts 24 | return false if deadline and now > deadline 25 | 26 | true 27 | end 28 | 29 | def send! 30 | @attempts += 1 31 | end 32 | 33 | def succeed! 34 | end 35 | 36 | def fail! 37 | end 38 | 39 | def abort! 40 | end 41 | 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/faye/protocol/socket.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | class Server 3 | 4 | class Socket 5 | def initialize(server, socket, env) 6 | @server = server 7 | @socket = socket 8 | @env = env 9 | end 10 | 11 | def send(message) 12 | @server.pipe_through_extensions(:outgoing, message, @env) do |piped_message| 13 | @socket.send(Faye.to_json([piped_message])) if @socket 14 | end 15 | end 16 | 17 | def close 18 | @socket.close if @socket 19 | @socket = nil 20 | end 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/faye/protocol/subscription.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | class Subscription 3 | include Deferrable 4 | 5 | def initialize(client, channels, callback) 6 | @client = client 7 | @channels = channels 8 | @callback = callback 9 | @cancelled = false 10 | end 11 | 12 | def with_channel(&callback) 13 | @with_channel = callback 14 | self 15 | end 16 | 17 | def call(*args) 18 | message = args.first 19 | 20 | @callback.call(message['data']) if @callback 21 | @with_channel.call(message['channel'], message['data']) if @with_channel 22 | end 23 | 24 | def to_proc 25 | @to_proc ||= lambda { |*a| call(*a) } 26 | end 27 | 28 | def cancel 29 | return if @cancelled 30 | @client.unsubscribe(@channels, self) 31 | @cancelled = true 32 | end 33 | 34 | def unsubscribe 35 | cancel 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/faye/transport/http.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | 3 | class Transport::Http < Transport 4 | def self.usable?(dispatcher, endpoint, &callback) 5 | callback.call(URI === endpoint) 6 | end 7 | 8 | def encode(messages) 9 | Faye.to_json(messages) 10 | end 11 | 12 | def request(messages) 13 | content = encode(messages) 14 | params = build_params(content) 15 | request = create_request(params) 16 | 17 | request.callback do 18 | handle_response(messages, request.response) 19 | store_cookies(request.response_header['SET_COOKIE']) 20 | end 21 | 22 | request.errback do 23 | handle_error(messages) 24 | end 25 | 26 | request 27 | end 28 | 29 | private 30 | 31 | def build_params(content) 32 | headers = { 33 | 'Content-Length' => content.bytesize, 34 | 'Content-Type' => 'application/json', 35 | 'Host' => @endpoint.host + (@endpoint.port ? ":#{ @endpoint.port }" : '') 36 | } 37 | 38 | params = { 39 | :head => headers.merge(@dispatcher.headers), 40 | :body => content 41 | } 42 | 43 | cookie = get_cookies 44 | params[:head]['Cookie'] = cookie unless cookie == '' 45 | 46 | params 47 | end 48 | 49 | def create_request(params) 50 | options = { 51 | :inactivity_timeout => 0, 52 | :tls => @dispatcher.tls 53 | } 54 | 55 | if @proxy[:origin] 56 | uri = URI(@proxy[:origin]) 57 | options[:proxy] = { :host => uri.host, :port => uri.port } 58 | if uri.user 59 | options[:proxy][:authorization] = [uri.user, uri.password] 60 | end 61 | end 62 | 63 | client = EventMachine::HttpRequest.new(@endpoint.to_s, options) 64 | client.post(params) 65 | end 66 | 67 | def handle_response(messages, response) 68 | replies = MultiJson.load(response) rescue nil 69 | if replies 70 | receive(replies) 71 | else 72 | handle_error(messages) 73 | end 74 | end 75 | end 76 | 77 | Transport.register 'long-polling', Transport::Http 78 | 79 | end 80 | -------------------------------------------------------------------------------- /lib/faye/transport/local.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | 3 | class Transport::Local < Transport 4 | def self.usable?(dispatcher, endpoint, &callback) 5 | callback.call(Server === endpoint) 6 | end 7 | 8 | def batching? 9 | false 10 | end 11 | 12 | def request(messages) 13 | EventMachine.next_tick do 14 | @endpoint.process(messages, nil) do |replies| 15 | receive(Faye.copy_object(replies)) 16 | end 17 | end 18 | end 19 | end 20 | 21 | Transport.register 'in-process', Transport::Local 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/faye/transport/web_socket.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | 3 | class Transport::WebSocket < Transport 4 | UNCONNECTED = 1 5 | CONNECTING = 2 6 | CONNECTED = 3 7 | 8 | PROTOCOLS = { 9 | 'http' => 'ws', 10 | 'https' => 'wss' 11 | } 12 | 13 | include Deferrable 14 | 15 | class Request 16 | include Deferrable 17 | 18 | def close 19 | callback { |socket| socket.close } 20 | end 21 | end 22 | 23 | def self.usable?(dispatcher, endpoint, &callback) 24 | create(dispatcher, endpoint).usable?(&callback) 25 | end 26 | 27 | def self.create(dispatcher, endpoint) 28 | sockets = dispatcher.transports[:websocket] ||= {} 29 | sockets[endpoint.to_s] ||= new(dispatcher, endpoint) 30 | end 31 | 32 | def batching? 33 | false 34 | end 35 | 36 | def usable?(&callback) 37 | self.callback { callback.call(true) } 38 | self.errback { callback.call(false) } 39 | connect 40 | end 41 | 42 | def request(messages) 43 | @pending ||= Set.new 44 | messages.each { |message| @pending.add(message) } 45 | 46 | promise = Request.new 47 | 48 | callback do |socket| 49 | next unless socket and socket.ready_state == 1 50 | socket.send(Faye.to_json(messages)) 51 | promise.succeed(socket) 52 | end 53 | 54 | connect 55 | promise 56 | end 57 | 58 | def connect 59 | @state ||= UNCONNECTED 60 | return unless @state == UNCONNECTED 61 | @state = CONNECTING 62 | 63 | url = @endpoint.dup 64 | headers = @dispatcher.headers.dup 65 | extensions = @dispatcher.ws_extensions 66 | cookie = get_cookies 67 | 68 | url.scheme = PROTOCOLS[url.scheme] 69 | headers['Cookie'] = cookie unless cookie == '' 70 | 71 | options = { 72 | :extensions => extensions, 73 | :headers => headers, 74 | :proxy => @proxy, 75 | :tls => @dispatcher.tls 76 | } 77 | 78 | socket = Faye::WebSocket::Client.new(url.to_s, [], options) 79 | 80 | socket.onopen = lambda do |*args| 81 | store_cookies(socket.headers['Set-Cookie']) 82 | @socket = socket 83 | @state = CONNECTED 84 | @ever_connected = true 85 | set_deferred_status(:succeeded, socket) 86 | end 87 | 88 | closed = false 89 | socket.onclose = socket.onerror = lambda do |*args| 90 | next if closed 91 | closed = true 92 | 93 | was_connected = (@state == CONNECTED) 94 | socket.onopen = socket.onclose = socket.onerror = socket.onmessage = nil 95 | 96 | @socket = nil 97 | @state = UNCONNECTED 98 | 99 | pending = @pending ? @pending.to_a : [] 100 | @pending = nil 101 | 102 | if was_connected or @ever_connected 103 | set_deferred_status(:unknown) 104 | handle_error(pending, was_connected) 105 | else 106 | set_deferred_status(:failed) 107 | end 108 | end 109 | 110 | socket.onmessage = lambda do |event| 111 | replies = MultiJson.load(event.data) rescue nil 112 | next if replies.nil? 113 | replies = [replies].flatten 114 | 115 | replies.each do |reply| 116 | next unless reply.has_key?('successful') 117 | next unless message = @pending.find { |m| m['id'] == reply['id'] } 118 | @pending.delete(message) 119 | end 120 | receive(replies) 121 | end 122 | end 123 | 124 | def close 125 | return unless @socket 126 | @socket.close 127 | end 128 | end 129 | 130 | Transport.register 'websocket', Transport::WebSocket 131 | 132 | end 133 | -------------------------------------------------------------------------------- /lib/faye/util/namespace.rb: -------------------------------------------------------------------------------- 1 | module Faye 2 | class Namespace 3 | 4 | extend Forwardable 5 | def_delegator :@used, :delete, :release 6 | def_delegator :@used, :has_key?, :exists? 7 | 8 | def initialize 9 | @used = {} 10 | end 11 | 12 | def generate 13 | name = Engine.random 14 | name = Engine.random while @used.has_key?(name) 15 | @used[name] = name 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "faye", 3 | "description": "Simple pub/sub messaging for the web", 4 | "homepage": "https://faye.jcoglan.com", 5 | "author": "James Coglan <jcoglan@gmail.com> (http://jcoglan.com/)", 6 | "keywords": [ 7 | "comet", 8 | "websocket", 9 | "pubsub", 10 | "bayeux", 11 | "ajax", 12 | "http" 13 | ], 14 | "license": "Apache-2.0", 15 | "version": "1.4.1", 16 | "engines": { 17 | "node": ">=0.8.0" 18 | }, 19 | "main": "src/faye_node", 20 | "browser": "src/faye_browser", 21 | "dependencies": { 22 | "asap": "*", 23 | "csprng": "*", 24 | "faye-websocket": ">=0.9.1", 25 | "safe-buffer": "*", 26 | "tough-cookie": "*", 27 | "tunnel-agent": "*" 28 | }, 29 | "devDependencies": { 30 | "jstest": "~1.0.0", 31 | "mime": "~1.2.0", 32 | "permessage-deflate": ">=0.1.0", 33 | "promises-aplus-tests": "~2.1.0", 34 | "webpack": "~4", 35 | "webpack-cli": "~4", 36 | "imports-loader": "<1.0.0" 37 | }, 38 | "scripts": { 39 | "start": "webpack --watch", 40 | "test": "find spec -name '*_spec.js' | xargs jstest", 41 | "promise": "promises-aplus-tests src/util/promise.js" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git://github.com/faye/faye.git" 46 | }, 47 | "bugs": "https://github.com/faye/faye/issues" 48 | } 49 | -------------------------------------------------------------------------------- /site/config/compass.rb: -------------------------------------------------------------------------------- 1 | require "staticmatic/compass" 2 | 3 | project_type = :staticmatic -------------------------------------------------------------------------------- /site/config/site.rb: -------------------------------------------------------------------------------- 1 | # Default is 3000 2 | # configuration.preview_server_port = 3000 3 | 4 | # Default is localhost 5 | # configuration.preview_server_host = "localhost" 6 | 7 | # Default is true 8 | # When false .html & index.html get stripped off generated urls 9 | # configuration.use_extensions_for_page_links = true 10 | 11 | # Default is an empty hash 12 | # configuration.sass_options = {} 13 | 14 | # Default is an empty hash 15 | # http://haml-lang.com/docs/yardoc/file.HAML_REFERENCE.html#options 16 | # configuration.haml_options = {} -------------------------------------------------------------------------------- /site/site/images/aha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/aha.png -------------------------------------------------------------------------------- /site/site/images/buster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/buster.png -------------------------------------------------------------------------------- /site/site/images/chaxpert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/chaxpert.png -------------------------------------------------------------------------------- /site/site/images/cloudblocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/cloudblocks.png -------------------------------------------------------------------------------- /site/site/images/faye-cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/faye-cluster.png -------------------------------------------------------------------------------- /site/site/images/faye-internals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/faye-internals.png -------------------------------------------------------------------------------- /site/site/images/faye-logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/faye-logo.gif -------------------------------------------------------------------------------- /site/site/images/gitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/gitter.png -------------------------------------------------------------------------------- /site/site/images/groupme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/groupme.png -------------------------------------------------------------------------------- /site/site/images/ineda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/ineda.png -------------------------------------------------------------------------------- /site/site/images/medeo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/medeo.png -------------------------------------------------------------------------------- /site/site/images/myspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/myspace.png -------------------------------------------------------------------------------- /site/site/images/nokia_mix_party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/nokia_mix_party.png -------------------------------------------------------------------------------- /site/site/images/pathient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/pathient.png -------------------------------------------------------------------------------- /site/site/images/podio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/podio.png -------------------------------------------------------------------------------- /site/site/images/xydo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faye/faye/5230ad568baf4d9d7b7b18fd990e4c068b8473ee/site/site/images/xydo.png -------------------------------------------------------------------------------- /site/site/javascripts/analytics.js: -------------------------------------------------------------------------------- 1 | var _gaq = _gaq || []; 2 | _gaq.push(['_setAccount', 'UA-873493-8']); 3 | _gaq.push(['_trackPageview']); 4 | 5 | (function() { 6 | var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 7 | ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 8 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 9 | })(); 10 | -------------------------------------------------------------------------------- /site/site/stylesheets/github.css: -------------------------------------------------------------------------------- 1 | .atn { color:#008080 } 2 | .atv { color:#008080 } 3 | .com { color:#999988 } 4 | .dec { color:#000000; font-weight:bold } 5 | .kwd { color:#000000; font-weight:bold } 6 | .lit { color:#009999 } 7 | .pln { color:#000000 } 8 | .pun { color:#666666 } 9 | .str { color:#dd1144 } 10 | .tag { color:#000080 } 11 | .typ { color:#445588 } 12 | -------------------------------------------------------------------------------- /site/src/layouts/default.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html 3 | %head 4 | %meta{:'http-equiv' => "Content-type", :content => "text/html; charset=utf-8"} 5 | %title Faye: Simple pub/sub messaging for the web 6 | = stylesheets 7 | %link{'rel' => 'stylesheet', 'type' => 'text/css', 'href' => '//fonts.googleapis.com/css?family=Inconsolata:400,700|Open+Sans:300italic,400italic,700italic,400,300,700'} 8 | %body{:onload => 'prettyPrint()'} 9 | 10 | .header 11 | .container 12 | %h1 13 | =link "Faye", "/" 14 | %h2 Simple pub/sub messaging for the web 15 | .docs 16 | %h3 Documentation 17 | %ul 18 | %li 19 | =link "Node.js server", "/node.html" 20 | %li 21 | =link "Ruby server", "/ruby.html" 22 | %li 23 | =link "Browser client", "/browser.html" 24 | %li 25 | =link "Security advice", "/security.html" 26 | .community 27 | %h3 Developers 28 | %ul 29 | %li 30 | =link "Architecture" 31 | %li 32 | =link "GitHub", "https://github.com/faye/faye" 33 | %li 34 | =link "Mailing list", "http://groups.google.com/group/faye-users" 35 | .download 36 | %h3 Download 37 | %ul 38 | %li 39 | =link "Packages for Node.js, Ruby and browsers", "/download.html" 40 | 41 | .main 42 | .container 43 | = yield 44 | .clear 45 | 46 | .footer 47 | .container 48 | :textile 49 | © 2009–2025 "James Coglan":http://jcoglan.com. 50 | Released under the Apache 2.0 license. 51 | 52 | = javascripts 'prettify' 53 | :plain 54 | <script type="text/javascript"> 55 | (function() { 56 | var pre = document.getElementsByTagName('pre'), n = pre.length 57 | while (n--) { 58 | if (!pre[n].className) pre[n].className = 'prettyprint' 59 | } 60 | prettyPrint() 61 | })() 62 | </script> 63 | -------------------------------------------------------------------------------- /site/src/pages/browser/extensions.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'browser_navigation' 3 | 4 | :textile 5 | h4. Extensions 6 | 7 | Faye clients support an extension system that lets you intercept messages as 8 | they pass between the client and the server. To add an extension to a client, 9 | just call: 10 | 11 | <pre>client.addExtension(extension);</pre> 12 | 13 | @extension@ should be an object with an @incoming()@ or @outgoing()@ method 14 | (or both). These methods accept a message and a callback function, and 15 | should call the callback with the message after any necessary modifications 16 | have been made. For example, a simple logging extension would look like: 17 | 18 | <pre>Logger = { 19 | incoming: function(message, callback) { 20 | console.log('incoming', message); 21 | callback(message); 22 | }, 23 | outgoing: function(message, callback) { 24 | console.log('outgoing', message); 25 | callback(message); 26 | } 27 | }; 28 | 29 | client.addExtension(Logger);</pre> 30 | 31 | For more information on writing extensions, see the "Node server":/node.html 32 | or "Ruby server":/ruby.html documentation. 33 | -------------------------------------------------------------------------------- /site/src/pages/browser/publishing.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'browser_navigation' 3 | 4 | :textile 5 | h4. Sending messages 6 | 7 | Clients do not send each other messages directly, instead they send their 8 | messages to channels, and the server figures out which clients need to 9 | receive the message. You can send a message using the @#publish()@ method, 10 | passing in the channel name and a message object. 11 | 12 | <pre>client.publish('/foo', {text: 'Hi there'});</pre> 13 | 14 | The message object can be any arbitrary JavaScript object that can be 15 | serialized to JSON, so it can contain strings, numbers, booleans, arrays and 16 | other objects. There are no required fields, and the object will be 17 | delivered verbatim to any subscriber functions listening to that channel. 18 | 19 | Just like @subscribe()@, the @publish()@ method returns a 20 | "promise":http://promisesaplus.com/ that is fulfilled when the server 21 | acknowledges the message. This just means the server received and routed the 22 | message successfully, not that it has been received by all other clients. 23 | The promise is rejected if the server explcitly returns an error saying it 24 | could not publish the message to other clients; network errors are therefore 25 | not covered by this API. 26 | 27 | <pre>var publication = client.publish('/foo', {text: 'Hi there'}); 28 | 29 | publication.then(function() { 30 | alert('Message received by server!'); 31 | }, function(error) { 32 | alert('There was a problem: ' + error.message); 33 | });</pre> 34 | 35 | The Faye client will automatically try to resend your messages if it 36 | encounters a network error. It will attempt to resend the message until it 37 | receives confirmation that the server has processed it. Sometimes, you might 38 | not want messages to be retried indefinitely, and Faye gives you two ways to 39 | limit this behaviour; see the "dispatch options":/browser/dispatch.html. 40 | 41 | -------------------------------------------------------------------------------- /site/src/pages/browser/subscribing.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'browser_navigation' 3 | 4 | :textile 5 | h4. Subscribing to channels 6 | 7 | Clients receive data from other clients by subscribing to channels. Whenever 8 | any client sends a message to a channel you're subscribed to, Faye will 9 | notify your client with the new message. 10 | 11 | Channel names must be formatted as absolute path names whose segments may 12 | contain only letters, numbers, and the symbols @-@, @_@, @!@, @~@, @(@, @)@, 13 | @$@ and @@@. Channel names may also end with wildcards: 14 | 15 | * The @*@ wildcard matches any channel segment. So @/foo/*@ matches @/foo/bar@ 16 | and @/foo/thing@ but not @/foo/bar/thing@. 17 | * The @**@ wildcard matches any channel name recursively. So @/foo/**@ 18 | matches @/foo/bar@, @/foo/thing@ and @/foo/bar/thing@. 19 | 20 | So for example if you subscribe to @/foo/*@ and someone sends a message to 21 | @/foo/bar@, you will receive that message. 22 | 23 | Clients should subscribe to channels using the @#subscribe()@ method: 24 | 25 | <pre>var subscription = client.subscribe('/foo', function(message) { 26 | // handle message 27 | });</pre> 28 | 29 | The subscriber function will be invoked when anybody sends a message to 30 | @/foo@, and the @message@ parameter will contain the sent message object. A 31 | client may bind multiple listeners to a channel, and the Faye client handles 32 | all the management of those listeners and makes sure the server sends it the 33 | right messages. 34 | 35 | The @subscribe()@ method returns a @Subscription@ object, which you can 36 | cancel if you want to remove that listener from the channel. 37 | 38 | <pre>subscription.cancel();</pre> 39 | 40 | The @Subscription@ object is a "promise":http://promisesaplus.com/ that is 41 | fulfilled when the subscription has been acknowledged by the server: 42 | 43 | <pre>subscription.then(function() { 44 | alert('Subscription is now active!'); 45 | });</pre> 46 | 47 | If you're subscribing to a wildcard channel, you may want to receive the 48 | specific channel the message was published to in your subscriber function. 49 | You can do this using the `withChannel()` method: 50 | 51 | <pre>client.subscribe('/foo/*').withChannel(function(channel, message) { 52 | // handle message 53 | });</pre> 54 | 55 | -------------------------------------------------------------------------------- /site/src/pages/browser/transport.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'browser_navigation' 3 | 4 | :textile 5 | h4. Network errors 6 | 7 | As explained in the "architecture documentation":/architecture.html, the 8 | Faye client does not talk to the network directly but uses a 'transport' 9 | object based on WebSocket, XMLHttpRequest, and other network APIs. 10 | 11 | The client exposes an abstract interface for checking the status of 12 | whichever connection type is in use, which is useful for giving the user 13 | feedback about the connection, or making your application deal with being 14 | offline. You can listen to the @transport:up@ and @transport:down@ events to 15 | be notified that the client is online or offline. 16 | 17 | <pre>client.on('transport:down', function() { 18 | // the client is offline 19 | }); 20 | 21 | client.on('transport:up', function() { 22 | // the client is online 23 | });</pre> 24 | 25 | Note that these events _do not_ reflect the status of the client's session, 26 | or whether there is literally a network connection active. For example, you 27 | are not told that the transport is down just because a long-polling request 28 | just completed. The transport is only considered down if a request or 29 | WebSocket connection explicitly fails, or times out, indicating the server 30 | is unreachable. 31 | 32 | Also remember that Faye deals with buffering and re-sending messages for you, 33 | so you don't need to deal with that. These events are simply for providing 34 | feedback on the health of the connection. 35 | -------------------------------------------------------------------------------- /site/src/pages/download.haml: -------------------------------------------------------------------------------- 1 | .content 2 | :textile 3 | h3. Download Faye 4 | 5 | The latest version is 1.4.1, released June 17 2025. It is open-source 6 | software, released under the Apache 2.0 license. You can follow development 7 | on Faye's "GitHub page":https://github.com/faye/faye. 8 | 9 | h4. Download for Node.js and web browsers 10 | 11 | The Node.js version is available through "npm":https://www.npmjs.com/. This 12 | package contains a copy of the browser client, which is served up by the 13 | Faye server when running. 14 | 15 | <pre>npm install faye</pre> 16 | 17 | If you're using "Browserify":http://browserify.org/, 18 | "Webpack":https://webpack.github.io/ or a similar build tool, then 19 | requiring @faye@ will get you the client-side package, for example: 20 | 21 | <pre>var faye = require('faye'); 22 | 23 | var client = new faye.Client('http://localhost:8000/faye');</pre> 24 | 25 | If you're not using such a toolchain, you can get the client bundle from 26 | the @client@ directory within the npm install. 27 | 28 | h4. Download for Ruby 29 | 30 | For Ruby platforms, Faye is installable through RubyGems. 31 | 32 | <pre>gem install faye</pre> 33 | 34 | This package also includes the browser client which is served up by the Faye 35 | server when running. 36 | -------------------------------------------------------------------------------- /site/src/pages/index.haml: -------------------------------------------------------------------------------- 1 | .front-matter 2 | .intro 3 | :textile 4 | h3. What is it? 5 | 6 | Faye is a publish-subscribe messaging system based on the 7 | "Bayeux":https://docs.cometd.org/reference/index.html#_bayeux protocol. 8 | It provides message servers for "Node.js":http://nodejs.org and 9 | "Ruby":http://www.ruby-lang.org, and clients for use on the server and in 10 | all major web browsers. 11 | 12 | h3. Who uses it? 13 | 14 | "!/images/aha.png!":http://www.aha.io/ 15 | "!/images/buster.png!":http://busterjs.org/ 16 | "!/images/chaxpert.png!":https://community.chaxpert.net/ 17 | "!/images/cloudblocks.png!":http://www.cloud66.com/ 18 | "!/images/gitter.png!":https://gitter.im/ 19 | "!/images/groupme.png!":http://groupme.com/ 20 | "!/images/ineda.png!":http://www.i-neda.com/ 21 | "!/images/medeo.png!":https://medeo.ca/ 22 | "!/images/myspace.png!":http://new.myspace.com/ 23 | "!/images/nokia_mix_party.png!":http://mixparty.nokia.com/ 24 | "!/images/pathient.png!":http://www.pathient.com/ 25 | "!/images/podio.png!":https://podio.com/ 26 | "!/images/xydo.png!":http://www.xydo.com/ 27 | 28 | :textile 29 | h3. %(number)1.% Start a server 30 | 31 | <pre>var http = require('http'), 32 | faye = require('faye'); 33 | 34 | var server = http.createServer(), 35 | bayeux = new faye.NodeAdapter({mount: '/'}); 36 | 37 | bayeux.attach(server); 38 | server.listen(8000);</pre> 39 | 40 | h3. %(number)2.% Create a client 41 | 42 | <pre>var client = new Faye.Client('http://localhost:8000/'); 43 | 44 | client.subscribe('/messages', function(message) { 45 | alert('Got a message: ' + message.text); 46 | }); 47 | </pre> 48 | 49 | h3. %(number)3.% Send messages 50 | 51 | <pre>client.publish('/messages', { 52 | text: 'Hello world' 53 | }); 54 | </pre> 55 | -------------------------------------------------------------------------------- /site/src/pages/node.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'node_navigation' 3 | 4 | :textile 5 | h4. Setting up 6 | 7 | All Faye clients need a central messaging server to communicate with; the 8 | server records which clients are subscribed to which channels and handles 9 | routing of messages between clients. Setting up a server in Node.js is 10 | simple: 11 | 12 | <pre>var http = require('http'), 13 | faye = require('faye'); 14 | 15 | var server = http.createServer(), 16 | bayeux = new faye.NodeAdapter({mount: '/faye', timeout: 45}); 17 | 18 | bayeux.attach(server); 19 | server.listen(8000);</pre> 20 | 21 | The @NodeAdapter@ class supports these options during setup: 22 | 23 | * *@mount@* - the path on the host at which the Faye service is available. 24 | In this example, clients would connect to @http://localhost:8000/faye@ to 25 | talk to the server. The server will handle _any_ request whose path begins 26 | with the @mount@ path; this is so that it can interoperate with clients 27 | that use different request paths for different channels. 28 | * *@timeout@* - the maximum time to hold a connection open before returning 29 | the response. This is given in seconds and must be smaller than the 30 | timeout on your frontend webserver. 31 | * *@engine@* - (optional) the type and parameters for the engine you want to 32 | use - see the "engines documentation":/node/engines.html 33 | * *@ping@* - (optional) how often, in seconds, to send keep-alive ping 34 | messages over WebSocket and EventSource connections. Use this if your Faye 35 | server will be accessed through a proxy that kills idle connections. 36 | 37 | It also allows WebSocket extensions to be plugged in. For example, to enable 38 | "permessage-deflate":https://github.com/faye/permessage-deflate-node for 39 | supporting clients: 40 | 41 | <pre>var faye = require('faye'), 42 | deflate = require('permessage-deflate'); 43 | 44 | var bayeux = new faye.NodeAdapter({mount: '/faye', timeout: 45}); 45 | bayeux.addWebsocketExtension(deflate);</pre> 46 | 47 | You can use any extension that's compatible with the 48 | "websocket-extensions":https://github.com/faye/websocket-extensions-node 49 | framework. 50 | 51 | Faye should be attached to an existing HTTP server using the @attach()@ 52 | method. It will handle all requests to paths matching the @mount@ path and 53 | delegate all other requests to your handlers. 54 | 55 | <pre>var http = require('http'), 56 | faye = require('faye'); 57 | 58 | var bayeux = new faye.NodeAdapter({mount: '/faye', timeout: 45}); 59 | 60 | // Handle non-Bayeux requests 61 | var server = http.createServer(function(request, response) { 62 | response.writeHead(200, {'Content-Type': 'text/plain'}); 63 | response.end('Hello, non-Bayeux request'); 64 | }); 65 | 66 | bayeux.attach(server); 67 | server.listen(8000);</pre> 68 | -------------------------------------------------------------------------------- /site/src/pages/node/clients.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'node_navigation' 3 | 4 | :textile 5 | h4. Server-side Node.js clients 6 | 7 | You can use Faye clients on the server side to send messages to in-browser 8 | clients or to other server-side processes. The API is identical to the 9 | "browser client":/browser.html. 10 | 11 | To create a client, just supply the host you want to connect to: 12 | 13 | <pre>var client = new faye.Client('http://localhost:8000/faye');</pre> 14 | 15 | You can then use @client.subscribe()@ and @client.publish()@ to send 16 | messages to other clients; see the "browser client":/browser.html 17 | documentation for more information. 18 | 19 | The server has its own client attached to it so you can use the server to 20 | send messages to browsers. This client has direct access to the server 21 | without going over HTTP, and is thus more efficient. To send messages 22 | through the server just use the @#getClient()@ method. 23 | 24 | <pre>bayeux.getClient().publish('/email/new', { 25 | text: 'New email has arrived!', 26 | inboxSize: 34 27 | });</pre> 28 | 29 | h4. Transport control 30 | 31 | When using the client on the server, you can control parts of the transport 32 | layer that the browser doesn't provide access to. For a start, headers 33 | added using the @client.setHeader()@ method will be added to WebSocket 34 | connections, not just regular HTTP requests. 35 | 36 | For the transport layer, the server-side client uses the Node.js 37 | "https":https://nodejs.org/api/https.html and 38 | "tls":https://nodejs.org/api/tls.html modules to handle HTTPS endpoints. If 39 | you need to configure anything about the TLS connection, use the `tls` 40 | option which is passed through to 41 | "@tls.connect()@":https://nodejs.org/api/tls.html#tls_tls_connect_options_callback. 42 | For example, to set your own root certificate instead of using the system 43 | defaults: 44 | 45 | <pre>var client = new faye.Client(url, { 46 | tls: { 47 | ca: fs.readFileSync('path/to/certificate.pem') 48 | } 49 | });</pre> 50 | 51 | You can also request that all connections go via an HTTP proxy: 52 | 53 | <pre>var client = new faye.Client(url, { 54 | proxy: 'http://username:password@proxy.example.com' 55 | });</pre> 56 | 57 | You can also set the @http_proxy@ or @https_proxy@ environment variables, 58 | which will make all Faye client connections use the given proxy by default; 59 | @http_proxy@ for @http:@ and @ws:@ requests, and @https_proxy@ for @https:@ 60 | and @wss:@ requests. 61 | 62 | Finally, the WebSocket transport can be configured to use protocol 63 | extensions; any extension library compatible with 64 | "websocket-extensions":https://github.com/faye/websocket-extensions-node 65 | will work. For example, to add 66 | "permessage-deflate":https://github.com/faye/permessage-deflate-node: 67 | 68 | <pre>var deflate = require('permessage-deflate'); 69 | 70 | client.addWebsocketExtension(deflate);</pre> 71 | -------------------------------------------------------------------------------- /site/src/pages/node/websockets.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'node_navigation' 3 | 4 | :textile 5 | h4. WebSockets for Node 6 | 7 | Since version 0.5, Faye has supported WebSockets as a network transport for 8 | sending messages to the browser. The code that handles this is decoupled 9 | from the rest of the library and can be used to make your own WebSocket 10 | applications. 11 | 12 | These classes are available as a stand-alone library, 13 | "faye-websocket":https://github.com/faye/faye-websocket-node 14 | -------------------------------------------------------------------------------- /site/src/pages/ruby/clients.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'ruby_navigation' 3 | 4 | :textile 5 | h4. Server-side Ruby clients 6 | 7 | You can use Faye clients on the server side to send messages to in-browser 8 | clients or to other server-side processes. The API is identical to the 9 | "browser client":/browser.html. 10 | 11 | To create a client, just supply the host you want to connect to: 12 | 13 | <pre>client = Faye::Client.new('http://localhost:9292/faye')</pre> 14 | 15 | You can then use @client.subscribe()@ and @client.publish()@ to send 16 | messages to other clients; the API is similar to the "browser client":/browser.html 17 | only you need to run the client inside EventMachine: 18 | 19 | <pre>require 'eventmachine' 20 | 21 | EM.run { 22 | client = Faye::Client.new('http://localhost:9292/faye') 23 | 24 | client.subscribe('/foo') do |message| 25 | puts message.inspect 26 | end 27 | 28 | client.publish('/foo', 'text' => 'Hello world') 29 | }</pre> 30 | 31 | Note that the Ruby client uses @EventMachine::Deferrable@ instead of 32 | "promises":http://promisesaplus.com/, for example you detect success and 33 | failure of a publication like so: 34 | 35 | <pre>publication = client.publish('/foo', 'text' => 'Hello world') 36 | 37 | publication.callback do 38 | puts 'Message received by server!' 39 | end 40 | 41 | publication.errback do |error| 42 | puts 'There was a problem: ' + error.message 43 | end</pre> 44 | 45 | If you need to set custom headers to talk to your Bayeux server, use the 46 | @set_header@ method: 47 | 48 | <pre>client.set_header('Authorization', 'OAuth abcd-1234')</pre> 49 | 50 | The server has its own client attached to it so you can use the server to 51 | send messages to browsers. This client has direct access to the server 52 | without going over HTTP, and is thus more efficient. To send messages 53 | through the server just use the @#get_client@ method. 54 | 55 | <pre>bayeux.get_client.publish('/email/new', { 56 | 'text' => 'New email has arrived!', 57 | 'inboxSize' => 34 58 | })</pre> 59 | 60 | h4. Transport control 61 | 62 | When using the client on the server, you can control parts of the transport 63 | layer that the browser doesn't provide access to. For a start, headers 64 | added using the @client.set_header@ method will be added to WebSocket 65 | connections, not just regular HTTP requests. 66 | 67 | From version 1.4.0 onwards, @Faye::Client@ uses the underlying transport 68 | libraries ("em-http-request":https://rubygems.org/gems/em-http-request and 69 | "faye-websocket":https://rubygems.org/gems/em-http-request, both based on 70 | "EventMachine":https://rubygems.org/gems/eventmachine) to perform TLS 71 | certificate validation by default for HTTPS endpoints. If you don't want 72 | this behaviour, you can turn it off via the @:tls@ option: 73 | 74 | <pre>client = Faye::Client.new(url, { 75 | :tls => { :verify_peer => false } 76 | })</pre> 77 | 78 | You can also request that all connections go via an HTTP proxy: 79 | 80 | <pre>client = Faye::Client.new(url, { 81 | :proxy => 'http://username:password@proxy.example.com' 82 | })</pre> 83 | 84 | You can also set the @http_proxy@ or @https_proxy@ environment variables, 85 | which will make all Faye client connections use the given proxy by default; 86 | @http_proxy@ for @http:@ and @ws:@ requests, and @https_proxy@ for @https:@ 87 | and @wss:@ requests. 88 | 89 | Finally, the WebSocket transport can be configured to use protocol 90 | extensions; any extension library compatible with 91 | "websocket-extensions":https://github.com/faye/websocket-extensions-ruby 92 | will work. For example, to add 93 | "permessage-deflate":https://github.com/faye/permessage-deflate-ruby: 94 | 95 | <pre>require 'permessage_deflate' 96 | 97 | client.add_websocket_extension(PermessageDeflate)</pre> 98 | -------------------------------------------------------------------------------- /site/src/pages/ruby/websockets.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'ruby_navigation' 3 | 4 | :textile 5 | h4. WebSockets for Ruby 6 | 7 | Since version 0.5, Faye has supported WebSockets as a network transport for 8 | sending messages to the browser. The code that handles this is decoupled 9 | from the rest of the library and can be used to make your own WebSocket 10 | applications. 11 | 12 | These classes are available as a stand-alone library, 13 | "faye-websocket":https://github.com/faye/faye-websocket-ruby 14 | -------------------------------------------------------------------------------- /site/src/pages/security.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'security_navigation' 3 | 4 | :textile 5 | h4. Securing your realtime applications 6 | 7 | Like any web-accessible service, Faye must be protected against malicious 8 | usage by attackers. Out of the box, it is a cross-domain-accessible server 9 | with no restrictions on subscribing and publishing, but its extension system 10 | allows you to easily impose restrictions appropriate to your application. 11 | 12 | Though written primarily for Faye, this guide contains advice that affects 13 | many realtime and socket-based applications. The core concerns with such 14 | applications are: 15 | 16 | * Can a client get access to data it should not have access to? 17 | * Can a client trust the origin of the messages it receives? 18 | 19 | The details of these questions will depend on your application, but the 20 | following is a set of general guidelines for avoiding broad classes of 21 | mistakes. It is not prescriptive, in that the solutions presented here are 22 | not how you _have to_ implement things. They simply try to illustrate 23 | patterns of security risks and possible solutions. 24 | 25 | As with all web applications, it is crucial to remember that any program 26 | with Internet access, including server-side scripts and in-browser 27 | JavaScript code on other domains, can send requests to your server. It is up 28 | to you to protect it and your users from harm. 29 | 30 | h4. What is Faye? 31 | 32 | Faye is an implementation of the "Bayeux":https://docs.cometd.org/current/reference/#_bayeux 33 | protocol. Clients send messages to each other via a central server, by 34 | sending JSON messages over various flavours of HTTP-based transport, 35 | including WebSocket, EventSource, XMLHttpRequest, CORS and JSON-P. Clients 36 | that run in the browser are not constrained by the same-origin policy. 37 | 38 | Messages are routed using subscriptions. When a client publishes a message, 39 | the server determines which clients are subscribed to the message's 40 | @channel@ and forwards the message to them verbatim. This means the 41 | _whole wire message is forwarded_, not just the @data@ field containing the 42 | application payload. 43 | 44 | A special set of channels whose names begin with @/meta/@ are used for 45 | operating the protocol itself, and messages sent to these channels are 46 | never forwarded to other clients. 47 | 48 | Messages pass through extensions on their way into and out of the clients 49 | and server. Data required by extensions is typically sent in the message's 50 | @ext@ field. Any changes made to a message by a server-side extension will 51 | be reflected in the messages forward to subscribed clients. 52 | 53 | Authorization credentials embedded in messages should be deleted from them 54 | by server-side extensions to prevent these credentials being forwarded to 55 | subscribed clients. 56 | 57 | h4. Transport layer security 58 | 59 | It should go without saying that if you want to keep the messages or any 60 | aspect of the HTTP transport layer secret from potential eavesdroppers, you 61 | should access the Faye server over a secure connection. You should use 62 | Node's @https@ module to create a server, or run Thin in SSL mode, or you 63 | can use an SSL terminator like STunnel in front of your Faye server. 64 | 65 | On the client side, you should make sure you use an @https:@ URL for the 66 | client to connect to. 67 | -------------------------------------------------------------------------------- /site/src/pages/security/headers.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'security_navigation' 3 | 4 | :textile 5 | h4. Can I use cookies? 6 | 7 | As of version 1.0, Faye allows server-side extensions to access the request 8 | data for the current message. We have introduced this capability in order to 9 | support integration with various other HTTP-based authorization mechanisms, 10 | but still recommend that this task is done within the messaging protocol 11 | using signed or encrypted data. 12 | 13 | However some users will want to use HTTP-based methods, in particular the 14 | @Cookie@ header that carries the user's session. There are several caveats 15 | you must be aware of to use cookies safely. 16 | 17 | First, the browser will send cookies regardless of which site the request 18 | came from, so to make sure you're only granting access to your own pages, 19 | you must "implement CSRF protection":/security/csrf.html. If you don't do 20 | this, any site the user has open will get privileged access to your Faye 21 | server by impersonating the user. 22 | 23 | Second, some transports like WebSocket and EventSource use a very long-lived 24 | request and only send one @Cookie@ header on first connection. This means 25 | the information you're using to authorize messages may have been sent a long 26 | time ago and may therefore be stale. If the session has changed or been 27 | invalidated since the initial connection, relying on stale data can cause 28 | security holes. 29 | 30 | Instead of cookies that contain the session data, we recommend keeping a 31 | session ID in the cookies and storing the data on the server, either in 32 | memory or on disk or in a database. This way, you will look up the session 33 | afresh on every message instead of using stale data. 34 | 35 | h4. Can I use Origin, Referer, etc.? 36 | 37 | Although the @Origin@ header was introduced to combat CSRF, these headers 38 | can be easily guessed and spoofed by server-side clients, browser 39 | extensions and malicious JavaScript applications. They are not 40 | cryptographically secure proof that you can trust where the request claims 41 | to have come from. 42 | 43 | This still allows third parties to inject messages into your application, 44 | and is especially bad if you have clients that receive JavaScript and 45 | @eval()@ it. 46 | 47 | The @Origin@ header is also not sent by most browser transports that Faye 48 | uses, so filtering based on it will actually block most legit traffic, 49 | including the initial handshake request. 50 | -------------------------------------------------------------------------------- /site/src/pages/security/javascript.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'security_navigation' 3 | 4 | :textile 5 | h4. Publishing JavaScript 6 | 7 | Most realtime applications work by pushing data to the client for it to act 8 | on. Assuming the data can be trusted by the client, this is a good setup: 9 | the client's behaviour is somewhat constrained. It can only do what its code 10 | allows it to do, with the caveat that some crafted inputs may lead to 11 | unexpected behaviour. 12 | 13 | However some realtime applications directly script the client by pushing 14 | JavaScript code that the client runs with @eval()@. This is extremely 15 | dangerous unless you make sure that nobody but your own private servers can 16 | publish to your Faye server. I recommend that realtime apps operate by 17 | exchanging data, not sending code. If anyone but your own server-side 18 | applications can push JavaScript unchecked, your site has a serious XSS 19 | problem that can allow an attacker to easily steal the user's session and 20 | other private data. 21 | 22 | To illustrate how easy this is, I have in the past hijacked the browsers of 23 | all the attendees at a conference that were running a demo app hosted by the 24 | speaker. The speaker put the app's publishing key on the screen and the 25 | application ran any JavaScript pushed to it, so was trivial to exploit. Of 26 | course, normally this key would have been kept private, but if you don't 27 | have any such key your app automatically has an XSS hole. 28 | 29 | A JavaScript-pushing server can be made safe by ensuring your clients only 30 | run code sent by your application, and this can be done by turning Faye into 31 | a "push-only server":/security/push.html. 32 | -------------------------------------------------------------------------------- /site/src/pages/security/publication.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'security_navigation' 3 | 4 | :textile 5 | h4. Restricting publication access 6 | 7 | Applications typically only allow authenticated users to modify things: you 8 | must prove you 'own' a resource or that someone has given you permission 9 | before you go and change someone's database. In Faye, publishing is the 10 | write operation: publishing a message to the server causes it to be sent to 11 | all subscribed clients, which will act based on the data in the message. By 12 | publishing a message, you are sending instructions to other clients, and the 13 | clients must be able to trust that the data they receive is genuine. 14 | 15 | If you do not protect publication, your site probably has a "Cross-Site 16 | Request Forgery":/security/csrf.html (CSRF) vulnerability, and possibly a 17 | "Cross-Site Scripting":/security/javascript.html (XSS) one too. 18 | 19 | Protecting publication on the server side is simpler than protecting 20 | subscription, because publication messages (those with channels other than 21 | @/meta/*@) cannot be addressed to wildcards. So, to protect a channel's 22 | publications, you _only_ need to check that literal channel name. 23 | 24 | The channel the message is being published to will be in the 25 | @message.channel@ field. An important fact to remember here is that messages 26 | are forwarded verbatim to other clients, so if they contain authentication 27 | data you should delete this from the message during the @authorized()@ 28 | function so it is not leaked to third parties. 29 | 30 | <pre>var channel = '/foo/bar/qux'; 31 | 32 | var authorized = function(message) { 33 | // returns true or false 34 | }; 35 | 36 | server.addExtension({ 37 | incoming: function(message, callback) { 38 | if (message.channel === channel) { 39 | if (!authorized(message)) 40 | message.error = '403::Authentication required'; 41 | } 42 | callback(message); 43 | } 44 | });</pre> 45 | 46 | See "Authentication":/security/authentication.html for a discussing of the 47 | @authorized()@ function. 48 | -------------------------------------------------------------------------------- /site/src/pages/security/push.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'security_navigation' 3 | 4 | :textile 5 | h4. Push-only servers 6 | 7 | Sometimes you only want to use Faye to push events from your server-side 8 | application to your clients, and you don't want clients to be able to 9 | publish at all. This can easily be done by requiring a password for 10 | publishing. On any non-@/meta/@ message, check for the password. If it's not 11 | present, add an error to the message. Finally, delete the password from the 12 | message to prevent leaking it to clients. 13 | 14 | <pre>var secret = 'some long and unguessable application-specific string'; 15 | 16 | server.addExtension({ 17 | incoming: function(message, callback) { 18 | if (!message.channel.match(/^\/meta\//)) { 19 | var password = message.ext && message.ext.password; 20 | if (password !== secret) 21 | message.error = '403::Password required'; 22 | } 23 | callback(message); 24 | }, 25 | 26 | outgoing: function(message, callback) { 27 | if (message.ext) delete message.ext.password; 28 | callback(message); 29 | } 30 | });</pre> 31 | 32 | Then you can add a client-side extension to your server-side client to add 33 | the password: 34 | 35 | <pre>var secret = 'some long and unguessable application-specific string'; 36 | 37 | client.addExtension({ 38 | outgoing: function(message, callback) { 39 | message.ext = message.ext || {}; 40 | message.ext.password = secret; 41 | callback(message); 42 | } 43 | });</pre> 44 | 45 | If you're using a plain HTTP client to publish messages, include the 46 | password in the JSON body: 47 | 48 | <pre>$ curl -X POST www.example.com/faye \ 49 | -H 'Content-Type: application/json' \ 50 | -d '{"channel": "/foo", "data": "hi", "ext": {"password": "..."}}'</pre> 51 | 52 | Remember to keep the password secret, and do not let it leak out of your 53 | servers into the outside world. 54 | -------------------------------------------------------------------------------- /site/src/pages/security/subscription.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'security_navigation' 3 | 4 | :textile 5 | h4. Restricting subscription access 6 | 7 | Most web applications have a concept of access control: some content is only 8 | accessible to certain people, and you must be logged in to prove your 9 | identity. In Faye, you might want subscriptions to certain channels to 10 | require authentication, if you are publishing private data on such channels. 11 | This is easily done with a server-side extension that filters incoming 12 | @/meta/subscribe@ messages. 13 | 14 | An important point here is that subscriptions can contain wildcards, and you 15 | must protect these. A message published to @/foo/bar/qux@ will be routed to 16 | any client subscribed to @/foo/bar/qux@, @/foo/bar/*@, @/foo/bar/**@, 17 | @/foo/**@ or @/**@. 18 | 19 | Adding an @error@ field to any incoming message will stop the server from 20 | processing it. The channel the client is attempting to subscribe to will be 21 | in the @message.subscription@ field. 22 | 23 | <pre>var subscriptions = [ 24 | '/foo/bar/qux', 25 | '/foo/bar/*', 26 | '/foo/bar/**', 27 | '/foo/**', 28 | '/**' 29 | ]; 30 | 31 | var authorized = function(message) { 32 | // returns true or false 33 | }; 34 | 35 | server.addExtension({ 36 | incoming: function(message, callback) { 37 | if (message.channel === '/meta/subscribe') { 38 | if (subscriptions.indexOf(message.subscription) >= 0) { 39 | if (!authorized(message)) 40 | message.error = '403::Authentication required'; 41 | } 42 | } 43 | callback(message); 44 | } 45 | });</pre> 46 | 47 | How the @authorized()@ function is implemented depends on your clients' 48 | capabilities and is covered in more detail under 49 | "Authentication":/security/authentication.html. Note that because extensions 50 | are asynchronous (you hand the message back to the server using a callback), 51 | your authentication logic can contain async operations, which is useful if 52 | you need to do some I/O. 53 | 54 | A simple extension like this, with properly implemented authentication, will 55 | prevent unauthorized access to published data on the selected channel. 56 | -------------------------------------------------------------------------------- /site/src/pages/security/summary.haml: -------------------------------------------------------------------------------- 1 | .content 2 | = partial 'security_navigation' 3 | 4 | :textile 5 | h4. Other techniques 6 | 7 | The above is a fairly comprehensive picture of restricting access to your 8 | Faye server. The important thing to remember is that when exchanging 9 | messages, you just need a way to prove that the data is genuine. This relies 10 | heavily on cryptograhpic techniques and you should always use standard 11 | functions for this rather than inventing your own. 12 | 13 | However, sometimes, it's just a case of using data that is very hard to 14 | guess. For example, say you want to send messages to one particular user and 15 | nobody else. Instead of naming a channel after a username and requiring an 16 | access token to subcribe to it, you could just make the channel name 17 | _contain_ the access token. For example, the client could call an endpoint 18 | on your server to get a channel name for the logged-in user, then subscribe 19 | to that channel in Faye. When publishing, you would just regenerate the 20 | channel name from the username you want to publish to. 21 | 22 | These channel names may be a cryptograhpically signed copy of the user's 23 | name or ID, or they could simply be very large random numbers (larger than 24 | 160 bits is advisable) that you store in a database next to each user ID. As 25 | long as they cannot be guessed by a third party, you're alright. Just 26 | remember that 'cannot be guessed' is surprisingly hard to implement 27 | correctly, and you should consult someone with a grounding in crypto if 28 | you're not sure what you're doing is safe. 29 | 30 | h4. Summary 31 | 32 | This guide, while not exhaustive should give you enough grounding on the 33 | topic to safely implement a real-time application using Faye. If you have 34 | further questions you should "ask on the mailing 35 | list":http://groups.google.com/group/faye-users - many people there have run 36 | into the same problems as you and will likely have already thought of a 37 | solution. If you have a genuinely unusual case then you will most likely 38 | benefit from their sage advice. 39 | 40 | Thank you for taking the time to familiarise yourself with this advice and 41 | for using Faye. Your feedback on this document is eagerly solicited; issues 42 | and pull requests can be submitted on the "Faye 43 | project":https://github.com/faye/faye on GitHub. 44 | -------------------------------------------------------------------------------- /site/src/partials/browser_navigation.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h3. Browser client 3 | 4 | <div class="sections"> 5 | 6 | * "Setting up":/browser.html 7 | * "Subscribing to channels":/browser/subscribing.html 8 | * "Sending messages":/browser/publishing.html 9 | * "Controlling dispatch":/browser/dispatch.html 10 | * "Network errors":/browser/transport.html 11 | * "Extensions":/browser/extensions.html 12 | 13 | </div> 14 | -------------------------------------------------------------------------------- /site/src/partials/node_navigation.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h3. Node.js server 3 | 4 | <div class="sections"> 5 | 6 | * "Setting up":/node.html 7 | * "Extensions":/node/extensions.html 8 | * "Monitoring":/node/monitoring.html 9 | * "Server-side clients":/node/clients.html 10 | * "Engines":/node/engines.html 11 | * "WebSockets":/node/websockets.html 12 | 13 | </div> 14 | -------------------------------------------------------------------------------- /site/src/partials/ruby_navigation.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h3. Ruby server 3 | 4 | <div class="sections"> 5 | 6 | * "Setting up":/ruby.html 7 | * "Extensions":/ruby/extensions.html 8 | * "Monitoring":/ruby/monitoring.html 9 | * "Server-side clients":/ruby/clients.html 10 | * "Engines":/ruby/engines.html 11 | * "WebSockets":/ruby/websockets.html 12 | 13 | </div> 14 | -------------------------------------------------------------------------------- /site/src/partials/security_navigation.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h3. Security advice 3 | 4 | <div class="sections"> 5 | 6 | * "Overview":/security.html 7 | * "Restricting subscriptions":/security/subscription.html 8 | * "Restricting publication":/security/publication.html 9 | * "Publishing JavaScript":/security/javascript.html 10 | * "Push-only servers":/security/push.html 11 | * "Authentication":/security/authentication.html 12 | * "Cookies, Origin, and Referer":/security/headers.html 13 | * "CSRF protection":/security/csrf.html 14 | * "Other techniques":/security/summary.html 15 | 16 | </div> 17 | -------------------------------------------------------------------------------- /site/src/stylesheets/screen.sass: -------------------------------------------------------------------------------- 1 | body 2 | background: #3f3f3f 3 | color: #fff 4 | font: 14px/1.5 Open Sans, FreeSans, Helvetica, Arial, sans-serif 5 | text-align: center 6 | margin: 0 0 0 0 7 | padding: 0 0 0 0 8 | 9 | .container 10 | width: 960px 11 | margin: 0 auto 12 | text-align: left 13 | position: relative 14 | 15 | a 16 | color: #6fbc62 17 | font-weight: bold 18 | text-decoration: none 19 | 20 | a:hover 21 | text-decoration: underline 22 | 23 | .clear 24 | clear: both 25 | display: block 26 | height: 0 27 | overflow: hidden 28 | 29 | .header 30 | border-bottom: 4px solid #ccc 31 | 32 | h1 33 | margin: 0 0 0 0 34 | padding: 26px 0 0 0 35 | a 36 | display: block 37 | width: 286px 38 | height: 0 39 | overflow: hidden 40 | padding: 84px 0 0 0 41 | background: #3f3f3f url(/images/faye-logo.gif) 0 0 no-repeat 42 | border-bottom: 8px solid #3f3f3f 43 | 44 | h2 45 | font-size: 16px 46 | font-weight: normal 47 | margin: 0 0 0 0 48 | padding: 0 8px 40px 49 | 50 | .docs, .download, .community 51 | position: absolute 52 | top: 0 53 | border-left: 1px solid #666 54 | padding: 40px 0 0 20px 55 | height: 112px 56 | 57 | a 58 | color: #999 59 | font-weight: normal 60 | a:hover 61 | color: #fff 62 | 63 | h3 64 | margin: 0 0 0 0 65 | padding: 0 0 0 0 66 | font-weight: bold 67 | font-size: 100% 68 | 69 | ul, li 70 | list-style: none 71 | margin: 0 0 0 0 72 | padding: 0 0 0 0 73 | 74 | .docs 75 | left: 480px 76 | .community 77 | left: 640px 78 | .download 79 | left: 800px 80 | 81 | .footer 82 | border-top: 4px solid #ccc 83 | padding-bottom: 2em 84 | p 85 | font-size: 80% 86 | margin: 1em 0 1em 320px 87 | 88 | .main 89 | background: #fff 90 | color: #3f3f3f 91 | padding: 2em 0 4em 92 | 93 | .front-matter 94 | .intro 95 | font-size: 140% 96 | h3 97 | margin-top: 0 98 | p 99 | border-top: none 100 | margin-top: 0.5em 101 | padding-top: 0 102 | 103 | a img 104 | margin-bottom: 18px 105 | margin-right: 32px 106 | h3 107 | font-weight: normal 108 | font-style: italic 109 | font-size: 150% 110 | float: left 111 | margin: 1em 0 0 80px 112 | position: relative 113 | 114 | .number 115 | color: #6fbc62 116 | font-weight: bold 117 | 118 | p, pre 119 | border-top: 1px solid #eee 120 | margin: 1.5em 0 121 | padding: 1.5em 0 0 320px 122 | 123 | pre 124 | font-family: Inconsolata, Monaco, Lucida Console, Courier New, monospace 125 | 126 | .content 127 | h3 128 | float: left 129 | margin: -0.5em 0 0 0 130 | font-size: 200% 131 | font-weight: bold 132 | 133 | .sections 134 | clear: left 135 | float: left 136 | margin: 32px 0 137 | padding: 0 0 0 2em 138 | 139 | ul, li 140 | margin: 0 141 | padding: 0 142 | 143 | h4 144 | font-size: 120% 145 | font-weight: bold 146 | 147 | h4, p, pre, ul, .image 148 | margin: 1.5em 0 1.5em 320px 149 | 150 | .image img 151 | display: block 152 | margin: 1em auto 153 | 154 | pre 155 | border-left: 1em solid #f0f0f0 156 | font-family: Inconsolata, Monaco, Lucida Console, Courier New, monospace 157 | padding-left: 2em 158 | 159 | code 160 | background: #eee 161 | -------------------------------------------------------------------------------- /spec/browser.js: -------------------------------------------------------------------------------- 1 | require("./javascript/util/copy_object_spec") 2 | require("./javascript/channel_spec") 3 | require("./javascript/client_spec") 4 | require("./javascript/dispatcher_spec") 5 | require("./javascript/grammar_spec") 6 | require("./javascript/publisher_spec") 7 | require("./javascript/transport_spec") 8 | require("./javascript/uri_spec") 9 | 10 | require("jstest").Test.autorun() 11 | -------------------------------------------------------------------------------- /spec/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html> 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>Faye test suite</title> 6 | </head> 7 | <body> 8 | <script src="./browser_bundle.js"></script> 9 | </body> 10 | </html> 11 | -------------------------------------------------------------------------------- /spec/javascript/channel_spec.js: -------------------------------------------------------------------------------- 1 | var jstest = require("jstest").Test 2 | 3 | var Channel = require("../../src/protocol/channel") 4 | 5 | jstest.describe("Channel", function() { with(this) { 6 | describe("expand", function() { with(this) { 7 | it("returns all patterns that match a channel", function() { with(this) { 8 | 9 | assertEqual( ["/**", "/foo", "/*"], 10 | Channel.expand("/foo") ) 11 | 12 | assertEqual( ["/**", "/foo/bar", "/foo/*", "/foo/**"], 13 | Channel.expand("/foo/bar") ) 14 | 15 | assertEqual( ["/**", "/foo/bar/qux", "/foo/bar/*", "/foo/**", "/foo/bar/**"], 16 | Channel.expand("/foo/bar/qux") ) 17 | }}) 18 | }}) 19 | 20 | describe("Set", function() { with(this) { 21 | describe("subscribe", function() { with(this) { 22 | it("subscribes and unsubscribes without callback", function() { with(this) { 23 | var channels = new Channel.Set() 24 | channels.subscribe(["/foo/**"], null) 25 | assertEqual( ["/foo/**"], channels.getKeys() ) 26 | assert( channels.unsubscribe("/foo/**", null) ) 27 | }}) 28 | }}) 29 | }}) 30 | }}) 31 | -------------------------------------------------------------------------------- /spec/javascript/engine/memory_spec.js: -------------------------------------------------------------------------------- 1 | var jstest = require("jstest").Test 2 | 3 | var Memory = require("../../../src/engines/memory") 4 | 5 | require("../engine_spec") 6 | 7 | jstest.describe("Memory engine", function() { with(this) { 8 | before(function() { 9 | this.engineOpts = { type: Memory } 10 | }) 11 | 12 | itShouldBehaveLike("faye engine") 13 | }}) 14 | -------------------------------------------------------------------------------- /spec/javascript/grammar_spec.js: -------------------------------------------------------------------------------- 1 | var jstest = require("jstest").Test 2 | 3 | var Grammar = require("../../src/protocol/grammar") 4 | 5 | jstest.describe("Grammar", function() { with(this) { 6 | describe("CHANNEL_NAME", function() { with(this) { 7 | it("matches valid channel names", function() { with(this) { 8 | assertMatch( Grammar.CHANNEL_NAME, "/fo_o/$@()bar" ) 9 | }}) 10 | 11 | it("does not match channel patterns", function() { with(this) { 12 | assertNoMatch( Grammar.CHANNEL_NAME, "/foo/**" ) 13 | }}) 14 | 15 | it("does not match invalid channel names", function() { with(this) { 16 | assertNoMatch( Grammar.CHANNEL_NAME, "foo/$@()bar" ) 17 | assertNoMatch( Grammar.CHANNEL_NAME, "/foo/$@()bar/" ) 18 | assertNoMatch( Grammar.CHANNEL_NAME, "/fo o/$@()bar" ) 19 | }}) 20 | }}) 21 | 22 | describe("CHANNEL_PATTERN", function() { with(this) { 23 | it("does not match channel names", function() { with(this) { 24 | assertNoMatch( Grammar.CHANNEL_PATTERN, "/fo_o/$@()bar" ) 25 | }}) 26 | 27 | it("matches valid channel patterns", function() { with(this) { 28 | assertMatch( Grammar.CHANNEL_PATTERN, "/foo/**" ) 29 | assertMatch( Grammar.CHANNEL_PATTERN, "/foo/*" ) 30 | }}) 31 | 32 | it("does not match invalid channel patterns", function() { with(this) { 33 | assertNoMatch( Grammar.CHANNEL_PATTERN, "/foo/**/*" ) 34 | }}) 35 | }}) 36 | 37 | describe("ERROR", function() { with(this) { 38 | it("matches an error with an argument", function() { with(this) { 39 | assertMatch( Grammar.ERROR, "402:xj3sjdsjdsjad:Unknown Client ID" ) 40 | }}) 41 | 42 | it("matches an error with many arguments", function() { with(this) { 43 | assertMatch( Grammar.ERROR, "403:xj3sjdsjdsjad,/foo/bar:Subscription denied" ) 44 | }}) 45 | 46 | it("matches an error with no arguments", function() { with(this) { 47 | assertMatch( Grammar.ERROR, "402::Unknown Client ID" ) 48 | }}) 49 | 50 | it("does not match an error with no code", function() { with(this) { 51 | assertNoMatch( Grammar.ERROR, ":xj3sjdsjdsjad:Unknown Client ID" ) 52 | }}) 53 | 54 | it("does not match an error with an invalid code", function() { with(this) { 55 | assertNoMatch( Grammar.ERROR, "40:xj3sjdsjdsjad:Unknown Client ID" ) 56 | }}) 57 | }}) 58 | 59 | describe("VERSION", function() { with(this) { 60 | it("matches a version number", function() { with(this) { 61 | assertMatch( Grammar.VERSION, "9" ) 62 | assertMatch( Grammar.VERSION, "9.0.a-delta1" ) 63 | }}) 64 | 65 | it("does not match invalid version numbers", function() { with(this) { 66 | assertNoMatch( Grammar.VERSION, "9.0.a-delta1." ) 67 | assertNoMatch( Grammar.VERSION, "" ) 68 | }}) 69 | }}) 70 | }}) 71 | -------------------------------------------------------------------------------- /spec/javascript/publisher_spec.js: -------------------------------------------------------------------------------- 1 | var jstest = require("jstest").Test 2 | 3 | var Publisher = require("../../src/mixins/publisher"), 4 | assign = require("../../src/util/assign") 5 | 6 | jstest.describe("Publisher", function() { with(this) { 7 | before(function() { with(this) { 8 | this.publisher = assign({}, Publisher) 9 | }}) 10 | 11 | describe("with subscribers that remove themselves", function() { with(this) { 12 | before(function() { with(this) { 13 | this.calledA = false 14 | this.calledB = false 15 | 16 | this.handler = function() { 17 | calledA = true 18 | publisher.unbind("event", handler) 19 | } 20 | 21 | publisher.bind("event", handler) 22 | publisher.bind("event", function() { calledB = true }) 23 | }}) 24 | 25 | it("successfully calls all the callbacks", function() { with(this) { 26 | publisher.trigger("event") 27 | assert( calledA ) 28 | assert( calledB ) 29 | }}) 30 | }}) 31 | }}) 32 | -------------------------------------------------------------------------------- /spec/javascript/server/extensions_spec.js: -------------------------------------------------------------------------------- 1 | var jstest = require("jstest").Test 2 | 3 | var Engine = require("../../../src/engines/proxy"), 4 | Server = require("../../../src/protocol/server") 5 | 6 | jstest.describe("Server extensions", function() { with(this) { 7 | before(function() { with(this) { 8 | this.engine = {} 9 | stub(Engine, "get").returns(engine) 10 | this.server = new Server() 11 | }}) 12 | 13 | describe("with an incoming extension installed", function() { with(this) { 14 | before(function() { with(this) { 15 | var extension = { 16 | incoming: function(message, callback) { 17 | message.ext = { auth: "password" } 18 | callback(message) 19 | } 20 | } 21 | server.addExtension(extension) 22 | this.message = { channel: "/foo", data: "hello" } 23 | }}) 24 | 25 | it("passes incoming messages through the extension", function() { with(this) { 26 | expect(engine, "publish").given({ channel: "/foo", data: "hello", ext: { auth: "password" }}) 27 | server.process(message, false, function() {}) 28 | }}) 29 | 30 | it("does not pass outgoing messages through the extension", function() { with(this) { 31 | stub(server, "handshake").yields([message]) 32 | stub(engine, "publish") 33 | var response = null 34 | server.process({ channel: "/meta/handshake" }, false, function(r) { response = r }) 35 | assertEqual( [{ channel: "/foo", data: "hello" }], response ) 36 | }}) 37 | }}) 38 | 39 | describe("with subscription auth installed", function() { with(this) { 40 | before(function() { with(this) { 41 | var extension = { 42 | incoming: function(message, callback) { 43 | if (message.channel === "/meta/subscribe" && !message.auth) { 44 | message.error = "Invalid auth" 45 | } 46 | callback(message) 47 | } 48 | } 49 | server.addExtension(extension) 50 | }}) 51 | 52 | it("does not subscribe using the intended channel", function() { with(this) { 53 | var message = { 54 | channel: "/meta/subscribe", 55 | clientId: "fakeclientid", 56 | subscription: "/foo" 57 | } 58 | stub(engine, "clientExists").yields([true]) 59 | expect(engine, "subscribe").exactly(0) 60 | server.process(message, false, function() {}) 61 | }}) 62 | 63 | it("does not subscribe using an extended channel", function() { with(this) { 64 | var message = { 65 | channel: "/meta/subscribe/x", 66 | clientId: "fakeclientid", 67 | subscription: "/foo" 68 | } 69 | stub(engine, "clientExists").yields([true]) 70 | expect(engine, "subscribe").exactly(0) 71 | server.process(message, false, function() {}) 72 | }}) 73 | }}) 74 | 75 | describe("with an outgoing extension installed", function() { with(this) { 76 | before(function() { with(this) { 77 | var extension = { 78 | outgoing: function(message, callback) { 79 | message.ext = { auth: "password" } 80 | callback(message) 81 | } 82 | } 83 | server.addExtension(extension) 84 | this.message = { channel: "/foo", data: "hello" } 85 | }}) 86 | 87 | it("does not pass incoming messages through the extension", function() { with(this) { 88 | expect(engine, "publish").given({ channel: "/foo", data: "hello" }) 89 | server.process(message, false, function() {}) 90 | }}) 91 | 92 | it("passes outgoing messages through the extension", function() { with(this) { 93 | stub(server, "handshake").yields([message]) 94 | stub(engine, "publish") 95 | var response = null 96 | server.process({ channel: "/meta/handshake" }, false, function(r) { response = r }) 97 | assertEqual( [{ channel: "/foo", data: "hello", ext: { auth: "password" }}], response ) 98 | }}) 99 | }}) 100 | }}) 101 | -------------------------------------------------------------------------------- /spec/javascript/server/publish_spec.js: -------------------------------------------------------------------------------- 1 | var jstest = require("jstest").Test 2 | 3 | var Engine = require("../../../src/engines/proxy"), 4 | Server = require("../../../src/protocol/server") 5 | 6 | jstest.describe("Server publish", function() { with(this) { 7 | before(function() { with(this) { 8 | this.engine = {} 9 | stub(Engine, "get").returns(engine) 10 | this.server = new Server() 11 | 12 | this.message = { channel: "/some/channel", data: "publish" } 13 | }}) 14 | 15 | describe("publishing a message", function() { with(this) { 16 | it("tells the engine to publish the message", function() { with(this) { 17 | expect(engine, "publish").given(message) 18 | server.process(message, false, function() {}) 19 | }}) 20 | 21 | it("returns a successful response", function() { with(this) { 22 | stub(engine, "publish") 23 | server.process(message, false, function(response) { 24 | assertEqual([ 25 | { channel: "/some/channel", 26 | successful: true 27 | } 28 | ], response) 29 | }) 30 | }}) 31 | 32 | describe("with an invalid channel", function() { with(this) { 33 | before(function() { with(this) { 34 | message.channel = "channel" 35 | }}) 36 | 37 | it("does not tell the engine to publish the message", function() { with(this) { 38 | expect(engine, "publish").exactly(0) 39 | server.process(message, false, function() {}) 40 | }}) 41 | 42 | it("returns an unsuccessful response", function() { with(this) { 43 | stub(engine, "publish") 44 | server.process(message, false, function(response) { 45 | assertEqual([ 46 | { channel: "channel", 47 | successful: false, 48 | error: "405:channel:Invalid channel" 49 | } 50 | ], response) 51 | }) 52 | }}) 53 | }}) 54 | 55 | describe("with no data", function() { with(this) { 56 | before(function() { with(this) { 57 | delete message.data 58 | }}) 59 | 60 | it("does not tell the engine to publish the message", function() { with(this) { 61 | expect(engine, "publish").exactly(0) 62 | server.process(message, false, function() {}) 63 | }}) 64 | 65 | it("returns an unsuccessful response", function() { with(this) { 66 | stub(engine, "publish") 67 | server.process(message, false, function(response) { 68 | assertEqual([ 69 | { channel: "/some/channel", 70 | successful: false, 71 | error: "402:data:Missing required parameter" 72 | } 73 | ], response) 74 | }) 75 | }}) 76 | }}) 77 | 78 | describe("with an error", function() { with(this) { 79 | before(function() { with(this) { 80 | message.error = "invalid" 81 | }}) 82 | 83 | it("does not tell the engine to publish the message", function() { with(this) { 84 | expect(engine, "publish").exactly(0) 85 | server.process(message, false, function() {}) 86 | }}) 87 | 88 | it("returns an unsuccessful response", function() { with(this) { 89 | stub(engine, "publish") 90 | server.process(message, false, function(response) { 91 | assertEqual([ 92 | { channel: "/some/channel", 93 | successful: false, 94 | error: "invalid" 95 | } 96 | ], response) 97 | }) 98 | }}) 99 | }}) 100 | 101 | describe("to an invalid channel", function() { with(this) { 102 | before(function() { with(this) { 103 | message.channel = "/invalid/*" 104 | }}) 105 | 106 | it("does not tell the engine to publish the message", function() { with(this) { 107 | expect(engine, "publish").exactly(0) 108 | server.process(message, false, function() {}) 109 | }}) 110 | }}) 111 | }}) 112 | }}) 113 | -------------------------------------------------------------------------------- /spec/javascript/uri_spec.js: -------------------------------------------------------------------------------- 1 | var jstest = require("jstest").Test 2 | 3 | var URI = require("../../src/util/uri") 4 | 5 | jstest.describe("URI", function() { with(this) { 6 | describe("parse", function() { with(this) { 7 | it("parses all the bits of a URI", function() { with(this) { 8 | assertEqual( { 9 | href: "http://example.com:80/foo.html?foo=bar&hello=%2Fworld#cloud", 10 | protocol: "http:", 11 | host: "example.com:80", 12 | hostname: "example.com", 13 | port: "80", 14 | path: "/foo.html?foo=bar&hello=%2Fworld", 15 | pathname: "/foo.html", 16 | search: "?foo=bar&hello=%2Fworld", 17 | query: { foo: "bar", hello: "/world" }, 18 | hash: "#cloud" 19 | }, URI.parse("http://example.com:80/foo.html?foo=bar&hello=%2Fworld#cloud") ) 20 | }}) 21 | 22 | it("parses a URI with no hash", function() { with(this) { 23 | assertEqual( { 24 | href: "http://example.com:80/foo.html?foo=bar&hello=%2Fworld", 25 | protocol: "http:", 26 | host: "example.com:80", 27 | hostname: "example.com", 28 | port: "80", 29 | path: "/foo.html?foo=bar&hello=%2Fworld", 30 | pathname: "/foo.html", 31 | search: "?foo=bar&hello=%2Fworld", 32 | query: { foo: "bar", hello: "/world" }, 33 | hash: "" 34 | }, URI.parse("http://example.com:80/foo.html?foo=bar&hello=%2Fworld") ) 35 | }}) 36 | 37 | it("parses a URI with no query", function() { with(this) { 38 | assertEqual( { 39 | href: "http://example.com:80/foo.html#cloud", 40 | protocol: "http:", 41 | host: "example.com:80", 42 | hostname: "example.com", 43 | port: "80", 44 | path: "/foo.html", 45 | pathname: "/foo.html", 46 | search: "", 47 | query: {}, 48 | hash: "#cloud" 49 | }, URI.parse("http://example.com:80/foo.html#cloud") ) 50 | }}) 51 | 52 | it("parses a URI with an encoded path", function() { with(this) { 53 | assertEqual( { 54 | href: "http://example.com:80/fo%20o.html?foo=bar&hello=%2Fworld#cloud", 55 | protocol: "http:", 56 | host: "example.com:80", 57 | hostname: "example.com", 58 | port: "80", 59 | path: "/fo%20o.html?foo=bar&hello=%2Fworld", 60 | pathname: "/fo%20o.html", 61 | search: "?foo=bar&hello=%2Fworld", 62 | query: { foo: "bar", hello: "/world" }, 63 | hash: "#cloud" 64 | }, URI.parse("http://example.com:80/fo%20o.html?foo=bar&hello=%2Fworld#cloud") ) 65 | }}) 66 | 67 | it("parses a URI with no path", function() { with(this) { 68 | assertEqual( { 69 | href: "http://example.com:80/?foo=bar&hello=%2Fworld#cloud", 70 | protocol: "http:", 71 | host: "example.com:80", 72 | hostname: "example.com", 73 | port: "80", 74 | path: "/?foo=bar&hello=%2Fworld", 75 | pathname: "/", 76 | search: "?foo=bar&hello=%2Fworld", 77 | query: { foo: "bar", hello: "/world" }, 78 | hash: "#cloud" 79 | }, URI.parse("http://example.com:80?foo=bar&hello=%2Fworld#cloud") ) 80 | }}) 81 | 82 | it("parses a URI with no port", function() { with(this) { 83 | assertEqual( { 84 | href: "http://example.com/foo.html?foo=bar&hello=%2Fworld#cloud", 85 | protocol: "http:", 86 | host: "example.com", 87 | hostname: "example.com", 88 | port: "", 89 | path: "/foo.html?foo=bar&hello=%2Fworld", 90 | pathname: "/foo.html", 91 | search: "?foo=bar&hello=%2Fworld", 92 | query: { foo: "bar", hello: "/world" }, 93 | hash: "#cloud" 94 | }, URI.parse("http://example.com/foo.html?foo=bar&hello=%2Fworld#cloud") ) 95 | }}) 96 | }}) 97 | }}) 98 | -------------------------------------------------------------------------------- /spec/javascript/util/copy_object_spec.js: -------------------------------------------------------------------------------- 1 | var jstest = require("jstest").Test 2 | 3 | var copyObject = require("../../../src/util/copy_object") 4 | 5 | jstest.describe("copyObject", function() { with(this) { 6 | before(function() { with(this) { 7 | this.object = { foo: "bar", qux: 42, hey: null, obj: { bar: 67 }} 8 | }}) 9 | 10 | it("returns an equal object", function() { with(this) { 11 | assertEqual( { foo: "bar", qux: 42, hey: null, obj: { bar: 67 }}, 12 | copyObject(object) ) 13 | }}) 14 | 15 | it("does not return the same object", function() { with(this) { 16 | assertNotSame( object, copyObject(object) ) 17 | }}) 18 | 19 | it("performs a deep clone", function() { with(this) { 20 | assertNotSame( object.obj, copyObject(object).obj ) 21 | }}) 22 | }}) 23 | -------------------------------------------------------------------------------- /spec/javascript/util/random_spec.js: -------------------------------------------------------------------------------- 1 | var jstest = require("jstest").Test, 2 | Range = require("jstest").Range 3 | 4 | var random = require("../../../src/util/random") 5 | 6 | jstest.describe("random", function() { with(this) { 7 | if (typeof document !== "undefined") return 8 | 9 | it("returns a 160-bit random number in base 36", function() { with(this) { 10 | assertMatch( /^[a-z0-9]+$/, random() ) 11 | }}) 12 | 13 | it("always produces the same length of string", function() { with(this) { 14 | var ids = new Range(1,100).map(function() { return random().length }) 15 | var expected = new Range(1,100).map(function() { return 31 }) 16 | assertEqual( expected, ids ) 17 | }}) 18 | }}) 19 | -------------------------------------------------------------------------------- /spec/phantom.js: -------------------------------------------------------------------------------- 1 | phantom.injectJs('node_modules/jstest/jstest.js') 2 | 3 | var options = { format: 'dot' }, 4 | reporter = new JS.Test.Reporters.Headless(options) 5 | 6 | reporter.open('spec/index.html') 7 | -------------------------------------------------------------------------------- /spec/ruby/channel_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Faye::Channel do 4 | describe :expand do 5 | it "returns all patterns that match a channel" do 6 | Faye::Channel.expand("/foo").should == [ 7 | "/**", "/foo", "/*"] 8 | 9 | Faye::Channel.expand("/foo/bar").should == [ 10 | "/**", "/foo/bar", "/foo/*", "/foo/**"] 11 | 12 | Faye::Channel.expand("/foo/bar/qux").should == [ 13 | "/**", "/foo/bar/qux", "/foo/bar/*", "/foo/**", "/foo/bar/**"] 14 | end 15 | end 16 | 17 | describe Faye::Channel::Set do 18 | describe :subscribe do 19 | it "subscribes and unsubscribes without callback" do 20 | channels = Faye::Channel::Set.new 21 | channels.subscribe(["/foo/**"], nil) 22 | channels.keys.should == ["/foo/**"] 23 | channels.unsubscribe("/foo/**", nil).should == true 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/ruby/encoding_helper.rb: -------------------------------------------------------------------------------- 1 | module EncodingHelper 2 | def encode(string) 3 | return string unless string.respond_to?(:force_encoding) 4 | string.force_encoding("UTF-8") 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/ruby/engine/memory_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Faye::Engine::Memory do 4 | let(:engine_opts) { { :type => Faye::Engine::Memory } } 5 | it_should_behave_like "faye engine" 6 | end 7 | -------------------------------------------------------------------------------- /spec/ruby/faye_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Faye do 4 | describe :random do 5 | it "returns a 160-bit random number in base 36" do 6 | Faye.random.should =~ /^[a-z0-9]+$/ 7 | end 8 | 9 | it "always produces the same length of string" do 10 | ids = (1..100).map { Faye.random } 11 | ids.should be_all { |id| id.size == 31 } 12 | end 13 | end 14 | 15 | describe :copy_obect do 16 | let(:object) { { "foo" => "bar", "qux" => 42, "hey" => nil, "obj" => { "bar" => 67 } } } 17 | 18 | it "returns an equal object" do 19 | Faye.copy_object(object).should == { "foo" => "bar", "qux" => 42, "hey" => nil, "obj" => { "bar" => 67 }} 20 | end 21 | 22 | it "does not return the same object" do 23 | Faye.copy_object(object).should_not be_equal(object) 24 | end 25 | 26 | it "performs a deep clone" do 27 | Faye.copy_object(object)["obj"].should_not be_equal(object["obj"]) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/ruby/grammar_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Faye::Grammar do 4 | describe :CHANNEL_NAME do 5 | it "matches valid channel names" do 6 | Faye::Grammar::CHANNEL_NAME.should =~ "/fo_o/$@()bar" 7 | end 8 | 9 | it "does not match channel patterns" do 10 | Faye::Grammar::CHANNEL_NAME.should_not =~ "/foo/**" 11 | end 12 | 13 | it "does not match invalid channel names" do 14 | Faye::Grammar::CHANNEL_NAME.should_not =~ "foo/$@()bar" 15 | Faye::Grammar::CHANNEL_NAME.should_not =~ "/foo/$@()bar/" 16 | Faye::Grammar::CHANNEL_NAME.should_not =~ "/fo o/$@()bar" 17 | end 18 | end 19 | 20 | describe :CHANNEL_PATTERN do 21 | it "does not match channel names" do 22 | Faye::Grammar::CHANNEL_PATTERN.should_not =~ "/fo_o/$@()bar" 23 | end 24 | 25 | it "matches valid channel patterns" do 26 | Faye::Grammar::CHANNEL_PATTERN.should =~ "/foo/**" 27 | Faye::Grammar::CHANNEL_PATTERN.should =~ "/foo/*" 28 | end 29 | 30 | it "does not match invalid channel patterns" do 31 | Faye::Grammar::CHANNEL_PATTERN.should_not =~ "/foo/**/*" 32 | end 33 | end 34 | 35 | describe :ERROR do 36 | it "matches an error with an argument" do 37 | Faye::Grammar::ERROR.should =~ "402:xj3sjdsjdsjad:Unknown Client ID" 38 | end 39 | 40 | it "matches an error with many arguments" do 41 | Faye::Grammar::ERROR.should =~ "403:xj3sjdsjdsjad,/foo/bar:Subscription denied" 42 | end 43 | 44 | it "matches an error with no arguments" do 45 | Faye::Grammar::ERROR.should =~ "402::Unknown Client ID" 46 | end 47 | 48 | it "does not match an error with no code" do 49 | Faye::Grammar::ERROR.should_not =~ ":xj3sjdsjdsjad:Unknown Client ID" 50 | end 51 | 52 | it "does not match an error with an invalid code" do 53 | Faye::Grammar::ERROR.should_not =~ "40:xj3sjdsjdsjad:Unknown Client ID" 54 | end 55 | end 56 | 57 | describe :VERSION do 58 | it "matches a version number" do 59 | Faye::Grammar::VERSION.should =~ "9" 60 | Faye::Grammar::VERSION.should =~ "9.0.a-delta1" 61 | end 62 | 63 | it "does not match invalid version numbers" do 64 | Faye::Grammar::VERSION.should_not =~ "9.0.a-delta1." 65 | Faye::Grammar::VERSION.should_not =~ "" 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/ruby/publisher_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Faye::Publisher do 4 | let(:publisher) { Class.new { include Faye::Publisher }.new } 5 | 6 | describe "with subscribers that remove themselves" do 7 | before do 8 | @called_a = false 9 | @called_b = false 10 | 11 | handler = lambda do 12 | @called_a = true 13 | publisher.unbind(:event, &handler) 14 | end 15 | 16 | publisher.bind(:event, &handler) 17 | publisher.bind(:event) { @called_b = true } 18 | end 19 | 20 | it "successfully calls all the callbacks" do 21 | publisher.trigger(:event) 22 | @called_a.should == true 23 | @called_b.should == true 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/ruby/server/disconnect_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "server disconnect" do 4 | let(:engine) { double "engine" } 5 | let(:server) { Faye::Server.new } 6 | 7 | before do 8 | Faye::Engine.stub(:get).and_return engine 9 | end 10 | 11 | describe :disconnect do 12 | let(:client_id) { "fakeclientid" } 13 | let(:message) { { "channel" => "/meta/disconnect", 14 | "clientId" => "fakeclientid" 15 | } } 16 | 17 | describe "with valid parameters" do 18 | before do 19 | engine.should_receive(:client_exists).with(client_id).and_yield true 20 | end 21 | 22 | it "destroys the client" do 23 | engine.should_receive(:destroy_client).with(client_id) 24 | server.disconnect(message) {} 25 | end 26 | 27 | it "returns a successful response" do 28 | engine.stub(:destroy_client) 29 | server.disconnect(message) do |response| 30 | response.should == { 31 | "channel" => "/meta/disconnect", 32 | "successful" => true, 33 | "clientId" => client_id 34 | } 35 | end 36 | end 37 | 38 | describe "with a message id" do 39 | before { message["id"] = "foo" } 40 | 41 | it "returns the same id" do 42 | engine.stub(:destroy_client) 43 | server.disconnect(message) do |response| 44 | response.should == { 45 | "channel" => "/meta/disconnect", 46 | "successful" => true, 47 | "clientId" => client_id, 48 | "id" => "foo" 49 | } 50 | end 51 | end 52 | end 53 | end 54 | 55 | describe "with an unknown client" do 56 | before do 57 | engine.should_receive(:client_exists).with(client_id).and_yield false 58 | end 59 | 60 | it "does not destroy the client" do 61 | engine.should_not_receive(:destroy_client) 62 | server.disconnect(message) {} 63 | end 64 | 65 | it "returns an unsuccessful response" do 66 | server.disconnect(message) do |response| 67 | response.should == { 68 | "channel" => "/meta/disconnect", 69 | "successful" => false, 70 | "error" => "401:fakeclientid:Unknown client" 71 | } 72 | end 73 | end 74 | end 75 | 76 | describe "missing clientId" do 77 | before do 78 | message.delete("clientId") 79 | engine.should_receive(:client_exists).with(nil).and_yield false 80 | end 81 | 82 | it "does not destroy the client" do 83 | engine.should_not_receive(:destroy_client) 84 | server.disconnect(message) {} 85 | end 86 | 87 | it "returns an unsuccessful response" do 88 | server.disconnect(message) do |response| 89 | response.should == { 90 | "channel" => "/meta/disconnect", 91 | "successful" => false, 92 | "error" => "402:clientId:Missing required parameter" 93 | } 94 | end 95 | end 96 | end 97 | 98 | describe "with an error" do 99 | before do 100 | message["error"] = "invalid" 101 | engine.should_receive(:client_exists).with(client_id).and_yield true 102 | end 103 | 104 | it "does not destroy the client" do 105 | engine.should_not_receive(:destroy_client) 106 | server.disconnect(message) {} 107 | end 108 | 109 | it "returns an unsuccessful response" do 110 | server.disconnect(message) do |response| 111 | response.should == { 112 | "channel" => "/meta/disconnect", 113 | "successful" => false, 114 | "error" => "invalid" 115 | } 116 | end 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/ruby/server/extensions_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "server extensions" do 4 | let(:engine) do 5 | engine = double "engine" 6 | engine.stub(:interval).and_return(0) 7 | engine.stub(:timeout).and_return(60) 8 | engine 9 | end 10 | 11 | let(:server) { Faye::Server.new } 12 | let(:message) { { "channel" => "/foo", "data" => "hello" } } 13 | 14 | before do 15 | Faye::Engine.stub(:get).and_return engine 16 | end 17 | 18 | describe "with an incoming extension installed" do 19 | before do 20 | extension = Class.new do 21 | def incoming(message, callback) 22 | message["ext"] = { "auth" => "password" } 23 | callback.call(message) 24 | end 25 | end 26 | server.add_extension(extension.new) 27 | end 28 | 29 | it "passes incoming messages through the extension" do 30 | engine.should_receive(:publish).with({ "channel" => "/foo", "data" => "hello", "ext" => { "auth" => "password" }}) 31 | server.process(message, false) {} 32 | end 33 | 34 | it "does not pass outgoing messages through the extension" do 35 | server.stub(:handshake).and_yield(message) 36 | engine.stub(:publish) 37 | response = nil 38 | server.process({ "channel" => "/meta/handshake" }, false) { |r| response = r } 39 | response.should == [{ "channel" => "/foo", "data" => "hello" }] 40 | end 41 | end 42 | 43 | describe "with subscription auth installed" do 44 | before do 45 | extension = Class.new do 46 | def incoming(message, callback) 47 | if message["channel"] == "/meta/subscribe" and !message["auth"] 48 | message["error"] = "Invalid auth" 49 | end 50 | callback.call(message) 51 | end 52 | end 53 | server.add_extension(extension.new) 54 | end 55 | 56 | it "does not subscribe using the intended channel" do 57 | message = { 58 | "channel" => "/meta/subscribe", 59 | "clientId" => "fakeclientid", 60 | "subscription" => "/foo" 61 | } 62 | engine.stub(:client_exists).and_yield(true) 63 | engine.should_not_receive(:subscribe) 64 | server.process(message, false) {} 65 | end 66 | 67 | it "does not subscribe using an extended channel" do 68 | message = { 69 | "channel" => "/meta/subscribe/x", 70 | "clientId" => "fakeclientid", 71 | "subscription" => "/foo" 72 | } 73 | engine.stub(:client_exists).and_yield(true) 74 | engine.should_not_receive(:subscribe) 75 | server.process(message, false) {} 76 | end 77 | end 78 | 79 | describe "with an outgoing extension installed" do 80 | before do 81 | extension = Class.new do 82 | def outgoing(message, callback) 83 | message["ext"] = { "auth" => "password" } 84 | callback.call(message) 85 | end 86 | end 87 | server.add_extension(extension.new) 88 | end 89 | 90 | it "does not pass incoming messages through the extension" do 91 | engine.should_receive(:publish).with({ "channel" => "/foo", "data" => "hello" }) 92 | server.process(message, false) {} 93 | end 94 | 95 | it "passes outgoing messages through the extension" do 96 | server.stub(:handshake).and_yield(message) 97 | engine.stub(:publish) 98 | response = nil 99 | server.process({ "channel" => "/meta/handshake" }, false) { |r| response = r } 100 | response.should == [{ "channel" => "/foo", "data" => "hello", "ext" => { "auth" => "password" }}] 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/ruby/server/integration_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | require "spec_helper" 4 | 5 | IntegrationSteps = RSpec::EM.async_steps do 6 | class Tagger 7 | def incoming(message, callback) 8 | message["data"]["tagged"] = true if message["data"] 9 | callback.call(message) 10 | end 11 | 12 | def outgoing(message, request, callback) 13 | message["data"]["url"] = request.path_info if message["data"] 14 | callback.call(message) 15 | end 16 | end 17 | 18 | def server(port, &callback) 19 | @faye = Faye::RackAdapter.new(:mount => "/bayeux", :timeout => 25) 20 | @faye.add_extension(Tagger.new) 21 | 22 | @server = ServerProxy::App.new(@faye) 23 | @port = port 24 | 25 | @server.listen(@port) 26 | EM.next_tick(&callback) 27 | end 28 | 29 | def stop(&callback) 30 | @server.stop 31 | EM.next_tick(&callback) 32 | end 33 | 34 | def client(name, channels, &callback) 35 | @clients ||= {} 36 | @inboxes ||= {} 37 | @clients[name] = Faye::Client.new("http://0.0.0.0:#{ @port }/bayeux") 38 | @inboxes[name] = {} 39 | 40 | n = channels.size 41 | return @clients[name].connect(&callback) if n.zero? 42 | 43 | channels.each do |channel| 44 | subscription = @clients[name].subscribe(channel) do |message| 45 | @inboxes[name][channel] ||= [] 46 | @inboxes[name][channel] << message 47 | end 48 | subscription.callback do 49 | n -= 1 50 | callback.call if n.zero? 51 | end 52 | end 53 | end 54 | 55 | def publish(name, channel, message, &callback) 56 | @clients[name].publish(channel, message) 57 | EM.add_timer(0.1, &callback) 58 | end 59 | 60 | def check_inbox(name, channel, messages, &callback) 61 | inbox = @inboxes[name][channel] || [] 62 | inbox.should == messages 63 | callback.call 64 | end 65 | end 66 | 67 | describe "server integration" do 68 | next if RUBY_PLATFORM =~ /java/ 69 | 70 | include IntegrationSteps 71 | include EncodingHelper 72 | 73 | before do 74 | server 4180 75 | client :alice, [] 76 | client :bob, ["/foo"] 77 | end 78 | 79 | after { stop } 80 | 81 | shared_examples_for "message bus" do 82 | it "delivers a message between clients" do 83 | publish :alice, "/foo", { "hello" => "world", "extra" => nil } 84 | check_inbox :bob, "/foo", [{ "hello" => "world", "extra" => nil, "tagged" => true, "url" => "/bayeux" }] 85 | end 86 | 87 | it "does not deliver messages for unsubscribed channels" do 88 | publish :alice, "/bar", { "hello" => "world" } 89 | check_inbox :bob, "/foo", [] 90 | end 91 | 92 | it "delivers multiple messages" do 93 | publish :alice, "/foo", { "hello" => "world" } 94 | publish :alice, "/foo", { "hello" => "world" } 95 | check_inbox :bob, "/foo", [{ "hello" => "world", "tagged" => true, "url" => "/bayeux" }, { "hello" => "world", "tagged" => true, "url" => "/bayeux" }] 96 | end 97 | 98 | it "delivers multibyte strings" do 99 | publish :alice, "/foo", { "hello" => encode("Apple = "), "tagged" => true, "url" => "/bayeux" } 100 | check_inbox :bob, "/foo", [{ "hello" => encode("Apple = "), "tagged" => true, "url" => "/bayeux" }] 101 | end 102 | end 103 | 104 | shared_examples_for "network transports" do 105 | describe "with HTTP transport" do 106 | before do 107 | Faye::Transport::WebSocket.stub(:usable?).and_yield(false) 108 | end 109 | 110 | it_should_behave_like "message bus" 111 | end 112 | 113 | describe "with WebSocket transport" do 114 | before do 115 | Faye::Transport::WebSocket.stub(:usable?).and_yield(true) 116 | end 117 | 118 | it_should_behave_like "message bus" 119 | end 120 | end 121 | 122 | describe "with HTTP server" do 123 | let(:server_options) { { :ssl => false } } 124 | it_should_behave_like "network transports" 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/ruby/server/publish_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "server publish" do 4 | let(:engine) { double "engine" } 5 | let(:server) { Faye::Server.new } 6 | let(:message) { { "channel" => "/some/channel", "data" => "publish" } } 7 | 8 | before do 9 | Faye::Engine.stub(:get).and_return engine 10 | end 11 | 12 | describe "publishing a message" do 13 | it "tells the engine to publish the message" do 14 | engine.should_receive(:publish).with(message) 15 | server.process(message, false) {} 16 | end 17 | 18 | it "returns a successful response" do 19 | engine.stub(:publish) 20 | server.process(message, false) do |response| 21 | response.should == [ 22 | { "channel" => "/some/channel", 23 | "successful" => true 24 | } 25 | ] 26 | end 27 | end 28 | 29 | describe "with an invalid channel" do 30 | before { message["channel"] = "channel" } 31 | 32 | it "does not tell the engine to publish the message" do 33 | engine.should_not_receive(:publish) 34 | server.process(message, false) {} 35 | end 36 | 37 | it "returns an unsuccessful response" do 38 | engine.stub(:publish) 39 | server.process(message, false) do |response| 40 | response.should == [ 41 | { "channel" => "channel", 42 | "successful" => false, 43 | "error" => "405:channel:Invalid channel" 44 | } 45 | ] 46 | end 47 | end 48 | end 49 | 50 | describe "with no data" do 51 | before { message.delete("data") } 52 | 53 | it "does not tell the engine to publish the message" do 54 | engine.should_not_receive(:publish) 55 | server.process(message, false) {} 56 | end 57 | 58 | it "returns an unsuccessful response" do 59 | engine.stub(:publish) 60 | server.process(message, false) do |response| 61 | response.should == [ 62 | { "channel" => "/some/channel", 63 | "successful" => false, 64 | "error" => "402:data:Missing required parameter" 65 | } 66 | ] 67 | end 68 | end 69 | end 70 | 71 | 72 | describe "with an error" do 73 | before { message["error"] = "invalid" } 74 | 75 | it "does not tell the engine to publish the message" do 76 | engine.should_not_receive(:publish) 77 | server.process(message, false) {} 78 | end 79 | 80 | it "returns an unsuccessful response" do 81 | engine.stub(:publish) 82 | server.process(message, false) do |response| 83 | response.should == [ 84 | { "channel" => "/some/channel", 85 | "successful" => false, 86 | "error" => "invalid" 87 | } 88 | ] 89 | end 90 | end 91 | end 92 | 93 | describe "to an invalid channel" do 94 | before { message["channel"] = "/invalid/*" } 95 | 96 | it "does not tell the engine to publish the message" do 97 | engine.should_not_receive(:publish) 98 | server.process(message, false) {} 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/ruby/server_proxy.rb: -------------------------------------------------------------------------------- 1 | class ServerProxy < Rack::Proxy 2 | HOST = 'localhost' 3 | PORT = '4180' 4 | 5 | class App 6 | def initialize(app) 7 | @app = app 8 | end 9 | 10 | def listen(port) 11 | events = Puma::Events.new($stdout, $stderr) 12 | binder = Puma::Binder.new(events) 13 | binder.parse(["tcp://0.0.0.0:#{ PORT }"], self) 14 | 15 | @server = Puma::Server.new(self, events) 16 | @server.binder = binder 17 | @thread = @server.run 18 | rescue => e 19 | end 20 | 21 | def stop 22 | @server.stop 23 | @thread.join 24 | end 25 | 26 | def call(env) 27 | @app.call(env) 28 | end 29 | 30 | def log(message) 31 | end 32 | end 33 | 34 | def initialize(rack_app) 35 | @app = App.new(rack_app) 36 | @app.listen(PORT) 37 | end 38 | 39 | def stop 40 | @app.stop 41 | end 42 | 43 | def rewrite_env(env) 44 | env['HTTP_HOST'] = HOST 45 | env['SERVER_PORT'] = PORT 46 | env[Faye::RackAdapter::HTTP_X_NO_CONTENT_LENGTH] = '1' 47 | env 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/ruby/server_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Faye::Server do 4 | let(:engine) { double "engine" } 5 | let(:server) { Faye::Server.new } 6 | 7 | before do 8 | Faye::Engine.stub(:get).and_return engine 9 | end 10 | 11 | describe :process do 12 | let(:handshake) { { "channel" => "/meta/handshake", "data" => "handshake" } } 13 | let(:connect) { { "channel" => "/meta/connect", "data" => "connect" } } 14 | let(:disconnect) { { "channel" => "/meta/disconnect", "data" => "disconnect" } } 15 | let(:subscribe) { { "channel" => "/meta/subscribe", "data" => "subscribe" } } 16 | let(:unsubscribe) { { "channel" => "/meta/unsubscribe", "data" => "unsubscribe" } } 17 | let(:publish) { { "channel" => "/some/channel", "data" => "publish" } } 18 | 19 | before do 20 | engine.stub(:interval).and_return(0) 21 | engine.stub(:timeout).and_return(60) 22 | end 23 | 24 | it "returns an empty response for no messages" do 25 | response = nil 26 | server.process([], false) { |r| response = r } 27 | response.should == [] 28 | end 29 | 30 | it "ignores invalid messages" do 31 | response = nil 32 | server.process([{}, { "channel" => "invalid" }], false) { |r| response = r } 33 | response.should == [ 34 | { "successful" => false, 35 | "error" => "402:data:Missing required parameter" 36 | }, 37 | { "channel" => "invalid", 38 | "successful" => false, 39 | "error" => "402:data:Missing required parameter" 40 | } 41 | ] 42 | end 43 | 44 | it "rejects unknown meta channels" do 45 | response = nil 46 | server.process([{ "channel" => "/meta/p" }], false) { |r| response = r } 47 | response.should == [ 48 | { "channel" => "/meta/p", 49 | "successful" => false, 50 | "error" => "403:/meta/p:Forbidden channel" 51 | } 52 | ] 53 | end 54 | 55 | it "routes single messages to appropriate handlers" do 56 | server.should_receive(:handshake).with(handshake, false) 57 | server.process(handshake, false) {} 58 | end 59 | 60 | it "routes a list of messages to appropriate handlers" do 61 | server.should_receive(:handshake).with(handshake, false) 62 | server.should_receive(:connect).with(connect, false) 63 | server.should_receive(:disconnect).with(disconnect, false) 64 | server.should_receive(:subscribe).with(subscribe, false) 65 | server.should_receive(:unsubscribe).with(unsubscribe, false) 66 | 67 | engine.should_not_receive(:publish).with(handshake) 68 | engine.should_not_receive(:publish).with(connect) 69 | engine.should_not_receive(:publish).with(disconnect) 70 | engine.should_not_receive(:publish).with(subscribe) 71 | engine.should_not_receive(:publish).with(unsubscribe) 72 | engine.should_receive(:publish).with(publish) 73 | 74 | server.process([handshake, connect, disconnect, subscribe, unsubscribe, publish], false) 75 | end 76 | 77 | describe "handshaking" do 78 | before do 79 | response = { "channel" => "/meta/handshake", "successful" => true } 80 | server.should_receive(:handshake).with(handshake, false).and_yield(response) 81 | end 82 | 83 | it "returns the handshake response with advice" do 84 | server.process(handshake, false) do |response| 85 | response.should == [ 86 | { "channel" => "/meta/handshake", 87 | "successful" => true, 88 | "advice" => { "reconnect" => "retry", "interval" => 0, "timeout" => 60000 } 89 | } 90 | ] 91 | end 92 | end 93 | end 94 | 95 | describe "connecting for messages" do 96 | let(:messages) { [{ "channel" => "/a" }, { "channel" => "/b" }] } 97 | 98 | before do 99 | server.should_receive(:connect).with(connect, false).and_yield(messages) 100 | end 101 | 102 | it "returns the new messages" do 103 | server.process(connect, false) { |r| r.should == messages } 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rack/proxy' 4 | require 'rack/test' 5 | require 'rspec/em' 6 | 7 | require 'puma' 8 | require 'puma/binder' 9 | require 'puma/events' 10 | 11 | require File.expand_path('../../lib/faye', __FILE__) 12 | 13 | require 'ruby/encoding_helper' 14 | require 'ruby/server_proxy' 15 | require 'ruby/engine_examples' 16 | -------------------------------------------------------------------------------- /src/adapters/content_types.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | TYPE_JSON: { 'Content-Type': 'application/json; charset=utf-8' }, 3 | TYPE_SCRIPT: { 'Content-Type': 'text/javascript; charset=utf-8' }, 4 | TYPE_TEXT: { 'Content-Type': 'text/plain; charset=utf-8' } 5 | }; 6 | -------------------------------------------------------------------------------- /src/adapters/static_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'), 4 | fs = require('fs'), 5 | path = require('path'), 6 | url = require('url'); 7 | 8 | var Class = require('../util/class'), 9 | assign = require('../util/assign'), 10 | contenttypes = require('./content_types'); 11 | 12 | var StaticServer = Class({ 13 | initialize: function(directory, pathRegex) { 14 | this._directory = directory; 15 | this._pathRegex = pathRegex; 16 | this._pathMap = {}; 17 | this._index = {}; 18 | }, 19 | 20 | map: function(requestPath, filename) { 21 | this._pathMap[requestPath] = filename; 22 | }, 23 | 24 | test: function(pathname) { 25 | return this._pathRegex.test(pathname); 26 | }, 27 | 28 | call: function(request, response) { 29 | var pathname = url.parse(request.url, true).pathname, 30 | filename = path.basename(pathname); 31 | 32 | filename = this._pathMap[filename] || filename; 33 | this._index[filename] = this._index[filename] || {}; 34 | 35 | var cache = this._index[filename], 36 | fullpath = path.join(this._directory, filename); 37 | 38 | try { 39 | cache.content = cache.content || fs.readFileSync(fullpath); 40 | cache.digest = cache.digest || crypto.createHash('sha1').update(cache.content).digest('hex'); 41 | cache.mtime = cache.mtime || fs.statSync(fullpath).mtime; 42 | } catch (error) { 43 | response.writeHead(404, {}); 44 | return response.end(); 45 | } 46 | 47 | var type = /\.js$/.test(pathname) ? 'TYPE_SCRIPT' : 'TYPE_JSON', 48 | ims = request.headers['if-modified-since']; 49 | 50 | var headers = { 51 | 'ETag': cache.digest, 52 | 'Last-Modified': cache.mtime.toGMTString() 53 | }; 54 | 55 | if (request.headers['if-none-match'] === cache.digest) { 56 | response.writeHead(304, headers); 57 | response.end(); 58 | } 59 | else if (ims && cache.mtime <= new Date(ims)) { 60 | response.writeHead(304, headers); 61 | response.end(); 62 | } 63 | else { 64 | headers['Content-Length'] = cache.content.length; 65 | assign(headers, contenttypes[type]); 66 | response.writeHead(200, headers); 67 | response.end(cache.content); 68 | } 69 | } 70 | }); 71 | 72 | module.exports = StaticServer; 73 | -------------------------------------------------------------------------------- /src/engines/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('../util/class'), 4 | assign = require('../util/assign'), 5 | Deferrable = require('../mixins/deferrable'), 6 | Timeouts = require('../mixins/timeouts'); 7 | 8 | var Connection = Class({ 9 | initialize: function(engine, id, options) { 10 | this._engine = engine; 11 | this._id = id; 12 | this._options = options; 13 | this._inbox = []; 14 | }, 15 | 16 | deliver: function(message) { 17 | delete message.clientId; 18 | if (this.socket) return this.socket.send(message); 19 | this._inbox.push(message); 20 | this._beginDeliveryTimeout(); 21 | }, 22 | 23 | connect: function(options, callback, context) { 24 | options = options || {}; 25 | var timeout = (options.timeout !== undefined) ? options.timeout / 1000 : this._engine.timeout; 26 | 27 | this.setDeferredStatus('unknown'); 28 | this.callback(callback, context); 29 | 30 | this._beginDeliveryTimeout(); 31 | this._beginConnectionTimeout(timeout); 32 | }, 33 | 34 | flush: function() { 35 | this.removeTimeout('connection'); 36 | this.removeTimeout('delivery'); 37 | 38 | this.setDeferredStatus('succeeded', this._inbox); 39 | this._inbox = []; 40 | 41 | if (!this.socket) this._engine.closeConnection(this._id); 42 | }, 43 | 44 | _beginDeliveryTimeout: function() { 45 | if (this._inbox.length === 0) return; 46 | this.addTimeout('delivery', this._engine.MAX_DELAY, this.flush, this); 47 | }, 48 | 49 | _beginConnectionTimeout: function(timeout) { 50 | this.addTimeout('connection', timeout, this.flush, this); 51 | } 52 | }); 53 | 54 | assign(Connection.prototype, Deferrable); 55 | assign(Connection.prototype, Timeouts); 56 | 57 | module.exports = Connection; 58 | -------------------------------------------------------------------------------- /src/faye_browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('./util/constants'), 4 | Logging = require('./mixins/logging'); 5 | 6 | var Faye = { 7 | VERSION: constants.VERSION, 8 | 9 | Client: require('./protocol/client'), 10 | Scheduler: require('./protocol/scheduler') 11 | }; 12 | 13 | Logging.wrapper = Faye; 14 | 15 | module.exports = Faye; 16 | -------------------------------------------------------------------------------- /src/faye_node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('./util/constants'), 4 | Logging = require('./mixins/logging'); 5 | 6 | var Faye = { 7 | VERSION: constants.VERSION, 8 | 9 | Client: require('./protocol/client'), 10 | Scheduler: require('./protocol/scheduler'), 11 | NodeAdapter: require('./adapters/node_adapter') 12 | }; 13 | 14 | Logging.wrapper = Faye; 15 | 16 | module.exports = Faye; 17 | -------------------------------------------------------------------------------- /src/mixins/deferrable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = require('../util/promise'); 4 | 5 | module.exports = { 6 | then: function(callback, errback) { 7 | var self = this; 8 | if (!this._promise) 9 | this._promise = new Promise(function(resolve, reject) { 10 | self._resolve = resolve; 11 | self._reject = reject; 12 | }); 13 | 14 | if (arguments.length === 0) 15 | return this._promise; 16 | else 17 | return this._promise.then(callback, errback); 18 | }, 19 | 20 | callback: function(callback, context) { 21 | return this.then(function(value) { callback.call(context, value) }); 22 | }, 23 | 24 | errback: function(callback, context) { 25 | return this.then(null, function(reason) { callback.call(context, reason) }); 26 | }, 27 | 28 | timeout: function(seconds, message) { 29 | this.then(); 30 | var self = this; 31 | this._timer = global.setTimeout(function() { 32 | self._reject(message); 33 | }, seconds * 1000); 34 | }, 35 | 36 | setDeferredStatus: function(status, value) { 37 | if (this._timer) global.clearTimeout(this._timer); 38 | 39 | this.then(); 40 | 41 | if (status === 'succeeded') 42 | this._resolve(value); 43 | else if (status === 'failed') 44 | this._reject(value); 45 | else 46 | delete this._promise; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/mixins/logging.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var toJSON = require('../util/to_json'); 4 | 5 | var Logging = { 6 | LOG_LEVELS: { 7 | fatal: 4, 8 | error: 3, 9 | warn: 2, 10 | info: 1, 11 | debug: 0 12 | }, 13 | 14 | writeLog: function(messageArgs, level) { 15 | var logger = Logging.logger || (Logging.wrapper || Logging).logger; 16 | if (!logger) return; 17 | 18 | var args = Array.prototype.slice.apply(messageArgs), 19 | banner = '[Faye', 20 | klass = this.className, 21 | 22 | message = args.shift().replace(/\?/g, function() { 23 | try { 24 | return toJSON(args.shift()); 25 | } catch (error) { 26 | return '[Object]'; 27 | } 28 | }); 29 | 30 | if (klass) banner += '.' + klass; 31 | banner += '] '; 32 | 33 | if (typeof logger[level] === 'function') 34 | logger[level](banner + message); 35 | else if (typeof logger === 'function') 36 | logger(banner + message); 37 | } 38 | }; 39 | 40 | for (var key in Logging.LOG_LEVELS) 41 | (function(level) { 42 | Logging[level] = function() { 43 | this.writeLog(arguments, level); 44 | }; 45 | })(key); 46 | 47 | module.exports = Logging; 48 | -------------------------------------------------------------------------------- /src/mixins/publisher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assign = require('../util/assign'), 4 | EventEmitter = require('../util/event_emitter'); 5 | 6 | var Publisher = { 7 | countListeners: function(eventType) { 8 | return this.listeners(eventType).length; 9 | }, 10 | 11 | bind: function(eventType, listener, context) { 12 | var slice = Array.prototype.slice, 13 | handler = function() { listener.apply(context, slice.call(arguments)) }; 14 | 15 | this._listeners = this._listeners || []; 16 | this._listeners.push([eventType, listener, context, handler]); 17 | return this.on(eventType, handler); 18 | }, 19 | 20 | unbind: function(eventType, listener, context) { 21 | this._listeners = this._listeners || []; 22 | var n = this._listeners.length, tuple; 23 | 24 | while (n--) { 25 | tuple = this._listeners[n]; 26 | if (tuple[0] !== eventType) continue; 27 | if (listener && (tuple[1] !== listener || tuple[2] !== context)) continue; 28 | this._listeners.splice(n, 1); 29 | this.removeListener(eventType, tuple[3]); 30 | } 31 | } 32 | }; 33 | 34 | assign(Publisher, EventEmitter.prototype); 35 | Publisher.trigger = Publisher.emit; 36 | 37 | module.exports = Publisher; 38 | -------------------------------------------------------------------------------- /src/mixins/timeouts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | addTimeout: function(name, delay, callback, context) { 5 | this._timeouts = this._timeouts || {}; 6 | if (this._timeouts.hasOwnProperty(name)) return; 7 | var self = this; 8 | this._timeouts[name] = global.setTimeout(function() { 9 | delete self._timeouts[name]; 10 | callback.call(context); 11 | }, 1000 * delay); 12 | }, 13 | 14 | removeTimeout: function(name) { 15 | this._timeouts = this._timeouts || {}; 16 | var timeout = this._timeouts[name]; 17 | if (!timeout) return; 18 | global.clearTimeout(timeout); 19 | delete this._timeouts[name]; 20 | }, 21 | 22 | removeAllTimeouts: function() { 23 | this._timeouts = this._timeouts || {}; 24 | for (var name in this._timeouts) this.removeTimeout(name); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/protocol/channel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('../util/class'), 4 | assign = require('../util/assign'), 5 | Publisher = require('../mixins/publisher'), 6 | Grammar = require('./grammar'); 7 | 8 | var Channel = Class({ 9 | initialize: function(name) { 10 | this.id = this.name = name; 11 | }, 12 | 13 | push: function(message) { 14 | this.trigger('message', message); 15 | }, 16 | 17 | isUnused: function() { 18 | return this.countListeners('message') === 0; 19 | } 20 | }); 21 | 22 | assign(Channel.prototype, Publisher); 23 | 24 | assign(Channel, { 25 | HANDSHAKE: '/meta/handshake', 26 | CONNECT: '/meta/connect', 27 | SUBSCRIBE: '/meta/subscribe', 28 | UNSUBSCRIBE: '/meta/unsubscribe', 29 | DISCONNECT: '/meta/disconnect', 30 | 31 | META: 'meta', 32 | SERVICE: 'service', 33 | 34 | expand: function(name) { 35 | var segments = this.parse(name), 36 | channels = ['/**', name]; 37 | 38 | var copy = segments.slice(); 39 | copy[copy.length - 1] = '*'; 40 | channels.push(this.unparse(copy)); 41 | 42 | for (var i = 1, n = segments.length; i < n; i++) { 43 | copy = segments.slice(0, i); 44 | copy.push('**'); 45 | channels.push(this.unparse(copy)); 46 | } 47 | 48 | return channels; 49 | }, 50 | 51 | isValid: function(name) { 52 | return Grammar.CHANNEL_NAME.test(name) || 53 | Grammar.CHANNEL_PATTERN.test(name); 54 | }, 55 | 56 | parse: function(name) { 57 | if (!this.isValid(name)) return null; 58 | return name.split('/').slice(1); 59 | }, 60 | 61 | unparse: function(segments) { 62 | return '/' + segments.join('/'); 63 | }, 64 | 65 | isMeta: function(name) { 66 | var segments = this.parse(name); 67 | return segments ? (segments[0] === this.META) : null; 68 | }, 69 | 70 | isService: function(name) { 71 | var segments = this.parse(name); 72 | return segments ? (segments[0] === this.SERVICE) : null; 73 | }, 74 | 75 | isSubscribable: function(name) { 76 | if (!this.isValid(name)) return null; 77 | return !this.isMeta(name) && !this.isService(name); 78 | }, 79 | 80 | Set: Class({ 81 | initialize: function() { 82 | this._channels = {}; 83 | }, 84 | 85 | getKeys: function() { 86 | var keys = []; 87 | for (var key in this._channels) keys.push(key); 88 | return keys; 89 | }, 90 | 91 | remove: function(name) { 92 | delete this._channels[name]; 93 | }, 94 | 95 | hasSubscription: function(name) { 96 | return this._channels.hasOwnProperty(name); 97 | }, 98 | 99 | subscribe: function(names, subscription) { 100 | var name; 101 | for (var i = 0, n = names.length; i < n; i++) { 102 | name = names[i]; 103 | var channel = this._channels[name] = this._channels[name] || new Channel(name); 104 | channel.bind('message', subscription); 105 | } 106 | }, 107 | 108 | unsubscribe: function(name, subscription) { 109 | var channel = this._channels[name]; 110 | if (!channel) return false; 111 | channel.unbind('message', subscription); 112 | 113 | if (channel.isUnused()) { 114 | this.remove(name); 115 | return true; 116 | } else { 117 | return false; 118 | } 119 | }, 120 | 121 | distributeMessage: function(message) { 122 | var channels = Channel.expand(message.channel); 123 | 124 | for (var i = 0, n = channels.length; i < n; i++) { 125 | var channel = this._channels[channels[i]]; 126 | if (channel) channel.trigger('message', message); 127 | } 128 | } 129 | }) 130 | }); 131 | 132 | module.exports = Channel; 133 | -------------------------------------------------------------------------------- /src/protocol/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('../util/class'), 4 | Grammar = require('./grammar'); 5 | 6 | var Error = Class({ 7 | initialize: function(code, params, message) { 8 | this.code = code; 9 | this.params = Array.prototype.slice.call(params); 10 | this.message = message; 11 | }, 12 | 13 | toString: function() { 14 | return this.code + ':' + 15 | this.params.join(',') + ':' + 16 | this.message; 17 | } 18 | }); 19 | 20 | Error.parse = function(message) { 21 | message = message || ''; 22 | if (!Grammar.ERROR.test(message)) return new Error(null, [], message); 23 | 24 | var parts = message.split(':'), 25 | code = parseInt(parts[0]), 26 | params = parts[1].split(','), 27 | message = parts[2]; 28 | 29 | return new Error(code, params, message); 30 | }; 31 | 32 | // http://code.google.com/p/cometd/wiki/BayeuxCodes 33 | var errors = { 34 | versionMismatch: [300, 'Version mismatch'], 35 | conntypeMismatch: [301, 'Connection types not supported'], 36 | extMismatch: [302, 'Extension mismatch'], 37 | badRequest: [400, 'Bad request'], 38 | clientUnknown: [401, 'Unknown client'], 39 | parameterMissing: [402, 'Missing required parameter'], 40 | channelForbidden: [403, 'Forbidden channel'], 41 | channelUnknown: [404, 'Unknown channel'], 42 | channelInvalid: [405, 'Invalid channel'], 43 | extUnknown: [406, 'Unknown extension'], 44 | publishFailed: [407, 'Failed to publish'], 45 | serverError: [500, 'Internal server error'] 46 | }; 47 | 48 | for (var name in errors) 49 | (function(name) { 50 | Error[name] = function() { 51 | return new Error(errors[name][0], arguments, errors[name][1]).toString(); 52 | }; 53 | })(name); 54 | 55 | module.exports = Error; 56 | -------------------------------------------------------------------------------- /src/protocol/extensible.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assign = require('../util/assign'), 4 | Logging = require('../mixins/logging'); 5 | 6 | var Extensible = { 7 | addExtension: function(extension) { 8 | this._extensions = this._extensions || []; 9 | this._extensions.push(extension); 10 | if (extension.added) extension.added(this); 11 | }, 12 | 13 | removeExtension: function(extension) { 14 | if (!this._extensions) return; 15 | var i = this._extensions.length; 16 | while (i--) { 17 | if (this._extensions[i] !== extension) continue; 18 | this._extensions.splice(i,1); 19 | if (extension.removed) extension.removed(this); 20 | } 21 | }, 22 | 23 | pipeThroughExtensions: function(stage, message, request, callback, context) { 24 | this.debug('Passing through ? extensions: ?', stage, message); 25 | 26 | if (!this._extensions) return callback.call(context, message); 27 | var extensions = this._extensions.slice(); 28 | 29 | var pipe = function(message) { 30 | if (!message) return callback.call(context, message); 31 | 32 | var extension = extensions.shift(); 33 | if (!extension) return callback.call(context, message); 34 | 35 | var fn = extension[stage]; 36 | if (!fn) return pipe(message); 37 | 38 | if (fn.length >= 3) extension[stage](message, request, pipe); 39 | else extension[stage](message, pipe); 40 | }; 41 | pipe(message); 42 | } 43 | }; 44 | 45 | assign(Extensible, Logging); 46 | 47 | module.exports = Extensible; 48 | -------------------------------------------------------------------------------- /src/protocol/grammar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | CHANNEL_NAME: /^\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+(\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+)*$/, 5 | CHANNEL_PATTERN: /^(\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+)*\/\*{1,2}$/, 6 | ERROR: /^([0-9][0-9][0-9]:(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*(,(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*)*:(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*|[0-9][0-9][0-9]::(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*)$/, 7 | VERSION: /^([0-9])+(\.(([a-z]|[A-Z])|[0-9])(((([a-z]|[A-Z])|[0-9])|\-|\_))*)*$/ 8 | }; 9 | -------------------------------------------------------------------------------- /src/protocol/publication.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('../util/class'), 4 | Deferrable = require('../mixins/deferrable'); 5 | 6 | module.exports = Class(Deferrable); 7 | -------------------------------------------------------------------------------- /src/protocol/scheduler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assign = require('../util/assign'); 4 | 5 | var Scheduler = function(message, options) { 6 | this.message = message; 7 | this.options = options; 8 | this.attempts = 0; 9 | }; 10 | 11 | assign(Scheduler.prototype, { 12 | getTimeout: function() { 13 | return this.options.timeout; 14 | }, 15 | 16 | getInterval: function() { 17 | return this.options.interval; 18 | }, 19 | 20 | isDeliverable: function() { 21 | var attempts = this.options.attempts, 22 | made = this.attempts, 23 | deadline = this.options.deadline, 24 | now = new Date().getTime(); 25 | 26 | if (attempts !== undefined && made >= attempts) 27 | return false; 28 | 29 | if (deadline !== undefined && now > deadline) 30 | return false; 31 | 32 | return true; 33 | }, 34 | 35 | send: function() { 36 | this.attempts += 1; 37 | }, 38 | 39 | succeed: function() {}, 40 | 41 | fail: function() {}, 42 | 43 | abort: function() {} 44 | }); 45 | 46 | module.exports = Scheduler; 47 | -------------------------------------------------------------------------------- /src/protocol/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('../util/class'), 4 | toJSON = require('../util/to_json'); 5 | 6 | module.exports = Class({ 7 | initialize: function(server, socket, request) { 8 | this._server = server; 9 | this._socket = socket; 10 | this._request = request; 11 | }, 12 | 13 | send: function(message) { 14 | this._server.pipeThroughExtensions('outgoing', message, this._request, function(pipedMessage) { 15 | if (this._socket) 16 | this._socket.send(toJSON([pipedMessage])); 17 | }, this); 18 | }, 19 | 20 | close: function() { 21 | if (this._socket) this._socket.close(); 22 | delete this._socket; 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/protocol/subscription.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('../util/class'), 4 | assign = require('../util/assign'), 5 | Deferrable = require('../mixins/deferrable'); 6 | 7 | var Subscription = Class({ 8 | initialize: function(client, channels, callback, context) { 9 | this._client = client; 10 | this._channels = channels; 11 | this._callback = callback; 12 | this._context = context; 13 | this._cancelled = false; 14 | }, 15 | 16 | withChannel: function(callback, context) { 17 | this._withChannel = [callback, context]; 18 | return this; 19 | }, 20 | 21 | apply: function(context, args) { 22 | var message = args[0]; 23 | 24 | if (this._callback) 25 | this._callback.call(this._context, message.data); 26 | 27 | if (this._withChannel) 28 | this._withChannel[0].call(this._withChannel[1], message.channel, message.data); 29 | }, 30 | 31 | cancel: function() { 32 | if (this._cancelled) return; 33 | this._client.unsubscribe(this._channels, this); 34 | this._cancelled = true; 35 | }, 36 | 37 | unsubscribe: function() { 38 | this.cancel(); 39 | } 40 | }); 41 | 42 | assign(Subscription.prototype, Deferrable); 43 | 44 | module.exports = Subscription; 45 | -------------------------------------------------------------------------------- /src/transport/browser_transports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Transport = require('./transport'); 4 | 5 | Transport.register('websocket', require('./web_socket')); 6 | Transport.register('eventsource', require('./event_source')); 7 | Transport.register('long-polling', require('./xhr')); 8 | Transport.register('cross-origin-long-polling', require('./cors')); 9 | Transport.register('callback-polling', require('./jsonp')); 10 | 11 | module.exports = Transport; 12 | -------------------------------------------------------------------------------- /src/transport/cors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('../util/class'), 4 | Set = require('../util/set'), 5 | URI = require('../util/uri'), 6 | assign = require('../util/assign'), 7 | toJSON = require('../util/to_json'), 8 | Transport = require('./transport'); 9 | 10 | var CORS = assign(Class(Transport, { 11 | encode: function(messages) { 12 | return 'message=' + encodeURIComponent(toJSON(messages)); 13 | }, 14 | 15 | request: function(messages) { 16 | var xhrClass = global.XDomainRequest ? XDomainRequest : XMLHttpRequest, 17 | xhr = new xhrClass(), 18 | id = ++CORS._id, 19 | headers = this._dispatcher.headers, 20 | self = this, 21 | key; 22 | 23 | xhr.open('POST', this.endpoint.href, true); 24 | xhr.withCredentials = true; 25 | 26 | if (xhr.setRequestHeader) { 27 | xhr.setRequestHeader('Pragma', 'no-cache'); 28 | for (key in headers) { 29 | if (!headers.hasOwnProperty(key)) continue; 30 | xhr.setRequestHeader(key, headers[key]); 31 | } 32 | } 33 | 34 | var cleanUp = function() { 35 | if (!xhr) return false; 36 | CORS._pending.remove(id); 37 | xhr.onload = xhr.onerror = xhr.ontimeout = xhr.onprogress = null; 38 | xhr = null; 39 | }; 40 | 41 | xhr.onload = function() { 42 | var replies; 43 | try { replies = JSON.parse(xhr.responseText) } catch (error) {} 44 | 45 | cleanUp(); 46 | 47 | if (replies) 48 | self._receive(replies); 49 | else 50 | self._handleError(messages); 51 | }; 52 | 53 | xhr.onerror = xhr.ontimeout = function() { 54 | cleanUp(); 55 | self._handleError(messages); 56 | }; 57 | 58 | xhr.onprogress = function() {}; 59 | 60 | if (xhrClass === global.XDomainRequest) 61 | CORS._pending.add({ id: id, xhr: xhr }); 62 | 63 | xhr.send(this.encode(messages)); 64 | return xhr; 65 | } 66 | }), { 67 | _id: 0, 68 | _pending: new Set(), 69 | 70 | isUsable: function(dispatcher, endpoint, callback, context) { 71 | if (URI.isSameOrigin(endpoint)) 72 | return callback.call(context, false); 73 | 74 | if (global.XDomainRequest) 75 | return callback.call(context, endpoint.protocol === location.protocol); 76 | 77 | if (global.XMLHttpRequest) { 78 | var xhr = new XMLHttpRequest(); 79 | return callback.call(context, xhr.withCredentials !== undefined); 80 | } 81 | return callback.call(context, false); 82 | } 83 | }); 84 | 85 | module.exports = CORS; 86 | -------------------------------------------------------------------------------- /src/transport/event_source.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('../util/class'), 4 | URI = require('../util/uri'), 5 | copyObject = require('../util/copy_object'), 6 | assign = require('../util/assign'), 7 | Deferrable = require('../mixins/deferrable'), 8 | Transport = require('./transport'), 9 | XHR = require('./xhr'); 10 | 11 | var EventSource = assign(Class(Transport, { 12 | initialize: function(dispatcher, endpoint) { 13 | Transport.prototype.initialize.call(this, dispatcher, endpoint); 14 | if (!global.EventSource) return this.setDeferredStatus('failed'); 15 | 16 | this._xhr = new XHR(dispatcher, endpoint); 17 | 18 | endpoint = copyObject(endpoint); 19 | endpoint.pathname += '/' + dispatcher.clientId; 20 | 21 | var socket = new global.EventSource(URI.stringify(endpoint)), 22 | self = this; 23 | 24 | socket.onopen = function() { 25 | self._everConnected = true; 26 | self.setDeferredStatus('succeeded'); 27 | }; 28 | 29 | socket.onerror = function() { 30 | if (self._everConnected) { 31 | self._handleError([]); 32 | } else { 33 | self.setDeferredStatus('failed'); 34 | socket.close(); 35 | } 36 | }; 37 | 38 | socket.onmessage = function(event) { 39 | var replies; 40 | try { replies = JSON.parse(event.data) } catch (error) {} 41 | 42 | if (replies) 43 | self._receive(replies); 44 | else 45 | self._handleError([]); 46 | }; 47 | 48 | this._socket = socket; 49 | }, 50 | 51 | close: function() { 52 | if (!this._socket) return; 53 | this._socket.onopen = this._socket.onerror = this._socket.onmessage = null; 54 | this._socket.close(); 55 | delete this._socket; 56 | }, 57 | 58 | isUsable: function(callback, context) { 59 | this.callback(function() { callback.call(context, true) }); 60 | this.errback(function() { callback.call(context, false) }); 61 | }, 62 | 63 | encode: function(messages) { 64 | return this._xhr.encode(messages); 65 | }, 66 | 67 | request: function(messages) { 68 | return this._xhr.request(messages); 69 | } 70 | 71 | }), { 72 | isUsable: function(dispatcher, endpoint, callback, context) { 73 | var id = dispatcher.clientId; 74 | if (!id) return callback.call(context, false); 75 | 76 | XHR.isUsable(dispatcher, endpoint, function(usable) { 77 | if (!usable) return callback.call(context, false); 78 | this.create(dispatcher, endpoint).isUsable(callback, context); 79 | }, this); 80 | }, 81 | 82 | create: function(dispatcher, endpoint) { 83 | var sockets = dispatcher.transports.eventsource = dispatcher.transports.eventsource || {}, 84 | id = dispatcher.clientId; 85 | 86 | var url = copyObject(endpoint); 87 | url.pathname += '/' + (id || ''); 88 | url = URI.stringify(url); 89 | 90 | sockets[url] = sockets[url] || new this(dispatcher, endpoint); 91 | return sockets[url]; 92 | } 93 | }); 94 | 95 | assign(EventSource.prototype, Deferrable); 96 | 97 | module.exports = EventSource; 98 | -------------------------------------------------------------------------------- /src/transport/jsonp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('../util/class'), 4 | URI = require('../util/uri'), 5 | copyObject = require('../util/copy_object'), 6 | assign = require('../util/assign'), 7 | toJSON = require('../util/to_json'), 8 | Transport = require('./transport'); 9 | 10 | var JSONP = assign(Class(Transport, { 11 | encode: function(messages) { 12 | var url = copyObject(this.endpoint); 13 | url.query.message = toJSON(messages); 14 | url.query.jsonp = '__jsonp' + JSONP._cbCount + '__'; 15 | return URI.stringify(url); 16 | }, 17 | 18 | request: function(messages) { 19 | var head = document.getElementsByTagName('head')[0], 20 | script = document.createElement('script'), 21 | callbackName = JSONP.getCallbackName(), 22 | endpoint = copyObject(this.endpoint), 23 | self = this; 24 | 25 | endpoint.query.message = toJSON(messages); 26 | endpoint.query.jsonp = callbackName; 27 | 28 | var cleanup = function() { 29 | if (!global[callbackName]) return false; 30 | global[callbackName] = undefined; 31 | try { delete global[callbackName] } catch (error) {} 32 | script.parentNode.removeChild(script); 33 | }; 34 | 35 | global[callbackName] = function(replies) { 36 | cleanup(); 37 | self._receive(replies); 38 | }; 39 | 40 | script.type = 'text/javascript'; 41 | script.src = URI.stringify(endpoint); 42 | head.appendChild(script); 43 | 44 | script.onerror = function() { 45 | cleanup(); 46 | self._handleError(messages); 47 | }; 48 | 49 | return { abort: cleanup }; 50 | } 51 | }), { 52 | _cbCount: 0, 53 | 54 | getCallbackName: function() { 55 | this._cbCount += 1; 56 | return '__jsonp' + this._cbCount + '__'; 57 | }, 58 | 59 | isUsable: function(dispatcher, endpoint, callback, context) { 60 | callback.call(context, true); 61 | } 62 | }); 63 | 64 | module.exports = JSONP; 65 | -------------------------------------------------------------------------------- /src/transport/node_local.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var asap = require('asap'), 4 | Class = require('../util/class'), 5 | URI = require('../util/uri'), 6 | copyObject = require('../util/copy_object'), 7 | assign = require('../util/assign'), 8 | Server = require('../protocol/server'), 9 | Transport = require('./transport'); 10 | 11 | var NodeLocal = assign(Class(Transport, { 12 | batching: false, 13 | 14 | request: function(messages) { 15 | messages = copyObject(messages); 16 | var self = this; 17 | 18 | asap(function() { 19 | self.endpoint.process(messages, null, function(replies) { 20 | self._receive(copyObject(replies)); 21 | }); 22 | }); 23 | } 24 | }), { 25 | isUsable: function(client, endpoint, callback, context) { 26 | callback.call(context, endpoint instanceof Server); 27 | } 28 | }); 29 | 30 | module.exports = NodeLocal; 31 | -------------------------------------------------------------------------------- /src/transport/node_transports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Transport = require('./transport'); 4 | 5 | Transport.register('in-process', require('./node_local')); 6 | Transport.register('websocket', require('./web_socket')); 7 | Transport.register('long-polling', require('./node_http')); 8 | 9 | module.exports = Transport; 10 | -------------------------------------------------------------------------------- /src/transport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_transports", 3 | "browser": "browser_transports" 4 | } 5 | -------------------------------------------------------------------------------- /src/transport/xhr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('../util/class'), 4 | URI = require('../util/uri'), 5 | browser = require('../util/browser'), 6 | assign = require('../util/assign'), 7 | toJSON = require('../util/to_json'), 8 | Transport = require('./transport'); 9 | 10 | var XHR = assign(Class(Transport, { 11 | encode: function(messages) { 12 | return toJSON(messages); 13 | }, 14 | 15 | request: function(messages) { 16 | var href = this.endpoint.href, 17 | self = this, 18 | xhr; 19 | 20 | // Prefer XMLHttpRequest over ActiveXObject if they both exist 21 | if (global.XMLHttpRequest) { 22 | xhr = new XMLHttpRequest(); 23 | } else if (global.ActiveXObject) { 24 | xhr = new ActiveXObject('Microsoft.XMLHTTP'); 25 | } else { 26 | return this._handleError(messages); 27 | } 28 | 29 | xhr.open('POST', href, true); 30 | xhr.setRequestHeader('Content-Type', 'application/json'); 31 | xhr.setRequestHeader('Pragma', 'no-cache'); 32 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 33 | 34 | var headers = this._dispatcher.headers; 35 | for (var key in headers) { 36 | if (!headers.hasOwnProperty(key)) continue; 37 | xhr.setRequestHeader(key, headers[key]); 38 | } 39 | 40 | var abort = function() { xhr.abort() }; 41 | if (global.onbeforeunload !== undefined) 42 | browser.Event.on(global, 'beforeunload', abort); 43 | 44 | xhr.onreadystatechange = function() { 45 | if (!xhr || xhr.readyState !== 4) return; 46 | 47 | var replies = null, 48 | status = xhr.status, 49 | text = xhr.responseText, 50 | successful = (status >= 200 && status < 300) || status === 304 || status === 1223; 51 | 52 | if (global.onbeforeunload !== undefined) 53 | browser.Event.detach(global, 'beforeunload', abort); 54 | 55 | xhr.onreadystatechange = function() {}; 56 | xhr = null; 57 | 58 | if (!successful) return self._handleError(messages); 59 | 60 | try { 61 | replies = JSON.parse(text); 62 | } catch (error) {} 63 | 64 | if (replies) 65 | self._receive(replies); 66 | else 67 | self._handleError(messages); 68 | }; 69 | 70 | xhr.send(this.encode(messages)); 71 | return xhr; 72 | } 73 | }), { 74 | isUsable: function(dispatcher, endpoint, callback, context) { 75 | var usable = (navigator.product === 'ReactNative') 76 | || URI.isSameOrigin(endpoint); 77 | 78 | callback.call(context, usable); 79 | } 80 | }); 81 | 82 | module.exports = XHR; 83 | -------------------------------------------------------------------------------- /src/util/array.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | commonElement: function(lista, listb) { 5 | for (var i = 0, n = lista.length; i < n; i++) { 6 | if (this.indexOf(listb, lista[i]) !== -1) 7 | return lista[i]; 8 | } 9 | return null; 10 | }, 11 | 12 | indexOf: function(list, needle) { 13 | if (list.indexOf) return list.indexOf(needle); 14 | 15 | for (var i = 0, n = list.length; i < n; i++) { 16 | if (list[i] === needle) return i; 17 | } 18 | return -1; 19 | }, 20 | 21 | map: function(object, callback, context) { 22 | if (object.map) return object.map(callback, context); 23 | var result = []; 24 | 25 | if (object instanceof Array) { 26 | for (var i = 0, n = object.length; i < n; i++) { 27 | result.push(callback.call(context || null, object[i], i)); 28 | } 29 | } else { 30 | for (var key in object) { 31 | if (!object.hasOwnProperty(key)) continue; 32 | result.push(callback.call(context || null, key, object[key])); 33 | } 34 | } 35 | return result; 36 | }, 37 | 38 | filter: function(array, callback, context) { 39 | if (array.filter) return array.filter(callback, context); 40 | var result = []; 41 | for (var i = 0, n = array.length; i < n; i++) { 42 | if (callback.call(context || null, array[i], i)) 43 | result.push(array[i]); 44 | } 45 | return result; 46 | }, 47 | 48 | asyncEach: function(list, iterator, callback, context) { 49 | var n = list.length, 50 | i = -1, 51 | calls = 0, 52 | looping = false; 53 | 54 | var iterate = function() { 55 | calls -= 1; 56 | i += 1; 57 | if (i === n) return callback && callback.call(context); 58 | iterator(list[i], resume); 59 | }; 60 | 61 | var loop = function() { 62 | if (looping) return; 63 | looping = true; 64 | while (calls > 0) iterate(); 65 | looping = false; 66 | }; 67 | 68 | var resume = function() { 69 | calls += 1; 70 | loop(); 71 | }; 72 | resume(); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/util/assign.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var forEach = Array.prototype.forEach, 4 | hasOwn = Object.prototype.hasOwnProperty; 5 | 6 | module.exports = function(target) { 7 | forEach.call(arguments, function(source, i) { 8 | if (i === 0) return; 9 | 10 | for (var key in source) { 11 | if (hasOwn.call(source, key)) target[key] = source[key]; 12 | } 13 | }); 14 | 15 | return target; 16 | }; 17 | -------------------------------------------------------------------------------- /src/util/browser/event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Event = { 4 | _registry: [], 5 | 6 | on: function(element, eventName, callback, context) { 7 | var wrapped = function() { callback.call(context) }; 8 | 9 | if (element.addEventListener) 10 | element.addEventListener(eventName, wrapped, false); 11 | else 12 | element.attachEvent('on' + eventName, wrapped); 13 | 14 | this._registry.push({ 15 | _element: element, 16 | _type: eventName, 17 | _callback: callback, 18 | _context: context, 19 | _handler: wrapped 20 | }); 21 | }, 22 | 23 | detach: function(element, eventName, callback, context) { 24 | var i = this._registry.length, register; 25 | while (i--) { 26 | register = this._registry[i]; 27 | 28 | if ((element && element !== register._element) || 29 | (eventName && eventName !== register._type) || 30 | (callback && callback !== register._callback) || 31 | (context && context !== register._context)) 32 | continue; 33 | 34 | if (register._element.removeEventListener) 35 | register._element.removeEventListener(register._type, register._handler, false); 36 | else 37 | register._element.detachEvent('on' + register._type, register._handler); 38 | 39 | this._registry.splice(i,1); 40 | register = null; 41 | } 42 | } 43 | }; 44 | 45 | module.exports = { 46 | Event: Event 47 | }; 48 | -------------------------------------------------------------------------------- /src/util/browser/node_shim.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /src/util/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_shim", 3 | "browser": "event" 4 | } 5 | -------------------------------------------------------------------------------- /src/util/class.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assign = require('./assign'); 4 | 5 | module.exports = function(parent, methods) { 6 | if (typeof parent !== 'function') { 7 | methods = parent; 8 | parent = Object; 9 | } 10 | 11 | var klass = function() { 12 | if (!this.initialize) return this; 13 | return this.initialize.apply(this, arguments) || this; 14 | }; 15 | 16 | var bridge = function() {}; 17 | bridge.prototype = parent.prototype; 18 | 19 | klass.prototype = new bridge(); 20 | assign(klass.prototype, methods); 21 | 22 | return klass; 23 | }; 24 | -------------------------------------------------------------------------------- /src/util/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | VERSION: '1.4.1', 3 | 4 | BAYEUX_VERSION: '1.0', 5 | ID_LENGTH: 160, 6 | JSONP_CALLBACK: 'jsonpcallback', 7 | CONNECTION_TYPES: ['long-polling', 'cross-origin-long-polling', 'callback-polling', 'websocket', 'eventsource', 'in-process'], 8 | 9 | MANDATORY_CONNECTION_TYPES: ['long-polling', 'callback-polling', 'in-process'] 10 | }; 11 | -------------------------------------------------------------------------------- /src/util/cookies/browser_cookies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /src/util/cookies/node_cookies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('tough-cookie'); 4 | -------------------------------------------------------------------------------- /src/util/cookies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_cookies", 3 | "browser": "browser_cookies" 4 | } 5 | -------------------------------------------------------------------------------- /src/util/copy_object.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var copyObject = function(object) { 4 | var clone, i, key; 5 | if (object instanceof Array) { 6 | clone = []; 7 | i = object.length; 8 | while (i--) clone[i] = copyObject(object[i]); 9 | return clone; 10 | } else if (typeof object === 'object') { 11 | clone = (object === null) ? null : {}; 12 | for (key in object) clone[key] = copyObject(object[key]); 13 | return clone; 14 | } else { 15 | return object; 16 | } 17 | }; 18 | 19 | module.exports = copyObject; 20 | -------------------------------------------------------------------------------- /src/util/id_from_messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var array = require('./array'); 4 | 5 | module.exports = function(messages) { 6 | var connect = array.filter([].concat(messages), function(message) { 7 | return message.channel === '/meta/connect'; 8 | }); 9 | return connect[0] && connect[0].clientId; 10 | }; 11 | -------------------------------------------------------------------------------- /src/util/namespace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('./class'), 4 | random = require('./random'); 5 | 6 | module.exports = Class({ 7 | initialize: function() { 8 | this._used = {}; 9 | }, 10 | 11 | exists: function(id) { 12 | return this._used.hasOwnProperty(id); 13 | }, 14 | 15 | generate: function() { 16 | var name = random(); 17 | while (this._used.hasOwnProperty(name)) 18 | name = random(); 19 | return this._used[name] = name; 20 | }, 21 | 22 | release: function(id) { 23 | delete this._used[id]; 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/util/random.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var csprng = require('csprng'), 4 | constants = require('./constants'); 5 | 6 | module.exports = function(bitlength) { 7 | bitlength = bitlength || constants.ID_LENGTH; 8 | var maxLength = Math.ceil(bitlength * Math.log(2) / Math.log(36)); 9 | var string = csprng(bitlength, 36); 10 | while (string.length < maxLength) string = '0' + string; 11 | return string; 12 | }; 13 | -------------------------------------------------------------------------------- /src/util/set.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Class = require('./class'); 4 | 5 | module.exports = Class({ 6 | initialize: function() { 7 | this._index = {}; 8 | }, 9 | 10 | add: function(item) { 11 | var key = (item.id !== undefined) ? item.id : item; 12 | if (this._index.hasOwnProperty(key)) return false; 13 | this._index[key] = item; 14 | return true; 15 | }, 16 | 17 | forEach: function(block, context) { 18 | for (var key in this._index) { 19 | if (this._index.hasOwnProperty(key)) 20 | block.call(context, this._index[key]); 21 | } 22 | }, 23 | 24 | isEmpty: function() { 25 | for (var key in this._index) { 26 | if (this._index.hasOwnProperty(key)) return false; 27 | } 28 | return true; 29 | }, 30 | 31 | member: function(item) { 32 | for (var key in this._index) { 33 | if (this._index[key] === item) return true; 34 | } 35 | return false; 36 | }, 37 | 38 | remove: function(item) { 39 | var key = (item.id !== undefined) ? item.id : item; 40 | var removed = this._index[key]; 41 | delete this._index[key]; 42 | return removed; 43 | }, 44 | 45 | toArray: function() { 46 | var array = []; 47 | this.forEach(function(item) { array.push(item) }); 48 | return array; 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /src/util/to_json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // http://assanka.net/content/tech/2009/09/02/json2-js-vs-prototype/ 4 | 5 | module.exports = function(object) { 6 | return JSON.stringify(object, function(key, value) { 7 | return (this[key] instanceof Array) ? this[key] : value; 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/util/uri.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | isURI: function(uri) { 5 | return uri && uri.protocol && uri.host && uri.path; 6 | }, 7 | 8 | isSameOrigin: function(uri) { 9 | return uri.protocol === location.protocol && 10 | uri.hostname === location.hostname && 11 | uri.port === location.port; 12 | }, 13 | 14 | parse: function(url) { 15 | if (typeof url !== 'string') return url; 16 | var uri = {}, parts, query, pairs, i, n, data; 17 | 18 | var consume = function(name, pattern) { 19 | url = url.replace(pattern, function(match) { 20 | uri[name] = match; 21 | return ''; 22 | }); 23 | uri[name] = uri[name] || ''; 24 | }; 25 | 26 | consume('protocol', /^[a-z]+\:/i); 27 | consume('host', /^\/\/[^\/\?#]+/); 28 | 29 | if (!/^\//.test(url) && !uri.host) 30 | url = location.pathname.replace(/[^\/]*$/, '') + url; 31 | 32 | consume('pathname', /^[^\?#]*/); 33 | consume('search', /^\?[^#]*/); 34 | consume('hash', /^#.*/); 35 | 36 | uri.protocol = uri.protocol || location.protocol; 37 | 38 | if (uri.host) { 39 | uri.host = uri.host.substr(2); 40 | 41 | if (/@/.test(uri.host)) { 42 | uri.auth = uri.host.split('@')[0]; 43 | uri.host = uri.host.split('@')[1]; 44 | } 45 | parts = uri.host.match(/^\[([^\]]+)\]|^[^:]+/); 46 | uri.hostname = parts[1] || parts[0]; 47 | uri.port = (uri.host.match(/:(\d+)$/) || [])[1] || ''; 48 | } else { 49 | uri.host = location.host; 50 | uri.hostname = location.hostname; 51 | uri.port = location.port; 52 | } 53 | 54 | uri.pathname = uri.pathname || '/'; 55 | uri.path = uri.pathname + uri.search; 56 | 57 | query = uri.search.replace(/^\?/, ''); 58 | pairs = query ? query.split('&') : []; 59 | data = {}; 60 | 61 | for (i = 0, n = pairs.length; i < n; i++) { 62 | parts = pairs[i].split('='); 63 | data[decodeURIComponent(parts[0] || '')] = decodeURIComponent(parts[1] || ''); 64 | } 65 | 66 | uri.query = data; 67 | 68 | uri.href = this.stringify(uri); 69 | return uri; 70 | }, 71 | 72 | stringify: function(uri) { 73 | var auth = uri.auth ? uri.auth + '@' : '', 74 | string = uri.protocol + '//' + auth + uri.host; 75 | 76 | string += uri.pathname + this.queryString(uri.query) + (uri.hash || ''); 77 | 78 | return string; 79 | }, 80 | 81 | queryString: function(query) { 82 | var pairs = []; 83 | for (var key in query) { 84 | if (!query.hasOwnProperty(key)) continue; 85 | pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(query[key])); 86 | } 87 | if (pairs.length === 0) return ''; 88 | return '?' + pairs.join('&'); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /src/util/validate_options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var array = require('./array'); 4 | 5 | module.exports = function(options, validKeys) { 6 | for (var key in options) { 7 | if (array.indexOf(validKeys, key) < 0) 8 | throw new Error('Unrecognized option: ' + key); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/util/websocket/browser_websocket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var WS = global.MozWebSocket || global.WebSocket; 4 | 5 | module.exports = { 6 | create: function(url, protocols, options) { 7 | if (typeof WS !== 'function') return null; 8 | return new WS(url); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/util/websocket/node_websocket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var WS = require('faye-websocket').Client; 4 | 5 | module.exports = { 6 | create: function(url, protocols, options) { 7 | return new WS(url, protocols, options); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/util/websocket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_websocket", 3 | "browser": "browser_websocket" 4 | } 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | let mode = process.env.NODE_ENV || 'development', 2 | name; 3 | 4 | if (mode === 'production') { 5 | name = 'faye-browser-min'; 6 | } else { 7 | name = 'faye-browser'; 8 | } 9 | 10 | module.exports = { 11 | mode, 12 | devtool: 'source-map', 13 | 14 | entry: { 15 | ['build/client/' + name]: '.', 16 | 'spec/browser_bundle': './spec/browser' 17 | }, 18 | 19 | output: { 20 | path: __dirname, 21 | filename: '[name].js', 22 | library: 'Faye' 23 | }, 24 | 25 | module: { 26 | rules: [ 27 | { 28 | test: /\/spec\/.*\.js$/, 29 | loader: 'imports-loader?define=>false' 30 | } 31 | ], 32 | 33 | noParse: /jstest/ 34 | }, 35 | 36 | node: { 37 | process: false 38 | } 39 | }; 40 | --------------------------------------------------------------------------------