rage-rb 1.6.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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),"