actioncable 5.0.7.2 → 5.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +13 -169
- data/MIT-LICENSE +1 -1
- data/README.md +18 -15
- data/lib/action_cable.rb +9 -9
- data/lib/action_cable/channel/base.rb +17 -19
- data/lib/action_cable/channel/broadcasting.rb +2 -2
- data/lib/action_cable/channel/callbacks.rb +1 -1
- data/lib/action_cable/channel/naming.rb +2 -1
- data/lib/action_cable/channel/periodic_timers.rb +1 -1
- data/lib/action_cable/channel/streams.rb +7 -7
- data/lib/action_cable/connection.rb +0 -2
- data/lib/action_cable/connection/authorization.rb +6 -6
- data/lib/action_cable/connection/base.rb +20 -21
- data/lib/action_cable/connection/client_socket.rb +16 -16
- data/lib/action_cable/connection/identification.rb +1 -1
- data/lib/action_cable/connection/internal_channel.rb +2 -2
- data/lib/action_cable/connection/message_buffer.rb +2 -0
- data/lib/action_cable/connection/stream.rb +5 -5
- data/lib/action_cable/connection/stream_event_loop.rb +2 -2
- data/lib/action_cable/connection/subscriptions.rb +12 -10
- data/lib/action_cable/connection/tagged_logger_proxy.rb +2 -2
- data/lib/action_cable/connection/web_socket.rb +5 -3
- data/lib/action_cable/engine.rb +4 -4
- data/lib/action_cable/gem_version.rb +3 -3
- data/lib/action_cable/helpers/action_cable_helper.rb +1 -1
- data/lib/action_cable/remote_connections.rb +2 -2
- data/lib/action_cable/server.rb +1 -1
- data/lib/action_cable/server/base.rb +5 -5
- data/lib/action_cable/server/broadcasting.rb +7 -3
- data/lib/action_cable/server/configuration.rb +3 -19
- data/lib/action_cable/server/worker.rb +3 -3
- data/lib/action_cable/subscription_adapter.rb +1 -0
- data/lib/action_cable/subscription_adapter/async.rb +1 -1
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +26 -0
- data/lib/action_cable/subscription_adapter/evented_redis.rb +13 -5
- data/lib/action_cable/subscription_adapter/postgresql.rb +4 -4
- data/lib/action_cable/subscription_adapter/redis.rb +9 -7
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +1 -1
- data/lib/action_cable/version.rb +1 -1
- data/lib/assets/compiled/action_cable.js +554 -567
- data/lib/rails/generators/channel/USAGE +2 -2
- data/lib/rails/generators/channel/channel_generator.rb +9 -9
- data/lib/rails/generators/channel/templates/assets/cable.js +1 -1
- metadata +13 -33
- data/lib/action_cable/connection/faye_client_socket.rb +0 -48
- data/lib/action_cable/connection/faye_event_loop.rb +0 -44
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "active_support/core_ext/object/to_param"
|
2
2
|
|
3
3
|
module ActionCable
|
4
4
|
module Channel
|
@@ -16,7 +16,7 @@ module ActionCable
|
|
16
16
|
def broadcasting_for(model) #:nodoc:
|
17
17
|
case
|
18
18
|
when model.is_a?(Array)
|
19
|
-
model.map { |m| broadcasting_for(m) }.join(
|
19
|
+
model.map { |m| broadcasting_for(m) }.join(":")
|
20
20
|
when model.respond_to?(:to_gid_param)
|
21
21
|
model.to_gid_param
|
22
22
|
else
|
@@ -10,8 +10,9 @@ module ActionCable
|
|
10
10
|
#
|
11
11
|
# ChatChannel.channel_name # => 'chat'
|
12
12
|
# Chats::AppearancesChannel.channel_name # => 'chats:appearances'
|
13
|
+
# FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances'
|
13
14
|
def channel_name
|
14
|
-
@channel_name ||= name.sub(/Channel$/,
|
15
|
+
@channel_name ||= name.sub(/Channel$/, "").gsub("::", ":").underscore
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
@@ -30,7 +30,7 @@ module ActionCable
|
|
30
30
|
def periodically(callback_or_method_name = nil, every:, &block)
|
31
31
|
callback =
|
32
32
|
if block_given?
|
33
|
-
raise ArgumentError,
|
33
|
+
raise ArgumentError, "Pass a block or provide a callback arg, not both" if callback_or_method_name
|
34
34
|
block
|
35
35
|
else
|
36
36
|
case callback_or_method_name
|
@@ -19,14 +19,14 @@ module ActionCable
|
|
19
19
|
# end
|
20
20
|
#
|
21
21
|
# Based on the above example, the subscribers of this channel will get whatever data is put into the,
|
22
|
-
# let's say,
|
22
|
+
# let's say, <tt>comments_for_45</tt> broadcasting as soon as it's put there.
|
23
23
|
#
|
24
24
|
# An example broadcasting for this channel looks like so:
|
25
25
|
#
|
26
26
|
# ActionCable.server.broadcast "comments_for_45", author: 'DHH', content: 'Rails is just swell'
|
27
27
|
#
|
28
28
|
# If you have a stream that is related to a model, then the broadcasting used can be generated from the model and channel.
|
29
|
-
# The following example would subscribe to a broadcasting like
|
29
|
+
# The following example would subscribe to a broadcasting like <tt>comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE</tt>.
|
30
30
|
#
|
31
31
|
# class CommentsChannel < ApplicationCable::Channel
|
32
32
|
# def subscribed
|
@@ -69,8 +69,8 @@ module ActionCable
|
|
69
69
|
|
70
70
|
# Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used
|
71
71
|
# instead of the default of just transmitting the updates straight to the subscriber.
|
72
|
-
# Pass
|
73
|
-
# Defaults to
|
72
|
+
# Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
|
73
|
+
# Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
|
74
74
|
def stream_from(broadcasting, callback = nil, coder: nil, &block)
|
75
75
|
broadcasting = String(broadcasting)
|
76
76
|
|
@@ -94,8 +94,8 @@ module ActionCable
|
|
94
94
|
# <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight
|
95
95
|
# to the subscriber.
|
96
96
|
#
|
97
|
-
# Pass
|
98
|
-
# Defaults to
|
97
|
+
# Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
|
98
|
+
# Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
|
99
99
|
def stream_for(model, callback = nil, coder: nil, &block)
|
100
100
|
stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder)
|
101
101
|
end
|
@@ -138,7 +138,7 @@ module ActionCable
|
|
138
138
|
end
|
139
139
|
|
140
140
|
# May be overridden to change the default stream handling behavior
|
141
|
-
# which decodes JSON and transmits to client.
|
141
|
+
# which decodes JSON and transmits to the client.
|
142
142
|
#
|
143
143
|
# TODO: Tests demonstrating this.
|
144
144
|
#
|
@@ -3,11 +3,11 @@ module ActionCable
|
|
3
3
|
module Authorization
|
4
4
|
class UnauthorizedError < StandardError; end
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
private
|
7
|
+
def reject_unauthorized_connection
|
8
|
+
logger.error "An unauthorized connection attempt was rejected"
|
9
|
+
raise UnauthorizedError
|
10
|
+
end
|
11
11
|
end
|
12
12
|
end
|
13
|
-
end
|
13
|
+
end
|
@@ -1,8 +1,8 @@
|
|
1
|
-
require
|
1
|
+
require "action_dispatch"
|
2
2
|
|
3
3
|
module ActionCable
|
4
4
|
module Connection
|
5
|
-
# For every WebSocket the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
|
5
|
+
# For every WebSocket connection the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
|
6
6
|
# of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
|
7
7
|
# based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
|
8
8
|
# authentication and authorization.
|
@@ -22,13 +22,10 @@ module ActionCable
|
|
22
22
|
# # Any cleanup work needed when the cable connection is cut.
|
23
23
|
# end
|
24
24
|
#
|
25
|
-
#
|
25
|
+
# private
|
26
26
|
# def find_verified_user
|
27
|
-
#
|
28
|
-
# current_user
|
29
|
-
# else
|
27
|
+
# User.find_by_identity(cookies.signed[:identity_id]) ||
|
30
28
|
# reject_unauthorized_connection
|
31
|
-
# end
|
32
29
|
# end
|
33
30
|
# end
|
34
31
|
# end
|
@@ -57,7 +54,7 @@ module ActionCable
|
|
57
54
|
@worker_pool = server.worker_pool
|
58
55
|
@logger = new_tagged_logger
|
59
56
|
|
60
|
-
@websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop
|
57
|
+
@websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop)
|
61
58
|
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
|
62
59
|
@message_buffer = ActionCable::Connection::MessageBuffer.new(self)
|
63
60
|
|
@@ -105,14 +102,14 @@ module ActionCable
|
|
105
102
|
worker_pool.async_invoke(self, method, *arguments)
|
106
103
|
end
|
107
104
|
|
108
|
-
# Return a basic hash of statistics for the connection keyed with
|
105
|
+
# 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
106
|
# This can be returned by a health check against the connection.
|
110
107
|
def statistics
|
111
108
|
{
|
112
109
|
identifier: connection_identifier,
|
113
110
|
started_at: @started_at,
|
114
111
|
subscriptions: subscriptions.identifiers,
|
115
|
-
request_id: @env[
|
112
|
+
request_id: @env["action_dispatch.request_id"]
|
116
113
|
}
|
117
114
|
end
|
118
115
|
|
@@ -136,9 +133,15 @@ module ActionCable
|
|
136
133
|
send_async :handle_close
|
137
134
|
end
|
138
135
|
|
136
|
+
# TODO Change this to private once we've dropped Ruby 2.2 support.
|
137
|
+
# Workaround for Ruby 2.2 "private attribute?" warning.
|
139
138
|
protected
|
139
|
+
attr_reader :websocket
|
140
|
+
attr_reader :message_buffer
|
141
|
+
|
142
|
+
private
|
140
143
|
# The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc.
|
141
|
-
def request
|
144
|
+
def request # :doc:
|
142
145
|
@request ||= begin
|
143
146
|
environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
|
144
147
|
ActionDispatch::Request.new(environment || env)
|
@@ -146,14 +149,10 @@ module ActionCable
|
|
146
149
|
end
|
147
150
|
|
148
151
|
# The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks.
|
149
|
-
def cookies
|
152
|
+
def cookies # :doc:
|
150
153
|
request.cookie_jar
|
151
154
|
end
|
152
155
|
|
153
|
-
attr_reader :websocket
|
154
|
-
attr_reader :message_buffer
|
155
|
-
|
156
|
-
private
|
157
156
|
def encode(cable_message)
|
158
157
|
@coder.encode cable_message
|
159
158
|
end
|
@@ -216,7 +215,7 @@ module ActionCable
|
|
216
215
|
|
217
216
|
logger.error invalid_request_message
|
218
217
|
logger.info finished_request_message
|
219
|
-
[ 404, {
|
218
|
+
[ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ]
|
220
219
|
end
|
221
220
|
|
222
221
|
# Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags.
|
@@ -229,7 +228,7 @@ module ActionCable
|
|
229
228
|
'Started %s "%s"%s for %s at %s' % [
|
230
229
|
request.request_method,
|
231
230
|
request.filtered_path,
|
232
|
-
websocket.possible? ?
|
231
|
+
websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
|
233
232
|
request.ip,
|
234
233
|
Time.now.to_s ]
|
235
234
|
end
|
@@ -237,19 +236,19 @@ module ActionCable
|
|
237
236
|
def finished_request_message
|
238
237
|
'Finished "%s"%s for %s at %s' % [
|
239
238
|
request.filtered_path,
|
240
|
-
websocket.possible? ?
|
239
|
+
websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
|
241
240
|
request.ip,
|
242
241
|
Time.now.to_s ]
|
243
242
|
end
|
244
243
|
|
245
244
|
def invalid_request_message
|
246
|
-
|
245
|
+
"Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
|
247
246
|
env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
|
248
247
|
]
|
249
248
|
end
|
250
249
|
|
251
250
|
def successful_request_message
|
252
|
-
|
251
|
+
"Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
|
253
252
|
env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
|
254
253
|
]
|
255
254
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "websocket/driver"
|
2
2
|
|
3
3
|
module ActionCable
|
4
4
|
module Connection
|
@@ -8,16 +8,16 @@ module ActionCable
|
|
8
8
|
# Copyright (c) 2010-2015 James Coglan
|
9
9
|
class ClientSocket # :nodoc:
|
10
10
|
def self.determine_url(env)
|
11
|
-
scheme = secure_request?(env) ?
|
11
|
+
scheme = secure_request?(env) ? "wss:" : "ws:"
|
12
12
|
"#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
|
13
13
|
end
|
14
14
|
|
15
15
|
def self.secure_request?(env)
|
16
|
-
return true if env[
|
17
|
-
return true if env[
|
18
|
-
return true if env[
|
19
|
-
return true if env[
|
20
|
-
return true if 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"
|
21
21
|
|
22
22
|
return false
|
23
23
|
end
|
@@ -37,7 +37,7 @@ module ActionCable
|
|
37
37
|
@url = ClientSocket.determine_url(@env)
|
38
38
|
|
39
39
|
@driver = @driver_started = nil
|
40
|
-
@close_params = [
|
40
|
+
@close_params = ["", 1006]
|
41
41
|
|
42
42
|
@ready_state = CONNECTING
|
43
43
|
|
@@ -56,7 +56,7 @@ module ActionCable
|
|
56
56
|
return if @driver.nil? || @driver_started
|
57
57
|
@stream.hijack_rack_socket
|
58
58
|
|
59
|
-
if callback = @env[
|
59
|
+
if callback = @env["async.callback"]
|
60
60
|
callback.call([101, {}, @stream])
|
61
61
|
end
|
62
62
|
|
@@ -78,20 +78,20 @@ module ActionCable
|
|
78
78
|
def transmit(message)
|
79
79
|
return false if @ready_state > OPEN
|
80
80
|
case message
|
81
|
-
|
82
|
-
|
83
|
-
|
81
|
+
when Numeric then @driver.text(message.to_s)
|
82
|
+
when String then @driver.text(message)
|
83
|
+
when Array then @driver.binary(message)
|
84
84
|
else false
|
85
85
|
end
|
86
86
|
end
|
87
87
|
|
88
88
|
def close(code = nil, reason = nil)
|
89
89
|
code ||= 1000
|
90
|
-
reason ||=
|
90
|
+
reason ||= ""
|
91
91
|
|
92
|
-
unless code == 1000
|
93
|
-
raise ArgumentError, "Failed to execute 'close' on WebSocket: "
|
94
|
-
"The code must be either 1000, or between 3000 and 4999. "
|
92
|
+
unless code == 1000 || (code >= 3000 && code <= 4999)
|
93
|
+
raise ArgumentError, "Failed to execute 'close' on WebSocket: " \
|
94
|
+
"The code must be either 1000, or between 3000 and 4999. " \
|
95
95
|
"#{code} is neither."
|
96
96
|
end
|
97
97
|
|
@@ -28,6 +28,8 @@ module ActionCable
|
|
28
28
|
receive_buffered_messages
|
29
29
|
end
|
30
30
|
|
31
|
+
# TODO Change this to private once we've dropped Ruby 2.2 support.
|
32
|
+
# Workaround for Ruby 2.2 "private attribute?" warning.
|
31
33
|
protected
|
32
34
|
attr_reader :connection
|
33
35
|
attr_reader :buffered_messages
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "thread"
|
2
2
|
|
3
3
|
module ActionCable
|
4
4
|
module Connection
|
@@ -10,7 +10,7 @@ module ActionCable
|
|
10
10
|
def initialize(event_loop, socket)
|
11
11
|
@event_loop = event_loop
|
12
12
|
@socket_object = socket
|
13
|
-
@stream_send = socket.env[
|
13
|
+
@stream_send = socket.env["stream.send"]
|
14
14
|
|
15
15
|
@rack_hijack_io = nil
|
16
16
|
@write_lock = Mutex.new
|
@@ -94,10 +94,10 @@ module ActionCable
|
|
94
94
|
end
|
95
95
|
|
96
96
|
def hijack_rack_socket
|
97
|
-
return unless @socket_object.env[
|
97
|
+
return unless @socket_object.env["rack.hijack"]
|
98
98
|
|
99
|
-
@socket_object.env[
|
100
|
-
@rack_hijack_io = @socket_object.env[
|
99
|
+
@socket_object.env["rack.hijack"].call
|
100
|
+
@rack_hijack_io = @socket_object.env["rack.hijack_io"]
|
101
101
|
|
102
102
|
@event_loop.attach(@rack_hijack_io, self)
|
103
103
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "active_support/core_ext/hash/indifferent_access"
|
2
2
|
|
3
3
|
module ActionCable
|
4
4
|
module Connection
|
@@ -11,19 +11,19 @@ module ActionCable
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def execute_command(data)
|
14
|
-
case data[
|
15
|
-
when
|
16
|
-
when
|
17
|
-
when
|
14
|
+
case data["command"]
|
15
|
+
when "subscribe" then add data
|
16
|
+
when "unsubscribe" then remove data
|
17
|
+
when "message" then perform_action data
|
18
18
|
else
|
19
19
|
logger.error "Received unrecognized command in #{data.inspect}"
|
20
20
|
end
|
21
21
|
rescue Exception => e
|
22
|
-
logger.error "Could not execute command from #{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
|
22
|
+
logger.error "Could not execute command from (#{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
|
23
23
|
end
|
24
24
|
|
25
25
|
def add(data)
|
26
|
-
id_key = data[
|
26
|
+
id_key = data["identifier"]
|
27
27
|
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
|
28
28
|
|
29
29
|
return if subscriptions.key?(id_key)
|
@@ -41,7 +41,7 @@ module ActionCable
|
|
41
41
|
|
42
42
|
def remove(data)
|
43
43
|
logger.info "Unsubscribing from channel: #{data['identifier']}"
|
44
|
-
remove_subscription subscriptions[data[
|
44
|
+
remove_subscription subscriptions[data["identifier"]]
|
45
45
|
end
|
46
46
|
|
47
47
|
def remove_subscription(subscription)
|
@@ -50,7 +50,7 @@ module ActionCable
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def perform_action(data)
|
53
|
-
find(data).perform_action ActiveSupport::JSON.decode(data[
|
53
|
+
find(data).perform_action ActiveSupport::JSON.decode(data["data"])
|
54
54
|
end
|
55
55
|
|
56
56
|
def identifiers
|
@@ -61,6 +61,8 @@ module ActionCable
|
|
61
61
|
subscriptions.each { |id, channel| remove_subscription(channel) }
|
62
62
|
end
|
63
63
|
|
64
|
+
# TODO Change this to private once we've dropped Ruby 2.2 support.
|
65
|
+
# Workaround for Ruby 2.2 "private attribute?" warning.
|
64
66
|
protected
|
65
67
|
attr_reader :connection, :subscriptions
|
66
68
|
|
@@ -68,7 +70,7 @@ module ActionCable
|
|
68
70
|
delegate :logger, to: :connection
|
69
71
|
|
70
72
|
def find(data)
|
71
|
-
if subscription = subscriptions[data[
|
73
|
+
if subscription = subscriptions[data["identifier"]]
|
72
74
|
subscription
|
73
75
|
else
|
74
76
|
raise "Unable to find subscription with identifier: #{data['identifier']}"
|