actioncable 5.0.0.1 → 5.0.1.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f43514fcc46cfd5617c2d196bcff9428d27ec104
4
- data.tar.gz: 82c7c9deca20b9aa6dadcdd1b6e7d054596ef6c2
3
+ metadata.gz: 1e71477ad06403305b2d7cb74ae7154a6ca54d8a
4
+ data.tar.gz: 375e2e323c8c7e987410a5fff1f035d3dba94081
5
5
  SHA512:
6
- metadata.gz: e80bf03f4cd1aad741bcec6621f39070dfa1c2ac65b13aa3b7e6af4e2beedb863bf497249d469643b48e765a6198ff4222c175195040106f81cfc30df669258b
7
- data.tar.gz: c9f4ef262508a5aed0077be0f537bb66d3c47c57a2c3d61c3b47a3323ea5ffb92b926247cc5a90027055b10e8334070c56d14ac4602c2b894ca8c21d4bf11066
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 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.
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
- @defer_subscription_confirmation = false
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
- @defer_subscription_confirmation = true
224
+ @defer_subscription_confirmation_counter.increment
207
225
  end
208
226
 
209
227
  def defer_subscription_confirmation?
210
- @defer_subscription_confirmation
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
- transmit_subscription_confirmation
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
- if Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env['HTTP_ORIGIN'] }
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
- return @rack_hijack_io.write(data) if @rack_hijack_io
31
- return @stream_send.call(data) if @stream_send
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
- Concurrent.global_io_executor << task
23
+ spawn
24
+ @executor << task
24
25
  end
25
26
 
26
27
  def attach(io, stream)
27
28
  @todo << lambda do
28
- @map[io] = stream
29
- @nio.register(io, :r)
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 = @map[io]
98
+ stream = monitor.value
81
99
 
82
100
  begin
83
- stream.receive io.read_nonblock(4096)
84
- rescue IO::WaitReadable
85
- next
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
- subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options)
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
@@ -7,8 +7,8 @@ module ActionCable
7
7
  module VERSION
8
8
  MAJOR = 5
9
9
  MINOR = 0
10
- TINY = 0
11
- PRE = "1"
10
+ TINY = 1
11
+ PRE = "rc1"
12
12
 
13
13
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
14
14
  end
@@ -37,9 +37,13 @@ module ActionCable
37
37
  connections.each(&:close)
38
38
 
39
39
  @mutex.synchronize do
40
- worker_pool.halt if @worker_pool
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.
@@ -25,7 +25,7 @@ module ActionCable
25
25
  # Stop processing work: any work that has not already started
26
26
  # running will be discarded from the queue
27
27
  def halt
28
- @executor.kill
28
+ @executor.shutdown
29
29
  end
30
30
 
31
31
  def stopping?