diff options
-rw-r--r-- | README.md | 45 | ||||
-rw-r--r-- | lib/action_cable/connection/base.rb | 10 | ||||
-rw-r--r-- | lib/action_cable/connection/identification.rb | 4 | ||||
-rw-r--r-- | lib/action_cable/connection/tagged_logger_proxy.rb | 12 | ||||
-rw-r--r-- | lib/action_cable/engine.rb | 5 | ||||
-rw-r--r-- | lib/action_cable/helpers/action_cable_helper.rb | 29 | ||||
-rw-r--r-- | lib/action_cable/process/logging.rb | 2 | ||||
-rw-r--r-- | lib/action_cable/server/configuration.rb | 9 | ||||
-rw-r--r-- | lib/action_cable/server/worker/active_record_connection_management.rb | 2 | ||||
-rw-r--r-- | lib/assets/javascripts/cable.coffee.erb | 6 | ||||
-rw-r--r-- | test/connection/cross_site_forgery_test.rb | 14 | ||||
-rw-r--r-- | test/connection/multiple_identifiers_test.rb | 41 | ||||
-rw-r--r-- | test/stubs/global_id.rb | 8 | ||||
-rw-r--r-- | test/stubs/room.rb | 2 | ||||
-rw-r--r-- | test/stubs/user.rb | 6 |
15 files changed, 179 insertions, 16 deletions
@@ -274,8 +274,11 @@ See the [rails/actioncable-examples](http://github.com/rails/actioncable-example ## Configuration -The only must-configure part of Action Cable is the Redis connection. By default, `ActionCable::Server::Base` will look for a configuration -file in `Rails.root.join('config/redis/cable.yml')`. The file must follow the following format: +Action Cable has two required configurations: the Redis connection and specifying allowed request origins. + +### Redis + +By default, `ActionCable::Server::Base` will look for a configuration file in `Rails.root.join('config/redis/cable.yml')`. The file must follow the following format: ```yaml production: &production @@ -299,6 +302,24 @@ a Rails initializer with something like: ActionCable.server.config.redis_path = Rails.root('somewhere/else/cable.yml') ``` +### Allowed Request Origins + +Action Cable will only accept requests from specified origins, which are passed to the server config as an array. The origins can be instances of strings or regular expressions, against which a check for match will be performed. + +```ruby +ActionCable.server.config.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/] +``` + +To disable and allow requests from any origin: + +```ruby +ActionCable.server.config.disable_request_forgery_protection = true +``` + +By default, Action Cable allows all requests from localhost:3000 when running in the development environment. + +### Other Configurations + The other common option to configure is the log tags applied to the per-connection logger. Here's close to what we're using in Basecamp: ```ruby @@ -309,6 +330,26 @@ ActionCable.server.config.log_tags = [ ] ``` +Your websocket url might change between environments. If you host your production server via https, you will need to use the wss scheme +for your ActionCable server, but development might remain http and use the ws scheme. You might use localhost in development and your +domain in production. In any case, to vary the websocket url between environments, add the following configuration to each environment: + +```ruby +config.action_cable.url = "ws://example.com:28080" +``` + +Then add the following line to your layout before your JavaScript tag: + +```erb +<%= action_cable_meta_tag %> +``` + +And finally, create your consumer like so: + +```coffeescript +App.cable = Cable.createConsumer() +``` + For a full list of all configuration options, see the `ActionCable::Server::Configuration` class. Also note that your server must provide at least the same number of database connections as you have workers. The default worker pool is set to 100, so that means you have to make at least that available. You can change that in `config/database.yml` through the `pool` attribute. diff --git a/lib/action_cable/connection/base.rb b/lib/action_cable/connection/base.rb index 6df168e4c3..7e9eec7508 100644 --- a/lib/action_cable/connection/base.rb +++ b/lib/action_cable/connection/base.rb @@ -56,7 +56,7 @@ module ActionCable def initialize(server, env) @server, @env = server, env - @logger = new_tagged_logger || server.logger + @logger = new_tagged_logger @websocket = ActionCable::Connection::WebSocket.new(env) @subscriptions = ActionCable::Connection::Subscriptions.new(self) @@ -172,7 +172,7 @@ module ActionCable def allow_request_origin? return true if server.config.disable_request_forgery_protection - if Array(server.config.allowed_request_origins).include? env['HTTP_ORIGIN'] + if Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env['HTTP_ORIGIN'] } true else logger.error("Request origin not allowed: #{env['HTTP_ORIGIN']}") @@ -194,10 +194,8 @@ module ActionCable # Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags. def new_tagged_logger - if server.logger.respond_to?(:tagged) - TaggedLoggerProxy.new server.logger, - tags: server.config.log_tags.map { |tag| tag.respond_to?(:call) ? tag.call(request) : tag.to_s.camelize } - end + TaggedLoggerProxy.new server.logger, + tags: server.config.log_tags.map { |tag| tag.respond_to?(:call) ? tag.call(request) : tag.to_s.camelize } end def started_request_message diff --git a/lib/action_cable/connection/identification.rb b/lib/action_cable/connection/identification.rb index 431493aa70..2d75ff8d6d 100644 --- a/lib/action_cable/connection/identification.rb +++ b/lib/action_cable/connection/identification.rb @@ -34,8 +34,8 @@ module ActionCable private def connection_gid(ids) ids.map do |o| - if o.respond_to? :to_global_id - o.to_global_id + if o.respond_to? :to_gid_param + o.to_gid_param else o.to_s end diff --git a/lib/action_cable/connection/tagged_logger_proxy.rb b/lib/action_cable/connection/tagged_logger_proxy.rb index 34063c1d42..e5319087fb 100644 --- a/lib/action_cable/connection/tagged_logger_proxy.rb +++ b/lib/action_cable/connection/tagged_logger_proxy.rb @@ -16,6 +16,15 @@ module ActionCable @tags = @tags.uniq end + def tag(logger) + if logger.respond_to?(:tagged) + current_tags = tags - logger.formatter.current_tags + logger.tagged(*current_tags) { yield } + else + yield + end + end + %i( debug info warn error fatal unknown ).each do |severity| define_method(severity) do |message| log severity, message @@ -24,8 +33,7 @@ module ActionCable protected def log(type, message) - current_tags = tags - @logger.formatter.current_tags - @logger.tagged(*current_tags) { @logger.send type, message } + tag(@logger) { @logger.send type, message } end end end diff --git a/lib/action_cable/engine.rb b/lib/action_cable/engine.rb index 613a9b99f2..4777c3886b 100644 --- a/lib/action_cable/engine.rb +++ b/lib/action_cable/engine.rb @@ -1,10 +1,15 @@ require 'rails/engine' require 'active_support/ordered_options' +require 'action_cable/helpers/action_cable_helper' module ActionCable class Engine < ::Rails::Engine config.action_cable = ActiveSupport::OrderedOptions.new + config.to_prepare do + ApplicationController.helper ActionCable::Helpers::ActionCableHelper + end + initializer "action_cable.logger" do ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger } end diff --git a/lib/action_cable/helpers/action_cable_helper.rb b/lib/action_cable/helpers/action_cable_helper.rb new file mode 100644 index 0000000000..b82751468a --- /dev/null +++ b/lib/action_cable/helpers/action_cable_helper.rb @@ -0,0 +1,29 @@ +module ActionCable + module Helpers + module ActionCableHelper + # Returns an "action-cable-url" meta tag with the value of the url specified in your + # configuration. Ensure this is above your javascript tag: + # + # <head> + # <%= action_cable_meta_tag %> + # <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + # </head> + # + # This is then used by ActionCable to determine the url of your websocket server. + # Your CoffeeScript can then connect to the server without needing to specify the + # url directly: + # + # #= require cable + # @App = {} + # App.cable = Cable.createConsumer() + # + # Make sure to specify the correct server location in each of your environments + # config file: + # + # config.action_cable.url = "ws://example.com:28080" + def action_cable_meta_tag + tag "meta", name: "action-cable-url", content: Rails.application.config.action_cable.url + end + end + end +end diff --git a/lib/action_cable/process/logging.rb b/lib/action_cable/process/logging.rb index 72b1a080d1..618ba7357a 100644 --- a/lib/action_cable/process/logging.rb +++ b/lib/action_cable/process/logging.rb @@ -8,3 +8,5 @@ EM.error_handler do |e| end Celluloid.logger = ActionCable.server.logger + +ActionCable.server.config.log_to_stdout if Rails.env.development?
\ No newline at end of file diff --git a/lib/action_cable/server/configuration.rb b/lib/action_cable/server/configuration.rb index b22de273b8..f7fcee019b 100644 --- a/lib/action_cable/server/configuration.rb +++ b/lib/action_cable/server/configuration.rb @@ -9,6 +9,7 @@ module ActionCable attr_accessor :connection_class, :worker_pool_size attr_accessor :redis_path, :channels_path attr_accessor :disable_request_forgery_protection, :allowed_request_origins + attr_accessor :url def initialize @logger = Rails.logger @@ -23,6 +24,14 @@ module ActionCable @disable_request_forgery_protection = false end + def log_to_stdout + console = ActiveSupport::Logger.new($stdout) + console.formatter = @logger.formatter + console.level = @logger.level + + @logger.extend(ActiveSupport::Logger.broadcast(console)) + end + def channel_paths @channels ||= Dir["#{channels_path}/**/*_channel.rb"] end diff --git a/lib/action_cable/server/worker/active_record_connection_management.rb b/lib/action_cable/server/worker/active_record_connection_management.rb index 1ede0095f8..ecece4e270 100644 --- a/lib/action_cable/server/worker/active_record_connection_management.rb +++ b/lib/action_cable/server/worker/active_record_connection_management.rb @@ -12,7 +12,7 @@ module ActionCable end def with_database_connections - ActiveRecord::Base.logger.tagged(*connection.logger.tags) { yield } + connection.logger.tag(ActiveRecord::Base.logger) { yield } ensure ActiveRecord::Base.clear_active_connections! end diff --git a/lib/assets/javascripts/cable.coffee.erb b/lib/assets/javascripts/cable.coffee.erb index 8498233c11..25a9fc79c2 100644 --- a/lib/assets/javascripts/cable.coffee.erb +++ b/lib/assets/javascripts/cable.coffee.erb @@ -4,5 +4,9 @@ @Cable = INTERNAL: <%= ActionCable::INTERNAL.to_json %> - createConsumer: (url) -> + createConsumer: (url = @getConfig("url")) -> new Cable.Consumer url + + getConfig: (name) -> + element = document.head.querySelector("meta[name='action-cable-#{name}']") + element?.getAttribute("content") diff --git a/test/connection/cross_site_forgery_test.rb b/test/connection/cross_site_forgery_test.rb index 166abb7b38..ede3057e30 100644 --- a/test/connection/cross_site_forgery_test.rb +++ b/test/connection/cross_site_forgery_test.rb @@ -40,6 +40,20 @@ class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase assert_origin_not_allowed 'http://hax.com' end + test "explicitly specified a single regexp allowed origin" do + @server.config.allowed_request_origins = /.*ha.*/ + assert_origin_not_allowed 'http://rubyonrails.com' + assert_origin_allowed 'http://hax.com' + end + + test "explicitly specified multiple regexp allowed origins" do + @server.config.allowed_request_origins = [/http:\/\/ruby.*/, /.*rai.s.*com/, 'string' ] + assert_origin_allowed 'http://rubyonrails.com' + assert_origin_allowed 'http://www.rubyonrails.com' + assert_origin_not_allowed 'http://hax.com' + assert_origin_not_allowed 'http://rails.co.uk' + end + private def assert_origin_allowed(origin) response = connect_with_origin origin diff --git a/test/connection/multiple_identifiers_test.rb b/test/connection/multiple_identifiers_test.rb new file mode 100644 index 0000000000..55a9f96cb3 --- /dev/null +++ b/test/connection/multiple_identifiers_test.rb @@ -0,0 +1,41 @@ +require 'test_helper' +require 'stubs/test_server' +require 'stubs/user' + +class ActionCable::Connection::MultipleIdentifiersTest < ActionCable::TestCase + class Connection < ActionCable::Connection::Base + identified_by :current_user, :current_room + + def connect + self.current_user = User.new "lifo" + self.current_room = Room.new "my", "room" + end + end + + test "multiple connection identifiers" do + run_in_eventmachine do + open_connection_with_stubbed_pubsub + assert_equal "Room#my-room:User#lifo", @connection.connection_identifier + end + end + + protected + def open_connection_with_stubbed_pubsub + server = TestServer.new + server.stubs(:pubsub).returns(stub_everything('pubsub')) + + open_connection server: server + end + + def open_connection(server:) + env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' + @connection = Connection.new(server, env) + + @connection.process + @connection.send :on_open + end + + def close_connection + @connection.send :on_close + end +end diff --git a/test/stubs/global_id.rb b/test/stubs/global_id.rb new file mode 100644 index 0000000000..334f0d03e8 --- /dev/null +++ b/test/stubs/global_id.rb @@ -0,0 +1,8 @@ +class GlobalID + attr_reader :uri + delegate :to_param, :to_s, to: :uri + + def initialize(gid, options = {}) + @uri = gid + end +end diff --git a/test/stubs/room.rb b/test/stubs/room.rb index 246d6a98af..cd66a0b687 100644 --- a/test/stubs/room.rb +++ b/test/stubs/room.rb @@ -7,7 +7,7 @@ class Room end def to_global_id - "Room##{id}-#{name}" + GlobalID.new("Room##{id}-#{name}") end def to_gid_param diff --git a/test/stubs/user.rb b/test/stubs/user.rb index bce7dfc49e..a66b4f87d5 100644 --- a/test/stubs/user.rb +++ b/test/stubs/user.rb @@ -6,6 +6,10 @@ class User end def to_global_id - "User##{name}" + GlobalID.new("User##{name}") + end + + def to_gid_param + to_global_id.to_param end end |