brevio-session-store 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.rubocop.yml +38 -0
- data/.simplecov +5 -0
- data/.travis.yml +24 -0
- data/AUTHORS.md +25 -0
- data/CHANGELOG.md +269 -0
- data/CODE_OF_CONDUCT.md +75 -0
- data/CONTRIBUTING.md +6 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +119 -0
- data/Rakefile +15 -0
- data/lib/redis-session-store.rb +219 -0
- data/redis-session-store.gemspec +22 -0
- data/spec/redis_session_store_spec.rb +564 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support.rb +51 -0
- metadata +159 -0
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
# Redis session storage for Rails, and for Rails only. Derived from
|
4
|
+
# the MemCacheStore code, simply dropping in Redis instead.
|
5
|
+
class RedisSessionStore < ActionDispatch::Session::AbstractStore
|
6
|
+
# Rails 3.1 and beyond defines the constant elsewhere
|
7
|
+
unless defined?(ENV_SESSION_OPTIONS_KEY)
|
8
|
+
ENV_SESSION_OPTIONS_KEY = if Rack.release.split('.').first.to_i > 1
|
9
|
+
Rack::RACK_SESSION_OPTIONS
|
10
|
+
else
|
11
|
+
Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
USE_INDIFFERENT_ACCESS = defined?(ActiveSupport).freeze
|
16
|
+
# ==== Options
|
17
|
+
# * +:key+ - Same as with the other cookie stores, key name
|
18
|
+
# * +:redis+ - A hash with redis-specific options
|
19
|
+
# * +:url+ - Redis url, default is redis://localhost:6379/0
|
20
|
+
# * +:key_prefix+ - Prefix for keys used in Redis, e.g. +myapp:+
|
21
|
+
# * +:expire_after+ - A number in seconds for session timeout
|
22
|
+
# * +:client+ - Connect to Redis with given object rather than create one
|
23
|
+
# * +:on_redis_down:+ - Called with err, env, and SID on Errno::ECONNREFUSED
|
24
|
+
# * +:on_session_load_error:+ - Called with err and SID on Marshal.load fail
|
25
|
+
# * +:serializer:+ - Serializer to use on session data, default is :marshal.
|
26
|
+
#
|
27
|
+
# ==== Examples
|
28
|
+
#
|
29
|
+
# Rails.application.config.session_store :redis_session_store, {
|
30
|
+
# key: 'your_session_key',
|
31
|
+
# redis: {
|
32
|
+
# expire_after: 120.minutes,
|
33
|
+
# key_prefix: 'myapp:session:',
|
34
|
+
# url: 'redis://localhost:6379/0'
|
35
|
+
# },
|
36
|
+
# on_redis_down: ->(*a) { logger.error("Redis down! #{a.inspect}") }
|
37
|
+
# serializer: :hybrid # migrate from Marshal to JSON
|
38
|
+
# }
|
39
|
+
#
|
40
|
+
def initialize(app, options = {})
|
41
|
+
super
|
42
|
+
|
43
|
+
@default_options.merge!(options[:redis] || {})
|
44
|
+
init_options = options[:redis]&.reject { |k, _v| %i[expire_after key_prefix].include?(k) } || {}
|
45
|
+
@redis = init_options[:client] || Redis.new(init_options)
|
46
|
+
@on_redis_down = options[:on_redis_down]
|
47
|
+
@serializer = determine_serializer(options[:serializer])
|
48
|
+
@on_session_load_error = options[:on_session_load_error]
|
49
|
+
verify_handlers!
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_accessor :on_redis_down, :on_session_load_error
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
attr_reader :redis, :key, :default_options, :serializer
|
57
|
+
|
58
|
+
# overrides method defined in rack to actually verify session existence
|
59
|
+
# Prevents needless new sessions from being created in scenario where
|
60
|
+
# user HAS session id, but it already expired, or is invalid for some
|
61
|
+
# other reason, and session was accessed only for reading.
|
62
|
+
def session_exists?(env)
|
63
|
+
value = get_cookie(env)
|
64
|
+
|
65
|
+
!!(
|
66
|
+
value && !value.empty? &&
|
67
|
+
key_exists?(value)
|
68
|
+
)
|
69
|
+
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
|
70
|
+
on_redis_down.call(e, env, value) if on_redis_down
|
71
|
+
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def key_exists?(value)
|
76
|
+
if redis.respond_to?(:exists?)
|
77
|
+
# added in redis gem v4.2
|
78
|
+
redis.exists?(prefixed(value))
|
79
|
+
else
|
80
|
+
# older method, will return an integer starting in redis gem v4.3
|
81
|
+
redis.exists(prefixed(value))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def verify_handlers!
|
86
|
+
%w(on_redis_down on_session_load_error).each do |h|
|
87
|
+
next unless (handler = public_send(h)) && !handler.respond_to?(:call)
|
88
|
+
|
89
|
+
raise ArgumentError, "#{h} handler is not callable"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def prefixed(sid)
|
94
|
+
"#{default_options[:key_prefix]}#{sid}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def session_default_values
|
98
|
+
[generate_sid, USE_INDIFFERENT_ACCESS ? {}.with_indifferent_access : {}]
|
99
|
+
end
|
100
|
+
|
101
|
+
def get_session(env, cookie)
|
102
|
+
sid = get_cookie(env)
|
103
|
+
|
104
|
+
sid && (session = load_session_from_redis(sid)) ? [sid, session] : session_default_values
|
105
|
+
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
|
106
|
+
on_redis_down.call(e, env, sid) if on_redis_down
|
107
|
+
session_default_values
|
108
|
+
end
|
109
|
+
alias find_session get_session
|
110
|
+
|
111
|
+
def load_session_from_redis(sid)
|
112
|
+
data = redis.get(prefixed(sid))
|
113
|
+
begin
|
114
|
+
data ? decode(data) : nil
|
115
|
+
rescue StandardError => e
|
116
|
+
destroy_session_from_sid(sid, drop: true)
|
117
|
+
on_session_load_error.call(e, sid) if on_session_load_error
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def decode(data)
|
123
|
+
session = serializer.load(data)
|
124
|
+
USE_INDIFFERENT_ACCESS ? session.with_indifferent_access : session
|
125
|
+
end
|
126
|
+
|
127
|
+
def set_session(env, sid, session_data, options = nil)
|
128
|
+
expiry = get_expiry(env, options)
|
129
|
+
if expiry
|
130
|
+
redis.setex(prefixed(sid), expiry, encode(session_data))
|
131
|
+
else
|
132
|
+
redis.set(prefixed(sid), encode(session_data))
|
133
|
+
end
|
134
|
+
sid
|
135
|
+
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
|
136
|
+
on_redis_down.call(e, env, sid) if on_redis_down
|
137
|
+
false
|
138
|
+
end
|
139
|
+
alias write_session set_session
|
140
|
+
|
141
|
+
def get_expiry(env, options)
|
142
|
+
session_storage_options = options || env.fetch(ENV_SESSION_OPTIONS_KEY, {})
|
143
|
+
session_storage_options[:ttl] || session_storage_options[:expire_after]
|
144
|
+
end
|
145
|
+
|
146
|
+
def encode(session_data)
|
147
|
+
serializer.dump(session_data)
|
148
|
+
end
|
149
|
+
|
150
|
+
def destroy_session(env, sid, options)
|
151
|
+
destroy_session_from_sid(sid, (options || {}).to_hash.merge(env: env))
|
152
|
+
end
|
153
|
+
alias delete_session destroy_session
|
154
|
+
|
155
|
+
def destroy(env)
|
156
|
+
if env['rack.request.cookie_hash'] &&
|
157
|
+
(sid = env['rack.request.cookie_hash'][key])
|
158
|
+
destroy_session_from_sid(sid, drop: true, env: env)
|
159
|
+
end
|
160
|
+
false
|
161
|
+
end
|
162
|
+
|
163
|
+
def destroy_session_from_sid(sid, options = {})
|
164
|
+
redis.del(prefixed(sid))
|
165
|
+
(options || {})[:drop] ? nil : generate_sid
|
166
|
+
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
|
167
|
+
on_redis_down.call(e, options[:env] || {}, sid) if on_redis_down
|
168
|
+
end
|
169
|
+
|
170
|
+
def determine_serializer(serializer)
|
171
|
+
serializer ||= :marshal
|
172
|
+
case serializer
|
173
|
+
when :marshal then Marshal
|
174
|
+
when :json then JsonSerializer
|
175
|
+
when :hybrid then HybridSerializer
|
176
|
+
else serializer
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def set_cookie(request, sid, cookie)
|
181
|
+
cookie_jar(request)[@key] = cookie
|
182
|
+
end
|
183
|
+
|
184
|
+
def get_cookie(request)
|
185
|
+
cookie_jar(request)[@key]
|
186
|
+
end
|
187
|
+
|
188
|
+
def cookie_jar(request)
|
189
|
+
request.cookie_jar.signed_or_encrypted
|
190
|
+
end
|
191
|
+
|
192
|
+
# Uses built-in JSON library to encode/decode session
|
193
|
+
class JsonSerializer
|
194
|
+
def self.load(value)
|
195
|
+
JSON.parse(value, quirks_mode: true)
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.dump(value)
|
199
|
+
JSON.generate(value, quirks_mode: true)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Transparently migrates existing session values from Marshal to JSON
|
204
|
+
class HybridSerializer < JsonSerializer
|
205
|
+
MARSHAL_SIGNATURE = "\x04\x08".freeze
|
206
|
+
|
207
|
+
def self.load(value)
|
208
|
+
if needs_migration?(value)
|
209
|
+
Marshal.load(value)
|
210
|
+
else
|
211
|
+
super
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.needs_migration?(value)
|
216
|
+
value.start_with?(MARSHAL_SIGNATURE)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.name = 'brevio-session-store'
|
3
|
+
gem.version = '0.1.0'
|
4
|
+
gem.authors = ['Brevio AS']
|
5
|
+
gem.email = ['support@brevio.com']
|
6
|
+
gem.summary = 'A drop-in replacement for e.g. MemCacheStore to ' \
|
7
|
+
'store Rails sessions (and Rails sessions only) in Redis.'
|
8
|
+
gem.description = gem.summary + ' For great glory!'
|
9
|
+
gem.homepage = 'https://github.com/team-brevio/redis-session-store'
|
10
|
+
gem.license = 'MIT'
|
11
|
+
|
12
|
+
gem.files = `git ls-files -z`.split("\x0")
|
13
|
+
gem.require_paths = %w(lib)
|
14
|
+
|
15
|
+
gem.add_runtime_dependency 'actionpack', '>= 3', '< 8'
|
16
|
+
gem.add_runtime_dependency 'redis', '>= 3', '< 8'
|
17
|
+
|
18
|
+
gem.add_development_dependency 'rake'
|
19
|
+
gem.add_development_dependency 'rspec'
|
20
|
+
gem.add_development_dependency 'rubocop'
|
21
|
+
gem.add_development_dependency 'simplecov'
|
22
|
+
end
|