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.
- 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),"
|