rack-session-rethinkdb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|