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 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