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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/Gemfile +10 -7
- data/OVERVIEW.md +1 -1
- data/README.md +25 -9
- data/lib/rage/all.rb +1 -0
- data/lib/rage/cable/cable.rb +130 -0
- data/lib/rage/cable/channel.rb +452 -0
- data/lib/rage/cable/connection.rb +78 -0
- data/lib/rage/cable/protocol/actioncable_v1_json.rb +167 -0
- data/lib/rage/cable/router.rb +138 -0
- data/lib/rage/cli.rb +2 -1
- data/lib/rage/code_loader.rb +9 -0
- data/lib/rage/configuration.rb +53 -0
- data/lib/rage/controller/api.rb +51 -13
- data/lib/rage/cookies.rb +7 -9
- data/lib/rage/ext/active_record/connection_pool.rb +1 -1
- data/lib/rage/fiber.rb +3 -3
- data/lib/rage/fiber_scheduler.rb +1 -1
- data/lib/rage/logger/json_formatter.rb +1 -1
- data/lib/rage/logger/logger.rb +1 -1
- data/lib/rage/logger/text_formatter.rb +1 -1
- data/lib/rage/middleware/cors.rb +2 -2
- data/lib/rage/middleware/fiber_wrapper.rb +3 -1
- data/lib/rage/middleware/origin_validator.rb +38 -0
- data/lib/rage/middleware/reloader.rb +1 -1
- data/lib/rage/params_parser.rb +1 -1
- data/lib/rage/router/backend.rb +4 -6
- data/lib/rage/router/constrainer.rb +1 -1
- data/lib/rage/router/dsl.rb +7 -7
- data/lib/rage/router/dsl_plugins/legacy_hash_notation.rb +1 -1
- data/lib/rage/router/dsl_plugins/legacy_root_notation.rb +1 -1
- data/lib/rage/router/handler_storage.rb +1 -1
- data/lib/rage/session.rb +2 -2
- data/lib/rage/setup.rb +5 -1
- data/lib/rage/sidekiq_session.rb +1 -1
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +23 -15
- data/rage.gemspec +1 -1
- 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
|
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
|
data/lib/rage/code_loader.rb
CHANGED
@@ -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?
|
data/lib/rage/configuration.rb
CHANGED
@@ -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
|
data/lib/rage/controller/api.rb
CHANGED
@@ -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
|
-
|
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.
|
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
|
182
|
+
# def deny_access
|
159
183
|
# head :forbidden
|
160
184
|
# end
|
161
185
|
# @example
|
162
|
-
# rescue_from User::NotAuthorized do |
|
163
|
-
#
|
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 [
|
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
|
-
|
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,
|
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(
|
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,
|
17
|
+
def initialize(env, headers)
|
18
18
|
@env = env
|
19
|
-
@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 ||=
|
234
|
-
|
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
|
-
|
71
|
+
# @private
|
72
72
|
def __get_id
|
73
73
|
@__rage_id
|
74
74
|
end
|
data/lib/rage/fiber_scheduler.rb
CHANGED
@@ -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"
|
data/lib/rage/logger/logger.rb
CHANGED
@@ -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"
|
data/lib/rage/middleware/cors.rb
CHANGED
@@ -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
|
@@ -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/params_parser.rb
CHANGED
data/lib/rage/router/backend.rb
CHANGED
@@ -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
|
-
|
205
|
-
|
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,
|
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),"
|