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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/README.md +23 -0
- data/Rakefile +25 -0
- data/lib/rack/session/rethinkdb.rb +115 -0
- data/spec/rethinkdb_session_spec.rb +211 -0
- data/spec/spec_helper.rb +14 -0
- metadata +104 -0
checksums.yaml
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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
|
+
}
|
data/Rakefile
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|