rack-session-rethinkdb 0.1.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.
@@ -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: []