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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9148a3bedfc1ab56951717dccdaed7c30e33e356
4
- data.tar.gz: ca00490849f6276f4262ddfa100fa8d90c819087
3
+ metadata.gz: f983cb90979acec73acae5cdfb437c49c55ab2ac
4
+ data.tar.gz: db3641121bde418a8e3414e494e055f2cf9e1c38
5
5
  SHA512:
6
- metadata.gz: fdbe30259d8e8eb71a650a73c59f613f830276f913d0e5bd062b5212db29704aa3ff56d7b5d443e95fabdd441085065a5ca8b02aa0c4b998264c3efeb4685cd9
7
- data.tar.gz: ad31abd62bb1eb5b093aa03ba1e0f2c11bc197e3e3ff395bb78dabfb1d7659ef2a06f05435cd03a3db060510700a8a1f4ca509cd6561849e017de2014f2e068c
6
+ metadata.gz: 8d342305a4a45503a240145c8dbbfaca8405ffafff0887a7c4bccdf0512cb701acb401509903d31b5b14901f9e5907c87ea9aa6096a12c727551b914ea1fcade
7
+ data.tar.gz: a1558f707a7f04a99c7c52851f4cfe0540696a4bbe94b3450f707316c1c5bde503e279cabe0dc360ffbcc627353178f2463243bbcb1a797a715267275dc15484
data/.rubocop.yml CHANGED
@@ -8,3 +8,8 @@
8
8
  # Offense count: 1
9
9
  FileName:
10
10
  Enabled: false
11
+
12
+ # Offense count: 1
13
+ # Configuration parameters: CountComments.
14
+ ClassLength:
15
+ Max: 103
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
 
@@ -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.5.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
- data ? Marshal.load(data) : nil
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, Marshal.dump(session_data))
114
+ redis.setex(prefixed(sid), expiry, encode(session_data))
91
115
  else
92
- redis.set(prefixed(sid), Marshal.dump(session_data))
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
- redis.del(prefixed(sid))
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
- redis.del(prefixed(sid))
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-session-store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mathias Meyer