Skip to content

Instantly share code, notes, and snippets.

@envygeeks
Last active November 5, 2021 21:41
Show Gist options
  • Save envygeeks/8246199 to your computer and use it in GitHub Desktop.
Save envygeeks/8246199 to your computer and use it in GitHub Desktop.
require 'action_dispatch/middleware/session/abstract_store'
require 'rack/session/abstract/id'
module ActionDispatch
module Session
class RedisStore < Rack::Session::Abstract::ID
include StaleSessionCheck
include Compatibility
attr_reader :server
attr_reader :expire_after
attr_reader :key
class SessionError < StandardError
def initialize(e = nil)
Rails.logger.error(e) if e
super("There was an issue saving or loading the session.")
end
end
def initialize(app, server:, redis_key:, expire_after: 1.hour, key: '_sid', **kwd)
@server = parse_server(server)
@redis_key = parse_redis_key(redis_key)
@expire_after = expire_after || 1.hour
@key = key || "_sid"
super(app, **kwd,
key: key
)
end
def redis
@redis ||= Redis.new(
@server
)
end
def redis_key(sid = nil)
sid ? "#{@redis_key}:#{sid}" : @redis_key
end
def get_session(env, sid)
rsid = redis_key(sid)
if sid && sid != rsid && redis.exists(rsid)
begin
session = \
YAML.load(redis.get(rsid))
if ! session.is_a?(Hash)
raise SessionError
end
rescue => e
Rails.logger.error(e)
redis.del(rsid)
session, sid = {}, generate_sid
end
else
sid, session = generate_sid, {}
end
unless session["session_id"]
session.merge!({
"session_id" => sid
})
end
[
sid,
session
]
end
# ---------------------------------------------------------------
# Set the session inside of Redis and pass it out.
# ---------------------------------------------------------------
def set_session(env, sid, session, opts)
unless session["session_id"] && session["session_id"] == sid
session["session_id"] = sid
end
serialized_data = YAML.dump(session)
rsid = redis_key(sid)
if redis_key == rsid || ! \
redis.set(rsid, serialized_data) == "OK"
raise SessionError
else
#2: The double tap.
redis.expire(rsid, @expire_after)
end
sid
end
# ---------------------------------------------------------------
# Drop the session from Redis and pass a new sid.
# ---------------------------------------------------------------
def destroy_session(env, sid, opts)
redis.del(sid)
unless opts[:drop]
generate_sid
end
end
# ---------------------------------------------------------------
# Pull the session id from the cookie: This is where we differ a
# lot from the way that Rails and Rack handle cookies, because
# the cookie is the session ID we simply pull the session ID
# straight from the cookie jar and send it into `#get_session`
# ---------------------------------------------------------------
def load_session(env)
get_session(env, cookie_jar(env)[@key])
end
# ---------------------------------------------------------------
def set_cookie(env, sid, cookie)
cookie_jar(env)[@key] = \
cookie
end
# ---------------------------------------------------------------
# The most important piece, we need to sign our cookies.
# ---------------------------------------------------------------
def cookie_jar(env)
ActionDispatch::Request.new(env).cookie_jar.signed
end
# ---------------------------------------------------------------
# Ensures all the server options are properly set for the
# user so you can either pass just a few if you need or all if
# you really want to pass them all.
# ---------------------------------------------------------------
protected
def parse_server(srv = nil)
if ! srv.is_a?(Hash)
srv = {}
end
srv[:database] ||= 0
srv[:namespace] ||= :session
srv[:host] ||= "127.0.0.1"
srv[:port] ||= 6379
srv
end
# ---------------------------------------------------------------
protected
def parse_redis_key(rkey = nil)
(rkey || "rails:session").chomp(":")
end
end
end
end
# ---------------------------------------------------------------------
# Copyright 2013 Jordon Bedwell.
# ---------------------------------------------------------------------
require "rspec/helper"
describe ActionDispatch::Session::RedisStore, :type => :request do
before do
@request_forgery_orig = ActionController::Base.allow_forgery_protection
ActionController::Base.allow_forgery_protection = true
stub_request(:get, /.*/).to_return({
:body => ""
})
end
after do
ActionController::Base.allow_forgery_protection = \
@request_forgery_orig
end
before :each do
get "/"
end
describe "opts (through #initialize)" do
it "allows :expire_after" do
subj = described_class.new({}, {
:expire_after => 3600
})
expect(subj.expire_after).to eq 3600
expect(subj.default_options[:expire_after]).to eq 3600
end
it "allows :redis_key" do
subj = described_class.new({}, {
:redis_key => "foo"
})
expect(subj.redis_key).to eq "foo"
end
[:namespace, :host, :port, :database].each do |k|
it "allows server[:#{k}] opt" do
subj = described_class.new({}, :server => {
k => "foo"
})
expect(subj.server[k]).to eq "foo"
expect(subj.server).to be_instance_of Hash
end
end
it "allows :key (for the cookie)" do
subj = described_class.new({}, {
:key => "foo"
})
expect(subj.key).to eq "foo"
end
end
describe "#redis_key" do
it "appends if an sid is provided" do
subj = described_class.new({})
expect(subj.redis_key("foo")).to end_with ":foo"
end
end
describe "#get_session" do
context "on bad deserialize" do
before :each do
subject.redis.set(subject.redis_key("foo"), "foo")
end
subject do
described_class.new({})
end
it "resets the session" do
expect(subject.get_session( \
request.env, "foo").first).not_to eq "foo"
end
it "logs" do
expect(Rails.logger).to receive(:error).and_call_original
subject.get_session(request.env, "foo")
end
it "removes the redis key" do
subject.get_session(request.env, "foo")
expect(subject.redis.keys \
).not_to include subject.redis_key("foo")
end
end
context "on bad sid" do
it "resets" do
subj = described_class.new({ })
expect(subj.get_session(request.env, "foo").first).not_to eq "foo"
end
end
it "adds session_id to the session hash" do
get "/"
expect(request.session).to have_key "session_id"
end
end
describe "#set_session" do
it "saves" do
subj = described_class.new({})
rtrn = subj.get_session(request.env, request.session["session_id"])
expect(rtrn.last).to eq request.session.to_hash
end
it "adds session_id" do
subj, session = described_class.new({}), {}
expect(session).to receive(:[]).with("session_id").and_call_original
subj.set_session(request.env, "foo", session, {})
end
context "on bad sid" do
it "raises" do
subj = described_class.new({})
expect_error described_class::SessionError do
subj.set_session(request.env, nil, {}, {})
end
end
end
end
describe "#cookie_jar" do
it "uses a signed cookie" do
subj, expected = described_class.new({}), [
ActionDispatch::Cookies::SignedCookieJar,
ActionDispatch::Cookies::UpgradeLegacySignedCookieJar
]
expect(expected).to include subj.cookie_jar(request.env).class
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment