-
Notifications
You must be signed in to change notification settings - Fork 150
Expand file tree
/
Copy pathredis-session-store.rb
More file actions
198 lines (169 loc) · 6.04 KB
/
Copy pathredis-session-store.rb
File metadata and controls
198 lines (169 loc) · 6.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
require 'redis'
# Redis session storage for Rails, and for Rails only. Derived from
# the MemCacheStore code, simply dropping in Redis instead.
class RedisSessionStore < ActionDispatch::Session::AbstractStore
VERSION = '0.11.1'.freeze
# Rails 3.1 and beyond defines the constant elsewhere
unless defined?(ENV_SESSION_OPTIONS_KEY)
ENV_SESSION_OPTIONS_KEY = if Rack.release.split('.').first.to_i > 1
Rack::RACK_SESSION_OPTIONS
else
Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY
end
end
USE_INDIFFERENT_ACCESS = defined?(ActiveSupport).freeze
# ==== Options
# * +:key+ - Same as with the other cookie stores, key name
# * +:redis+ - A hash with redis-specific options
# * +:url+ - Redis url, default is redis://localhost:6379/0
# * +:key_prefix+ - Prefix for keys used in Redis, e.g. +myapp:+
# * +:expire_after+ - A number in seconds for session timeout
# * +:client+ - Connect to Redis with given object rather than create one
# * +:on_redis_down:+ - Called with err, env, and SID on Errno::ECONNREFUSED
# * +:on_session_load_error:+ - Called with err and SID on Marshal.load fail
# * +:serializer:+ - Serializer to use on session data, default is :marshal.
#
# ==== Examples
#
# Rails.application.config.session_store :redis_session_store, {
# key: 'your_session_key',
# redis: {
# expire_after: 120.minutes,
# key_prefix: 'myapp:session:',
# url: 'redis://localhost:6379/0'
# },
# on_redis_down: ->(*a) { logger.error("Redis down! #{a.inspect}") }
# serializer: :hybrid # migrate from Marshal to JSON
# }
#
def initialize(app, options = {})
super
redis_options = options[:redis] || {}
@default_options[:namespace] = 'rack:session'
@default_options.merge!(redis_options)
@redis = redis_options[:client] || Redis.new(redis_options)
@on_redis_down = options[:on_redis_down]
@serializer = determine_serializer(options[:serializer])
@on_session_load_error = options[:on_session_load_error]
verify_handlers!
end
attr_accessor :on_redis_down, :on_session_load_error
private
attr_reader :redis, :key, :default_options, :serializer
# overrides method defined in rack to actually verify session existence
# Prevents needless new sessions from being created in scenario where
# user HAS session id, but it already expired, or is invalid for some
# other reason, and session was accessed only for reading.
def session_exists?(env)
value = current_session_id(env)
!!(
value && !value.empty? &&
redis.exists(prefixed(value))
)
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
on_redis_down.call(e, env, value) if on_redis_down
true
end
def verify_handlers!
%w(on_redis_down on_session_load_error).each do |h|
next unless (handler = public_send(h)) && !handler.respond_to?(:call)
raise ArgumentError, "#{h} handler is not callable"
end
end
def prefixed(sid)
"#{default_options[:key_prefix]}#{sid}"
end
def session_default_values
[generate_sid, USE_INDIFFERENT_ACCESS ? {}.with_indifferent_access : {}]
end
def get_session(env, sid)
sid && (session = load_session_from_redis(sid)) ? [sid, session] : session_default_values
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
on_redis_down.call(e, env, sid) if on_redis_down
session_default_values
end
alias find_session get_session
def load_session_from_redis(sid)
data = redis.get(prefixed(sid))
begin
data ? decode(data) : nil
rescue StandardError => e
destroy_session_from_sid(sid, drop: true)
on_session_load_error.call(e, sid) if on_session_load_error
nil
end
end
def decode(data)
session = serializer.load(data)
USE_INDIFFERENT_ACCESS ? session.with_indifferent_access : session
end
def set_session(env, sid, session_data, options = nil)
expiry = get_expiry(env, options)
if expiry
redis.setex(prefixed(sid), expiry, encode(session_data))
else
redis.set(prefixed(sid), encode(session_data))
end
sid
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
on_redis_down.call(e, env, sid) if on_redis_down
false
end
alias write_session set_session
def get_expiry(env, options)
session_storage_options = options || env.fetch(ENV_SESSION_OPTIONS_KEY, {})
session_storage_options[:ttl] || session_storage_options[:expire_after]
end
def encode(session_data)
serializer.dump(session_data)
end
def destroy_session(env, sid, options)
destroy_session_from_sid(sid, (options || {}).to_hash.merge(env: env))
end
alias delete_session destroy_session
def destroy(env)
if env['rack.request.cookie_hash'] &&
(sid = env['rack.request.cookie_hash'][key])
destroy_session_from_sid(sid, drop: true, env: env)
end
false
end
def destroy_session_from_sid(sid, options = {})
redis.del(prefixed(sid))
(options || {})[:drop] ? nil : generate_sid
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
on_redis_down.call(e, options[:env] || {}, sid) if on_redis_down
end
def determine_serializer(serializer)
serializer ||= :marshal
case serializer
when :marshal then Marshal
when :json then JsonSerializer
when :hybrid then HybridSerializer
else serializer
end
end
# Uses built-in JSON library to encode/decode session
class JsonSerializer
def self.load(value)
JSON.parse(value, quirks_mode: true)
end
def self.dump(value)
JSON.generate(value, quirks_mode: true)
end
end
# Transparently migrates existing session values from Marshal to JSON
class HybridSerializer < JsonSerializer
MARSHAL_SIGNATURE = "\x04\x08".freeze
def self.load(value)
if needs_migration?(value)
Marshal.load(value)
else
super
end
end
def self.needs_migration?(value)
value.start_with?(MARSHAL_SIGNATURE)
end
end
end