actioncable 5.0.0.1 → 5.0.1.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +12 -2
- data/lib/action_cable/channel/base.rb +23 -17
- data/lib/action_cable/channel/streams.rb +1 -1
- data/lib/action_cable/connection/base.rb +4 -1
- data/lib/action_cable/connection/stream.rb +58 -2
- data/lib/action_cable/connection/stream_event_loop.rb +39 -8
- data/lib/action_cable/connection/subscriptions.rb +5 -1
- data/lib/action_cable/gem_version.rb +2 -2
- data/lib/action_cable/server/base.rb +6 -2
- data/lib/action_cable/server/configuration.rb +2 -1
- data/lib/action_cable/server/worker.rb +1 -1
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +5 -1
- data/lib/assets/compiled/action_cable.js +566 -559
- data/lib/rails/generators/channel/templates/application_cable/channel.rb +0 -1
- data/lib/rails/generators/channel/templates/application_cable/connection.rb +0 -1
- data/lib/rails/generators/channel/templates/channel.rb +0 -1
- metadata +7 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e71477ad06403305b2d7cb74ae7154a6ca54d8a
|
|
4
|
+
data.tar.gz: 375e2e323c8c7e987410a5fff1f035d3dba94081
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c6c756bfca7b69592aadc17004a36f993cd86c1de5010d8e98c7a52897823e5a7164b4a9dfdf433127c4b4b41dfb56d5ae6ab9f79ea1831c97d3259aaa12ed7
|
|
7
|
+
data.tar.gz: 93862ebfe578dc52dc841f47258031ffc58938138631c101de4cb89c0fa09ceaf4eae2c18103d6e694fc86a7e879094f8bfe19598b00c1938ae5c46cf31452ce
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
+
## Rails 5.0.1.rc1 (December 01, 2016) ##
|
|
2
|
+
|
|
3
|
+
* Permit same-origin connections by default.
|
|
4
|
+
|
|
5
|
+
New option `config.action_cable.allow_same_origin_as_host = false`
|
|
6
|
+
to disable.
|
|
7
|
+
|
|
8
|
+
*Dávid Halász*, *Matthew Draper*
|
|
9
|
+
|
|
10
|
+
* Fixed and added a workaround to avoid race condition, when one
|
|
11
|
+
thread closed the IO, when an another thread was still trying read
|
|
12
|
+
from IO on a connection.
|
|
13
|
+
|
|
14
|
+
*Matthew Draper*
|
|
15
|
+
|
|
16
|
+
* Shutdown pubsub connection before classes are reloaded, to avoid
|
|
17
|
+
hangups caused by pubsub still holding reference to Active Record
|
|
18
|
+
connection from the pool, and Active Record trying to cleanup the pool.
|
|
19
|
+
|
|
20
|
+
*Jon Moss*
|
|
21
|
+
|
|
22
|
+
* Prevent race where the client could receive and act upon a
|
|
23
|
+
subscription confirmation before the channel's `subscribed` method
|
|
24
|
+
completed.
|
|
25
|
+
|
|
26
|
+
Fixes #25381.
|
|
27
|
+
|
|
28
|
+
*Vladimir Dementyev*
|
|
29
|
+
|
|
30
|
+
* Buffer writes to websocket connections, to avoid blocking threads
|
|
31
|
+
that could be doing more useful things.
|
|
32
|
+
|
|
33
|
+
*Matthew Draper*, *Tinco Andringa*
|
|
34
|
+
|
|
35
|
+
* Invocation of channel action is now prevented, if subscription
|
|
36
|
+
connection was rejected.
|
|
37
|
+
|
|
38
|
+
Fixes #23757.
|
|
39
|
+
|
|
40
|
+
*Jon Moss*
|
|
41
|
+
|
|
42
|
+
* Protect against concurrent writes to a websocket connection from
|
|
43
|
+
multiple threads; the underlying OS write is not always threadsafe.
|
|
44
|
+
|
|
45
|
+
*Tinco Andringa*
|
|
46
|
+
|
|
47
|
+
* Close hijacked socket when connection is shut down.
|
|
48
|
+
|
|
49
|
+
Fixes #25613.
|
|
50
|
+
|
|
51
|
+
*Tinco Andringa*
|
|
52
|
+
|
|
53
|
+
|
|
1
54
|
## Rails 5.0.0 (June 30, 2016) ##
|
|
2
55
|
|
|
3
56
|
* Fix development reloading support: new cable connections are now correctly
|
data/README.md
CHANGED
|
@@ -323,7 +323,10 @@ Rails.application.paths.add "config/cable", with: "somewhere/else/cable.yml"
|
|
|
323
323
|
|
|
324
324
|
### Allowed Request Origins
|
|
325
325
|
|
|
326
|
-
Action Cable will only accept requests from
|
|
326
|
+
Action Cable will only accept requests from specific origins.
|
|
327
|
+
|
|
328
|
+
By default, only an origin matching the cable server itself will be permitted.
|
|
329
|
+
Additional origins can be specified using strings or regular expressions, provided in an array.
|
|
327
330
|
|
|
328
331
|
```ruby
|
|
329
332
|
Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/]
|
|
@@ -331,12 +334,19 @@ Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonr
|
|
|
331
334
|
|
|
332
335
|
When running in the development environment, this defaults to "http://localhost:3000".
|
|
333
336
|
|
|
334
|
-
To disable and allow requests from any origin:
|
|
337
|
+
To disable protection and allow requests from any origin:
|
|
335
338
|
|
|
336
339
|
```ruby
|
|
337
340
|
Rails.application.config.action_cable.disable_request_forgery_protection = true
|
|
338
341
|
```
|
|
339
342
|
|
|
343
|
+
To disable automatic access for same-origin requests, and strictly allow
|
|
344
|
+
only the configured origins:
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
Rails.application.config.action_cable.allow_same_origin_as_host = false
|
|
348
|
+
```
|
|
349
|
+
|
|
340
350
|
### Consumer Configuration
|
|
341
351
|
|
|
342
352
|
Once you have decided how to run your cable server (see below), you must provide the server URL (or path) to your client-side setup.
|
|
@@ -144,13 +144,14 @@ module ActionCable
|
|
|
144
144
|
|
|
145
145
|
# When a channel is streaming via pubsub, we want to delay the confirmation
|
|
146
146
|
# transmission until pubsub subscription is confirmed.
|
|
147
|
-
|
|
147
|
+
#
|
|
148
|
+
# The counter starts at 1 because it's awaiting a call to #subscribe_to_channel
|
|
149
|
+
@defer_subscription_confirmation_counter = Concurrent::AtomicFixnum.new(1)
|
|
148
150
|
|
|
149
151
|
@reject_subscription = nil
|
|
150
152
|
@subscription_confirmation_sent = nil
|
|
151
153
|
|
|
152
154
|
delegate_connection_identifiers
|
|
153
|
-
subscribe_to_channel
|
|
154
155
|
end
|
|
155
156
|
|
|
156
157
|
# Extract the action name from the passed data and process it via the channel. The process will ensure
|
|
@@ -169,6 +170,17 @@ module ActionCable
|
|
|
169
170
|
end
|
|
170
171
|
end
|
|
171
172
|
|
|
173
|
+
# This method is called after subscription has been added to the connection
|
|
174
|
+
# and confirms or rejects the subscription.
|
|
175
|
+
def subscribe_to_channel
|
|
176
|
+
run_callbacks :subscribe do
|
|
177
|
+
subscribed
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
reject_subscription if subscription_rejected?
|
|
181
|
+
ensure_confirmation_sent
|
|
182
|
+
end
|
|
183
|
+
|
|
172
184
|
# Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks.
|
|
173
185
|
# This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
|
|
174
186
|
def unsubscribe_from_channel # :nodoc:
|
|
@@ -202,12 +214,18 @@ module ActionCable
|
|
|
202
214
|
end
|
|
203
215
|
end
|
|
204
216
|
|
|
217
|
+
def ensure_confirmation_sent
|
|
218
|
+
return if subscription_rejected?
|
|
219
|
+
@defer_subscription_confirmation_counter.decrement
|
|
220
|
+
transmit_subscription_confirmation unless defer_subscription_confirmation?
|
|
221
|
+
end
|
|
222
|
+
|
|
205
223
|
def defer_subscription_confirmation!
|
|
206
|
-
@
|
|
224
|
+
@defer_subscription_confirmation_counter.increment
|
|
207
225
|
end
|
|
208
226
|
|
|
209
227
|
def defer_subscription_confirmation?
|
|
210
|
-
@
|
|
228
|
+
@defer_subscription_confirmation_counter.value > 0
|
|
211
229
|
end
|
|
212
230
|
|
|
213
231
|
def subscription_confirmation_sent?
|
|
@@ -231,24 +249,12 @@ module ActionCable
|
|
|
231
249
|
end
|
|
232
250
|
end
|
|
233
251
|
|
|
234
|
-
def subscribe_to_channel
|
|
235
|
-
run_callbacks :subscribe do
|
|
236
|
-
subscribed
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
if subscription_rejected?
|
|
240
|
-
reject_subscription
|
|
241
|
-
else
|
|
242
|
-
transmit_subscription_confirmation unless defer_subscription_confirmation?
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
252
|
def extract_action(data)
|
|
247
253
|
(data['action'].presence || :receive).to_sym
|
|
248
254
|
end
|
|
249
255
|
|
|
250
256
|
def processable_action?(action)
|
|
251
|
-
self.class.action_methods.include?(action.to_s)
|
|
257
|
+
self.class.action_methods.include?(action.to_s) unless subscription_rejected?
|
|
252
258
|
end
|
|
253
259
|
|
|
254
260
|
def dispatch_action(action, data)
|
|
@@ -84,7 +84,7 @@ module ActionCable
|
|
|
84
84
|
|
|
85
85
|
connection.server.event_loop.post do
|
|
86
86
|
pubsub.subscribe(broadcasting, handler, lambda do
|
|
87
|
-
|
|
87
|
+
ensure_confirmation_sent
|
|
88
88
|
logger.info "#{self.class.name} is streaming from #{broadcasting}"
|
|
89
89
|
end)
|
|
90
90
|
end
|
|
@@ -195,7 +195,10 @@ module ActionCable
|
|
|
195
195
|
def allow_request_origin?
|
|
196
196
|
return true if server.config.disable_request_forgery_protection
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
proto = Rack::Request.new(env).ssl? ? "https" : "http"
|
|
199
|
+
if server.config.allow_same_origin_as_host && env["HTTP_ORIGIN"] == "#{proto}://#{env['HTTP_HOST']}"
|
|
200
|
+
true
|
|
201
|
+
elsif Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env["HTTP_ORIGIN"] }
|
|
199
202
|
true
|
|
200
203
|
else
|
|
201
204
|
logger.error("Request origin not allowed: #{env['HTTP_ORIGIN']}")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
|
|
1
3
|
module ActionCable
|
|
2
4
|
module Connection
|
|
3
5
|
#--
|
|
@@ -11,6 +13,10 @@ module ActionCable
|
|
|
11
13
|
@stream_send = socket.env['stream.send']
|
|
12
14
|
|
|
13
15
|
@rack_hijack_io = nil
|
|
16
|
+
@write_lock = Mutex.new
|
|
17
|
+
|
|
18
|
+
@write_head = nil
|
|
19
|
+
@write_buffer = Queue.new
|
|
14
20
|
end
|
|
15
21
|
|
|
16
22
|
def each(&callback)
|
|
@@ -27,12 +33,62 @@ module ActionCable
|
|
|
27
33
|
end
|
|
28
34
|
|
|
29
35
|
def write(data)
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
if @stream_send
|
|
37
|
+
return @stream_send.call(data)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if @write_lock.try_lock
|
|
41
|
+
begin
|
|
42
|
+
if @write_head.nil? && @write_buffer.empty?
|
|
43
|
+
written = @rack_hijack_io.write_nonblock(data, exception: false)
|
|
44
|
+
|
|
45
|
+
case written
|
|
46
|
+
when :wait_writable
|
|
47
|
+
# proceed below
|
|
48
|
+
when data.bytesize
|
|
49
|
+
return data.bytesize
|
|
50
|
+
else
|
|
51
|
+
@write_head = data.byteslice(written, data.bytesize)
|
|
52
|
+
@event_loop.writes_pending @rack_hijack_io
|
|
53
|
+
|
|
54
|
+
return data.bytesize
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
ensure
|
|
58
|
+
@write_lock.unlock
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@write_buffer << data
|
|
63
|
+
@event_loop.writes_pending @rack_hijack_io
|
|
64
|
+
|
|
65
|
+
data.bytesize
|
|
32
66
|
rescue EOFError, Errno::ECONNRESET
|
|
33
67
|
@socket_object.client_gone
|
|
34
68
|
end
|
|
35
69
|
|
|
70
|
+
def flush_write_buffer
|
|
71
|
+
@write_lock.synchronize do
|
|
72
|
+
loop do
|
|
73
|
+
if @write_head.nil?
|
|
74
|
+
return true if @write_buffer.empty?
|
|
75
|
+
@write_head = @write_buffer.pop
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
written = @rack_hijack_io.write_nonblock(@write_head, exception: false)
|
|
79
|
+
case written
|
|
80
|
+
when :wait_writable
|
|
81
|
+
return false
|
|
82
|
+
when @write_head.bytesize
|
|
83
|
+
@write_head = nil
|
|
84
|
+
else
|
|
85
|
+
@write_head = @write_head.byteslice(written, @write_head.bytesize)
|
|
86
|
+
return false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
36
92
|
def receive(data)
|
|
37
93
|
@socket_object.parse(data)
|
|
38
94
|
end
|
|
@@ -5,7 +5,7 @@ module ActionCable
|
|
|
5
5
|
module Connection
|
|
6
6
|
class StreamEventLoop
|
|
7
7
|
def initialize
|
|
8
|
-
@nio = @thread = nil
|
|
8
|
+
@nio = @executor = @thread = nil
|
|
9
9
|
@map = {}
|
|
10
10
|
@stopping = false
|
|
11
11
|
@todo = Queue.new
|
|
@@ -20,13 +20,14 @@ module ActionCable
|
|
|
20
20
|
def post(task = nil, &block)
|
|
21
21
|
task ||= block
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
spawn
|
|
24
|
+
@executor << task
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def attach(io, stream)
|
|
27
28
|
@todo << lambda do
|
|
28
|
-
@map[io] =
|
|
29
|
-
@
|
|
29
|
+
@map[io] = @nio.register(io, :r)
|
|
30
|
+
@map[io].value = stream
|
|
30
31
|
end
|
|
31
32
|
wakeup
|
|
32
33
|
end
|
|
@@ -35,6 +36,16 @@ module ActionCable
|
|
|
35
36
|
@todo << lambda do
|
|
36
37
|
@nio.deregister io
|
|
37
38
|
@map.delete io
|
|
39
|
+
io.close
|
|
40
|
+
end
|
|
41
|
+
wakeup
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def writes_pending(io)
|
|
45
|
+
@todo << lambda do
|
|
46
|
+
if monitor = @map[io]
|
|
47
|
+
monitor.interests = :rw
|
|
48
|
+
end
|
|
38
49
|
end
|
|
39
50
|
wakeup
|
|
40
51
|
end
|
|
@@ -52,6 +63,13 @@ module ActionCable
|
|
|
52
63
|
return if @thread && @thread.status
|
|
53
64
|
|
|
54
65
|
@nio ||= NIO::Selector.new
|
|
66
|
+
|
|
67
|
+
@executor ||= Concurrent::ThreadPoolExecutor.new(
|
|
68
|
+
min_threads: 1,
|
|
69
|
+
max_threads: 10,
|
|
70
|
+
max_queue: 0,
|
|
71
|
+
)
|
|
72
|
+
|
|
55
73
|
@thread = Thread.new { run }
|
|
56
74
|
|
|
57
75
|
return true
|
|
@@ -77,12 +95,25 @@ module ActionCable
|
|
|
77
95
|
|
|
78
96
|
monitors.each do |monitor|
|
|
79
97
|
io = monitor.io
|
|
80
|
-
stream =
|
|
98
|
+
stream = monitor.value
|
|
81
99
|
|
|
82
100
|
begin
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
if monitor.writable?
|
|
102
|
+
if stream.flush_write_buffer
|
|
103
|
+
monitor.interests = :r
|
|
104
|
+
end
|
|
105
|
+
next unless monitor.readable?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
incoming = io.read_nonblock(4096, exception: false)
|
|
109
|
+
case incoming
|
|
110
|
+
when :wait_readable
|
|
111
|
+
next
|
|
112
|
+
when nil
|
|
113
|
+
stream.close
|
|
114
|
+
else
|
|
115
|
+
stream.receive incoming
|
|
116
|
+
end
|
|
86
117
|
rescue
|
|
87
118
|
# We expect one of EOFError or Errno::ECONNRESET in
|
|
88
119
|
# normal operation (when the client goes away). But if
|
|
@@ -26,10 +26,14 @@ module ActionCable
|
|
|
26
26
|
id_key = data['identifier']
|
|
27
27
|
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
|
|
28
28
|
|
|
29
|
+
return if subscriptions.key?(id_key)
|
|
30
|
+
|
|
29
31
|
subscription_klass = id_options[:channel].safe_constantize
|
|
30
32
|
|
|
31
33
|
if subscription_klass && ActionCable::Channel::Base >= subscription_klass
|
|
32
|
-
|
|
34
|
+
subscription = subscription_klass.new(connection, id_key, id_options)
|
|
35
|
+
subscriptions[id_key] = subscription
|
|
36
|
+
subscription.subscribe_to_channel
|
|
33
37
|
else
|
|
34
38
|
logger.error "Subscription class not found: #{id_options[:channel].inspect}"
|
|
35
39
|
end
|
|
@@ -37,9 +37,13 @@ module ActionCable
|
|
|
37
37
|
connections.each(&:close)
|
|
38
38
|
|
|
39
39
|
@mutex.synchronize do
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
# Shutdown the worker pool
|
|
41
|
+
@worker_pool.halt if @worker_pool
|
|
42
42
|
@worker_pool = nil
|
|
43
|
+
|
|
44
|
+
# Shutdown the pub/sub adapter
|
|
45
|
+
@pubsub.shutdown if @pubsub
|
|
46
|
+
@pubsub = nil
|
|
43
47
|
end
|
|
44
48
|
end
|
|
45
49
|
|
|
@@ -5,7 +5,7 @@ module ActionCable
|
|
|
5
5
|
class Configuration
|
|
6
6
|
attr_accessor :logger, :log_tags
|
|
7
7
|
attr_accessor :use_faye, :connection_class, :worker_pool_size
|
|
8
|
-
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
|
|
8
|
+
attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
|
|
9
9
|
attr_accessor :cable, :url, :mount_path
|
|
10
10
|
|
|
11
11
|
def initialize
|
|
@@ -15,6 +15,7 @@ module ActionCable
|
|
|
15
15
|
@worker_pool_size = 4
|
|
16
16
|
|
|
17
17
|
@disable_request_forgery_protection = false
|
|
18
|
+
@allow_same_origin_as_host = true
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
# Returns constant of subscription adapter specified in config/cable.yml.
|