rage-rb 1.7.0 → 1.9.0

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.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::Cable::Connection
4
+ # @private
5
+ attr_reader :__identified_by_map
6
+
7
+ # Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
8
+ # Common identifiers are `current_user` and `current_account`, but could be anything.
9
+ #
10
+ # @param identifiers [Symbol,Array<Symbol>]
11
+ def self.identified_by(*identifiers)
12
+ identifiers.each do |method_name|
13
+ define_method(method_name) do
14
+ @__identified_by_map[method_name]
15
+ end
16
+
17
+ define_method("#{method_name}=") do |data|
18
+ @__identified_by_map[method_name] = data
19
+ end
20
+
21
+ Rage::Cable::Channel.__prepare_id_method(method_name)
22
+ end
23
+ end
24
+
25
+ # @private
26
+ def initialize(env, identified_by = {})
27
+ @__env = env
28
+ @__identified_by_map = identified_by
29
+ end
30
+
31
+ # @private
32
+ def connect
33
+ end
34
+
35
+ # Reject the WebSocket connection.
36
+ def reject_unauthorized_connection
37
+ @rejected = true
38
+ end
39
+
40
+ def rejected?
41
+ !!@rejected
42
+ end
43
+
44
+ # Get the request object. See {Rage::Request}.
45
+ #
46
+ # @return [Rage::Request]
47
+ def request
48
+ @__request ||= Rage::Request.new(@__env)
49
+ end
50
+
51
+ # Get the cookie object. See {Rage::Cookies}.
52
+ #
53
+ # @return [Rage::Cookies]
54
+ def cookies
55
+ @__cookies ||= Rage::Cookies.new(@__env, ReadOnlyHash.new)
56
+ end
57
+
58
+ # Get the session object. See {Rage::Session}.
59
+ #
60
+ # @return [Rage::Session]
61
+ def session
62
+ @__session ||= Rage::Session.new(cookies)
63
+ end
64
+
65
+ # Get URL query parameters.
66
+ #
67
+ # @return [Hash{Symbol=>String,Array,Hash}]
68
+ def params
69
+ @__params ||= Iodine::Rack::Utils.parse_nested_query(@__env["QUERY_STRING"])
70
+ end
71
+
72
+ # @private
73
+ class ReadOnlyHash < Hash
74
+ def []=(_, _)
75
+ raise "Cookies cannot be set for WebSocket clients"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # A protocol defines the structure, rules and semantics for exchanging data between the client and the server.
5
+ # The class that defines a protocol should respond to the following methods:
6
+ #
7
+ # * `protocol_definition`
8
+ # * `init`
9
+ # * `on_open`
10
+ # * `on_message`
11
+ # * `serialize`
12
+ # * `subscribe`
13
+ # * `broadcast`
14
+ #
15
+ # The two optional methods are:
16
+ #
17
+ # * `on_shutdown`
18
+ # * `on_close`
19
+ #
20
+ class Rage::Cable::Protocol::ActioncableV1Json
21
+ module TYPE
22
+ WELCOME = "welcome"
23
+ DISCONNECT = "disconnect"
24
+ PING = "ping"
25
+ CONFIRM = "confirm_subscription"
26
+ REJECT = "reject_subscription"
27
+ end
28
+
29
+ module REASON
30
+ UNAUTHORIZED = "unauthorized"
31
+ INVALID = "invalid_request"
32
+ end
33
+
34
+ module COMMAND
35
+ SUBSCRIBE = "subscribe"
36
+ MESSAGE = "message"
37
+ end
38
+
39
+ module MESSAGES
40
+ WELCOME = { type: TYPE::WELCOME }.to_json
41
+ UNAUTHORIZED = { type: TYPE::DISCONNECT, reason: REASON::UNAUTHORIZED, reconnect: false }.to_json
42
+ INVALID = { type: TYPE::DISCONNECT, reason: REASON::INVALID, reconnect: true }.to_json
43
+ end
44
+
45
+ HANDSHAKE_HEADERS = { "Sec-WebSocket-Protocol" => "actioncable-v1-json" }
46
+
47
+ # The method defines the headers to send to the client after the handshake process.
48
+ def self.protocol_definition
49
+ HANDSHAKE_HEADERS
50
+ end
51
+
52
+ # This method serves as a constructor to prepare the object or set up recurring tasks (e.g. heartbeats).
53
+ #
54
+ # @param router [Rage::Cable::Router]
55
+ def self.init(router)
56
+ @router = router
57
+
58
+ ping_counter = Time.now.to_i
59
+ ::Iodine.run_every(3000) do
60
+ ping_counter += 1
61
+ ::Iodine.publish("cable:ping", { type: TYPE::PING, message: ping_counter }.to_json)
62
+ end
63
+
64
+ # Hash<String(stream name) => Array<Hash>(subscription params)>
65
+ @subscription_identifiers = Hash.new { |hash, key| hash[key] = [] }
66
+ end
67
+
68
+ # The method is called any time a new WebSocket connection is established.
69
+ # It is expected to call {Rage::Cable::Router#process_connection} and handle its return value.
70
+ #
71
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
72
+ # @see Rage::Cable::Router
73
+ def self.on_open(connection)
74
+ accepted = @router.process_connection(connection)
75
+
76
+ if accepted
77
+ connection.subscribe("cable:ping")
78
+ connection.write(MESSAGES::WELCOME)
79
+ else
80
+ connection.write(MESSAGES::UNAUTHORIZED)
81
+ connection.close
82
+ end
83
+ end
84
+
85
+ # The method processes messages from existing connections. It should parse the message, call either
86
+ # {Rage::Cable::Router#process_subscription} or {Rage::Cable::Router#process_message}, and handle its return value.
87
+ #
88
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
89
+ # @param raw_data [String] the message body
90
+ # @see Rage::Cable::Router
91
+ def self.on_message(connection, raw_data)
92
+ parsed_data = Rage::ParamsParser.json_parse(raw_data)
93
+
94
+ command, identifier = parsed_data[:command], parsed_data[:identifier]
95
+ params = Rage::ParamsParser.json_parse(identifier)
96
+
97
+ # process subscription messages
98
+ if command == COMMAND::SUBSCRIBE
99
+ status = @router.process_subscription(connection, identifier, params[:channel], params)
100
+ if status == :subscribed
101
+ connection.write({ identifier: identifier, type: TYPE::CONFIRM }.to_json)
102
+ elsif status == :rejected
103
+ connection.write({ identifier: identifier, type: TYPE::REJECT }.to_json)
104
+ elsif status == :invalid
105
+ connection.write(MESSAGES::INVALID)
106
+ end
107
+
108
+ return
109
+ end
110
+
111
+ # process data messages;
112
+ # plain `JSON` is used here to conform with the ActionCable API that passes `data` as a Hash with string keys;
113
+ data = JSON.parse(parsed_data[:data])
114
+
115
+ message_status = if command == COMMAND::MESSAGE && data.has_key?("action")
116
+ @router.process_message(connection, identifier, data["action"].to_sym, data)
117
+
118
+ elsif command == COMMAND::MESSAGE
119
+ @router.process_message(connection, identifier, :receive, data)
120
+ end
121
+
122
+ unless message_status == :processed
123
+ connection.write(MESSAGES::INVALID)
124
+ end
125
+ end
126
+
127
+ # The method should process client disconnections and call {Rage::Cable::Router#process_message}.
128
+ #
129
+ # @note This method is optional.
130
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
131
+ # @see Rage::Cable::Router
132
+ def self.on_close(connection)
133
+ @router.process_disconnection(connection)
134
+ end
135
+
136
+ # Serialize a Ruby object into the format the client would understand.
137
+ #
138
+ # @param params [Hash] parameters associated with the client
139
+ # @param data [Object] the object to serialize
140
+ def self.serialize(params, data)
141
+ { identifier: params.to_json, message: data }.to_json
142
+ end
143
+
144
+ # Subscribe to a stream.
145
+ #
146
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
147
+ # @param name [String] the stream name
148
+ # @param params [Hash] parameters associated with the client
149
+ def self.subscribe(connection, name, params)
150
+ connection.subscribe("cable:#{name}:#{params.hash}")
151
+ @subscription_identifiers[name] << params unless @subscription_identifiers[name].include?(params)
152
+ end
153
+
154
+ # Broadcast data to all clients connected to a stream.
155
+ #
156
+ # @param name [String] the stream name
157
+ # @param data [Object] the data to send
158
+ def self.broadcast(name, data)
159
+ i, identifiers = 0, @subscription_identifiers[name]
160
+
161
+ while i < identifiers.length
162
+ params = identifiers[i]
163
+ ::Iodine.publish("cable:#{name}:#{params.hash}", serialize(params, data))
164
+ i += 1
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::Cable::Router
4
+ # @private
5
+ def initialize
6
+ # Hash<String(channel name) => Proc(new channel instance)>
7
+ @channels_map = {}
8
+ init_connection_class
9
+ end
10
+
11
+ # Calls the `connect` method on the `Connection` class to handle authentication.
12
+ #
13
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
14
+ # @return [true] if the connection was accepted
15
+ # @return [false] if the connection was rejected
16
+ def process_connection(connection)
17
+ cable_connection = @connection_class.new(connection.env)
18
+ cable_connection.connect
19
+
20
+ if cable_connection.rejected?
21
+ Rage.logger.debug { "An unauthorized connection attempt was rejected" }
22
+ else
23
+ connection.env["rage.identified_by"] = cable_connection.__identified_by_map
24
+ connection.env["rage.cable"] = {}
25
+ end
26
+
27
+ !cable_connection.rejected?
28
+ end
29
+
30
+ # Calls the `subscribed` method on the specified channel.
31
+ #
32
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
33
+ # @param identifier [String] the identifier of the subscription
34
+ # @param channel_name [String] the name of the channel class
35
+ # @param params [Hash] the params hash associated with the subscription
36
+ #
37
+ # @return [:invalid] if the subscription class does not exist
38
+ # @return [:rejected] if the subscription was rejected
39
+ # @return [:subscribed] if the subscription was accepted
40
+ def process_subscription(connection, identifier, channel_name, params)
41
+ channel_class = @channels_map[channel_name] || begin
42
+ begin
43
+ klass = Object.const_get(channel_name)
44
+ rescue NameError
45
+ nil
46
+ end
47
+
48
+ if klass.nil? || !klass.ancestors.include?(Rage::Cable::Channel)
49
+ Rage.logger.debug { "Subscription class not found: #{channel_name}" }
50
+ return :invalid
51
+ end
52
+
53
+ klass.__register_actions.tap do |available_actions|
54
+ Rage.logger.debug { "Compiled #{channel_name}. Available remote actions: #{available_actions}." }
55
+ end
56
+
57
+ @channels_map[channel_name] = klass
58
+ end
59
+
60
+ channel = channel_class.new(connection, params, connection.env["rage.identified_by"])
61
+ channel.__run_action(:subscribed)
62
+
63
+ if channel.subscription_rejected?
64
+ Rage.logger.debug { "#{channel_name} is transmitting the subscription rejection" }
65
+ # if the subscription is rejected in the `subscribed` method, ActionCable will additionally run
66
+ # the `unsubscribed` method; this makes little sense to me as the client was never subscribed in
67
+ # the first place; additionally, I don't think this behaviour is documented anywhere;
68
+ # so, I'm going to leave this line commented out for now;
69
+ # channel.__run_action(:unsubscribed)
70
+ :rejected
71
+ else
72
+ Rage.logger.debug { "#{channel_name} is transmitting the subscription confirmation" }
73
+ connection.env["rage.cable"][identifier] = channel
74
+ :subscribed
75
+ end
76
+ end
77
+
78
+ # Calls the handler method on the specified channel.
79
+ #
80
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
81
+ # @param identifier [String] the identifier of the subscription
82
+ # @param action_name [Symbol] the name of the handler method
83
+ # @param data [Object] the data sent by the client
84
+ #
85
+ # @return [:no_subscription] if the client is not subscribed to the specified channel
86
+ # @return [:unknown_action] if the action does not exist on the specified channel
87
+ # @return [:processed] if the message has been successfully processed
88
+ def process_message(connection, identifier, action_name, data)
89
+ channel = connection.env["rage.cable"][identifier]
90
+ unless channel
91
+ Rage.logger.debug { "Unable to find the subscription" }
92
+ return :no_subscription
93
+ end
94
+
95
+ if channel.__has_action?(action_name)
96
+ channel.__run_action(action_name, data)
97
+ :processed
98
+ else
99
+ Rage.logger.debug { "Unable to process #{channel.class.name}##{action_name}" }
100
+ :unknown_action
101
+ end
102
+ end
103
+
104
+ # Runs the `unsubscribed` methods on all the channels the client is subscribed to.
105
+ #
106
+ # @param connection [Rage::Cable::WebSocketConnection] the connection object
107
+ def process_disconnection(connection)
108
+ connection.env["rage.cable"]&.each do |_, channel|
109
+ channel.__run_action(:unsubscribed)
110
+ end
111
+
112
+ if @connection_can_disconnect
113
+ cable_connection = @connection_class.new(connection.env, connection.env["rage.identified_by"])
114
+ cable_connection.disconnect
115
+ end
116
+ end
117
+
118
+ # @private
119
+ def reset
120
+ @channels_map.clear
121
+ init_connection_class
122
+ end
123
+
124
+ private
125
+
126
+ def init_connection_class
127
+ @connection_class = if Object.const_defined?("RageCable::Connection")
128
+ RageCable::Connection
129
+ elsif Object.const_defined?("ApplicationCable::Connection")
130
+ ApplicationCable::Connection
131
+ else
132
+ puts "WARNING: Could not find the RageCable connection class! All connections will be accepted by default."
133
+ Rage::Cable::Connection
134
+ end
135
+
136
+ @connection_can_disconnect = @connection_class.method_defined?(:disconnect)
137
+ end
138
+ end
data/lib/rage/cli.rb CHANGED
@@ -29,12 +29,15 @@ module Rage
29
29
  app = ::Rack::Builder.parse_file(options[:config] || "config.ru")
30
30
  app = app[0] if app.is_a?(Array)
31
31
 
32
- port = options[:port] || Rage.config.server.port
33
- address = options[:binding] || (Rage.env.production? ? "0.0.0.0" : "localhost")
34
- timeout = Rage.config.server.timeout
35
- max_clients = Rage.config.server.max_clients
32
+ server_options = { service: :http, handler: app }
36
33
 
37
- ::Iodine.listen service: :http, handler: app, port: port, address: address, timeout: timeout, max_clients: max_clients
34
+ server_options[:port] = options[:port] || Rage.config.server.port
35
+ server_options[:address] = options[:binding] || (Rage.env.production? ? "0.0.0.0" : "localhost")
36
+ server_options[:timeout] = Rage.config.server.timeout
37
+ server_options[:max_clients] = Rage.config.server.max_clients
38
+ server_options[:public] = Rage.config.public_file_server.enabled ? Rage.root.join("public").to_s : nil
39
+
40
+ ::Iodine.listen(**server_options)
38
41
  ::Iodine.threads = Rage.config.server.threads_count
39
42
  ::Iodine.workers = Rage.config.server.workers_count
40
43
 
@@ -124,6 +127,10 @@ module Rage
124
127
 
125
128
  def set_env(options)
126
129
  ENV["RAGE_ENV"] = options[:environment] if options[:environment]
130
+
131
+ # at this point we don't know whether the app is running in standalone or Rails mode;
132
+ # we set both variables to make sure applications are running in the same environment;
133
+ ENV["RAILS_ENV"] = ENV["RAGE_ENV"] if ENV["RAGE_ENV"] && ENV["RAILS_ENV"] != ENV["RAGE_ENV"]
127
134
  end
128
135
  end
129
136
 
@@ -30,8 +30,13 @@ class Rage::CodeLoader
30
30
 
31
31
  @reloading = true
32
32
  @loader.reload
33
+
33
34
  Rage.__router.reset_routes
34
35
  load("#{Rage.root}/config/routes.rb")
36
+
37
+ unless Rage.autoload?(:Cable) # the `Cable` component is loaded
38
+ Rage::Cable.__router.reset
39
+ end
35
40
  end
36
41
 
37
42
  # in Rails mode - reset the routes; everything else will be done by Rails
@@ -40,6 +45,10 @@ class Rage::CodeLoader
40
45
 
41
46
  @reloading = true
42
47
  Rage.__router.reset_routes
48
+
49
+ unless Rage.autoload?(:Cable) # the `Cable` component is loaded
50
+ Rage::Cable.__router.reset
51
+ end
43
52
  end
44
53
 
45
54
  def reloading?
@@ -1,6 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  ##
4
+ # `Rage.configure` can be used to adjust the behavior of your Rage application:
5
+ #
6
+ # ```ruby
7
+ # Rage.configure do
8
+ # config.logger = Rage::Logger.new(STDOUT)
9
+ # config.server.workers_count = 2
10
+ # end
11
+ # ```
12
+ #
4
13
  # # General Configuration
5
14
  #
6
15
  # • _config.logger_
@@ -93,6 +102,26 @@
93
102
  #
94
103
  # > Specifies connection timeout.
95
104
  #
105
+ # # Static file server
106
+ #
107
+ # • _config.public_file_server.enabled_
108
+ #
109
+ # > Configures whether Rage should serve static files from the public directory. Defaults to `false`.
110
+ #
111
+ # # Cable Configuration
112
+ #
113
+ # • _config.cable.protocol_
114
+ #
115
+ # > Specifies the protocol the server will use. The only value currently supported is `Rage::Cable::Protocol::ActioncableV1Json`. The client application will need to use [@rails/actioncable](https://www.npmjs.com/package/@rails/actioncable) to talk to the server.
116
+ #
117
+ # • _config.cable.allowed_request_origins_
118
+ #
119
+ # > Restricts the server to only accept requests from specified origins. The origins can be instances of strings or regular expressions, against which a check for the match will be performed.
120
+ #
121
+ # • _config.cable.disable_request_forgery_protection_
122
+ #
123
+ # > Allows requests from any origin.
124
+ #
96
125
  # # Transient Settings
97
126
  #
98
127
  # The settings described in this section should be configured using **environment variables** and are either temporary or will become the default in the future.
@@ -138,6 +167,14 @@ class Rage::Configuration
138
167
  @middleware ||= Middleware.new
139
168
  end
140
169
 
170
+ def cable
171
+ @cable ||= Cable.new
172
+ end
173
+
174
+ def public_file_server
175
+ @public_file_server ||= PublicFileServer.new
176
+ end
177
+
141
178
  def internal
142
179
  @internal ||= Internal.new
143
180
  end
@@ -193,6 +230,36 @@ class Rage::Configuration
193
230
  end
194
231
  end
195
232
 
233
+ class Cable
234
+ attr_accessor :protocol, :allowed_request_origins, :disable_request_forgery_protection
235
+
236
+ def initialize
237
+ @protocol = Rage::Cable::Protocol::ActioncableV1Json
238
+ @allowed_request_origins = if Rage.env.development? || Rage.env.test?
239
+ /localhost/
240
+ end
241
+ end
242
+
243
+ # @private
244
+ def middlewares
245
+ @middlewares ||= begin
246
+ origin_middleware = if @disable_request_forgery_protection
247
+ []
248
+ else
249
+ [[Rage::OriginValidator, Array(@allowed_request_origins), nil]]
250
+ end
251
+
252
+ origin_middleware + Rage.config.middleware.middlewares.reject do |middleware, _, _|
253
+ middleware == Rage::FiberWrapper
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ class PublicFileServer
260
+ attr_accessor :enabled
261
+ end
262
+
196
263
  # @private
197
264
  class Internal
198
265
  attr_accessor :rails_mode
@@ -142,6 +142,7 @@ class RageController::API
142
142
 
143
143
  # @private
144
144
  attr_writer :__before_actions, :__after_actions, :__rescue_handlers
145
+ # @private
145
146
  attr_accessor :__wrap_parameters_key, :__wrap_parameters_options
146
147
 
147
148
  # @private
@@ -299,14 +300,12 @@ class RageController::API
299
300
  @__before_actions[i] = action
300
301
  end
301
302
 
302
- # Initialize controller params wrapping into a nested hash.
303
- # If initialized, params wrapping logic will be added to the controller.
304
- # Params get wrapped only if the CONTENT_TYPE header is present and params hash doesn't contain a param that
305
- # has the same name as the wrapper key.
303
+ # Wraps the parameters hash into a nested hash. This will allow clients to submit requests without having to specify any root elements.
304
+ # Params get wrapped only if the `Content-Type` header is present and the `params` hash doesn't contain a param with the same name as the wrapper key.
306
305
  #
307
- # @param key [Symbol] key that the wrapped params hash will nested under
308
- # @param include [Array] array of params that should be included to the wrapped params hash
309
- # @param exclude [Array] array of params that should be excluded from the wrapped params hash
306
+ # @param key [Symbol] the wrapper key
307
+ # @param include [Symbol, Array<Symbol>] the list of attribute names which parameters wrapper will wrap into a nested hash
308
+ # @param exclude [Symbol, Array<Symbol>] the list of attribute names which parameters wrapper will exclude from a nested hash
310
309
  # @example
311
310
  # wrap_parameters :user, include: %i[name age]
312
311
  # @example
@@ -366,13 +365,13 @@ class RageController::API
366
365
  # Get the cookie object. See {Rage::Cookies}.
367
366
  # @return [Rage::Cookies]
368
367
  def cookies
369
- @cookies ||= Rage::Cookies.new(@__env, self)
368
+ @cookies ||= Rage::Cookies.new(@__env, @__headers)
370
369
  end
371
370
 
372
371
  # Get the session object. See {Rage::Session}.
373
372
  # @return [Rage::Session]
374
373
  def session
375
- @session ||= Rage::Session.new(self)
374
+ @session ||= Rage::Session.new(cookies)
376
375
  end
377
376
 
378
377
  # Send a response to the client.
data/lib/rage/cookies.rb CHANGED
@@ -14,9 +14,9 @@ end
14
14
 
15
15
  class Rage::Cookies
16
16
  # @private
17
- def initialize(env, controller)
17
+ def initialize(env, headers)
18
18
  @env = env
19
- @headers = controller.headers
19
+ @headers = headers
20
20
  @request_cookies = {}
21
21
  @parsed = false
22
22
 
@@ -148,10 +148,14 @@ module Rage::Ext::ActiveRecord::ConnectionPool
148
148
  end
149
149
 
150
150
  # Yields a connection from the connection pool to the block.
151
- def with_connection
152
- yield connection
151
+ def with_connection(_ = nil)
152
+ unless (conn = @__in_use[Fiber.current])
153
+ conn = connection
154
+ fresh_connection = true
155
+ end
156
+ yield conn
153
157
  ensure
154
- release_connection
158
+ release_connection if fresh_connection
155
159
  end
156
160
 
157
161
  # Returns an array containing the connections currently in the pool.
@@ -230,6 +234,10 @@ module Rage::Ext::ActiveRecord::ConnectionPool
230
234
  connection
231
235
  end
232
236
 
237
+ def lease_connection
238
+ connection
239
+ end
240
+
233
241
  # Check in a database connection back into the pool, indicating that you no longer need this connection.
234
242
  def checkin(conn)
235
243
  fiber = @__in_use.key(conn)
@@ -31,6 +31,6 @@ if defined?(ActiveRecord::ConnectionAdapters::ConnectionPool)
31
31
  end
32
32
 
33
33
  # patch `ActiveRecord::ConnectionPool`
34
- if defined?(ActiveRecord) && ENV["RAGE_PATCH_AR_POOL"]
34
+ if defined?(ActiveRecord) && ENV["RAGE_PATCH_AR_POOL"] && !Rage.env.test?
35
35
  Rage.patch_active_record_connection_pool
36
36
  end
@@ -7,7 +7,9 @@
7
7
  class Rage::FiberWrapper
8
8
  def initialize(app)
9
9
  Iodine.on_state(:on_start) do
10
- Fiber.set_scheduler(Rage::FiberScheduler.new)
10
+ unless Fiber.scheduler
11
+ Fiber.set_scheduler(Rage::FiberScheduler.new)
12
+ end
11
13
  end
12
14
  @app = app
13
15
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::OriginValidator
4
+ def initialize(app, *allowed_origins)
5
+ @app = app
6
+ @validator = build_validator(allowed_origins)
7
+ end
8
+
9
+ def call(env)
10
+ if @validator.call(env)
11
+ @app.call(env)
12
+ else
13
+ Rage.logger.error("Request origin not allowed: #{env["HTTP_ORIGIN"]}")
14
+ [404, {}, ["Not Found"]]
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def build_validator(allowed_origins)
21
+ if allowed_origins.empty?
22
+ ->(env) { false }
23
+ else
24
+ origins_eval = allowed_origins.map { |origin|
25
+ origin.is_a?(Regexp) ?
26
+ "origin =~ /#{origin.source}/.freeze" :
27
+ "origin == '#{origin}'.freeze"
28
+ }.join(" || ")
29
+
30
+ eval <<-RUBY
31
+ ->(env) do
32
+ origin = env["HTTP_ORIGIN".freeze]
33
+ #{origins_eval}
34
+ end
35
+ RUBY
36
+ end
37
+ end
38
+ end
data/lib/rage/rails.rb CHANGED
@@ -44,7 +44,10 @@ end
44
44
  # clone Rails logger
45
45
  Rails.configuration.after_initialize do
46
46
  if Rails.logger && !Rage.logger
47
- rails_logdev = Rails.logger.instance_variable_get(:@logdev)
47
+ rails_logdev = Rails.logger.yield_self { |logger|
48
+ logger.respond_to?(:broadcasts) ? logger.broadcasts.last : logger
49
+ }.instance_variable_get(:@logdev)
50
+
48
51
  Rage.configure do
49
52
  config.logger = Rage::Logger.new(rails_logdev.dev) if rails_logdev.is_a?(Logger::LogDevice)
50
53
  end