rage-rb 1.3.0 → 1.4.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 +10 -0
- data/Gemfile +3 -0
- data/lib/rage/configuration.rb +17 -0
- data/lib/rage/controller/api.rb +17 -0
- data/lib/rage/cookies.rb +241 -0
- data/lib/rage/ext/setup.rb +1 -1
- data/lib/rage/fiber.rb +1 -1
- data/lib/rage/session.rb +105 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +3 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43393d351828980659e1a5e574887b818727744bf679bca2c1e0e0a164848496
|
4
|
+
data.tar.gz: d732652d4966c9cdcdd1825ec692b6c88286e467c229a2e4acdb7658e2b0a455
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e23ca7a0309a24e665850c6c7798a3a4d7cb5dd5d8f239346907bc2be0ae3c6446530e5299e2f5a6b08f773281a8e3c570dc19b9ebe1f6cbe1b0712b643ed688
|
7
|
+
data.tar.gz: 9578c4f912c7ce713639acd0c043de9f4b757ff816e60dd5ae9637434699190d8c84af29a0a42fd7c0f0f32ae6724b710429cde6fab20011c23fd79bade36d13
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/lib/rage/configuration.rb
CHANGED
@@ -15,6 +15,14 @@
|
|
15
15
|
#
|
16
16
|
# > Defines the verbosity of the Rage logger. This option defaults to `:debug` for all environments except production, where it defaults to `:info`. The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, and `:unknown`.
|
17
17
|
#
|
18
|
+
# • _config.secret_key_base_
|
19
|
+
#
|
20
|
+
# > The `secret_key_base` is used as the input secret to the application's key generator, which is used to encrypt cookies. Rage will fall back to the `SECRET_KEY_BASE` environment variable if this is not set.
|
21
|
+
#
|
22
|
+
# • _config.fallback_secret_key_base_
|
23
|
+
#
|
24
|
+
# > Defines one or several old secrets that need to be rotated. Can accept a single key or an array of keys. Rage will fall back to the `FALLBACK_SECRET_KEY_BASE` environment variable if this is not set.
|
25
|
+
#
|
18
26
|
# # Middleware Configuration
|
19
27
|
#
|
20
28
|
# • _config.middleware.use_
|
@@ -100,6 +108,7 @@
|
|
100
108
|
class Rage::Configuration
|
101
109
|
attr_accessor :logger
|
102
110
|
attr_reader :log_formatter, :log_level
|
111
|
+
attr_writer :secret_key_base, :fallback_secret_key_base
|
103
112
|
|
104
113
|
# used in DSL
|
105
114
|
def config = self
|
@@ -113,6 +122,14 @@ class Rage::Configuration
|
|
113
122
|
@log_level = level.is_a?(Symbol) ? Logger.const_get(level.to_s.upcase) : level
|
114
123
|
end
|
115
124
|
|
125
|
+
def secret_key_base
|
126
|
+
@secret_key_base || ENV["SECRET_KEY_BASE"]
|
127
|
+
end
|
128
|
+
|
129
|
+
def fallback_secret_key_base
|
130
|
+
Array(@fallback_secret_key_base || ENV["FALLBACK_SECRET_KEY_BASE"])
|
131
|
+
end
|
132
|
+
|
116
133
|
def server
|
117
134
|
@server ||= Server.new
|
118
135
|
end
|
data/lib/rage/controller/api.rb
CHANGED
@@ -324,6 +324,18 @@ class RageController::API
|
|
324
324
|
@response ||= Rage::Response.new(@__headers, @__body)
|
325
325
|
end
|
326
326
|
|
327
|
+
# Get the cookie object. See {Rage::Cookies}.
|
328
|
+
# @return [Rage::Cookies]
|
329
|
+
def cookies
|
330
|
+
@cookies ||= Rage::Cookies.new(@__env, self)
|
331
|
+
end
|
332
|
+
|
333
|
+
# Get the session object. See {Rage::Session}.
|
334
|
+
# @return [Rage::Session]
|
335
|
+
def session
|
336
|
+
@session ||= Rage::Session.new(self)
|
337
|
+
end
|
338
|
+
|
327
339
|
# Send a response to the client.
|
328
340
|
#
|
329
341
|
# @param json [String, Object] send a json response to the client; objects like arrays will be serialized automatically
|
@@ -477,4 +489,9 @@ class RageController::API
|
|
477
489
|
# def append_info_to_payload(payload)
|
478
490
|
# payload[:response] = response.body
|
479
491
|
# end
|
492
|
+
|
493
|
+
# Reset the entire session. See {Rage::Session}.
|
494
|
+
def reset_session
|
495
|
+
session.clear
|
496
|
+
end
|
480
497
|
end
|
data/lib/rage/cookies.rb
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
require "time"
|
5
|
+
|
6
|
+
if !defined?(DomainName)
|
7
|
+
fail <<~ERR
|
8
|
+
|
9
|
+
rage-rb depends on domain_name to specify the domain name for cookies. Add the following line to your Gemfile:
|
10
|
+
gem "domain_name"
|
11
|
+
|
12
|
+
ERR
|
13
|
+
end
|
14
|
+
|
15
|
+
class Rage::Cookies
|
16
|
+
# @private
|
17
|
+
def initialize(env, controller)
|
18
|
+
@env = env
|
19
|
+
@headers = controller.headers
|
20
|
+
@request_cookies = {}
|
21
|
+
@parsed = false
|
22
|
+
|
23
|
+
@jar = SimpleJar
|
24
|
+
end
|
25
|
+
|
26
|
+
# Read a cookie.
|
27
|
+
#
|
28
|
+
# @param key [Symbol]
|
29
|
+
# @return [String]
|
30
|
+
def [](key)
|
31
|
+
value = request_cookies[key]
|
32
|
+
@jar.load(value) if value
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get the number of cookies.
|
36
|
+
#
|
37
|
+
# @return [Integer]
|
38
|
+
def size
|
39
|
+
request_cookies.count { |_, v| !v.nil? }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Delete a cookie.
|
43
|
+
#
|
44
|
+
# @param key [Symbol]
|
45
|
+
# @param path [String]
|
46
|
+
# @param domain [String]
|
47
|
+
def delete(key, path: "/", domain: nil)
|
48
|
+
@headers.compare_by_identity
|
49
|
+
|
50
|
+
@request_cookies[key] = nil
|
51
|
+
@headers[set_cookie_key(key)] = Rack::Utils.add_cookie_to_header(nil, key, {
|
52
|
+
value: "", expires: Time.at(0), path: path, domain: domain
|
53
|
+
})
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them
|
57
|
+
# for read. If the cookie was tampered with by the user (or a 3rd party), `nil` will be returned.
|
58
|
+
#
|
59
|
+
# This jar requires that you set a suitable secret for the verification on your app's `secret_key_base`.
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# cookies.encrypted[:user_id] = current_user.id
|
63
|
+
def encrypted
|
64
|
+
dup.tap { |c| c.jar = EncryptedJar }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now.
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# cookies.permanent[:user_id] = current_user.id
|
71
|
+
def permanent
|
72
|
+
dup.tap { |c| c.expires = Time.now + 20 * 365 * 24 * 60 * 60 }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Set a cookie.
|
76
|
+
#
|
77
|
+
# @param key [Symbol]
|
78
|
+
# @param value [String, Hash]
|
79
|
+
# @option value [String] :path
|
80
|
+
# @option value [Boolean] :secure
|
81
|
+
# @option value [Boolean] :httponly
|
82
|
+
# @option value [nil, :none, :lax, :strict] :same_site
|
83
|
+
# @option value [Time] :expires
|
84
|
+
# @option value [String, Array<String>, :all] :domain
|
85
|
+
# @option value [String] :value
|
86
|
+
# @example
|
87
|
+
# cookie[:user_id] = current_user.id
|
88
|
+
# @example
|
89
|
+
# cookie[:user_id] = { value: current_user.id, httponly: true, secure: true }
|
90
|
+
def []=(key, value)
|
91
|
+
@headers.compare_by_identity
|
92
|
+
|
93
|
+
unless value.is_a?(Hash)
|
94
|
+
serialized_value = @jar.dump(value)
|
95
|
+
@request_cookies[key] = serialized_value
|
96
|
+
@headers[set_cookie_key(key)] = Rack::Utils.add_cookie_to_header(nil, key, { value: serialized_value, expires: @expires })
|
97
|
+
return
|
98
|
+
end
|
99
|
+
|
100
|
+
if domain = value[:domain]
|
101
|
+
host = @env["HTTP_HOST"]
|
102
|
+
|
103
|
+
_domain = if domain.is_a?(String)
|
104
|
+
domain
|
105
|
+
elsif domain == :all
|
106
|
+
DomainName(host).domain
|
107
|
+
elsif domain.is_a?(Array)
|
108
|
+
host if domain.include?(host)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
serialized_value = @jar.dump(value[:value])
|
113
|
+
cookie = Rack::Utils.add_cookie_to_header(nil, key, {
|
114
|
+
path: value[:path],
|
115
|
+
secure: value[:secure],
|
116
|
+
expires: value[:expires] || @expires,
|
117
|
+
httponly: value[:httponly],
|
118
|
+
same_site: value[:same_site],
|
119
|
+
value: serialized_value,
|
120
|
+
domain: _domain
|
121
|
+
})
|
122
|
+
|
123
|
+
@request_cookies[key] = serialized_value
|
124
|
+
@headers[set_cookie_key(key)] = cookie
|
125
|
+
end
|
126
|
+
|
127
|
+
def inspect
|
128
|
+
cookies = request_cookies.transform_values do |v|
|
129
|
+
decoded = Base64.urlsafe_decode64(v) rescue nil
|
130
|
+
is_encrypted = decoded&.start_with?(EncryptedJar::PADDING)
|
131
|
+
|
132
|
+
is_encrypted ? "<encrypted>" : v
|
133
|
+
end
|
134
|
+
|
135
|
+
"#<#{self.class.name} @request_cookies=#{cookies.inspect}"
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def request_cookies
|
141
|
+
return @request_cookies if @parsed
|
142
|
+
|
143
|
+
@parsed = true
|
144
|
+
if cookie_header = @env["HTTP_COOKIE"]
|
145
|
+
cookie_header.split(/; */n).each do |cookie|
|
146
|
+
next if cookie.empty?
|
147
|
+
key, value = cookie.split("=", 2).yield_self { |k, _| [k.to_sym, _] }
|
148
|
+
unless @request_cookies.has_key?(key)
|
149
|
+
@request_cookies[key] = (Rack::Utils.unescape(value, Encoding::UTF_8) rescue value)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
@request_cookies
|
155
|
+
end
|
156
|
+
|
157
|
+
def set_cookie_key(key)
|
158
|
+
@set_cookie_keys ||= Hash.new { |hash, key| hash[key] = "Set-Cookie".dup }
|
159
|
+
@set_cookie_keys[key]
|
160
|
+
end
|
161
|
+
|
162
|
+
protected
|
163
|
+
|
164
|
+
attr_writer :jar, :expires
|
165
|
+
|
166
|
+
####################
|
167
|
+
#
|
168
|
+
# Cookie Jars
|
169
|
+
#
|
170
|
+
####################
|
171
|
+
|
172
|
+
class SimpleJar
|
173
|
+
def self.load(_)
|
174
|
+
_
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.dump(value)
|
178
|
+
value.to_s
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class EncryptedJar
|
183
|
+
SALT = "encrypted cookie"
|
184
|
+
PADDING = "00"
|
185
|
+
|
186
|
+
class << self
|
187
|
+
def load(value)
|
188
|
+
box = primary_box
|
189
|
+
|
190
|
+
begin
|
191
|
+
box.decrypt(Base64.urlsafe_decode64(value).byteslice(2..))
|
192
|
+
rescue ArgumentError
|
193
|
+
nil
|
194
|
+
rescue RbNaCl::CryptoError
|
195
|
+
i ||= 0
|
196
|
+
if box = fallback_boxes[i]
|
197
|
+
i += 1
|
198
|
+
retry
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def dump(value)
|
204
|
+
# add two bytes to hold meta information, e.g. in case we
|
205
|
+
# need to change the encryption algorithm in the future
|
206
|
+
Base64.urlsafe_encode64(PADDING + primary_box.encrypt(value.to_s))
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def primary_box
|
212
|
+
@primary_box ||= begin
|
213
|
+
if !defined?(RbNaCl) || !(Gem::Version.create(RbNaCl::VERSION) >= Gem::Version.create("3.3.0") && Gem::Version.create(RbNaCl::VERSION) < Gem::Version.create("8.0.0"))
|
214
|
+
fail <<~ERR
|
215
|
+
|
216
|
+
rage-rb depends on rbnacl [>= 3.3, < 8.0] to encrypt cookies. Add the following line to your Gemfile:
|
217
|
+
gem "rbnacl"
|
218
|
+
|
219
|
+
ERR
|
220
|
+
end
|
221
|
+
|
222
|
+
unless Rage.config.secret_key_base
|
223
|
+
raise "Rage.config.secret_key_base should be set to use encrypted cookies"
|
224
|
+
end
|
225
|
+
|
226
|
+
RbNaCl::SimpleBox.from_secret_key(
|
227
|
+
RbNaCl::Hash.blake2b(Rage.config.secret_key_base, digest_size: 32, salt: SALT)
|
228
|
+
)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
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
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end # class << self
|
240
|
+
end
|
241
|
+
end
|
data/lib/rage/ext/setup.rb
CHANGED
@@ -4,7 +4,7 @@ if defined?(ActiveSupport::IsolatedExecutionState)
|
|
4
4
|
end
|
5
5
|
|
6
6
|
# release ActiveRecord connections on yield
|
7
|
-
if defined?(ActiveRecord)
|
7
|
+
if defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("7.1.0")
|
8
8
|
class Fiber
|
9
9
|
def self.defer
|
10
10
|
res = Fiber.yield
|
data/lib/rage/fiber.rb
CHANGED
@@ -91,7 +91,7 @@ class Fiber
|
|
91
91
|
|
92
92
|
# @private
|
93
93
|
# under normal circumstances, the method is a copy of `yield`, but it can be overriden to perform
|
94
|
-
# additional steps on yielding, e.g. releasing AR connections; see "lib/rage/
|
94
|
+
# additional steps on yielding, e.g. releasing AR connections; see "lib/rage/ext/setup.rb"
|
95
95
|
class << self
|
96
96
|
alias_method :defer, :yield
|
97
97
|
end
|
data/lib/rage/session.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
class Rage::Session
|
6
|
+
# @private
|
7
|
+
KEY = Rack::RACK_SESSION.to_sym
|
8
|
+
|
9
|
+
# @private
|
10
|
+
def initialize(controller)
|
11
|
+
@cookies = controller.cookies.encrypted
|
12
|
+
end
|
13
|
+
|
14
|
+
# Writes the value to the session.
|
15
|
+
#
|
16
|
+
# @param key [Symbol]
|
17
|
+
# @param value [String]
|
18
|
+
def []=(key, value)
|
19
|
+
write_session(add: { key => value })
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the value of the key stored in the session or `nil` if the given key is not found.
|
23
|
+
#
|
24
|
+
# @param key [Symbol]
|
25
|
+
def [](key)
|
26
|
+
read_session[key]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the value of the given key from the session, or raises `KeyError` if the given key is not found
|
30
|
+
# and no default value is set. Returns the default value if specified.
|
31
|
+
#
|
32
|
+
# @param key [Symbol]
|
33
|
+
def fetch(key, default = nil, &block)
|
34
|
+
if default.nil?
|
35
|
+
read_session.fetch(key, &block)
|
36
|
+
else
|
37
|
+
read_session.fetch(key, default, &block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Deletes the given key from the session.
|
42
|
+
#
|
43
|
+
# @param key [Symbol]
|
44
|
+
def delete(key)
|
45
|
+
write_session(remove: key)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Clears the session.
|
49
|
+
def clear
|
50
|
+
write_session(clear: true)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the session as Hash.
|
54
|
+
def to_hash
|
55
|
+
read_session
|
56
|
+
end
|
57
|
+
|
58
|
+
alias_method :to_h, :to_hash
|
59
|
+
|
60
|
+
def empty?
|
61
|
+
read_session.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns `true` if the given key is present in the session.
|
65
|
+
def has_key?(key)
|
66
|
+
read_session.has_key?(key)
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method :key?, :has_key?
|
70
|
+
alias_method :include?, :has_key?
|
71
|
+
|
72
|
+
def each(&block)
|
73
|
+
read_session.each(&block)
|
74
|
+
end
|
75
|
+
|
76
|
+
def dig(*keys)
|
77
|
+
read_session.dig(*keys)
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
"#<#{self.class.name} @session=#{to_h.inspect}"
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def write_session(add: nil, remove: nil, clear: nil)
|
87
|
+
if add
|
88
|
+
read_session.merge!(add)
|
89
|
+
elsif remove && read_session.has_key?(remove)
|
90
|
+
read_session.reject! { |k, _| k == remove }
|
91
|
+
elsif clear
|
92
|
+
read_session.clear
|
93
|
+
end
|
94
|
+
|
95
|
+
@cookies[KEY] = { httponly: true, same_site: :lax, value: read_session.to_json }
|
96
|
+
end
|
97
|
+
|
98
|
+
def read_session
|
99
|
+
@session ||= begin
|
100
|
+
JSON.parse(@cookies[KEY] || "{}", symbolize_names: true)
|
101
|
+
rescue JSON::ParserError
|
102
|
+
{}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/rage/version.rb
CHANGED
data/lib/rage-rb.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rage-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roman Samoilov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-05-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -105,6 +105,7 @@ files:
|
|
105
105
|
- lib/rage/code_loader.rb
|
106
106
|
- lib/rage/configuration.rb
|
107
107
|
- lib/rage/controller/api.rb
|
108
|
+
- lib/rage/cookies.rb
|
108
109
|
- lib/rage/env.rb
|
109
110
|
- lib/rage/errors.rb
|
110
111
|
- lib/rage/ext/active_record/connection_pool.rb
|
@@ -130,6 +131,7 @@ files:
|
|
130
131
|
- lib/rage/router/strategies/host.rb
|
131
132
|
- lib/rage/router/util.rb
|
132
133
|
- lib/rage/rspec.rb
|
134
|
+
- lib/rage/session.rb
|
133
135
|
- lib/rage/setup.rb
|
134
136
|
- lib/rage/sidekiq_session.rb
|
135
137
|
- lib/rage/templates/Gemfile
|