Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions lib/rage/all.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,3 @@
require_relative "middleware/body_finalizer"

require_relative "telemetry/telemetry"

if defined?(Sidekiq)
require_relative "sidekiq_session"
end
26 changes: 17 additions & 9 deletions lib/rage/cookies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
if !defined?(DomainName)
fail <<~ERR

rage-rb depends on domain_name to specify the domain name for cookies. Add the following line to your Gemfile:
Rage depends on `domain_name` to specify the domain name for cookies. Ensure the following line is added to your Gemfile:
gem "domain_name"

ERR
Expand Down Expand Up @@ -250,7 +250,7 @@ def self.dump(value)
end

class EncryptedJar
SALT = "encrypted cookie"
INFO = "encrypted cookie"
PADDING = "00"

class << self
Expand All @@ -266,7 +266,7 @@ def load(value)
Rage.logger.debug("Failed to decrypt encrypted cookie")
i ||= 0
if (box = fallback_boxes[i])
Rage.logger.debug("Trying to decrypt with fallback key ##{i + 1}")
Rage.logger.debug { "Trying to decrypt with fallback key ##{i + 1}" }
i += 1
retry
end
Expand All @@ -286,7 +286,7 @@ def primary_box
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"))
fail <<~ERR

rage-rb depends on rbnacl [>= 3.3, < 8.0] to encrypt cookies. Add the following line to your Gemfile:
Rage depends on `rbnacl` [>= 3.3, < 8.0] to encrypt cookies. Ensure the following line is added to your Gemfile:
gem "rbnacl"

ERR
Expand All @@ -296,17 +296,25 @@ def primary_box
raise "Rage.config.secret_key_base should be set to use encrypted cookies"
end

RbNaCl::SimpleBox.from_secret_key(
RbNaCl::Hash.blake2b(Rage.config.secret_key_base, digest_size: 32, salt: SALT)
)
RbNaCl::SimpleBox.from_secret_key(build_key(Rage.config.secret_key_base))
end
end

def fallback_boxes
@fallback_boxes ||= Rage.config.fallback_secret_key_base.map do |key|
RbNaCl::SimpleBox.from_secret_key(RbNaCl::Hash.blake2b(key, digest_size: 32, salt: SALT))
@fallback_boxes ||= begin
fallbacks = Rage.config.fallback_secret_key_base.map do |key|
RbNaCl::SimpleBox.from_secret_key(build_key(key))
end

fallbacks << RbNaCl::SimpleBox.from_secret_key(
RbNaCl::Hash.blake2b(Rage.config.secret_key_base, digest_size: 32, salt: INFO)
)
end
end

def build_key(secret)
RbNaCl::Hash.blake2b("", key: [secret].pack("H*"), digest_size: 32, personal: INFO)
end
end # class << self
end
end
49 changes: 37 additions & 12 deletions lib/rage/router/backend.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,25 @@ def mount(path, handler, methods)
raise ArgumentError, "Mount handler should respond to `call`" unless handler.respond_to?(:call)

raw_handler = handler
is_sidekiq = handler.respond_to?(:name) && handler.name == "Sidekiq::Web"
handler = wrap_in_rack_session(handler) if handler.respond_to?(:name) && handler.name == "Sidekiq::Web"

app = ->(env, _params) do
# rewind `rack.input` in case mounted application needs to access the request body;
# by the time the app is called, `rack.input` is already consumed in `Rage::ParamsParser`
env["rack.input"].rewind

handler = ->(env, _params) do
env["SCRIPT_NAME"] = path
sub_path = env["PATH_INFO"].delete_prefix!(path)
env["PATH_INFO"] = "/" if sub_path == ""

if is_sidekiq
Rage::SidekiqSession.with_session(env) do
raw_handler.call(env)
end
else
raw_handler.call(env)
end

handler.call(env)
ensure
env["PATH_INFO"] = "#{env["SCRIPT_NAME"]}#{sub_path}"
end

methods.each do |method|
__on(method, path, handler, {}, {}, { raw_handler:, mount: true })
__on(method, "#{path}/*", handler, {}, {}, { raw_handler:, mount: true })
__on(method, path, app, {}, {}, { raw_handler:, mount: true })
__on(method, "#{path}/*", app, {}, {}, { raw_handler:, mount: true })
end
end

Expand Down Expand Up @@ -278,4 +275,32 @@ def find(env, derived_constraints)
end
end
end

def wrap_in_rack_session(handler)
unless defined?(Rack::Session::Cookie)
fail <<~ERR

`#{handler.name}` depends on `Rack::Session`. Ensure the following line is added to your Gemfile:
gem "rack-session"

ERR
end

secret_key = if Rage.config.secret_key_base
require "openssl"
OpenSSL::KDF.hkdf(
[Rage.config.secret_key_base].pack("H*"),
salt: "rack.session",
info: handler.name,
length: 64,
hash: "SHA256"
)
else
puts "WARNING: `secret_key_base` is not set. Using a temporary random secret for `#{handler.name}` sessions. Sessions will not persist across server restarts."
require "securerandom"
SecureRandom.random_bytes(64)
end

Rack::Session::Cookie.new(handler, secret: secret_key, same_site: true, max_age: 86400)
end
end
72 changes: 0 additions & 72 deletions lib/rage/sidekiq_session.rb

This file was deleted.

26 changes: 20 additions & 6 deletions spec/controller/api/cookies_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@

context "with cookies" do
before do
allow(Rage.config).to receive(:secret_key_base).and_return("rage-test-key")
allow(Rage.config).to receive(:fallback_secret_key_base).and_return(%w(rage-fallback-key))
allow(Rage.config).to receive(:secret_key_base).and_return("b7ef8f0824ffbddb85818fb6898546a1")
allow(Rage.config).to receive(:fallback_secret_key_base).and_return(%w(707ae8b7c9655bd5cd30fd407d2791e4))
end

let(:cookies) do
{
user_id: 112,
callback_url: "https://test-host.com",
session: "MDB4tHk-Qm-da9_zeXE8NaywvyzidycD4qIhK1xWmYVJE_jYeuo_rzd9Eb6gwCLBn8eYaqT9QBomfAFA"
session: "MDAAeSoXJxjazIR1ER55uE2KYYT5Bwabdws3Mu_SSgt563O6VE9dGGoSYjTQ_ShcJKWmymNAQpFG-Mg5"
}
end

Expand Down Expand Up @@ -71,7 +71,7 @@

context "with data encrypted with rotated key" do
let(:cookies) do
{ session: "MDC9exZmtbHuQ0hKQbfuf69gBQE0oER1y0DInAq686395nMYPVRxt0D3W8wt0jjegw1LNu4MSvQf1LSWdw==" }
{ session: "MDBDgi9VGfN221gVpLfGx3Hs_IwW5cdjxAd7U-uBFSwxYthYMY32WjTW_C3e70rZthg1R936g8Jhwe59wg==" }
end

before do
Expand All @@ -83,8 +83,22 @@
end
end

context "with legacy key" do
let(:cookies) do
{ session: "MDDPbSUSGqXtww1LMHHcCSiYFE_EMswmzJRbs0koNSFUz9CHbHR-wAwv7vDj5MJFom_7XGwB-FNW5M8P-0K4uw==" }
end

before do
allow(Rage).to receive(:logger).and_return(double(debug: nil))
end

it "correctly decrypts data" do
expect(subject.cookies.encrypted[:session]).to eq("primary-old-test-value")
end
end

context "with incorrectly encrypted data" do
let(:cookies) { { session: "MDC9exZmtbHuQ0hK" } }
let(:cookies) { { session: "MDBDgi9VGfN221gVpLfGx3Hs" } }

before do
allow(Rage).to receive(:logger).and_return(double(debug: nil))
Expand Down Expand Up @@ -228,7 +242,7 @@
end

it "correctly sets permanent cookies with encrypted values" do
allow(Rage.config).to receive(:secret_key_base).and_return("rage-test-key")
allow(Rage.config).to receive(:secret_key_base).and_return("b7ef8f0824ffbddb85818fb6898546a1")

subject.cookies.encrypted.permanent[:user_id] = "secret"
expect(response_cookies[:user_id]).to match(/\S+; expires=\w{3}, \d{2} \w{3} #{Time.now.year + 20} \d{2}:\d{2}:\d{2} GMT/)
Expand Down
4 changes: 2 additions & 2 deletions spec/controller/api/session_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
let(:headers) { { "HTTP_COOKIE" => "#{Rage::Session.key}=#{encoded_session}" } }

before do
allow(Rage.config).to receive(:secret_key_base).and_return("rage-test-key")
allow(Rage.config).to receive(:secret_key_base).and_return("b7ef8f0824ffbddb85818fb6898546a1")
end

context "when reading a valid session" do
let(:encoded_session) { "MDDTFjPTyaIdJjZG2C-RJmDPC_5fMyBMTn87Dv7EID3g-OJwakyxFQUhoSlxwqdLRw4npvm08F0=" }
let(:encoded_session) { "MDCf27lpe1B7qJB-1bZpqzuZ-0Tf2i0YocehRwCz5cnm6ktYi0djcr-zDtgZZQFyUhp4Q-zPM_8=" }

it "correctly reads values" do
expect(subject.session[:a]).to eq(1)
Expand Down
56 changes: 56 additions & 0 deletions spec/router/mount_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,60 @@
router.mount("/rack_app", 5, %w(GET))
}.to raise_error(/should respond to `call`/)
end

context "with session" do
let(:app) do
Class.new do
def self.name
"Sidekiq::Web"
end

def self.call(env)
end
end
end

context "with Rack::Session available" do
let(:session) do
Class.new do
def initialize(app, **)
@app = app
end

def call(env)
:test_session
end
end
end

before do
allow(Rage).to receive(:config).and_return(double(secret_key_base: "test secret"))
stub_const("Rack::Session::Cookie", session)
end

it "exposes session object" do
router.mount("/test", app, %w(GET))

response, _ = perform_get_request("/test")
expect(response).to eq(:test_session)
end

it "rewinds request body" do
router.mount("/test", app, %w(GET))

body = StringIO.new("test body").tap(&:read)
perform_get_request("/test", body:)

expect(body.read).to eq("test body")
end
end

context "with Rack::Session unavailable" do
it "raises exception" do
expect {
router.mount("/test", app, %w(GET))
}.to raise_error(/`Sidekiq::Web` depends on `Rack::Session`/)
end
end
end
end
17 changes: 9 additions & 8 deletions spec/support/request_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@ def router
@router ||= Rage::Router::Backend.new
end

def perform_get_request(path, host: nil, params: {})
perform_request("GET", path, host, params)
def perform_get_request(path, host: nil, params: {}, body: nil)
perform_request("GET", path, host, params, body)
end

def perform_head_request(path, host: nil, params: {})
perform_request("HEAD", path, host, params)
def perform_head_request(path, host: nil, params: {}, body: nil)
perform_request("HEAD", path, host, params, body)
end

def perform_post_request(path, host: nil, params: {})
perform_request("POST", path, host, params)
def perform_post_request(path, host: nil, params: {}, body: nil)
perform_request("POST", path, host, params, body)
end

private

def perform_request(method, path, host, params)
def perform_request(method, path, host, params, body)
env = {
"REQUEST_METHOD" => method,
"PATH_INFO" => path,
"HTTP_HOST" => host
"HTTP_HOST" => host,
"rack.input" => body || StringIO.new
}
handler = router.lookup(env)

Expand Down