rack-session-rethinkdb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e787a86f4e27f5e0c3f38c5dafaad7ac7106e385
4
+ data.tar.gz: 831a1af09e0d0d71040762cb70ad4bb1e318d1b2
5
+ SHA512:
6
+ metadata.gz: 6e72c5e4121c33d6fd1735872fb7df1b1c30accb687ddef088291f1221b8cdba57ad46656b41561c09b8931879777045c898f21c42e5128ebf697510dade3261
7
+ data.tar.gz: edeb347d93712503c3858a058c715fd1efd9c211019724710041d87240a8e22179db11aae82020fe7955665c1f79ade61f341e95be6db558ab2b1f7fcdd9423f
@@ -0,0 +1,5 @@
1
+ # Change Log
2
+
3
+ ## 0.1.0
4
+
5
+ * Initial release
@@ -0,0 +1,23 @@
1
+ # rack-session-rethinkdb
2
+
3
+ Store rack sessions in a RethinkDB table.
4
+
5
+ ## Installation
6
+
7
+ gem install rack-session-rethinkdb
8
+
9
+ The database and table to be used for storage must be created prior to use. The database name must be provided, the table name will default to `sessions` unless an alternate is specified.
10
+
11
+ ## Usage
12
+
13
+ Sessions will be stored in the table `sessions` by default. The hostname/IP and
14
+ database name must be specified and must exist.
15
+
16
+ require 'rack/session/rethinkdb'
17
+
18
+ use Rack::Session::RethinkDB, {
19
+ host: '127.0.0.1', # required
20
+ db: 'myapp', # required
21
+ port: 28015, # optional (default: 28015)
22
+ table: 'sessions' # optional (default: 'sessions')
23
+ }
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ require 'bundler/setup'
4
+ Bundler.require
5
+ require 'rake/clean'
6
+ require 'rubygems/package_task'
7
+
8
+ load 'tasks/db.rake'
9
+
10
+ require 'rspec/core/rake_task'
11
+ RSpec::Core::RakeTask.new(:spec) do |t|
12
+ t.rspec_opts = %w(-fs --color)
13
+ end
14
+
15
+ task default: ['db:reset', :spec]
16
+
17
+ desc 'Build'
18
+ task build: [:clean, :doc, :gem]
19
+
20
+ gemspec = Gem::Specification.load('rack-session-rethinkdb.gemspec')
21
+
22
+ Gem::PackageTask.new(gemspec) do |pkg|
23
+ end
24
+
25
+ CLEAN.include(['pkg', '*.gem', '.yardoc'])
@@ -0,0 +1,115 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rack/session/abstract/id'
4
+ require 'thread'
5
+ require 'rethinkdb'
6
+
7
+ module Rack
8
+ module Session
9
+ class RethinkDB < Abstract::ID
10
+ DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
11
+ port: 28_015, table: 'sessions'
12
+
13
+ attr_reader :mutex, :pool, :host, :port, :db, :table
14
+
15
+ # @see Rack::Session#initialize
16
+ #
17
+ # @param [Hash<Symbol,Object>] options
18
+ # @option options [String] :host hostname or IP for RethinkDB server
19
+ # (required)
20
+ # @option options [Integer] :port port number for the RethinkDB server
21
+ # (default: 28015)
22
+ # @option options [String] :db database name (required)
23
+ # @option options [String] :table table name to store sessions in
24
+ # (default: 'sessions')
25
+ def initialize(app, options = {})
26
+ super
27
+ @host = options[:host]
28
+ @port = @default_options[:port]
29
+ @db = @default_options[:db]
30
+ @table = @default_options[:table]
31
+
32
+ @mutex = Mutex.new
33
+ end
34
+
35
+ def generate_sid
36
+ loop do
37
+ sid = super
38
+ break sid unless _exists?(sid)
39
+ end
40
+ end
41
+
42
+ def get_session(env, sid)
43
+ with_lock(env, [nil, {}]) do
44
+ unless sid && (session = _get(sid))
45
+ sid, session = generate_sid, {}
46
+ _put(sid, session)
47
+ end
48
+
49
+ [sid, session]
50
+ end
51
+ end
52
+
53
+ def set_session(env, session_id, new_session, _options)
54
+ with_lock(env, false) do
55
+ _put(session_id, new_session)
56
+ session_id
57
+ end
58
+ end
59
+
60
+ def destroy_session(env, session_id, options)
61
+ with_lock(env) do
62
+ # @pool.del(session_id)
63
+ ::RethinkDB::RQL.new.db(db).table(table).get(session_id).delete
64
+ .run(connection)
65
+ generate_sid unless options[:drop]
66
+ end
67
+ end
68
+
69
+ def with_lock(env, default = nil)
70
+ mutex.lock if env['rack.multithread']
71
+ yield
72
+ rescue
73
+ default
74
+ ensure
75
+ mutex.unlock if mutex.locked?
76
+ end
77
+
78
+ private
79
+
80
+ # @return [RethinkDB::Connection]
81
+ def connection
82
+ @connection ||= ::RethinkDB::RQL.new.connect(host: host, port: port)
83
+ rescue Exception => err
84
+ $stderr.puts("Cannot connect to database: #{err.message}")
85
+ end
86
+
87
+ # Handle to the RethinkDB::RQL query DSL helper
88
+ #
89
+ # @return [RethinkDB::RQL]
90
+ def r
91
+ @r ||= ::RethinkDB::RQL.new
92
+ end
93
+
94
+ def _exists?(sid)
95
+ !_get(sid).nil?
96
+ end
97
+
98
+ def _get(sid)
99
+ record = r.db(db).table(table).get(sid).run(connection)
100
+ return unless record
101
+ Marshal.load(record['data'].unpack('m*').first)
102
+ end
103
+
104
+ def _put(sid, session)
105
+ data = {
106
+ id: sid,
107
+ updated_at: Time.now,
108
+ data: [Marshal.dump(session)].pack('m*')
109
+ }
110
+
111
+ r.db(db).table(table).insert(data, upsert: true).run(connection)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,211 @@
1
+ # encoding: utf-8
2
+ require File.expand_path('./spec_helper.rb', File.dirname(__FILE__))
3
+
4
+ require 'rack/session/rethinkdb'
5
+ require 'rack/mock'
6
+
7
+ def get_sid(response)
8
+ /#{session_key}=(.+?)\W/.match(response['Set-Cookie'])[1]
9
+ end
10
+
11
+ describe Rack::Session::RethinkDB do
12
+ let(:db_name) { 'rack_session_rethinkdb_test' }
13
+ let(:config) { { host: '127.0.0.1', port: 28015, db: db_name } }
14
+ let(:session_key) { Rack::Session::RethinkDB::DEFAULT_OPTIONS[:key] }
15
+ let(:default_table) { Rack::Session::RethinkDB::DEFAULT_OPTIONS[:table] }
16
+ let(:session_match) { /#{session_key}=[0-9a-fA-F]+;/ }
17
+
18
+ let(:connection) do
19
+ RethinkDB::RQL.new.connect(host: '127.0.0.1', port: 28015)
20
+ end
21
+
22
+ let!(:incrementor) do
23
+ lambda do |env|
24
+ env['rack.session']['counter'] ||= 0
25
+ env['rack.session']['counter'] += 1
26
+ Rack::Response.new(env['rack.session'].inspect).to_a
27
+ end
28
+ end
29
+
30
+ describe 'configuration' do
31
+ describe 'when :table is specified' do
32
+ let(:table) { 'my_session_table_test' }
33
+ let(:pool) do
34
+ Rack::Session::RethinkDB.new(incrementor, config.merge(table: table))
35
+ end
36
+ let(:response) { Rack::MockRequest.new(pool).get('/') }
37
+ let(:sid) { /#{session_key}=(.+?)\W/.match(response['Set-Cookie'])[1] }
38
+ let(:record) do
39
+ RethinkDB::RQL.new.db(db_name).table(table).get(sid).run(connection)
40
+ end
41
+
42
+ it 'writes the session to the named table' do
43
+ expect(record).to include('id' => sid)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe 'when no session cookie is set' do
49
+ let(:pool) { Rack::Session::RethinkDB.new(incrementor, config) }
50
+ let(:response) { Rack::MockRequest.new(pool).get('/') }
51
+
52
+ it 'sets a cookie' do
53
+ expect(response['Set-Cookie']).to match(/#{session_key}=.+/)
54
+ end
55
+
56
+ describe 'persistence' do
57
+ let(:sid) { get_sid(response) }
58
+ let(:record) do
59
+ RethinkDB::RQL.new.db(db_name).table(default_table)
60
+ .get(sid).run(connection)
61
+ end
62
+
63
+ it 'writes a document for the session' do
64
+ expect(record).to include('id' => sid)
65
+ end
66
+
67
+ it 'persists the session data as a base64 encoded marshalled array' do
68
+ data = Marshal.load(record['data'].unpack('m*').first)
69
+
70
+ expect(data).to include('counter' => 1)
71
+ end
72
+
73
+ it 'sets the updated_at timestamp' do
74
+ expect(record['updated_at']).to be_a_kind_of(Time)
75
+ expect(record['updated_at']).to be_within(1).of(Time.now)
76
+ end
77
+ end
78
+ end
79
+
80
+ context 'when a session cookie is present' do
81
+ context 'when the session ID is found in the database' do
82
+ let(:pool) { Rack::Session::RethinkDB.new(incrementor, config) }
83
+ let(:request) { Rack::MockRequest.new(pool) }
84
+ let(:response) do
85
+ cookie = request.get('/')['Set-Cookie']
86
+ request.get('/', 'HTTP_COOKIE' => cookie)
87
+ end
88
+
89
+ it 'sets the serialized session data on the request' do
90
+ expect(response.body).to eq('{"counter"=>2}')
91
+ end
92
+
93
+ it 'does not resend the session id' do
94
+ expect(response['Set-Cookie']).to be_nil
95
+ end
96
+ end
97
+
98
+ context 'when the session has expired' do
99
+ end
100
+
101
+ context 'when the session ID is not found in the database' do
102
+ let(:pool) { Rack::Session::RethinkDB.new(incrementor, config) }
103
+ let(:request) { Rack::MockRequest.new(pool) }
104
+ let(:bad_sid) { 'foobarbaz' }
105
+ let(:response) do
106
+ request.get('/', 'HTTP_COOKIE' => "#{session_key}=#{bad_sid}")
107
+ end
108
+ let(:sid) { get_sid(response) }
109
+
110
+ it 'sets up fresh session data' do
111
+ expect(response.body).to eq('{"counter"=>1}')
112
+ end
113
+
114
+ it 'sets a new session ID' do
115
+ expect(sid).not_to eq(bad_sid)
116
+ end
117
+ end
118
+ end
119
+
120
+ describe 'rack.session.options' do
121
+ context 'when :drop is set' do
122
+ let!(:drop_session) do
123
+ Rack::Lint.new(lambda do |env|
124
+ env['rack.session.options'][:drop] = true
125
+ incrementor.call(env)
126
+ end)
127
+ end
128
+ let(:pool) { Rack::Session::RethinkDB.new(incrementor, config) }
129
+ let(:request) { Rack::MockRequest.new(pool) }
130
+ let(:drop_request) do
131
+ Rack::MockRequest.new(Rack::Utils::Context.new(pool, drop_session))
132
+ end
133
+
134
+ it 'deletes cookies with :drop option' do
135
+ res1 = request.get('/')
136
+ session = (cookie = res1['Set-Cookie'])[session_match]
137
+ expect(res1.body).to eq('{"counter"=>1}')
138
+
139
+ res2 = drop_request.get('/', 'HTTP_COOKIE' => cookie)
140
+ expect(res2['Set-Cookie']).to be_nil
141
+ expect(res2.body).to eq('{"counter"=>2}')
142
+
143
+ res3 = request.get('/', 'HTTP_COOKIE' => cookie)
144
+ expect(res3['Set-Cookie'][session_match]).not_to eq(session)
145
+ expect(res3.body).to eq('{"counter"=>1}')
146
+ end
147
+ end
148
+
149
+ context 'when :renew is set' do
150
+ let!(:renew_session) do
151
+ Rack::Lint.new(lambda do |env|
152
+ env['rack.session.options'][:renew] = true
153
+ incrementor.call(env)
154
+ end)
155
+ end
156
+ let(:pool) { Rack::Session::RethinkDB.new(incrementor, config) }
157
+ let(:request) { Rack::MockRequest.new(pool) }
158
+ let(:renew_request) do
159
+ Rack::MockRequest.new(Rack::Utils::Context.new(pool, renew_session))
160
+ end
161
+
162
+ it 'provides a new session ID' do
163
+ res1 = request.get('/')
164
+ session = (cookie = res1['Set-Cookie'])[session_match]
165
+ expect(res1.body).to eq('{"counter"=>1}')
166
+
167
+ res2 = renew_request.get('/', 'HTTP_COOKIE' => cookie)
168
+ new_cookie = res2['Set-Cookie']
169
+ new_session = new_cookie[session_match]
170
+ expect(new_session).not_to eq(session)
171
+ expect(res2.body).to eq('{"counter"=>2}')
172
+
173
+ res3 = request.get('/', 'HTTP_COOKIE' => new_cookie)
174
+ expect(res3.body).to eq('{"counter"=>3}')
175
+
176
+ res4 = request.get('/', 'HTTP_COOKIE' => cookie)
177
+ expect(res4.body).to eq('{"counter"=>1}')
178
+ end
179
+
180
+ it 'deletes the original session' do
181
+ res1 = request.get('/')
182
+ original_sid = get_sid(res1)
183
+ cookie = res1['Set-Cookie'][session_match]
184
+ res2 = renew_request.get('/', 'HTTP_COOKIE' => cookie)
185
+
186
+ expect(RethinkDB::RQL.new.db(db_name).table(default_table)
187
+ .get(original_sid).run(connection)).to be_nil
188
+ end
189
+ end
190
+
191
+ context 'when :defer is set' do
192
+ let(:pool) { Rack::Session::RethinkDB.new(incrementor, config) }
193
+ let!(:defer_session) do
194
+ Rack::Lint.new(lambda do |env|
195
+ env['rack.session.options'][:defer] = true
196
+ incrementor.call(env)
197
+ end)
198
+ end
199
+
200
+ let(:defer_request) do
201
+ Rack::MockRequest.new(Rack::Utils::Context.new(pool, defer_session))
202
+ end
203
+
204
+ it 'omits the cookie' do
205
+ res1 = defer_request.get('/')
206
+ expect(res1['Set-Cookie']).to be_nil
207
+ expect(res1.body).to eq('{"counter"=>1}')
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ require 'bundler/setup'
4
+ Bundler.require
5
+
6
+ require 'rspec'
7
+
8
+ $LOAD_PATH.unshift File.expand_path('../lib', File.dirname(__FILE__))
9
+
10
+ RSpec.configure do |config|
11
+ config.expect_with :rspec do |c|
12
+ c.syntax = :expect
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-session-rethinkdb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Dlug
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rethinkdb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Provides a rack session middleware to store sessions in a RethinkDB table.
70
+ email: paul.dlug@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - CHANGELOG.md
76
+ - README.md
77
+ - Rakefile
78
+ - lib/rack/session/rethinkdb.rb
79
+ - spec/rethinkdb_session_spec.rb
80
+ - spec/spec_helper.rb
81
+ homepage: http://github.com/pdlug/rack-session-rethinkdb
82
+ licenses: []
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 2.2.2
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Rack session storage in RethinkDB
104
+ test_files: []