redis-session-store 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|