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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50f1782c9cc160cdbcee7ac400f1d3643c5b2140e6024c5a1e9829df9d535441
4
- data.tar.gz: cacc044559ef38fef31477da4e17173de8533e5970739e7bfc90678d38bf08e3
3
+ metadata.gz: 197fa5e7a11e2cf9998fb2e456b2d2ae84d3eb21e4208d38741b17dc8356375f
4
+ data.tar.gz: f0f51a370a60eb8abf007d37e8f7763514a374c937b85529e1a0bf7e9c61eb8f
5
5
  SHA512:
6
- metadata.gz: 0b196f3055fdb3ccda5e0aff1a0aa3b852e1fb3676de3ed0508f2d92f2771e63c8b79ac8b3b7dc4c346467eadd178786e31b92501fb900418be6c7d0afe985de
7
- data.tar.gz: 9d200ef0353f8efdf0a92f072db2765fd2be3cd391db47a603c31be9f0a29b16dfb421e20693e889c38204171680fc5f859046d823fe4aaf0a6a6f26300b25b4
6
+ metadata.gz: f70b021ddb700f519752fc64c0ceed6ffe04c2d28ae2f7c15b32368c503e721fd804c2fce7e0f3052ae4b5d3c0c362918361b377bd068d7abce761ca2e916030
7
+ data.tar.gz: 179dba7f3f3f2610bd23f9a36c2d8cef776e8cf4cf9cba8f85c44a7a8e1b6ca473fdf08eabd22ac6a4b88b59ebc016c270c212ad67f979961e7ba6b16c07612f
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Released under the MIT License.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
+
3
6
  module Rack
4
7
  module Session
5
- VERSION = "0.3.0"
8
+ VERSION = "1.0.0"
6
9
  end
7
10
  end
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
@@ -0,0 +1,3 @@
1
+ # Security Policy
2
+
3
+ Please see our main security policy: https://github.com/rack/rack/security/policy
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.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
- - Rack Contributors
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: 2022-09-24 00:00:00.000000000 Z
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.0.0.beta1
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.0.0.beta1
29
+ version: '3'
27
30
  - !ruby/object:Gem::Dependency
28
- name: minitest
31
+ name: bundler
29
32
  requirement: !ruby/object:Gem::Requirement
30
33
  requirements:
31
- - - "~>"
34
+ - - ">="
32
35
  - !ruby/object:Gem::Version
33
- version: '5.0'
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: '5.0'
43
+ version: '0'
41
44
  - !ruby/object:Gem::Dependency
42
- name: minitest-sprint
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: bundler
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.0.dev
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
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rack
4
- module Session
5
- RACK_SESSION = 'rack.session'
6
- RACK_SESSION_OPTIONS = 'rack.session.options'
7
- RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data'
8
- end
9
- end
@@ -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
@@ -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
data/lib/rack/session.rb DELETED
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rack
4
- module Session
5
- autoload :Cookie, "rack/session/cookie"
6
- autoload :Pool, "rack/session/pool"
7
- autoload :Memcache, "rack/session/memcache"
8
- end
9
- end