rage-rb 1.6.0 → 1.8.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/Gemfile +10 -7
  4. data/OVERVIEW.md +1 -1
  5. data/README.md +25 -9
  6. data/lib/rage/all.rb +1 -0
  7. data/lib/rage/cable/cable.rb +130 -0
  8. data/lib/rage/cable/channel.rb +452 -0
  9. data/lib/rage/cable/connection.rb +78 -0
  10. data/lib/rage/cable/protocol/actioncable_v1_json.rb +167 -0
  11. data/lib/rage/cable/router.rb +138 -0
  12. data/lib/rage/cli.rb +2 -1
  13. data/lib/rage/code_loader.rb +9 -0
  14. data/lib/rage/configuration.rb +53 -0
  15. data/lib/rage/controller/api.rb +51 -13
  16. data/lib/rage/cookies.rb +7 -9
  17. data/lib/rage/ext/active_record/connection_pool.rb +1 -1
  18. data/lib/rage/fiber.rb +3 -3
  19. data/lib/rage/fiber_scheduler.rb +1 -1
  20. data/lib/rage/logger/json_formatter.rb +1 -1
  21. data/lib/rage/logger/logger.rb +1 -1
  22. data/lib/rage/logger/text_formatter.rb +1 -1
  23. data/lib/rage/middleware/cors.rb +2 -2
  24. data/lib/rage/middleware/fiber_wrapper.rb +3 -1
  25. data/lib/rage/middleware/origin_validator.rb +38 -0
  26. data/lib/rage/middleware/reloader.rb +1 -1
  27. data/lib/rage/params_parser.rb +1 -1
  28. data/lib/rage/router/backend.rb +4 -6
  29. data/lib/rage/router/constrainer.rb +1 -1
  30. data/lib/rage/router/dsl.rb +7 -7
  31. data/lib/rage/router/dsl_plugins/legacy_hash_notation.rb +1 -1
  32. data/lib/rage/router/dsl_plugins/legacy_root_notation.rb +1 -1
  33. data/lib/rage/router/handler_storage.rb +1 -1
  34. data/lib/rage/session.rb +2 -2
  35. data/lib/rage/setup.rb +5 -1
  36. data/lib/rage/sidekiq_session.rb +1 -1
  37. data/lib/rage/version.rb +1 -1
  38. data/lib/rage-rb.rb +23 -15
  39. data/rage.gemspec +1 -1
  40. metadata +8 -2
@@ -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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "thor"
3
4
  require "rack"
4
5
 
@@ -40,7 +41,7 @@ module Rage
40
41
  ::Iodine.start
41
42
  end
42
43
 
43
- desc 'routes', 'List all routes.'
44
+ desc "routes", "List all routes."
44
45
  option :grep, aliases: "-g", desc: "Filter routes by pattern"
45
46
  option :help, aliases: "-h", desc: "Show this message."
46
47
  def routes
@@ -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,20 @@
93
102
  #
94
103
  # > Specifies connection timeout.
95
104
  #
105
+ # # Cable Configuration
106
+ #
107
+ # • _config.cable.protocol_
108
+ #
109
+ # > 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.
110
+ #
111
+ # • _config.cable.allowed_request_origins_
112
+ #
113
+ # > 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.
114
+ #
115
+ # • _config.cable.disable_request_forgery_protection_
116
+ #
117
+ # > Allows requests from any origin.
118
+ #
96
119
  # # Transient Settings
97
120
  #
98
121
  # 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 +161,10 @@ class Rage::Configuration
138
161
  @middleware ||= Middleware.new
139
162
  end
140
163
 
164
+ def cable
165
+ @cable ||= Cable.new
166
+ end
167
+
141
168
  def internal
142
169
  @internal ||= Internal.new
143
170
  end
@@ -193,6 +220,32 @@ class Rage::Configuration
193
220
  end
194
221
  end
195
222
 
223
+ class Cable
224
+ attr_accessor :protocol, :allowed_request_origins, :disable_request_forgery_protection
225
+
226
+ def initialize
227
+ @protocol = Rage::Cable::Protocol::ActioncableV1Json
228
+ @allowed_request_origins = if Rage.env.development? || Rage.env.test?
229
+ /localhost/
230
+ end
231
+ end
232
+
233
+ # @private
234
+ def middlewares
235
+ @middlewares ||= begin
236
+ origin_middleware = if @disable_request_forgery_protection
237
+ []
238
+ else
239
+ [[Rage::OriginValidator, Array(@allowed_request_origins), nil]]
240
+ end
241
+
242
+ origin_middleware + Rage.config.middleware.middlewares.reject do |middleware, _, _|
243
+ middleware == Rage::FiberWrapper
244
+ end
245
+ end
246
+ end
247
+ end
248
+
196
249
  # @private
197
250
  class Internal
198
251
  attr_accessor :rails_mode
@@ -7,6 +7,7 @@ class RageController::API
7
7
  # registering means defining a new method which calls the action, makes additional calls (e.g. before actions) and
8
8
  # sends a correct response down to the server;
9
9
  # returns the name of the newly defined method;
10
+ # rubocop:disable Layout/IndentationWidth, Layout/EndAlignment, Layout/HeredocIndentation
10
11
  def __register_action(action)
11
12
  raise Rage::Errors::RouterError, "The action '#{action}' could not be found for #{self}" unless method_defined?(action)
12
13
 
@@ -65,7 +66,7 @@ class RageController::API
65
66
  lines = @__rescue_handlers.map do |klasses, handler|
66
67
  <<~RUBY
67
68
  rescue #{klasses.join(", ")} => __e
68
- #{handler}(__e)
69
+ #{instance_method(handler).arity == 0 ? handler : "#{handler}(__e)"}
69
70
  [@__status, @__headers, @__body]
70
71
  RUBY
71
72
  end
@@ -77,7 +78,24 @@ class RageController::API
77
78
 
78
79
  activerecord_loaded = defined?(::ActiveRecord)
79
80
 
80
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
81
+ wrap_parameters_chunk = if __wrap_parameters_key
82
+ <<~RUBY
83
+ wrap_key = self.class.__wrap_parameters_key
84
+ if !@__params.key?(wrap_key) && @__env["CONTENT_TYPE"]
85
+ wrap_options = self.class.__wrap_parameters_options
86
+ wrapped_params = if wrap_options[:include].any?
87
+ @__params.slice(*wrap_options[:include])
88
+ else
89
+ params_to_exclude_by_default = %i[action controller]
90
+ @__params.except(*(wrap_options[:exclude] + params_to_exclude_by_default))
91
+ end
92
+
93
+ @__params[wrap_key] = wrapped_params
94
+ end
95
+ RUBY
96
+ end
97
+
98
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
81
99
  def __run_#{action}
82
100
  #{if activerecord_loaded
83
101
  <<~RUBY
@@ -85,6 +103,7 @@ class RageController::API
85
103
  RUBY
86
104
  end}
87
105
 
106
+ #{wrap_parameters_chunk}
88
107
  #{before_actions_chunk}
89
108
  #{action}
90
109
 
@@ -119,9 +138,12 @@ class RageController::API
119
138
  end
120
139
  RUBY
121
140
  end
141
+ # rubocop:enable all
122
142
 
123
143
  # @private
124
144
  attr_writer :__before_actions, :__after_actions, :__rescue_handlers
145
+ # @private
146
+ attr_accessor :__wrap_parameters_key, :__wrap_parameters_options
125
147
 
126
148
  # @private
127
149
  # pass the variable down to the child; the child will continue to use it until changes need to be made;
@@ -130,6 +152,8 @@ class RageController::API
130
152
  klass.__before_actions = @__before_actions.freeze
131
153
  klass.__after_actions = @__after_actions.freeze
132
154
  klass.__rescue_handlers = @__rescue_handlers.freeze
155
+ klass.__wrap_parameters_key = __wrap_parameters_key
156
+ klass.__wrap_parameters_options = __wrap_parameters_options
133
157
  end
134
158
 
135
159
  # @private
@@ -151,18 +175,17 @@ class RageController::API
151
175
  # Register a global exception handler. Handlers are inherited and matched from bottom to top.
152
176
  #
153
177
  # @param klasses [Class, Array<Class>] exception classes to watch on
154
- # @param with [Symbol] the name of a handler method. The method must take one argument, which is the raised exception. Alternatively, you can pass a block, which must also take one argument.
178
+ # @param with [Symbol] the name of a handler method. Alternatively, you can pass a block.
155
179
  # @example
156
180
  # rescue_from User::NotAuthorized, with: :deny_access
157
181
  #
158
- # def deny_access(exception)
182
+ # def deny_access
159
183
  # head :forbidden
160
184
  # end
161
185
  # @example
162
- # rescue_from User::NotAuthorized do |_|
163
- # head :forbidden
186
+ # rescue_from User::NotAuthorized do |exception|
187
+ # render json: { message: exception.message }, status: :forbidden
164
188
  # end
165
- # @note Unlike in Rails, the handler must always take an argument. Use `_` if you don't care about the actual exception.
166
189
  def rescue_from(*klasses, with: nil, &block)
167
190
  unless with
168
191
  if block_given?
@@ -183,7 +206,7 @@ class RageController::API
183
206
 
184
207
  # Register a new `before_action` hook. Calls with the same `action_name` will overwrite the previous ones.
185
208
  #
186
- # @param action_name [String, nil] the name of the callback to add
209
+ # @param action_name [Symbol, nil] the name of the callback to add
187
210
  # @param [Hash] opts action options
188
211
  # @option opts [Symbol, Array<Symbol>] :only restrict the callback to run only for specific actions
189
212
  # @option opts [Symbol, Array<Symbol>] :except restrict the callback to run for all actions except specified
@@ -215,7 +238,7 @@ class RageController::API
215
238
 
216
239
  if @__before_actions.nil?
217
240
  @__before_actions = [action]
218
- elsif i = @__before_actions.find_index { |a| a[:name] == action_name }
241
+ elsif (i = @__before_actions.find_index { |a| a[:name] == action_name })
219
242
  @__before_actions[i] = action
220
243
  else
221
244
  @__before_actions << action
@@ -241,7 +264,7 @@ class RageController::API
241
264
 
242
265
  if @__after_actions.nil?
243
266
  @__after_actions = [action]
244
- elsif i = @__after_actions.find_index { |a| a[:name] == action_name }
267
+ elsif (i = @__after_actions.find_index { |a| a[:name] == action_name })
245
268
  @__after_actions[i] = action
246
269
  else
247
270
  @__after_actions << action
@@ -277,6 +300,21 @@ class RageController::API
277
300
  @__before_actions[i] = action
278
301
  end
279
302
 
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.
305
+ #
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
309
+ # @example
310
+ # wrap_parameters :user, include: %i[name age]
311
+ # @example
312
+ # wrap_parameters :user, exclude: %i[address]
313
+ def wrap_parameters(key, include: [], exclude: [])
314
+ @__wrap_parameters_key = key
315
+ @__wrap_parameters_options = { include:, exclude: }
316
+ end
317
+
280
318
  private
281
319
 
282
320
  # used by `before_action` and `after_action`
@@ -287,7 +325,7 @@ class RageController::API
287
325
  raise ArgumentError, "No handler provided. Pass the `action_name` parameter or provide a block."
288
326
  end
289
327
 
290
- _only, _except, _if, _unless = opts.values_at(:only, :except, :if, :unless)
328
+ _only, _except, _if, _unless = opts.values_at(:only, :except, :if, :unless)
291
329
 
292
330
  action = {
293
331
  name: action_name,
@@ -327,13 +365,13 @@ class RageController::API
327
365
  # Get the cookie object. See {Rage::Cookies}.
328
366
  # @return [Rage::Cookies]
329
367
  def cookies
330
- @cookies ||= Rage::Cookies.new(@__env, self)
368
+ @cookies ||= Rage::Cookies.new(@__env, @__headers)
331
369
  end
332
370
 
333
371
  # Get the session object. See {Rage::Session}.
334
372
  # @return [Rage::Session]
335
373
  def session
336
- @session ||= Rage::Session.new(self)
374
+ @session ||= Rage::Session.new(cookies)
337
375
  end
338
376
 
339
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
 
@@ -97,7 +97,7 @@ class Rage::Cookies
97
97
  return
98
98
  end
99
99
 
100
- if domain = value[:domain]
100
+ if (domain = value[:domain])
101
101
  host = @env["HTTP_HOST"]
102
102
 
103
103
  _domain = if domain.is_a?(String)
@@ -141,7 +141,7 @@ class Rage::Cookies
141
141
  return @request_cookies if @parsed
142
142
 
143
143
  @parsed = true
144
- if cookie_header = @env["HTTP_COOKIE"]
144
+ if (cookie_header = @env["HTTP_COOKIE"])
145
145
  cookie_header.split(/; */n).each do |cookie|
146
146
  next if cookie.empty?
147
147
  key, value = cookie.split("=", 2).yield_self { |k, _| [k.to_sym, _] }
@@ -193,7 +193,7 @@ class Rage::Cookies
193
193
  nil
194
194
  rescue RbNaCl::CryptoError
195
195
  i ||= 0
196
- if box = fallback_boxes[i]
196
+ if (box = fallback_boxes[i])
197
197
  i += 1
198
198
  retry
199
199
  end
@@ -230,10 +230,8 @@ class Rage::Cookies
230
230
  end
231
231
 
232
232
  def fallback_boxes
233
- @fallback_boxes ||= begin
234
- Rage.config.fallback_secret_key_base.map do |key|
235
- RbNaCl::SimpleBox.from_secret_key(RbNaCl::Hash.blake2b(key, digest_size: 32, salt: SALT))
236
- end
233
+ @fallback_boxes ||= Rage.config.fallback_secret_key_base.map do |key|
234
+ RbNaCl::SimpleBox.from_secret_key(RbNaCl::Hash.blake2b(key, digest_size: 32, salt: SALT))
237
235
  end
238
236
  end
239
237
  end # class << self
@@ -97,7 +97,7 @@ module Rage::Ext::ActiveRecord::ConnectionPool
97
97
 
98
98
  # Signal that the fiber is finished with the current connection and it can be returned to the pool.
99
99
  def release_connection(owner = Fiber.current)
100
- if conn = @__in_use.delete(owner)
100
+ if (conn = @__in_use.delete(owner))
101
101
  conn.__idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
102
102
  @__connections << conn
103
103
  Iodine.publish("ext:ar-connection-released", "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
data/lib/rage/fiber.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ##
4
4
  # Rage provides a simple and efficient API to wait on several instances of IO at the same time - {Fiber.await}.
5
- #
5
+ #
6
6
  # Let's say we have the following controller:
7
7
  # ```ruby
8
8
  # class UsersController < RageController::API
@@ -34,7 +34,7 @@
34
34
  # end
35
35
  # ```
36
36
  # With this change, if each request takes 1 second to execute, the total execution time will still be 1 second.
37
- #
37
+ #
38
38
  # ## Creating fibers
39
39
  # Many developers see fibers as "lightweight threads" that should be used in conjunction with fiber pools, the same way we use thread pools for threads.<br>
40
40
  # Instead, it makes sense to think of fibers as regular Ruby objects. We don't use a pool of arrays when we need to create an array - we create a new object and let Ruby and the GC do their job.<br>
@@ -68,7 +68,7 @@ class Fiber
68
68
  @__rage_id = object_id.to_s
69
69
  end
70
70
 
71
- # @private
71
+ # @private
72
72
  def __get_id
73
73
  @__rage_id
74
74
  end
@@ -66,7 +66,7 @@ class Rage::FiberScheduler
66
66
  end
67
67
 
68
68
  # TODO: GC works a little strange with this closure;
69
- #
69
+ #
70
70
  # def timeout_after(duration, exception_class = Timeout::Error, *exception_arguments, &block)
71
71
  # fiber, block_status = Fiber.current, :running
72
72
  # ::Iodine.run_after((duration * 1000).to_i) do
@@ -15,7 +15,7 @@ class Rage::JSONFormatter
15
15
  context.each { |k, v| context_msg << "\"#{k}\":#{v.to_json}," }
16
16
  end
17
17
 
18
- if final = logger[:final]
18
+ if (final = logger[:final])
19
19
  params, env = final[:params], final[:env]
20
20
  if params && params[:controller]
21
21
  return "{\"tags\":[\"#{tags[0]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"info\",\"method\":\"#{env["REQUEST_METHOD"]}\",\"path\":\"#{env["PATH_INFO"]}\",\"controller\":\"#{Rage::Router::Util.path_to_name(params[:controller])}\",\"action\":\"#{params[:action]}\",#{context_msg}\"status\":#{final[:response][0]},\"duration\":#{final[:duration]}}\n"
@@ -8,7 +8,7 @@ require "logger"
8
8
  # [fecbba0735355738] timestamp=2023-10-19T11:12:56+00:00 pid=1825 level=info message=hello
9
9
  # ```
10
10
  # In the log entry above, `timestamp`, `pid`, `level`, and `message` are keys, while `fecbba0735355738` is a tag.
11
- #
11
+ #
12
12
  # Use {tagged} to add custom tags to an entry:
13
13
  # ```ruby
14
14
  # Rage.logger.tagged("ApiCall") do
@@ -15,7 +15,7 @@ class Rage::TextFormatter
15
15
  context.each { |k, v| context_msg << "#{k}=#{v} " }
16
16
  end
17
17
 
18
- if final = logger[:final]
18
+ if (final = logger[:final])
19
19
  params, env = final[:params], final[:env]
20
20
  if params && params[:controller]
21
21
  return "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} controller=#{Rage::Router::Util.path_to_name(params[:controller])} action=#{params[:action]} #{context_msg}status=#{final[:response][0]} duration=#{final[:duration]}\n"
@@ -19,7 +19,7 @@ class Rage::Cors
19
19
 
20
20
  response
21
21
  ensure
22
- if !$! && origin = @cors_check.call(env)
22
+ if !$! && (origin = @cors_check.call(env))
23
23
  headers = response[1]
24
24
  headers["Access-Control-Allow-Origin"] = origin
25
25
  if @origins != "*"
@@ -99,7 +99,7 @@ class Rage::Cors
99
99
  def create_headers
100
100
  headers = {
101
101
  "Access-Control-Allow-Origin" => "",
102
- "Access-Control-Allow-Methods" => @methods,
102
+ "Access-Control-Allow-Methods" => @methods
103
103
  }
104
104
 
105
105
  if @allow_headers
@@ -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
@@ -8,7 +8,7 @@ class Rage::Reloader
8
8
  def call(env)
9
9
  Rage.code_loader.reload
10
10
  @app.call(env)
11
- rescue Exception => e
11
+ rescue Exception => e
12
12
  exception_str = "#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}"
13
13
  puts(exception_str)
14
14
  [500, {}, [exception_str]]
@@ -29,7 +29,7 @@ class Rage::ParamsParser
29
29
  url_params
30
30
  end
31
31
 
32
- rescue => e
32
+ rescue
33
33
  raise Rage::Errors::BadRequest
34
34
  end
35
35
 
@@ -51,7 +51,7 @@ class Rage::Router::Backend
51
51
  def on(method, path, handler, constraints: {}, defaults: nil)
52
52
  raise "Path could not be empty" if path&.empty?
53
53
 
54
- if match_index = (path =~ OPTIONAL_PARAM_REGEXP)
54
+ if (match_index = (path =~ OPTIONAL_PARAM_REGEXP))
55
55
  raise ArgumentError, "Optional Parameter has to be the last parameter of the path" if path.length != match_index + $&.length
56
56
 
57
57
  path_full = path.sub(OPTIONAL_PARAM_REGEXP, "/#{$1}")
@@ -200,11 +200,9 @@ class Rage::Router::Backend
200
200
  end
201
201
 
202
202
  @routes.each do |existing_route|
203
- if (
204
- existing_route[:method] == method &&
205
- existing_route[:pattern] == pattern &&
206
- existing_route[:constraints] == constraints
207
- )
203
+ if existing_route[:method] == method &&
204
+ existing_route[:pattern] == pattern &&
205
+ existing_route[:constraints] == constraints
208
206
  raise ArgumentError, "Method '#{method}' already declared for route '#{pattern}' with constraints '#{constraints.inspect}'"
209
207
  end
210
208
  end
@@ -73,7 +73,7 @@ class Rage::Router::Constrainer
73
73
  if key == :host
74
74
  lines << " host: env['HTTP_HOST'.freeze],"
75
75
  else
76
- raise ArgumentError, 'unknown non-custom strategy for compiling constraint derivation function'
76
+ raise ArgumentError, "unknown non-custom strategy for compiling constraint derivation function"
77
77
  end
78
78
  else
79
79
  lines << " #{strategy.name}: @strategies[#{key}].derive_constraint(env),"