actioncable 5.0.1 → 6.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +31 -117
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +4 -535
  5. data/app/assets/javascripts/action_cable.js +517 -0
  6. data/lib/action_cable.rb +20 -10
  7. data/lib/action_cable/channel.rb +3 -0
  8. data/lib/action_cable/channel/base.rb +31 -23
  9. data/lib/action_cable/channel/broadcasting.rb +22 -10
  10. data/lib/action_cable/channel/callbacks.rb +4 -2
  11. data/lib/action_cable/channel/naming.rb +5 -2
  12. data/lib/action_cable/channel/periodic_timers.rb +4 -3
  13. data/lib/action_cable/channel/streams.rb +39 -11
  14. data/lib/action_cable/channel/test_case.rb +310 -0
  15. data/lib/action_cable/connection.rb +3 -2
  16. data/lib/action_cable/connection/authorization.rb +8 -6
  17. data/lib/action_cable/connection/base.rb +34 -26
  18. data/lib/action_cable/connection/client_socket.rb +20 -18
  19. data/lib/action_cable/connection/identification.rb +5 -4
  20. data/lib/action_cable/connection/internal_channel.rb +4 -2
  21. data/lib/action_cable/connection/message_buffer.rb +3 -2
  22. data/lib/action_cable/connection/stream.rb +9 -5
  23. data/lib/action_cable/connection/stream_event_loop.rb +4 -2
  24. data/lib/action_cable/connection/subscriptions.rb +14 -13
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +4 -2
  26. data/lib/action_cable/connection/test_case.rb +234 -0
  27. data/lib/action_cable/connection/web_socket.rb +7 -5
  28. data/lib/action_cable/engine.rb +7 -5
  29. data/lib/action_cable/gem_version.rb +5 -3
  30. data/lib/action_cable/helpers/action_cable_helper.rb +6 -4
  31. data/lib/action_cable/remote_connections.rb +9 -4
  32. data/lib/action_cable/server.rb +2 -1
  33. data/lib/action_cable/server/base.rb +17 -10
  34. data/lib/action_cable/server/broadcasting.rb +9 -3
  35. data/lib/action_cable/server/configuration.rb +21 -22
  36. data/lib/action_cable/server/connections.rb +2 -0
  37. data/lib/action_cable/server/worker.rb +11 -11
  38. data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -0
  39. data/lib/action_cable/subscription_adapter.rb +4 -0
  40. data/lib/action_cable/subscription_adapter/async.rb +3 -1
  41. data/lib/action_cable/subscription_adapter/base.rb +6 -0
  42. data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
  43. data/lib/action_cable/subscription_adapter/inline.rb +2 -0
  44. data/lib/action_cable/subscription_adapter/postgresql.rb +40 -14
  45. data/lib/action_cable/subscription_adapter/redis.rb +19 -11
  46. data/lib/action_cable/subscription_adapter/subscriber_map.rb +3 -1
  47. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  48. data/lib/action_cable/test_case.rb +11 -0
  49. data/lib/action_cable/test_helper.rb +133 -0
  50. data/lib/action_cable/version.rb +3 -1
  51. data/lib/rails/generators/channel/USAGE +5 -6
  52. data/lib/rails/generators/channel/channel_generator.rb +16 -11
  53. data/lib/rails/generators/channel/templates/application_cable/{channel.rb → channel.rb.tt} +0 -0
  54. data/lib/rails/generators/channel/templates/application_cable/{connection.rb → connection.rb.tt} +0 -0
  55. data/lib/rails/generators/channel/templates/{channel.rb → channel.rb.tt} +0 -0
  56. data/lib/rails/generators/channel/templates/{assets/channel.js → javascript/channel.js.tt} +6 -4
  57. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  58. data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +46 -38
  62. data/lib/action_cable/connection/faye_client_socket.rb +0 -48
  63. data/lib/action_cable/connection/faye_event_loop.rb +0 -44
  64. data/lib/action_cable/subscription_adapter/evented_redis.rb +0 -79
  65. data/lib/assets/compiled/action_cable.js +0 -597
  66. data/lib/rails/generators/channel/templates/assets/cable.js +0 -13
  67. data/lib/rails/generators/channel/templates/assets/channel.coffee +0 -14
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Connection
3
5
  extend ActiveSupport::Autoload
@@ -8,13 +10,12 @@ module ActionCable
8
10
  autoload :ClientSocket
9
11
  autoload :Identification
10
12
  autoload :InternalChannel
11
- autoload :FayeClientSocket
12
- autoload :FayeEventLoop
13
13
  autoload :MessageBuffer
14
14
  autoload :Stream
15
15
  autoload :StreamEventLoop
16
16
  autoload :Subscriptions
17
17
  autoload :TaggedLoggerProxy
18
+ autoload :TestCase
18
19
  autoload :WebSocket
19
20
  end
20
21
  end
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Connection
3
5
  module Authorization
4
6
  class UnauthorizedError < StandardError; end
5
7
 
6
- private
7
- def reject_unauthorized_connection
8
- logger.error "An unauthorized connection attempt was rejected"
9
- raise UnauthorizedError
10
- end
8
+ # Closes the WebSocket connection if it is open and returns a 404 "File not Found" response.
9
+ def reject_unauthorized_connection
10
+ logger.error "An unauthorized connection attempt was rejected"
11
+ raise UnauthorizedError
12
+ end
11
13
  end
12
14
  end
13
- end
15
+ end
@@ -1,8 +1,11 @@
1
- require 'action_dispatch'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch"
4
+ require "active_support/rescuable"
2
5
 
3
6
  module ActionCable
4
7
  module Connection
5
- # For every WebSocket the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
8
+ # For every WebSocket connection the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
6
9
  # of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
7
10
  # based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
8
11
  # authentication and authorization.
@@ -22,13 +25,10 @@ module ActionCable
22
25
  # # Any cleanup work needed when the cable connection is cut.
23
26
  # end
24
27
  #
25
- # protected
28
+ # private
26
29
  # def find_verified_user
27
- # if current_user = User.find_by_identity cookies.signed[:identity_id]
28
- # current_user
29
- # else
30
+ # User.find_by_identity(cookies.encrypted[:identity_id]) ||
30
31
  # reject_unauthorized_connection
31
- # end
32
32
  # end
33
33
  # end
34
34
  # end
@@ -47,6 +47,7 @@ module ActionCable
47
47
  include Identification
48
48
  include InternalChannel
49
49
  include Authorization
50
+ include ActiveSupport::Rescuable
50
51
 
51
52
  attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
52
53
  delegate :event_loop, :pubsub, to: :server
@@ -57,7 +58,7 @@ module ActionCable
57
58
  @worker_pool = server.worker_pool
58
59
  @logger = new_tagged_logger
59
60
 
60
- @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop, server.config.client_socket_class)
61
+ @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop)
61
62
  @subscriptions = ActionCable::Connection::Subscriptions.new(self)
62
63
  @message_buffer = ActionCable::Connection::MessageBuffer.new(self)
63
64
 
@@ -96,7 +97,12 @@ module ActionCable
96
97
  end
97
98
 
98
99
  # Close the WebSocket connection.
99
- def close
100
+ def close(reason: nil, reconnect: true)
101
+ transmit(
102
+ type: ActionCable::INTERNAL[:message_types][:disconnect],
103
+ reason: reason,
104
+ reconnect: reconnect
105
+ )
100
106
  websocket.close
101
107
  end
102
108
 
@@ -105,14 +111,14 @@ module ActionCable
105
111
  worker_pool.async_invoke(self, method, *arguments)
106
112
  end
107
113
 
108
- # Return a basic hash of statistics for the connection keyed with `identifier`, `started_at`, and `subscriptions`.
114
+ # Return a basic hash of statistics for the connection keyed with <tt>identifier</tt>, <tt>started_at</tt>, <tt>subscriptions</tt>, and <tt>request_id</tt>.
109
115
  # This can be returned by a health check against the connection.
110
116
  def statistics
111
117
  {
112
118
  identifier: connection_identifier,
113
119
  started_at: @started_at,
114
120
  subscriptions: subscriptions.identifiers,
115
- request_id: @env['action_dispatch.request_id']
121
+ request_id: @env["action_dispatch.request_id"]
116
122
  }
117
123
  end
118
124
 
@@ -129,16 +135,20 @@ module ActionCable
129
135
  end
130
136
 
131
137
  def on_error(message) # :nodoc:
132
- # ignore
138
+ # log errors to make diagnosing socket errors easier
139
+ logger.error "WebSocket error occurred: #{message}"
133
140
  end
134
141
 
135
142
  def on_close(reason, code) # :nodoc:
136
143
  send_async :handle_close
137
144
  end
138
145
 
139
- protected
146
+ private
147
+ attr_reader :websocket
148
+ attr_reader :message_buffer
149
+
140
150
  # The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc.
141
- def request
151
+ def request # :doc:
142
152
  @request ||= begin
143
153
  environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
144
154
  ActionDispatch::Request.new(environment || env)
@@ -146,14 +156,10 @@ module ActionCable
146
156
  end
147
157
 
148
158
  # The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks.
149
- def cookies
159
+ def cookies # :doc:
150
160
  request.cookie_jar
151
161
  end
152
162
 
153
- attr_reader :websocket
154
- attr_reader :message_buffer
155
-
156
- private
157
163
  def encode(cable_message)
158
164
  @coder.encode cable_message
159
165
  end
@@ -171,7 +177,7 @@ module ActionCable
171
177
  message_buffer.process!
172
178
  server.add_connection(self)
173
179
  rescue ActionCable::Connection::Authorization::UnauthorizedError
174
- respond_to_invalid_request
180
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:unauthorized], reconnect: false) if websocket.alive?
175
181
  end
176
182
 
177
183
  def handle_close
@@ -212,11 +218,11 @@ module ActionCable
212
218
  end
213
219
 
214
220
  def respond_to_invalid_request
215
- close if websocket.alive?
221
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:invalid_request]) if websocket.alive?
216
222
 
217
223
  logger.error invalid_request_message
218
224
  logger.info finished_request_message
219
- [ 404, { 'Content-Type' => 'text/plain' }, [ 'Page not found' ] ]
225
+ [ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ]
220
226
  end
221
227
 
222
228
  # Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags.
@@ -229,7 +235,7 @@ module ActionCable
229
235
  'Started %s "%s"%s for %s at %s' % [
230
236
  request.request_method,
231
237
  request.filtered_path,
232
- websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]',
238
+ websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
233
239
  request.ip,
234
240
  Time.now.to_s ]
235
241
  end
@@ -237,22 +243,24 @@ module ActionCable
237
243
  def finished_request_message
238
244
  'Finished "%s"%s for %s at %s' % [
239
245
  request.filtered_path,
240
- websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]',
246
+ websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
241
247
  request.ip,
242
248
  Time.now.to_s ]
243
249
  end
244
250
 
245
251
  def invalid_request_message
246
- 'Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)' % [
252
+ "Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
247
253
  env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
248
254
  ]
249
255
  end
250
256
 
251
257
  def successful_request_message
252
- 'Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)' % [
258
+ "Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
253
259
  env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
254
260
  ]
255
261
  end
256
262
  end
257
263
  end
258
264
  end
265
+
266
+ ActiveSupport.run_load_hooks(:action_cable_connection, ActionCable::Connection::Base)
@@ -1,4 +1,6 @@
1
- require 'websocket/driver'
1
+ # frozen_string_literal: true
2
+
3
+ require "websocket/driver"
2
4
 
3
5
  module ActionCable
4
6
  module Connection
@@ -8,18 +10,18 @@ module ActionCable
8
10
  # Copyright (c) 2010-2015 James Coglan
9
11
  class ClientSocket # :nodoc:
10
12
  def self.determine_url(env)
11
- scheme = secure_request?(env) ? 'wss:' : 'ws:'
13
+ scheme = secure_request?(env) ? "wss:" : "ws:"
12
14
  "#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
13
15
  end
14
16
 
15
17
  def self.secure_request?(env)
16
- return true if env['HTTPS'] == 'on'
17
- return true if env['HTTP_X_FORWARDED_SSL'] == 'on'
18
- return true if env['HTTP_X_FORWARDED_SCHEME'] == 'https'
19
- return true if env['HTTP_X_FORWARDED_PROTO'] == 'https'
20
- return true if env['rack.url_scheme'] == 'https'
18
+ return true if env["HTTPS"] == "on"
19
+ return true if env["HTTP_X_FORWARDED_SSL"] == "on"
20
+ return true if env["HTTP_X_FORWARDED_SCHEME"] == "https"
21
+ return true if env["HTTP_X_FORWARDED_PROTO"] == "https"
22
+ return true if env["rack.url_scheme"] == "https"
21
23
 
22
- return false
24
+ false
23
25
  end
24
26
 
25
27
  CONNECTING = 0
@@ -37,7 +39,7 @@ module ActionCable
37
39
  @url = ClientSocket.determine_url(@env)
38
40
 
39
41
  @driver = @driver_started = nil
40
- @close_params = ['', 1006]
42
+ @close_params = ["", 1006]
41
43
 
42
44
  @ready_state = CONNECTING
43
45
 
@@ -56,7 +58,7 @@ module ActionCable
56
58
  return if @driver.nil? || @driver_started
57
59
  @stream.hijack_rack_socket
58
60
 
59
- if callback = @env['async.callback']
61
+ if callback = @env["async.callback"]
60
62
  callback.call([101, {}, @stream])
61
63
  end
62
64
 
@@ -78,20 +80,20 @@ module ActionCable
78
80
  def transmit(message)
79
81
  return false if @ready_state > OPEN
80
82
  case message
81
- when Numeric then @driver.text(message.to_s)
82
- when String then @driver.text(message)
83
- when Array then @driver.binary(message)
84
- else false
83
+ when Numeric then @driver.text(message.to_s)
84
+ when String then @driver.text(message)
85
+ when Array then @driver.binary(message)
86
+ else false
85
87
  end
86
88
  end
87
89
 
88
90
  def close(code = nil, reason = nil)
89
91
  code ||= 1000
90
- reason ||= ''
92
+ reason ||= ""
91
93
 
92
- unless code == 1000 or (code >= 3000 and code <= 4999)
93
- raise ArgumentError, "Failed to execute 'close' on WebSocket: " +
94
- "The code must be either 1000, or between 3000 and 4999. " +
94
+ unless code == 1000 || (code >= 3000 && code <= 4999)
95
+ raise ArgumentError, "Failed to execute 'close' on WebSocket: " \
96
+ "The code must be either 1000, or between 3000 and 4999. " \
95
97
  "#{code} is neither."
96
98
  end
97
99
 
@@ -1,4 +1,6 @@
1
- require 'set'
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
2
4
 
3
5
  module ActionCable
4
6
  module Connection
@@ -6,11 +8,10 @@ module ActionCable
6
8
  extend ActiveSupport::Concern
7
9
 
8
10
  included do
9
- class_attribute :identifiers
10
- self.identifiers = Set.new
11
+ class_attribute :identifiers, default: Set.new
11
12
  end
12
13
 
13
- class_methods do
14
+ module ClassMethods
14
15
  # Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
15
16
  # Common identifiers are current_user and current_account, but could be anything, really.
16
17
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Connection
3
5
  # Makes it possible for the RemoteConnection to disconnect a specific connection.
@@ -27,8 +29,8 @@ module ActionCable
27
29
  end
28
30
 
29
31
  def process_internal_message(message)
30
- case message['type']
31
- when 'disconnect'
32
+ case message["type"]
33
+ when "disconnect"
32
34
  logger.info "Removing connection (#{connection_identifier})"
33
35
  websocket.close
34
36
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Connection
3
5
  # Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized, and is ready to receive them.
@@ -28,11 +30,10 @@ module ActionCable
28
30
  receive_buffered_messages
29
31
  end
30
32
 
31
- protected
33
+ private
32
34
  attr_reader :connection
33
35
  attr_reader :buffered_messages
34
36
 
35
- private
36
37
  def valid?(message)
37
38
  message.is_a?(String)
38
39
  end
@@ -1,4 +1,6 @@
1
- require 'thread'
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
2
4
 
3
5
  module ActionCable
4
6
  module Connection
@@ -10,7 +12,7 @@ module ActionCable
10
12
  def initialize(event_loop, socket)
11
13
  @event_loop = event_loop
12
14
  @socket_object = socket
13
- @stream_send = socket.env['stream.send']
15
+ @stream_send = socket.env["stream.send"]
14
16
 
15
17
  @rack_hijack_io = nil
16
18
  @write_lock = Mutex.new
@@ -94,10 +96,12 @@ module ActionCable
94
96
  end
95
97
 
96
98
  def hijack_rack_socket
97
- return unless @socket_object.env['rack.hijack']
99
+ return unless @socket_object.env["rack.hijack"]
98
100
 
99
- @socket_object.env['rack.hijack'].call
100
- @rack_hijack_io = @socket_object.env['rack.hijack_io']
101
+ # This should return the underlying io according to the SPEC:
102
+ @rack_hijack_io = @socket_object.env["rack.hijack"].call
103
+ # Retain existing behaviour if required:
104
+ @rack_hijack_io ||= @socket_object.env["rack.hijack_io"]
101
105
 
102
106
  @event_loop.attach(@rack_hijack_io, self)
103
107
  end
@@ -1,5 +1,7 @@
1
- require 'nio'
2
- require 'thread'
1
+ # frozen_string_literal: true
2
+
3
+ require "nio"
4
+ require "thread"
3
5
 
4
6
  module ActionCable
5
7
  module Connection
@@ -1,4 +1,6 @@
1
- require 'active_support/core_ext/hash/indifferent_access'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
2
4
 
3
5
  module ActionCable
4
6
  module Connection
@@ -11,19 +13,20 @@ module ActionCable
11
13
  end
12
14
 
13
15
  def execute_command(data)
14
- case data['command']
15
- when 'subscribe' then add data
16
- when 'unsubscribe' then remove data
17
- when 'message' then perform_action data
16
+ case data["command"]
17
+ when "subscribe" then add data
18
+ when "unsubscribe" then remove data
19
+ when "message" then perform_action data
18
20
  else
19
21
  logger.error "Received unrecognized command in #{data.inspect}"
20
22
  end
21
23
  rescue Exception => e
22
- logger.error "Could not execute command from #{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
24
+ @connection.rescue_with_handler(e)
25
+ logger.error "Could not execute command from (#{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
23
26
  end
24
27
 
25
28
  def add(data)
26
- id_key = data['identifier']
29
+ id_key = data["identifier"]
27
30
  id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
28
31
 
29
32
  return if subscriptions.key?(id_key)
@@ -41,7 +44,7 @@ module ActionCable
41
44
 
42
45
  def remove(data)
43
46
  logger.info "Unsubscribing from channel: #{data['identifier']}"
44
- remove_subscription subscriptions[data['identifier']]
47
+ remove_subscription find(data)
45
48
  end
46
49
 
47
50
  def remove_subscription(subscription)
@@ -50,7 +53,7 @@ module ActionCable
50
53
  end
51
54
 
52
55
  def perform_action(data)
53
- find(data).perform_action ActiveSupport::JSON.decode(data['data'])
56
+ find(data).perform_action ActiveSupport::JSON.decode(data["data"])
54
57
  end
55
58
 
56
59
  def identifiers
@@ -61,14 +64,12 @@ module ActionCable
61
64
  subscriptions.each { |id, channel| remove_subscription(channel) }
62
65
  end
63
66
 
64
- protected
65
- attr_reader :connection, :subscriptions
66
-
67
67
  private
68
+ attr_reader :connection, :subscriptions
68
69
  delegate :logger, to: :connection
69
70
 
70
71
  def find(data)
71
- if subscription = subscriptions[data['identifier']]
72
+ if subscription = subscriptions[data["identifier"]]
72
73
  subscription
73
74
  else
74
75
  raise "Unable to find subscription with identifier: #{data['identifier']}"