rack-session 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rack/session/version.rb +4 -1
- data/license.md +24 -0
- data/readme.md +47 -0
- data/security.md +3 -0
- metadata +25 -26
- data/LICENSE.md +0 -23
- data/lib/rack/session/abstract/id.rb +0 -532
- data/lib/rack/session/constants.rb +0 -9
- data/lib/rack/session/cookie.rb +0 -308
- data/lib/rack/session/encryptor.rb +0 -188
- data/lib/rack/session/pool.rb +0 -78
- data/lib/rack/session.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 197fa5e7a11e2cf9998fb2e456b2d2ae84d3eb21e4208d38741b17dc8356375f
|
4
|
+
data.tar.gz: f0f51a370a60eb8abf007d37e8f7763514a374c937b85529e1a0bf7e9c61eb8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f70b021ddb700f519752fc64c0ceed6ffe04c2d28ae2f7c15b32368c503e721fd804c2fce7e0f3052ae4b5d3c0c362918361b377bd068d7abce761ca2e916030
|
7
|
+
data.tar.gz: 179dba7f3f3f2610bd23f9a36c2d8cef776e8cf4cf9cba8f85c44a7a8e1b6ca473fdf08eabd22ac6a4b88b59ebc016c270c212ad67f979961e7ba6b16c07612f
|
data/lib/rack/session/version.rb
CHANGED
data/license.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# MIT License
|
2
|
+
|
3
|
+
Copyright, 2022-2023, by Samuel Williams.
|
4
|
+
Copyright, 2022, by Jeremy Evans.
|
5
|
+
Copyright, 2022, by Philip Arndt.
|
6
|
+
Copyright, 2022, by Jon Dufresne.
|
7
|
+
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
10
|
+
in the Software without restriction, including without limitation the rights
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
13
|
+
furnished to do so, subject to the following conditions:
|
14
|
+
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
16
|
+
copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
24
|
+
SOFTWARE.
|
data/readme.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Rack::Session
|
2
|
+
|
3
|
+
Session management implementation for Rack.
|
4
|
+
|
5
|
+
[![Development Status](https://github.com/rack/rack-session/workflows/Test/badge.svg)](https://github.com/rack/rack-session/actions?workflow=Test)
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
In your `config.ru`:
|
10
|
+
|
11
|
+
``` ruby
|
12
|
+
# config.ru
|
13
|
+
|
14
|
+
require 'rack/session'
|
15
|
+
use Rack::Session::Cookie,
|
16
|
+
:domain => 'mywebsite.com',
|
17
|
+
:path => '/',
|
18
|
+
:expire_after => 3600*24,
|
19
|
+
:secret => '**unique secret key**'
|
20
|
+
```
|
21
|
+
|
22
|
+
Usage follows the standard outlined by `rack.session`, i.e.:
|
23
|
+
|
24
|
+
``` ruby
|
25
|
+
class MyApp
|
26
|
+
def call(env)
|
27
|
+
session = env['rack.session']
|
28
|
+
|
29
|
+
# Set some state:
|
30
|
+
session[:key] = "value"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
### Compatibility
|
36
|
+
|
37
|
+
`rack-session` code used to be part of Rack, but it was extracted in Rack v3 to this gem. The v1 release of this gem is compatible with Rack v2, and the v2 release of this gem is compatible with Rack v3+. That means you can add `gem "rack-session"` to your application and it will be compatible with all versions of Rack.
|
38
|
+
|
39
|
+
## Contributing
|
40
|
+
|
41
|
+
We welcome contributions to this project.
|
42
|
+
|
43
|
+
1. Fork it.
|
44
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
45
|
+
3. Commit your changes (`git commit -am 'Add some feature'`).
|
46
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
47
|
+
5. Create new Pull Request.
|
data/security.md
ADDED
metadata
CHANGED
@@ -1,57 +1,60 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-session
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Samuel Williams
|
8
|
+
- Jeremy Evans
|
9
|
+
- Jon Dufresne
|
10
|
+
- Philip Arndt
|
8
11
|
autorequire:
|
9
12
|
bindir: bin
|
10
13
|
cert_chain: []
|
11
|
-
date:
|
14
|
+
date: 2023-01-18 00:00:00.000000000 Z
|
12
15
|
dependencies:
|
13
16
|
- !ruby/object:Gem::Dependency
|
14
17
|
name: rack
|
15
18
|
requirement: !ruby/object:Gem::Requirement
|
16
19
|
requirements:
|
17
|
-
- - "
|
20
|
+
- - "<="
|
18
21
|
- !ruby/object:Gem::Version
|
19
|
-
version: 3
|
22
|
+
version: '3'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - "<="
|
25
28
|
- !ruby/object:Gem::Version
|
26
|
-
version: 3
|
29
|
+
version: '3'
|
27
30
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
31
|
+
name: bundler
|
29
32
|
requirement: !ruby/object:Gem::Requirement
|
30
33
|
requirements:
|
31
|
-
- - "
|
34
|
+
- - ">="
|
32
35
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
36
|
+
version: '0'
|
34
37
|
type: :development
|
35
38
|
prerelease: false
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
37
40
|
requirements:
|
38
|
-
- - "
|
41
|
+
- - ">="
|
39
42
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
43
|
+
version: '0'
|
41
44
|
- !ruby/object:Gem::Dependency
|
42
|
-
name: minitest
|
45
|
+
name: minitest
|
43
46
|
requirement: !ruby/object:Gem::Requirement
|
44
47
|
requirements:
|
45
|
-
- - "
|
48
|
+
- - "~>"
|
46
49
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
50
|
+
version: '5.0'
|
48
51
|
type: :development
|
49
52
|
prerelease: false
|
50
53
|
version_requirements: !ruby/object:Gem::Requirement
|
51
54
|
requirements:
|
52
|
-
- - "
|
55
|
+
- - "~>"
|
53
56
|
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
57
|
+
version: '5.0'
|
55
58
|
- !ruby/object:Gem::Dependency
|
56
59
|
name: minitest-global_expectations
|
57
60
|
requirement: !ruby/object:Gem::Requirement
|
@@ -67,7 +70,7 @@ dependencies:
|
|
67
70
|
- !ruby/object:Gem::Version
|
68
71
|
version: '0'
|
69
72
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
73
|
+
name: minitest-sprint
|
71
74
|
requirement: !ruby/object:Gem::Requirement
|
72
75
|
requirements:
|
73
76
|
- - ">="
|
@@ -100,14 +103,10 @@ executables: []
|
|
100
103
|
extensions: []
|
101
104
|
extra_rdoc_files: []
|
102
105
|
files:
|
103
|
-
- LICENSE.md
|
104
|
-
- lib/rack/session.rb
|
105
|
-
- lib/rack/session/abstract/id.rb
|
106
|
-
- lib/rack/session/constants.rb
|
107
|
-
- lib/rack/session/cookie.rb
|
108
|
-
- lib/rack/session/encryptor.rb
|
109
|
-
- lib/rack/session/pool.rb
|
110
106
|
- lib/rack/session/version.rb
|
107
|
+
- license.md
|
108
|
+
- readme.md
|
109
|
+
- security.md
|
111
110
|
homepage: https://github.com/rack/rack-session
|
112
111
|
licenses:
|
113
112
|
- MIT
|
@@ -127,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
126
|
- !ruby/object:Gem::Version
|
128
127
|
version: '0'
|
129
128
|
requirements: []
|
130
|
-
rubygems_version: 3.4.
|
129
|
+
rubygems_version: 3.4.1
|
131
130
|
signing_key:
|
132
131
|
specification_version: 4
|
133
132
|
summary: A session implementation for Rack.
|
data/LICENSE.md
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
The MIT License (MIT)
|
2
|
-
|
3
|
-
Copyright, 2007-2021, by [Leah Neukirchen](https://leahneukirchen.org).
|
4
|
-
Copyright, 2008, by Scytrin dai Kinthra.
|
5
|
-
Copyright, 2020, by [Michael Coyne](https://michaeljcoyne.me).
|
6
|
-
Copyright, 2021, by [Samuel G. D. Williams](https://www.codeotaku.com).
|
7
|
-
|
8
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
-
of this software and associated documentation files (the "Software"), to
|
10
|
-
deal in the Software without restriction, including without limitation the
|
11
|
-
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
12
|
-
sell copies of the Software, and to permit persons to whom the Software is
|
13
|
-
furnished to do so, subject to the following conditions:
|
14
|
-
|
15
|
-
The above copyright notice and this permission notice shall be included in
|
16
|
-
all copies or substantial portions of the Software.
|
17
|
-
|
18
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
21
|
-
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
22
|
-
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
23
|
-
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -1,532 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
|
4
|
-
# bugrep: Andreas Zehnder
|
5
|
-
|
6
|
-
require 'time'
|
7
|
-
require 'securerandom'
|
8
|
-
require 'digest/sha2'
|
9
|
-
|
10
|
-
require 'rack/constants'
|
11
|
-
require 'rack/request'
|
12
|
-
require 'rack/response'
|
13
|
-
|
14
|
-
require_relative '../constants'
|
15
|
-
|
16
|
-
module Rack
|
17
|
-
|
18
|
-
module Session
|
19
|
-
|
20
|
-
class SessionId
|
21
|
-
ID_VERSION = 2
|
22
|
-
|
23
|
-
attr_reader :public_id
|
24
|
-
|
25
|
-
def initialize(public_id)
|
26
|
-
@public_id = public_id
|
27
|
-
end
|
28
|
-
|
29
|
-
def private_id
|
30
|
-
"#{ID_VERSION}::#{hash_sid(public_id)}"
|
31
|
-
end
|
32
|
-
|
33
|
-
alias :cookie_value :public_id
|
34
|
-
alias :to_s :public_id
|
35
|
-
|
36
|
-
def empty?; false; end
|
37
|
-
def inspect; public_id.inspect; end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def hash_sid(sid)
|
42
|
-
Digest::SHA256.hexdigest(sid)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
module Abstract
|
47
|
-
# SessionHash is responsible to lazily load the session from store.
|
48
|
-
|
49
|
-
class SessionHash
|
50
|
-
include Enumerable
|
51
|
-
attr_writer :id
|
52
|
-
|
53
|
-
Unspecified = Object.new
|
54
|
-
|
55
|
-
def self.find(req)
|
56
|
-
req.get_header RACK_SESSION
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.set(req, session)
|
60
|
-
req.set_header RACK_SESSION, session
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.set_options(req, options)
|
64
|
-
req.set_header RACK_SESSION_OPTIONS, options.dup
|
65
|
-
end
|
66
|
-
|
67
|
-
def initialize(store, req)
|
68
|
-
@store = store
|
69
|
-
@req = req
|
70
|
-
@loaded = false
|
71
|
-
end
|
72
|
-
|
73
|
-
def id
|
74
|
-
return @id if @loaded or instance_variable_defined?(:@id)
|
75
|
-
@id = @store.send(:extract_session_id, @req)
|
76
|
-
end
|
77
|
-
|
78
|
-
def options
|
79
|
-
@req.session_options
|
80
|
-
end
|
81
|
-
|
82
|
-
def each(&block)
|
83
|
-
load_for_read!
|
84
|
-
@data.each(&block)
|
85
|
-
end
|
86
|
-
|
87
|
-
def [](key)
|
88
|
-
load_for_read!
|
89
|
-
@data[key.to_s]
|
90
|
-
end
|
91
|
-
|
92
|
-
def dig(key, *keys)
|
93
|
-
load_for_read!
|
94
|
-
@data.dig(key.to_s, *keys)
|
95
|
-
end
|
96
|
-
|
97
|
-
def fetch(key, default = Unspecified, &block)
|
98
|
-
load_for_read!
|
99
|
-
if default == Unspecified
|
100
|
-
@data.fetch(key.to_s, &block)
|
101
|
-
else
|
102
|
-
@data.fetch(key.to_s, default, &block)
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
def has_key?(key)
|
107
|
-
load_for_read!
|
108
|
-
@data.has_key?(key.to_s)
|
109
|
-
end
|
110
|
-
alias :key? :has_key?
|
111
|
-
alias :include? :has_key?
|
112
|
-
|
113
|
-
def []=(key, value)
|
114
|
-
load_for_write!
|
115
|
-
@data[key.to_s] = value
|
116
|
-
end
|
117
|
-
alias :store :[]=
|
118
|
-
|
119
|
-
def clear
|
120
|
-
load_for_write!
|
121
|
-
@data.clear
|
122
|
-
end
|
123
|
-
|
124
|
-
def destroy
|
125
|
-
clear
|
126
|
-
@id = @store.send(:delete_session, @req, id, options)
|
127
|
-
end
|
128
|
-
|
129
|
-
def to_hash
|
130
|
-
load_for_read!
|
131
|
-
@data.dup
|
132
|
-
end
|
133
|
-
|
134
|
-
def update(hash)
|
135
|
-
load_for_write!
|
136
|
-
@data.update(stringify_keys(hash))
|
137
|
-
end
|
138
|
-
alias :merge! :update
|
139
|
-
|
140
|
-
def replace(hash)
|
141
|
-
load_for_write!
|
142
|
-
@data.replace(stringify_keys(hash))
|
143
|
-
end
|
144
|
-
|
145
|
-
def delete(key)
|
146
|
-
load_for_write!
|
147
|
-
@data.delete(key.to_s)
|
148
|
-
end
|
149
|
-
|
150
|
-
def inspect
|
151
|
-
if loaded?
|
152
|
-
@data.inspect
|
153
|
-
else
|
154
|
-
"#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def exists?
|
159
|
-
return @exists if instance_variable_defined?(:@exists)
|
160
|
-
@data = {}
|
161
|
-
@exists = @store.send(:session_exists?, @req)
|
162
|
-
end
|
163
|
-
|
164
|
-
def loaded?
|
165
|
-
@loaded
|
166
|
-
end
|
167
|
-
|
168
|
-
def empty?
|
169
|
-
load_for_read!
|
170
|
-
@data.empty?
|
171
|
-
end
|
172
|
-
|
173
|
-
def keys
|
174
|
-
load_for_read!
|
175
|
-
@data.keys
|
176
|
-
end
|
177
|
-
|
178
|
-
def values
|
179
|
-
load_for_read!
|
180
|
-
@data.values
|
181
|
-
end
|
182
|
-
|
183
|
-
private
|
184
|
-
|
185
|
-
def load_for_read!
|
186
|
-
load! if !loaded? && exists?
|
187
|
-
end
|
188
|
-
|
189
|
-
def load_for_write!
|
190
|
-
load! unless loaded?
|
191
|
-
end
|
192
|
-
|
193
|
-
def load!
|
194
|
-
@id, session = @store.send(:load_session, @req)
|
195
|
-
@data = stringify_keys(session)
|
196
|
-
@loaded = true
|
197
|
-
end
|
198
|
-
|
199
|
-
def stringify_keys(other)
|
200
|
-
# Use transform_keys after dropping Ruby 2.4 support
|
201
|
-
hash = {}
|
202
|
-
other.to_hash.each do |key, value|
|
203
|
-
hash[key.to_s] = value
|
204
|
-
end
|
205
|
-
hash
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
# ID sets up a basic framework for implementing an id based sessioning
|
210
|
-
# service. Cookies sent to the client for maintaining sessions will only
|
211
|
-
# contain an id reference. Only #find_session, #write_session and
|
212
|
-
# #delete_session are required to be overwritten.
|
213
|
-
#
|
214
|
-
# All parameters are optional.
|
215
|
-
# * :key determines the name of the cookie, by default it is
|
216
|
-
# 'rack.session'
|
217
|
-
# * :path, :domain, :expire_after, :secure, :httponly, and :same_site set
|
218
|
-
# the related cookie options as by Rack::Response#set_cookie
|
219
|
-
# * :skip will not a set a cookie in the response nor update the session state
|
220
|
-
# * :defer will not set a cookie in the response but still update the session
|
221
|
-
# state if it is used with a backend
|
222
|
-
# * :renew (implementation dependent) will prompt the generation of a new
|
223
|
-
# session id, and migration of data to be referenced at the new id. If
|
224
|
-
# :defer is set, it will be overridden and the cookie will be set.
|
225
|
-
# * :sidbits sets the number of bits in length that a generated session
|
226
|
-
# id will be.
|
227
|
-
#
|
228
|
-
# These options can be set on a per request basis, at the location of
|
229
|
-
# <tt>env['rack.session.options']</tt>. Additionally the id of the
|
230
|
-
# session can be found within the options hash at the key :id. It is
|
231
|
-
# highly not recommended to change its value.
|
232
|
-
#
|
233
|
-
# Is Rack::Utils::Context compatible.
|
234
|
-
#
|
235
|
-
# Not included by default; you must require 'rack/session/abstract/id'
|
236
|
-
# to use.
|
237
|
-
|
238
|
-
class Persisted
|
239
|
-
DEFAULT_OPTIONS = {
|
240
|
-
key: RACK_SESSION,
|
241
|
-
path: '/',
|
242
|
-
domain: nil,
|
243
|
-
expire_after: nil,
|
244
|
-
secure: false,
|
245
|
-
httponly: true,
|
246
|
-
defer: false,
|
247
|
-
renew: false,
|
248
|
-
sidbits: 128,
|
249
|
-
cookie_only: true,
|
250
|
-
secure_random: ::SecureRandom
|
251
|
-
}.freeze
|
252
|
-
|
253
|
-
attr_reader :key, :default_options, :sid_secure, :same_site
|
254
|
-
|
255
|
-
def initialize(app, options = {})
|
256
|
-
@app = app
|
257
|
-
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
|
258
|
-
@key = @default_options.delete(:key)
|
259
|
-
@cookie_only = @default_options.delete(:cookie_only)
|
260
|
-
@same_site = @default_options.delete(:same_site)
|
261
|
-
initialize_sid
|
262
|
-
end
|
263
|
-
|
264
|
-
def call(env)
|
265
|
-
context(env)
|
266
|
-
end
|
267
|
-
|
268
|
-
def context(env, app = @app)
|
269
|
-
req = make_request env
|
270
|
-
prepare_session(req)
|
271
|
-
status, headers, body = app.call(req.env)
|
272
|
-
res = Rack::Response::Raw.new status, headers
|
273
|
-
commit_session(req, res)
|
274
|
-
[status, headers, body]
|
275
|
-
end
|
276
|
-
|
277
|
-
private
|
278
|
-
|
279
|
-
def make_request(env)
|
280
|
-
Rack::Request.new env
|
281
|
-
end
|
282
|
-
|
283
|
-
def initialize_sid
|
284
|
-
@sidbits = @default_options[:sidbits]
|
285
|
-
@sid_secure = @default_options[:secure_random]
|
286
|
-
@sid_length = @sidbits / 4
|
287
|
-
end
|
288
|
-
|
289
|
-
# Generate a new session id using Ruby #rand. The size of the
|
290
|
-
# session id is controlled by the :sidbits option.
|
291
|
-
# Monkey patch this to use custom methods for session id generation.
|
292
|
-
|
293
|
-
def generate_sid(secure = @sid_secure)
|
294
|
-
if secure
|
295
|
-
secure.hex(@sid_length)
|
296
|
-
else
|
297
|
-
"%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
|
298
|
-
end
|
299
|
-
rescue NotImplementedError
|
300
|
-
generate_sid(false)
|
301
|
-
end
|
302
|
-
|
303
|
-
# Sets the lazy session at 'rack.session' and places options and session
|
304
|
-
# metadata into 'rack.session.options'.
|
305
|
-
|
306
|
-
def prepare_session(req)
|
307
|
-
session_was = req.get_header RACK_SESSION
|
308
|
-
session = session_class.new(self, req)
|
309
|
-
req.set_header RACK_SESSION, session
|
310
|
-
req.set_header RACK_SESSION_OPTIONS, @default_options.dup
|
311
|
-
session.merge! session_was if session_was
|
312
|
-
end
|
313
|
-
|
314
|
-
# Extracts the session id from provided cookies and passes it and the
|
315
|
-
# environment to #find_session.
|
316
|
-
|
317
|
-
def load_session(req)
|
318
|
-
sid = current_session_id(req)
|
319
|
-
sid, session = find_session(req, sid)
|
320
|
-
[sid, session || {}]
|
321
|
-
end
|
322
|
-
|
323
|
-
# Extract session id from request object.
|
324
|
-
|
325
|
-
def extract_session_id(request)
|
326
|
-
sid = request.cookies[@key]
|
327
|
-
sid ||= request.params[@key] unless @cookie_only
|
328
|
-
sid
|
329
|
-
end
|
330
|
-
|
331
|
-
# Returns the current session id from the SessionHash.
|
332
|
-
|
333
|
-
def current_session_id(req)
|
334
|
-
req.get_header(RACK_SESSION).id
|
335
|
-
end
|
336
|
-
|
337
|
-
# Check if the session exists or not.
|
338
|
-
|
339
|
-
def session_exists?(req)
|
340
|
-
value = current_session_id(req)
|
341
|
-
value && !value.empty?
|
342
|
-
end
|
343
|
-
|
344
|
-
# Session should be committed if it was loaded, any of specific options like :renew, :drop
|
345
|
-
# or :expire_after was given and the security permissions match. Skips if skip is given.
|
346
|
-
|
347
|
-
def commit_session?(req, session, options)
|
348
|
-
if options[:skip]
|
349
|
-
false
|
350
|
-
else
|
351
|
-
has_session = loaded_session?(session) || forced_session_update?(session, options)
|
352
|
-
has_session && security_matches?(req, options)
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
def loaded_session?(session)
|
357
|
-
!session.is_a?(session_class) || session.loaded?
|
358
|
-
end
|
359
|
-
|
360
|
-
def forced_session_update?(session, options)
|
361
|
-
force_options?(options) && session && !session.empty?
|
362
|
-
end
|
363
|
-
|
364
|
-
def force_options?(options)
|
365
|
-
options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
|
366
|
-
end
|
367
|
-
|
368
|
-
def security_matches?(request, options)
|
369
|
-
return true unless options[:secure]
|
370
|
-
request.ssl?
|
371
|
-
end
|
372
|
-
|
373
|
-
# Acquires the session from the environment and the session id from
|
374
|
-
# the session options and passes them to #write_session. If successful
|
375
|
-
# and the :defer option is not true, a cookie will be added to the
|
376
|
-
# response with the session's id.
|
377
|
-
|
378
|
-
def commit_session(req, res)
|
379
|
-
session = req.get_header RACK_SESSION
|
380
|
-
options = session.options
|
381
|
-
|
382
|
-
if options[:drop] || options[:renew]
|
383
|
-
session_id = delete_session(req, session.id || generate_sid, options)
|
384
|
-
return unless session_id
|
385
|
-
end
|
386
|
-
|
387
|
-
return unless commit_session?(req, session, options)
|
388
|
-
|
389
|
-
session.send(:load!) unless loaded_session?(session)
|
390
|
-
session_id ||= session.id
|
391
|
-
session_data = session.to_hash.delete_if { |k, v| v.nil? }
|
392
|
-
|
393
|
-
if not data = write_session(req, session_id, session_data, options)
|
394
|
-
req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
|
395
|
-
elsif options[:defer] and not options[:renew]
|
396
|
-
req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
|
397
|
-
else
|
398
|
-
cookie = Hash.new
|
399
|
-
cookie[:value] = cookie_value(data)
|
400
|
-
cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
|
401
|
-
cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
|
402
|
-
|
403
|
-
if @same_site.respond_to? :call
|
404
|
-
cookie[:same_site] = @same_site.call(req, res)
|
405
|
-
else
|
406
|
-
cookie[:same_site] = @same_site
|
407
|
-
end
|
408
|
-
set_cookie(req, res, cookie.merge!(options))
|
409
|
-
end
|
410
|
-
end
|
411
|
-
public :commit_session
|
412
|
-
|
413
|
-
def cookie_value(data)
|
414
|
-
data
|
415
|
-
end
|
416
|
-
|
417
|
-
# Sets the cookie back to the client with session id. We skip the cookie
|
418
|
-
# setting if the value didn't change (sid is the same) or expires was given.
|
419
|
-
|
420
|
-
def set_cookie(request, response, cookie)
|
421
|
-
if request.cookies[@key] != cookie[:value] || cookie[:expires]
|
422
|
-
response.set_cookie(@key, cookie)
|
423
|
-
end
|
424
|
-
end
|
425
|
-
|
426
|
-
# Allow subclasses to prepare_session for different Session classes
|
427
|
-
|
428
|
-
def session_class
|
429
|
-
SessionHash
|
430
|
-
end
|
431
|
-
|
432
|
-
# All thread safety and session retrieval procedures should occur here.
|
433
|
-
# Should return [session_id, session].
|
434
|
-
# If nil is provided as the session id, generation of a new valid id
|
435
|
-
# should occur within.
|
436
|
-
|
437
|
-
def find_session(env, sid)
|
438
|
-
raise '#find_session not implemented.'
|
439
|
-
end
|
440
|
-
|
441
|
-
# All thread safety and session storage procedures should occur here.
|
442
|
-
# Must return the session id if the session was saved successfully, or
|
443
|
-
# false if the session could not be saved.
|
444
|
-
|
445
|
-
def write_session(req, sid, session, options)
|
446
|
-
raise '#write_session not implemented.'
|
447
|
-
end
|
448
|
-
|
449
|
-
# All thread safety and session destroy procedures should occur here.
|
450
|
-
# Should return a new session id or nil if options[:drop]
|
451
|
-
|
452
|
-
def delete_session(req, sid, options)
|
453
|
-
raise '#delete_session not implemented'
|
454
|
-
end
|
455
|
-
end
|
456
|
-
|
457
|
-
class PersistedSecure < Persisted
|
458
|
-
class SecureSessionHash < SessionHash
|
459
|
-
def [](key)
|
460
|
-
if key == "session_id"
|
461
|
-
load_for_read!
|
462
|
-
case id
|
463
|
-
when SessionId
|
464
|
-
id.public_id
|
465
|
-
else
|
466
|
-
id
|
467
|
-
end
|
468
|
-
else
|
469
|
-
super
|
470
|
-
end
|
471
|
-
end
|
472
|
-
end
|
473
|
-
|
474
|
-
def generate_sid(*)
|
475
|
-
public_id = super
|
476
|
-
|
477
|
-
SessionId.new(public_id)
|
478
|
-
end
|
479
|
-
|
480
|
-
def extract_session_id(*)
|
481
|
-
public_id = super
|
482
|
-
public_id && SessionId.new(public_id)
|
483
|
-
end
|
484
|
-
|
485
|
-
private
|
486
|
-
|
487
|
-
def session_class
|
488
|
-
SecureSessionHash
|
489
|
-
end
|
490
|
-
|
491
|
-
def cookie_value(data)
|
492
|
-
data.cookie_value
|
493
|
-
end
|
494
|
-
end
|
495
|
-
|
496
|
-
class ID < Persisted
|
497
|
-
def self.inherited(klass)
|
498
|
-
k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID }
|
499
|
-
unless k.instance_variable_defined?(:"@_rack_warned")
|
500
|
-
warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE
|
501
|
-
k.instance_variable_set(:"@_rack_warned", true)
|
502
|
-
end
|
503
|
-
super
|
504
|
-
end
|
505
|
-
|
506
|
-
# All thread safety and session retrieval procedures should occur here.
|
507
|
-
# Should return [session_id, session].
|
508
|
-
# If nil is provided as the session id, generation of a new valid id
|
509
|
-
# should occur within.
|
510
|
-
|
511
|
-
def find_session(req, sid)
|
512
|
-
get_session req.env, sid
|
513
|
-
end
|
514
|
-
|
515
|
-
# All thread safety and session storage procedures should occur here.
|
516
|
-
# Must return the session id if the session was saved successfully, or
|
517
|
-
# false if the session could not be saved.
|
518
|
-
|
519
|
-
def write_session(req, sid, session, options)
|
520
|
-
set_session req.env, sid, session, options
|
521
|
-
end
|
522
|
-
|
523
|
-
# All thread safety and session destroy procedures should occur here.
|
524
|
-
# Should return a new session id or nil if options[:drop]
|
525
|
-
|
526
|
-
def delete_session(req, sid, options)
|
527
|
-
destroy_session req.env, sid, options
|
528
|
-
end
|
529
|
-
end
|
530
|
-
end
|
531
|
-
end
|
532
|
-
end
|
data/lib/rack/session/cookie.rb
DELETED
@@ -1,308 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'openssl'
|
4
|
-
require 'zlib'
|
5
|
-
require 'json'
|
6
|
-
require 'base64'
|
7
|
-
require 'delegate'
|
8
|
-
|
9
|
-
require 'rack/constants'
|
10
|
-
require 'rack/utils'
|
11
|
-
|
12
|
-
require_relative 'abstract/id'
|
13
|
-
require_relative 'encryptor'
|
14
|
-
require_relative 'constants'
|
15
|
-
|
16
|
-
module Rack
|
17
|
-
|
18
|
-
module Session
|
19
|
-
|
20
|
-
# Rack::Session::Cookie provides simple cookie based session management.
|
21
|
-
# By default, the session is a Ruby Hash that is serialized and encoded as
|
22
|
-
# a cookie set to :key (default: rack.session).
|
23
|
-
#
|
24
|
-
# This middleware accepts a :secrets option which enables encryption of
|
25
|
-
# session cookies. This option should be one or more random "secret keys"
|
26
|
-
# that are each at least 64 bytes in length. Multiple secret keys can be
|
27
|
-
# supplied in an Array, which is useful when rotating secrets.
|
28
|
-
#
|
29
|
-
# Several options are also accepted that are passed to Rack::Session::Encryptor.
|
30
|
-
# These options include:
|
31
|
-
# * :serialize_json
|
32
|
-
# Use JSON for message serialization instead of Marshal. This can be
|
33
|
-
# viewed as a security ehancement.
|
34
|
-
# * :gzip_over
|
35
|
-
# For message data over this many bytes, compress it with the deflate
|
36
|
-
# algorithm.
|
37
|
-
#
|
38
|
-
# Refer to Rack::Session::Encryptor for more details on these options.
|
39
|
-
#
|
40
|
-
# Prior to version TODO, the session hash was stored as base64 encoded
|
41
|
-
# marshalled data. When a :secret option was supplied, the integrity of the
|
42
|
-
# encoded data was protected with HMAC-SHA1. This functionality is still
|
43
|
-
# supported using a set of a legacy options.
|
44
|
-
#
|
45
|
-
# Lastly, a :coder option is also accepted. When used, both encryption and
|
46
|
-
# the legacy HMAC will be skipped. This option could create security issues
|
47
|
-
# in your application!
|
48
|
-
#
|
49
|
-
# Example:
|
50
|
-
#
|
51
|
-
# use Rack::Session::Cookie, {
|
52
|
-
# key: 'rack.session',
|
53
|
-
# domain: 'foo.com',
|
54
|
-
# path: '/',
|
55
|
-
# expire_after: 2592000,
|
56
|
-
# secrets: 'a randomly generated, raw binary string 64 bytes in size',
|
57
|
-
# }
|
58
|
-
#
|
59
|
-
# Example using legacy HMAC options:
|
60
|
-
#
|
61
|
-
# Rack::Session:Cookie.new(application, {
|
62
|
-
# # The secret used for legacy HMAC cookies, this enables the functionality
|
63
|
-
# legacy_hmac_secret: 'legacy secret',
|
64
|
-
# # legacy_hmac_coder will default to Rack::Session::Cookie::Base64::Marshal
|
65
|
-
# legacy_hmac_coder: Rack::Session::Cookie::Identity.new,
|
66
|
-
# # legacy_hmac will default to OpenSSL::Digest::SHA1
|
67
|
-
# legacy_hmac: OpenSSL::Digest::SHA256
|
68
|
-
# })
|
69
|
-
#
|
70
|
-
# Example of a cookie with no encoding:
|
71
|
-
#
|
72
|
-
# Rack::Session::Cookie.new(application, {
|
73
|
-
# :coder => Rack::Session::Cookie::Identity.new
|
74
|
-
# })
|
75
|
-
#
|
76
|
-
# Example of a cookie with custom encoding:
|
77
|
-
#
|
78
|
-
# Rack::Session::Cookie.new(application, {
|
79
|
-
# :coder => Class.new {
|
80
|
-
# def encode(str); str.reverse; end
|
81
|
-
# def decode(str); str.reverse; end
|
82
|
-
# }.new
|
83
|
-
# })
|
84
|
-
#
|
85
|
-
|
86
|
-
class Cookie < Abstract::PersistedSecure
|
87
|
-
# Encode session cookies as Base64
|
88
|
-
class Base64
|
89
|
-
def encode(str)
|
90
|
-
::Base64.strict_encode64(str)
|
91
|
-
end
|
92
|
-
|
93
|
-
def decode(str)
|
94
|
-
::Base64.decode64(str)
|
95
|
-
end
|
96
|
-
|
97
|
-
# Encode session cookies as Marshaled Base64 data
|
98
|
-
class Marshal < Base64
|
99
|
-
def encode(str)
|
100
|
-
super(::Marshal.dump(str))
|
101
|
-
end
|
102
|
-
|
103
|
-
def decode(str)
|
104
|
-
return unless str
|
105
|
-
::Marshal.load(super(str)) rescue nil
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# N.B. Unlike other encoding methods, the contained objects must be a
|
110
|
-
# valid JSON composite type, either a Hash or an Array.
|
111
|
-
class JSON < Base64
|
112
|
-
def encode(obj)
|
113
|
-
super(::JSON.dump(obj))
|
114
|
-
end
|
115
|
-
|
116
|
-
def decode(str)
|
117
|
-
return unless str
|
118
|
-
::JSON.parse(super(str)) rescue nil
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
class ZipJSON < Base64
|
123
|
-
def encode(obj)
|
124
|
-
super(Zlib::Deflate.deflate(::JSON.dump(obj)))
|
125
|
-
end
|
126
|
-
|
127
|
-
def decode(str)
|
128
|
-
return unless str
|
129
|
-
::JSON.parse(Zlib::Inflate.inflate(super(str)))
|
130
|
-
rescue
|
131
|
-
nil
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
# Use no encoding for session cookies
|
137
|
-
class Identity
|
138
|
-
def encode(str); str; end
|
139
|
-
def decode(str); str; end
|
140
|
-
end
|
141
|
-
|
142
|
-
class Marshal
|
143
|
-
def encode(str)
|
144
|
-
::Marshal.dump(str)
|
145
|
-
end
|
146
|
-
|
147
|
-
def decode(str)
|
148
|
-
::Marshal.load(str) if str
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
attr_reader :coder, :encryptors
|
153
|
-
|
154
|
-
def initialize(app, options = {})
|
155
|
-
# support both :secrets and :secret for backwards compatibility
|
156
|
-
secrets = [*(options[:secrets] || options[:secret])]
|
157
|
-
|
158
|
-
encryptor_opts = {
|
159
|
-
purpose: options[:key], serialize_json: options[:serialize_json]
|
160
|
-
}
|
161
|
-
|
162
|
-
# For each secret, create an Encryptor. We have iterate this Array at
|
163
|
-
# decryption time to achieve key rotation.
|
164
|
-
@encryptors = secrets.map do |secret|
|
165
|
-
Rack::Session::Encryptor.new secret, encryptor_opts
|
166
|
-
end
|
167
|
-
|
168
|
-
# If a legacy HMAC secret is present, initialize those features.
|
169
|
-
# Fallback to :secret for backwards compatibility.
|
170
|
-
if options.has_key?(:legacy_hmac_secret) || options.has_key?(:secret)
|
171
|
-
@legacy_hmac = options.fetch(:legacy_hmac, 'SHA1')
|
172
|
-
|
173
|
-
@legacy_hmac_secret = options[:legacy_hmac_secret] || options[:secret]
|
174
|
-
@legacy_hmac_coder = options.fetch(:legacy_hmac_coder, Base64::Marshal.new)
|
175
|
-
else
|
176
|
-
@legacy_hmac = false
|
177
|
-
end
|
178
|
-
|
179
|
-
warn <<-MSG unless secure?(options)
|
180
|
-
SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
|
181
|
-
This poses a security threat. It is strongly recommended that you
|
182
|
-
provide a secret to prevent exploits that may be possible from crafted
|
183
|
-
cookies. This will not be supported in future versions of Rack, and
|
184
|
-
future versions will even invalidate your existing user cookies.
|
185
|
-
|
186
|
-
Called from: #{caller[0]}.
|
187
|
-
MSG
|
188
|
-
|
189
|
-
# Potential danger ahead! Marshal without verification and/or
|
190
|
-
# encryption could present a major security issue.
|
191
|
-
@coder = options[:coder] ||= Base64::Marshal.new
|
192
|
-
|
193
|
-
super(app, options.merge!(cookie_only: true))
|
194
|
-
end
|
195
|
-
|
196
|
-
private
|
197
|
-
|
198
|
-
def find_session(req, sid)
|
199
|
-
data = unpacked_cookie_data(req)
|
200
|
-
data = persistent_session_id!(data)
|
201
|
-
[data["session_id"], data]
|
202
|
-
end
|
203
|
-
|
204
|
-
def extract_session_id(request)
|
205
|
-
unpacked_cookie_data(request)&.[]("session_id")
|
206
|
-
end
|
207
|
-
|
208
|
-
def unpacked_cookie_data(request)
|
209
|
-
request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
|
210
|
-
if cookie_data = request.cookies[@key]
|
211
|
-
session_data = nil
|
212
|
-
|
213
|
-
# Try to decrypt the session data with our encryptors
|
214
|
-
encryptors.each do |encryptor|
|
215
|
-
begin
|
216
|
-
session_data = encryptor.decrypt(cookie_data)
|
217
|
-
break
|
218
|
-
rescue Rack::Session::Encryptor::Error => error
|
219
|
-
request.env[Rack::RACK_ERRORS].puts "Session cookie encryptor error: #{error.message}"
|
220
|
-
|
221
|
-
next
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
# If session decryption fails but there is @legacy_hmac_secret
|
226
|
-
# defined, attempt legacy HMAC verification
|
227
|
-
if !session_data && @legacy_hmac_secret
|
228
|
-
# Parse and verify legacy HMAC session cookie
|
229
|
-
session_data, _, digest = cookie_data.rpartition('--')
|
230
|
-
session_data = nil unless legacy_digest_match?(session_data, digest)
|
231
|
-
|
232
|
-
# Decode using legacy HMAC decoder
|
233
|
-
session_data = @legacy_hmac_coder.decode(session_data)
|
234
|
-
|
235
|
-
elsif !session_data && coder
|
236
|
-
# Use the coder option, which has the potential to be very unsafe
|
237
|
-
session_data = coder.decode(cookie_data)
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
request.set_header(k, session_data || {})
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
def persistent_session_id!(data, sid = nil)
|
246
|
-
data ||= {}
|
247
|
-
data["session_id"] ||= sid || generate_sid
|
248
|
-
data
|
249
|
-
end
|
250
|
-
|
251
|
-
class SessionId < DelegateClass(Session::SessionId)
|
252
|
-
attr_reader :cookie_value
|
253
|
-
|
254
|
-
def initialize(session_id, cookie_value)
|
255
|
-
super(session_id)
|
256
|
-
@cookie_value = cookie_value
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
def write_session(req, session_id, session, options)
|
261
|
-
session = session.merge("session_id" => session_id)
|
262
|
-
session_data = encode_session_data(session)
|
263
|
-
|
264
|
-
if session_data.size > (4096 - @key.size)
|
265
|
-
req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
|
266
|
-
nil
|
267
|
-
else
|
268
|
-
SessionId.new(session_id, session_data)
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
def delete_session(req, session_id, options)
|
273
|
-
# Nothing to do here, data is in the client
|
274
|
-
generate_sid unless options[:drop]
|
275
|
-
end
|
276
|
-
|
277
|
-
def legacy_digest_match?(data, digest)
|
278
|
-
return false unless data && digest
|
279
|
-
|
280
|
-
Rack::Utils.secure_compare(digest, legacy_generate_hmac(data))
|
281
|
-
end
|
282
|
-
|
283
|
-
def legacy_generate_hmac(data)
|
284
|
-
OpenSSL::HMAC.hexdigest(@legacy_hmac, @legacy_hmac_secret, data)
|
285
|
-
end
|
286
|
-
|
287
|
-
def encode_session_data(session)
|
288
|
-
if encryptors.empty?
|
289
|
-
coder.encode(session)
|
290
|
-
else
|
291
|
-
encryptors.first.encrypt(session)
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
# Were consider "secure" if:
|
296
|
-
# * Encrypted cookies are enabled and one or more encryptor is
|
297
|
-
# initialized
|
298
|
-
# * The legacy HMAC option is enabled
|
299
|
-
# * Customer :coder is used, with :let_coder_handle_secure_encoding
|
300
|
-
# set to true
|
301
|
-
def secure?(options)
|
302
|
-
!@encryptors.empty? ||
|
303
|
-
@legacy_hmac ||
|
304
|
-
(options[:coder] && options[:let_coder_handle_secure_encoding])
|
305
|
-
end
|
306
|
-
end
|
307
|
-
end
|
308
|
-
end
|
@@ -1,188 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'base64'
|
4
|
-
require 'openssl'
|
5
|
-
require 'securerandom'
|
6
|
-
require 'zlib'
|
7
|
-
|
8
|
-
require 'rack/utils'
|
9
|
-
|
10
|
-
module Rack
|
11
|
-
module Session
|
12
|
-
class Encryptor
|
13
|
-
class Error < StandardError
|
14
|
-
end
|
15
|
-
|
16
|
-
class InvalidSignature < Error
|
17
|
-
end
|
18
|
-
|
19
|
-
class InvalidMessage < Error
|
20
|
-
end
|
21
|
-
|
22
|
-
# The secret String must be at least 64 bytes in size. The first 32 bytes
|
23
|
-
# will be used for the encryption cipher key. The remainder will be used
|
24
|
-
# for an HMAC key.
|
25
|
-
#
|
26
|
-
# Options may include:
|
27
|
-
# * :serialize_json
|
28
|
-
# Use JSON for message serialization instead of Marshal. This can be
|
29
|
-
# viewed as a security enhancement.
|
30
|
-
# * :pad_size
|
31
|
-
# Pad encrypted message data, to a multiple of this many bytes
|
32
|
-
# (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
|
33
|
-
# padding.
|
34
|
-
# * :purpose
|
35
|
-
# Limit messages to a specific purpose. This can be viewed as a
|
36
|
-
# security enhancement to prevent message reuse from different contexts
|
37
|
-
# if keys are reused.
|
38
|
-
#
|
39
|
-
# Cryptography and Output Format:
|
40
|
-
#
|
41
|
-
# urlsafe_encode64(version + random_data + IV + encrypted data + HMAC)
|
42
|
-
#
|
43
|
-
# Where:
|
44
|
-
# * version - 1 byte and is currently always 0x01
|
45
|
-
# * random_data - 32 bytes used for generating the per-message secret
|
46
|
-
# * IV - 16 bytes random initialization vector
|
47
|
-
# * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
|
48
|
-
# value
|
49
|
-
def initialize(secret, opts = {})
|
50
|
-
raise ArgumentError, "secret must be a String" unless String === secret
|
51
|
-
raise ArgumentError, "invalid secret: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
|
52
|
-
|
53
|
-
case opts[:pad_size]
|
54
|
-
when nil
|
55
|
-
# padding is disabled
|
56
|
-
when Integer
|
57
|
-
raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size]
|
58
|
-
else
|
59
|
-
raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil"
|
60
|
-
end
|
61
|
-
|
62
|
-
@options = {
|
63
|
-
serialize_json: false, pad_size: 32, purpose: nil
|
64
|
-
}.update(opts)
|
65
|
-
|
66
|
-
@hmac_secret = secret.dup.force_encoding('BINARY')
|
67
|
-
@cipher_secret = @hmac_secret.slice!(0, 32)
|
68
|
-
|
69
|
-
@hmac_secret.freeze
|
70
|
-
@cipher_secret.freeze
|
71
|
-
end
|
72
|
-
|
73
|
-
def decrypt(base64_data)
|
74
|
-
data = Base64.urlsafe_decode64(base64_data)
|
75
|
-
|
76
|
-
signature = data.slice!(-32..-1)
|
77
|
-
|
78
|
-
verify_authenticity! data, signature
|
79
|
-
|
80
|
-
# The version is reserved for future
|
81
|
-
_version = data.slice!(0, 1)
|
82
|
-
message_secret = data.slice!(0, 32)
|
83
|
-
cipher_iv = data.slice!(0, 16)
|
84
|
-
|
85
|
-
cipher = new_cipher
|
86
|
-
cipher.decrypt
|
87
|
-
|
88
|
-
set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
|
89
|
-
|
90
|
-
cipher.iv = cipher_iv
|
91
|
-
data = cipher.update(data) << cipher.final
|
92
|
-
|
93
|
-
deserialized_message data
|
94
|
-
rescue ArgumentError
|
95
|
-
raise InvalidSignature, 'Message invalid'
|
96
|
-
end
|
97
|
-
|
98
|
-
def encrypt(message)
|
99
|
-
version = "\1"
|
100
|
-
|
101
|
-
serialized_payload = serialize_payload(message)
|
102
|
-
message_secret, cipher_secret = new_message_and_cipher_secret
|
103
|
-
|
104
|
-
cipher = new_cipher
|
105
|
-
cipher.encrypt
|
106
|
-
|
107
|
-
set_cipher_key(cipher, cipher_secret)
|
108
|
-
|
109
|
-
cipher_iv = cipher.random_iv
|
110
|
-
|
111
|
-
encrypted_data = cipher.update(serialized_payload) << cipher.final
|
112
|
-
|
113
|
-
data = String.new
|
114
|
-
data << version
|
115
|
-
data << message_secret
|
116
|
-
data << cipher_iv
|
117
|
-
data << encrypted_data
|
118
|
-
data << compute_signature(data)
|
119
|
-
|
120
|
-
Base64.urlsafe_encode64(data)
|
121
|
-
end
|
122
|
-
|
123
|
-
private
|
124
|
-
|
125
|
-
def new_cipher
|
126
|
-
OpenSSL::Cipher.new('aes-256-ctr')
|
127
|
-
end
|
128
|
-
|
129
|
-
def new_message_and_cipher_secret
|
130
|
-
message_secret = SecureRandom.random_bytes(32)
|
131
|
-
|
132
|
-
[message_secret, cipher_secret_from_message_secret(message_secret)]
|
133
|
-
end
|
134
|
-
|
135
|
-
def cipher_secret_from_message_secret(message_secret)
|
136
|
-
OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @cipher_secret, message_secret)
|
137
|
-
end
|
138
|
-
|
139
|
-
def set_cipher_key(cipher, key)
|
140
|
-
cipher.key = key
|
141
|
-
end
|
142
|
-
|
143
|
-
def serializer
|
144
|
-
@serializer ||= @options[:serialize_json] ? JSON : Marshal
|
145
|
-
end
|
146
|
-
|
147
|
-
def compute_signature(data)
|
148
|
-
signing_data = data
|
149
|
-
signing_data += @options[:purpose] if @options[:purpose]
|
150
|
-
|
151
|
-
OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @hmac_secret, signing_data)
|
152
|
-
end
|
153
|
-
|
154
|
-
def verify_authenticity!(data, signature)
|
155
|
-
raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
|
156
|
-
|
157
|
-
unless Rack::Utils.secure_compare(signature, compute_signature(data))
|
158
|
-
raise InvalidSignature, 'HMAC is invalid'
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
# Returns a serialized payload of the message. If a :pad_size is supplied,
|
163
|
-
# the message will be padded. The first 2 bytes of the returned string will
|
164
|
-
# indicating the amount of padding.
|
165
|
-
def serialize_payload(message)
|
166
|
-
serialized_data = serializer.dump(message)
|
167
|
-
|
168
|
-
return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
|
169
|
-
|
170
|
-
padding_bytes = @options[:pad_size] - (2 + serialized_data.size) % @options[:pad_size]
|
171
|
-
padding_data = SecureRandom.random_bytes(padding_bytes)
|
172
|
-
|
173
|
-
"#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
|
174
|
-
end
|
175
|
-
|
176
|
-
# Return the deserialized message. The first 2 bytes will be read as the
|
177
|
-
# amount of padding.
|
178
|
-
def deserialized_message(data)
|
179
|
-
# Read the first 2 bytes as the padding_bytes size
|
180
|
-
padding_bytes, = data.unpack('v')
|
181
|
-
|
182
|
-
# Slice out the serialized_data and deserialize it
|
183
|
-
serialized_data = data.slice(2 + padding_bytes, data.bytesize)
|
184
|
-
serializer.load serialized_data
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
data/lib/rack/session/pool.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
|
4
|
-
# THANKS:
|
5
|
-
# apeiros, for session id generation, expiry setup, and threadiness
|
6
|
-
# sergio, threadiness and bugreps
|
7
|
-
|
8
|
-
require_relative 'abstract/id'
|
9
|
-
|
10
|
-
module Rack
|
11
|
-
module Session
|
12
|
-
# Rack::Session::Pool provides simple cookie based session management.
|
13
|
-
# Session data is stored in a hash held by @pool.
|
14
|
-
# In the context of a multithreaded environment, sessions being
|
15
|
-
# committed to the pool is done in a merging manner.
|
16
|
-
#
|
17
|
-
# The :drop option is available in rack.session.options if you wish to
|
18
|
-
# explicitly remove the session from the session cache.
|
19
|
-
#
|
20
|
-
# Example:
|
21
|
-
# myapp = MyRackApp.new
|
22
|
-
# sessioned = Rack::Session::Pool.new(myapp,
|
23
|
-
# :domain => 'foo.com',
|
24
|
-
# :expire_after => 2592000
|
25
|
-
# )
|
26
|
-
# Rack::Handler::WEBrick.run sessioned
|
27
|
-
|
28
|
-
class Pool < Abstract::PersistedSecure
|
29
|
-
attr_reader :mutex, :pool
|
30
|
-
DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge(drop: false, allow_fallback: true)
|
31
|
-
|
32
|
-
def initialize(app, options = {})
|
33
|
-
super
|
34
|
-
@pool = Hash.new
|
35
|
-
@mutex = Mutex.new
|
36
|
-
@allow_fallback = @default_options.delete(:allow_fallback)
|
37
|
-
end
|
38
|
-
|
39
|
-
def generate_sid(*args, use_mutex: true)
|
40
|
-
loop do
|
41
|
-
sid = super(*args)
|
42
|
-
break sid unless use_mutex ? @mutex.synchronize { @pool.key? sid.private_id } : @pool.key?(sid.private_id)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def find_session(req, sid)
|
47
|
-
@mutex.synchronize do
|
48
|
-
unless sid and session = get_session_with_fallback(sid)
|
49
|
-
sid, session = generate_sid(use_mutex: false), {}
|
50
|
-
@pool.store sid.private_id, session
|
51
|
-
end
|
52
|
-
[sid, session]
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def write_session(req, session_id, new_session, options)
|
57
|
-
@mutex.synchronize do
|
58
|
-
@pool.store session_id.private_id, new_session
|
59
|
-
session_id
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def delete_session(req, session_id, options)
|
64
|
-
@mutex.synchronize do
|
65
|
-
@pool.delete(session_id.public_id)
|
66
|
-
@pool.delete(session_id.private_id)
|
67
|
-
generate_sid(use_mutex: false) unless options[:drop]
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
private
|
72
|
-
|
73
|
-
def get_session_with_fallback(sid)
|
74
|
-
@pool[sid.private_id] || (@pool[sid.public_id] if @allow_fallback)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|