redis-session-store 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +6 -0
- data/README.md +35 -0
- data/lib/redis-session-store.rb +83 -12
- data/spec/redis_session_store_spec.rb +166 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f983cb90979acec73acae5cdfb437c49c55ab2ac
|
4
|
+
data.tar.gz: db3641121bde418a8e3414e494e055f2cf9e1c38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d342305a4a45503a240145c8dbbfaca8405ffafff0887a7c4bccdf0512cb701acb401509903d31b5b14901f9e5907c87ea9aa6096a12c727551b914ea1fcade
|
7
|
+
data.tar.gz: a1558f707a7f04a99c7c52851f4cfe0540696a4bbe94b3450f707316c1c5bde503e279cabe0dc360ffbcc627353178f2463243bbcb1a797a715267275dc15484
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
redis-session-store history
|
2
2
|
===========================
|
3
3
|
|
4
|
+
## v0.6.0 (2014-03-17)
|
5
|
+
|
6
|
+
* Add custom serializer configuration
|
7
|
+
* Add custom handling capability for session load errors
|
8
|
+
* Always destroying sessions that cannot be loaded
|
9
|
+
|
4
10
|
## v0.5.0 (2014-03-16)
|
5
11
|
|
6
12
|
* Keep generating session IDs until one is found that doesn't collide
|
data/README.md
CHANGED
@@ -80,6 +80,41 @@ My::Application.config.session_store = :redis_session_store, {
|
|
80
80
|
}
|
81
81
|
```
|
82
82
|
|
83
|
+
### Serializer
|
84
|
+
|
85
|
+
By default the Marshal serializer is used. With Rails 4, you can use JSON as a
|
86
|
+
custom serializer:
|
87
|
+
|
88
|
+
* `:json` - serialize cookie values with `JSON` (Requires Rails 4+)
|
89
|
+
* `:marshal` - serialize cookie values with `Marshal` (Default)
|
90
|
+
* `:hybrid` - transparently migrate existing `Marshal` cookie values to `JSON` (Requires Rails 4+)
|
91
|
+
* `CustomClass` - You can just pass the constant name of any class that responds to `.load` and `.dump`
|
92
|
+
|
93
|
+
``` ruby
|
94
|
+
My::Application.config.session_store = :redis_session_store, {
|
95
|
+
# ... other options ...
|
96
|
+
serializer: :hybrid
|
97
|
+
}
|
98
|
+
```
|
99
|
+
|
100
|
+
**Note**: Rails 4 is required for using the `:json` and `:hybrid` serializers
|
101
|
+
because the `Flash` object doesn't serializer well in 3.2. See [Rails #13945](https://github.com/rails/rails/pull/13945) for more info.
|
102
|
+
|
103
|
+
### Session load error handling
|
104
|
+
|
105
|
+
If you want to handle cases where the session data cannot be loaded, a
|
106
|
+
custom callable handler may be provided as `on_session_load_error` which
|
107
|
+
will be given the error and the session ID.
|
108
|
+
|
109
|
+
``` ruby
|
110
|
+
My::Application.config.session_store = :redis_session_store, {
|
111
|
+
# ... other options ...
|
112
|
+
on_session_load_error: ->(e, sid) { do_something_will_ya!(e) }
|
113
|
+
}
|
114
|
+
```
|
115
|
+
|
116
|
+
**Note** The session will *always* be destroyed when it cannot be loaded.
|
117
|
+
|
83
118
|
Contributing, Authors, & License
|
84
119
|
--------------------------------
|
85
120
|
|
data/lib/redis-session-store.rb
CHANGED
@@ -4,7 +4,7 @@ require 'redis'
|
|
4
4
|
# Redis session storage for Rails, and for Rails only. Derived from
|
5
5
|
# the MemCacheStore code, simply dropping in Redis instead.
|
6
6
|
class RedisSessionStore < ActionDispatch::Session::AbstractStore
|
7
|
-
VERSION = '0.
|
7
|
+
VERSION = '0.6.0'
|
8
8
|
|
9
9
|
# ==== Options
|
10
10
|
# * +:key+ - Same as with the other cookie stores, key name
|
@@ -16,6 +16,8 @@ class RedisSessionStore < ActionDispatch::Session::AbstractStore
|
|
16
16
|
# * +:expire_after+ - A number in seconds for session timeout
|
17
17
|
# * +:on_sid_collision:+ - Called with SID string when generated SID collides
|
18
18
|
# * +:on_redis_down:+ - Called with err, env, and SID on Errno::ECONNREFUSED
|
19
|
+
# * +:on_session_load_error:+ - Called with err and SID on Marshal.load fail
|
20
|
+
# * +:serializer:+ - Serializer to use on session data, default is :marshal.
|
19
21
|
#
|
20
22
|
# ==== Examples
|
21
23
|
#
|
@@ -30,6 +32,7 @@ class RedisSessionStore < ActionDispatch::Session::AbstractStore
|
|
30
32
|
# },
|
31
33
|
# on_sid_collision: ->(sid) { logger.warn("SID collision! #{sid}") },
|
32
34
|
# on_redis_down: ->(*a) { logger.error("Redis down! #{a.inspect}") }
|
35
|
+
# serializer: :hybrid # migrate from Marshal to JSON
|
33
36
|
# }
|
34
37
|
#
|
35
38
|
def initialize(app, options = {})
|
@@ -42,13 +45,24 @@ class RedisSessionStore < ActionDispatch::Session::AbstractStore
|
|
42
45
|
@redis = Redis.new(redis_options)
|
43
46
|
@on_sid_collision = options[:on_sid_collision]
|
44
47
|
@on_redis_down = options[:on_redis_down]
|
48
|
+
@serializer = determine_serializer(options[:serializer])
|
49
|
+
@on_session_load_error = options[:on_session_load_error]
|
50
|
+
verify_handlers!
|
45
51
|
end
|
46
52
|
|
47
|
-
attr_accessor :on_sid_collision, :on_redis_down
|
53
|
+
attr_accessor :on_sid_collision, :on_redis_down, :on_session_load_error
|
48
54
|
|
49
55
|
private
|
50
56
|
|
51
|
-
attr_reader :redis, :key, :default_options
|
57
|
+
attr_reader :redis, :key, :default_options, :serializer
|
58
|
+
|
59
|
+
def verify_handlers!
|
60
|
+
%w(on_sid_collision on_redis_down on_session_load_error).each do |h|
|
61
|
+
if (handler = public_send(h)) && !handler.respond_to?(:call)
|
62
|
+
fail ArgumentError, "#{h} handler is not callable"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
52
66
|
|
53
67
|
def prefixed(sid)
|
54
68
|
"#{default_options[:key_prefix]}#{sid}"
|
@@ -81,15 +95,25 @@ class RedisSessionStore < ActionDispatch::Session::AbstractStore
|
|
81
95
|
|
82
96
|
def load_session_from_redis(sid)
|
83
97
|
data = redis.get(prefixed(sid))
|
84
|
-
|
98
|
+
begin
|
99
|
+
data ? decode(data) : nil
|
100
|
+
rescue => e
|
101
|
+
destroy_session_from_sid(sid, drop: true)
|
102
|
+
on_session_load_error.call(e, sid) if on_session_load_error
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def decode(data)
|
108
|
+
serializer.load(data)
|
85
109
|
end
|
86
110
|
|
87
111
|
def set_session(env, sid, session_data, options = nil)
|
88
112
|
expiry = (options || env[ENV_SESSION_OPTIONS_KEY])[:expire_after]
|
89
113
|
if expiry
|
90
|
-
redis.setex(prefixed(sid), expiry,
|
114
|
+
redis.setex(prefixed(sid), expiry, encode(session_data))
|
91
115
|
else
|
92
|
-
redis.set(prefixed(sid),
|
116
|
+
redis.set(prefixed(sid), encode(session_data))
|
93
117
|
end
|
94
118
|
return sid
|
95
119
|
rescue Errno::ECONNREFUSED => e
|
@@ -97,19 +121,66 @@ class RedisSessionStore < ActionDispatch::Session::AbstractStore
|
|
97
121
|
return false
|
98
122
|
end
|
99
123
|
|
124
|
+
def encode(session_data)
|
125
|
+
serializer.dump(session_data)
|
126
|
+
end
|
127
|
+
|
100
128
|
def destroy_session(env, sid, options)
|
101
|
-
|
102
|
-
return nil if (options || {})[:drop]
|
103
|
-
generate_sid
|
129
|
+
destroy_session_from_sid(sid, (options || {}).merge(env: env))
|
104
130
|
end
|
105
131
|
|
106
132
|
def destroy(env)
|
107
133
|
if env['rack.request.cookie_hash'] &&
|
108
134
|
(sid = env['rack.request.cookie_hash'][key])
|
109
|
-
|
135
|
+
destroy_session_from_sid(sid, drop: true, env: env)
|
110
136
|
end
|
111
|
-
rescue Errno::ECONNREFUSED => e
|
112
|
-
on_redis_down.call(e, env, sid) if on_redis_down
|
113
137
|
false
|
114
138
|
end
|
139
|
+
|
140
|
+
def destroy_session_from_sid(sid, options = {})
|
141
|
+
redis.del(prefixed(sid))
|
142
|
+
(options || {})[:drop] ? nil : generate_sid
|
143
|
+
rescue Errno::ECONNREFUSED => e
|
144
|
+
on_redis_down.call(e, options[:env] || {}, sid) if on_redis_down
|
145
|
+
end
|
146
|
+
|
147
|
+
def determine_serializer(serializer)
|
148
|
+
serializer ||= :marshal
|
149
|
+
case serializer
|
150
|
+
when :marshal then Marshal
|
151
|
+
when :json then JsonSerializer
|
152
|
+
when :hybrid then HybridSerializer
|
153
|
+
else serializer
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Uses built-in JSON library to encode/decode session
|
158
|
+
class JsonSerializer
|
159
|
+
def self.load(value)
|
160
|
+
JSON.parse(value, quirks_mode: true)
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.dump(value)
|
164
|
+
JSON.generate(value, quirks_mode: true)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Transparently migrates existing session values from Marshal to JSON
|
169
|
+
class HybridSerializer < JsonSerializer
|
170
|
+
MARSHAL_SIGNATURE = "\x04\x08".freeze
|
171
|
+
|
172
|
+
def self.load(value)
|
173
|
+
if needs_migration?(value)
|
174
|
+
Marshal.load(value)
|
175
|
+
else
|
176
|
+
super
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def self.needs_migration?(value)
|
183
|
+
value.start_with?(MARSHAL_SIGNATURE)
|
184
|
+
end
|
185
|
+
end
|
115
186
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# vim:fileencoding=utf-8
|
2
|
+
require 'json'
|
2
3
|
|
3
4
|
describe RedisSessionStore do
|
4
5
|
let :random_string do
|
@@ -284,4 +285,169 @@ describe RedisSessionStore do
|
|
284
285
|
end
|
285
286
|
end
|
286
287
|
end
|
288
|
+
|
289
|
+
describe 'session encoding' do
|
290
|
+
let(:env) { double('env') }
|
291
|
+
let(:session_id) { 12_345 }
|
292
|
+
let(:session_data) { { 'some' => 'data' } }
|
293
|
+
let(:options) { {} }
|
294
|
+
let(:encoded_data) { Marshal.dump(session_data) }
|
295
|
+
let(:redis) { double('redis', set: nil, get: encoded_data) }
|
296
|
+
let(:expected_encoding) { encoded_data }
|
297
|
+
|
298
|
+
before do
|
299
|
+
store.stub(:redis).and_return(redis)
|
300
|
+
end
|
301
|
+
|
302
|
+
shared_examples_for 'serializer' do
|
303
|
+
it 'encodes correctly' do
|
304
|
+
redis.should_receive(:set).with('12345', expected_encoding)
|
305
|
+
store.send(:set_session, env, session_id, session_data, options)
|
306
|
+
end
|
307
|
+
|
308
|
+
it 'decodes correctly' do
|
309
|
+
expect(store.send(:get_session, env, session_id))
|
310
|
+
.to eq([session_id, session_data])
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
context 'marshal' do
|
315
|
+
let(:options) { { serializer: :marshal } }
|
316
|
+
it_should_behave_like 'serializer'
|
317
|
+
end
|
318
|
+
|
319
|
+
context 'json' do
|
320
|
+
let(:options) { { serializer: :json } }
|
321
|
+
let(:encoded_data) { '{"some":"data"}' }
|
322
|
+
|
323
|
+
it_should_behave_like 'serializer'
|
324
|
+
end
|
325
|
+
|
326
|
+
context 'hybrid' do
|
327
|
+
let(:options) { { serializer: :hybrid } }
|
328
|
+
let(:expected_encoding) { '{"some":"data"}' }
|
329
|
+
|
330
|
+
context 'marshal encoded data' do
|
331
|
+
it_should_behave_like 'serializer'
|
332
|
+
end
|
333
|
+
|
334
|
+
context 'json encoded data' do
|
335
|
+
let(:encoded_data) { '{"some":"data"}' }
|
336
|
+
|
337
|
+
it_should_behave_like 'serializer'
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
context 'custom' do
|
342
|
+
let :custom_serializer do
|
343
|
+
Class.new do
|
344
|
+
def self.load(value)
|
345
|
+
{ 'some' => 'data' }
|
346
|
+
end
|
347
|
+
|
348
|
+
def self.dump(value)
|
349
|
+
'somedata'
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
let(:options) { { serializer: custom_serializer } }
|
355
|
+
let(:expected_encoding) { 'somedata' }
|
356
|
+
|
357
|
+
it_should_behave_like 'serializer'
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
describe 'handling decode errors' do
|
362
|
+
context 'when a class is serialized that does not exist' do
|
363
|
+
before do
|
364
|
+
store.stub(
|
365
|
+
redis: double('redis', get: "\x04\bo:\nNonExistentClass\x00",
|
366
|
+
del: true)
|
367
|
+
)
|
368
|
+
end
|
369
|
+
|
370
|
+
it 'returns an empty session' do
|
371
|
+
expect(store.send(:load_session_from_redis, 'whatever')).to be_nil
|
372
|
+
end
|
373
|
+
|
374
|
+
it 'destroys and drops the session' do
|
375
|
+
store.should_receive(:destroy_session_from_sid).with('wut', drop: true)
|
376
|
+
store.send(:load_session_from_redis, 'wut')
|
377
|
+
end
|
378
|
+
|
379
|
+
context 'when a custom on_session_load_error handler is provided' do
|
380
|
+
before do
|
381
|
+
store.on_session_load_error = lambda do |e, sid|
|
382
|
+
@e = e
|
383
|
+
@sid = sid
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
it 'passes the error and the sid to the handler' do
|
388
|
+
store.send(:load_session_from_redis, 'foo')
|
389
|
+
expect(@e).to be_kind_of(StandardError)
|
390
|
+
expect(@sid).to eql('foo')
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
context 'when the encoded data is invalid' do
|
396
|
+
before do
|
397
|
+
store.stub(
|
398
|
+
redis: double('redis', get: "\x00\x00\x00\x00", del: true)
|
399
|
+
)
|
400
|
+
end
|
401
|
+
|
402
|
+
it 'returns an empty session' do
|
403
|
+
expect(store.send(:load_session_from_redis, 'bar')).to be_nil
|
404
|
+
end
|
405
|
+
|
406
|
+
it 'destroys and drops the session' do
|
407
|
+
store.should_receive(:destroy_session_from_sid).with('wut', drop: true)
|
408
|
+
store.send(:load_session_from_redis, 'wut')
|
409
|
+
end
|
410
|
+
|
411
|
+
context 'when a custom on_session_load_error handler is provided' do
|
412
|
+
before do
|
413
|
+
store.on_session_load_error = lambda do |e, sid|
|
414
|
+
@e = e
|
415
|
+
@sid = sid
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
it 'passes the error and the sid to the handler' do
|
420
|
+
store.send(:load_session_from_redis, 'foo')
|
421
|
+
expect(@e).to be_kind_of(StandardError)
|
422
|
+
expect(@sid).to eql('foo')
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
describe 'validating custom handlers' do
|
429
|
+
%w(on_redis_down on_sid_collision on_session_load_error).each do |h|
|
430
|
+
context 'when nil' do
|
431
|
+
it 'does not explode at init' do
|
432
|
+
expect { store }.to_not raise_error
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
context 'when callable' do
|
437
|
+
let(:options) { { :"#{h}" => ->(*) { !nil } } }
|
438
|
+
|
439
|
+
it 'does not explode at init' do
|
440
|
+
expect { store }.to_not raise_error
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
context 'when not callable' do
|
445
|
+
let(:options) { { :"#{h}" => 'herpderp' } }
|
446
|
+
|
447
|
+
it 'explodes at init' do
|
448
|
+
expect { store }.to raise_error(ArgumentError)
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
287
453
|
end
|