rack-session 1.0.0 → 2.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: 197fa5e7a11e2cf9998fb2e456b2d2ae84d3eb21e4208d38741b17dc8356375f
4
- data.tar.gz: f0f51a370a60eb8abf007d37e8f7763514a374c937b85529e1a0bf7e9c61eb8f
3
+ metadata.gz: 80db83a32f94ca04708858cafb308daa26b94b9a4b95bdc74b92c288e56be8fd
4
+ data.tar.gz: 5502c441c1466396ce483c159fde5b909bc93aac9245b440368baad03f2246ff
5
5
  SHA512:
6
- metadata.gz: f70b021ddb700f519752fc64c0ceed6ffe04c2d28ae2f7c15b32368c503e721fd804c2fce7e0f3052ae4b5d3c0c362918361b377bd068d7abce761ca2e916030
7
- data.tar.gz: 179dba7f3f3f2610bd23f9a36c2d8cef776e8cf4cf9cba8f85c44a7a8e1b6ca473fdf08eabd22ac6a4b88b59ebc016c270c212ad67f979961e7ba6b16c07612f
6
+ metadata.gz: d7c2863fbe132f21f64fc6b03911e72af78a5d59557ad7ad1f370c6bd25411747e1b0f4f695bb87b52eb2ab1a1b80829f9757a1aec3f50d9ac1c99fe84d34481
7
+ data.tar.gz: 5678a825dd79056f106e4e942d3b930a737ce27baf4da886a555215a903307691e9edcd2afdd494f85a39f8f605244d3e697b52cc0fed7548e44ac59c648afac
@@ -0,0 +1,533 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
+ # Copyright, 2022, by Jeremy Evans.
6
+
7
+ require 'time'
8
+ require 'securerandom'
9
+ require 'digest/sha2'
10
+
11
+ require 'rack/constants'
12
+ require 'rack/request'
13
+ require 'rack/response'
14
+
15
+ require_relative '../constants'
16
+
17
+ module Rack
18
+
19
+ module Session
20
+
21
+ class SessionId
22
+ ID_VERSION = 2
23
+
24
+ attr_reader :public_id
25
+
26
+ def initialize(public_id)
27
+ @public_id = public_id
28
+ end
29
+
30
+ def private_id
31
+ "#{ID_VERSION}::#{hash_sid(public_id)}"
32
+ end
33
+
34
+ alias :cookie_value :public_id
35
+ alias :to_s :public_id
36
+
37
+ def empty?; false; end
38
+ def inspect; public_id.inspect; end
39
+
40
+ private
41
+
42
+ def hash_sid(sid)
43
+ Digest::SHA256.hexdigest(sid)
44
+ end
45
+ end
46
+
47
+ module Abstract
48
+ # SessionHash is responsible to lazily load the session from store.
49
+
50
+ class SessionHash
51
+ include Enumerable
52
+ attr_writer :id
53
+
54
+ Unspecified = Object.new
55
+
56
+ def self.find(req)
57
+ req.get_header RACK_SESSION
58
+ end
59
+
60
+ def self.set(req, session)
61
+ req.set_header RACK_SESSION, session
62
+ end
63
+
64
+ def self.set_options(req, options)
65
+ req.set_header RACK_SESSION_OPTIONS, options.dup
66
+ end
67
+
68
+ def initialize(store, req)
69
+ @store = store
70
+ @req = req
71
+ @loaded = false
72
+ end
73
+
74
+ def id
75
+ return @id if @loaded or instance_variable_defined?(:@id)
76
+ @id = @store.send(:extract_session_id, @req)
77
+ end
78
+
79
+ def options
80
+ @req.session_options
81
+ end
82
+
83
+ def each(&block)
84
+ load_for_read!
85
+ @data.each(&block)
86
+ end
87
+
88
+ def [](key)
89
+ load_for_read!
90
+ @data[key.to_s]
91
+ end
92
+
93
+ def dig(key, *keys)
94
+ load_for_read!
95
+ @data.dig(key.to_s, *keys)
96
+ end
97
+
98
+ def fetch(key, default = Unspecified, &block)
99
+ load_for_read!
100
+ if default == Unspecified
101
+ @data.fetch(key.to_s, &block)
102
+ else
103
+ @data.fetch(key.to_s, default, &block)
104
+ end
105
+ end
106
+
107
+ def has_key?(key)
108
+ load_for_read!
109
+ @data.has_key?(key.to_s)
110
+ end
111
+ alias :key? :has_key?
112
+ alias :include? :has_key?
113
+
114
+ def []=(key, value)
115
+ load_for_write!
116
+ @data[key.to_s] = value
117
+ end
118
+ alias :store :[]=
119
+
120
+ def clear
121
+ load_for_write!
122
+ @data.clear
123
+ end
124
+
125
+ def destroy
126
+ clear
127
+ @id = @store.send(:delete_session, @req, id, options)
128
+ end
129
+
130
+ def to_hash
131
+ load_for_read!
132
+ @data.dup
133
+ end
134
+
135
+ def update(hash)
136
+ load_for_write!
137
+ @data.update(stringify_keys(hash))
138
+ end
139
+ alias :merge! :update
140
+
141
+ def replace(hash)
142
+ load_for_write!
143
+ @data.replace(stringify_keys(hash))
144
+ end
145
+
146
+ def delete(key)
147
+ load_for_write!
148
+ @data.delete(key.to_s)
149
+ end
150
+
151
+ def inspect
152
+ if loaded?
153
+ @data.inspect
154
+ else
155
+ "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
156
+ end
157
+ end
158
+
159
+ def exists?
160
+ return @exists if instance_variable_defined?(:@exists)
161
+ @data = {}
162
+ @exists = @store.send(:session_exists?, @req)
163
+ end
164
+
165
+ def loaded?
166
+ @loaded
167
+ end
168
+
169
+ def empty?
170
+ load_for_read!
171
+ @data.empty?
172
+ end
173
+
174
+ def keys
175
+ load_for_read!
176
+ @data.keys
177
+ end
178
+
179
+ def values
180
+ load_for_read!
181
+ @data.values
182
+ end
183
+
184
+ private
185
+
186
+ def load_for_read!
187
+ load! if !loaded? && exists?
188
+ end
189
+
190
+ def load_for_write!
191
+ load! unless loaded?
192
+ end
193
+
194
+ def load!
195
+ @id, session = @store.send(:load_session, @req)
196
+ @data = stringify_keys(session)
197
+ @loaded = true
198
+ end
199
+
200
+ def stringify_keys(other)
201
+ # Use transform_keys after dropping Ruby 2.4 support
202
+ hash = {}
203
+ other.to_hash.each do |key, value|
204
+ hash[key.to_s] = value
205
+ end
206
+ hash
207
+ end
208
+ end
209
+
210
+ # ID sets up a basic framework for implementing an id based sessioning
211
+ # service. Cookies sent to the client for maintaining sessions will only
212
+ # contain an id reference. Only #find_session, #write_session and
213
+ # #delete_session are required to be overwritten.
214
+ #
215
+ # All parameters are optional.
216
+ # * :key determines the name of the cookie, by default it is
217
+ # 'rack.session'
218
+ # * :path, :domain, :expire_after, :secure, :httponly, and :same_site set
219
+ # the related cookie options as by Rack::Response#set_cookie
220
+ # * :skip will not a set a cookie in the response nor update the session state
221
+ # * :defer will not set a cookie in the response but still update the session
222
+ # state if it is used with a backend
223
+ # * :renew (implementation dependent) will prompt the generation of a new
224
+ # session id, and migration of data to be referenced at the new id. If
225
+ # :defer is set, it will be overridden and the cookie will be set.
226
+ # * :sidbits sets the number of bits in length that a generated session
227
+ # id will be.
228
+ #
229
+ # These options can be set on a per request basis, at the location of
230
+ # <tt>env['rack.session.options']</tt>. Additionally the id of the
231
+ # session can be found within the options hash at the key :id. It is
232
+ # highly not recommended to change its value.
233
+ #
234
+ # Is Rack::Utils::Context compatible.
235
+ #
236
+ # Not included by default; you must require 'rack/session/abstract/id'
237
+ # to use.
238
+
239
+ class Persisted
240
+ DEFAULT_OPTIONS = {
241
+ key: RACK_SESSION,
242
+ path: '/',
243
+ domain: nil,
244
+ expire_after: nil,
245
+ secure: false,
246
+ httponly: true,
247
+ defer: false,
248
+ renew: false,
249
+ sidbits: 128,
250
+ cookie_only: true,
251
+ secure_random: ::SecureRandom
252
+ }.freeze
253
+
254
+ attr_reader :key, :default_options, :sid_secure, :same_site
255
+
256
+ def initialize(app, options = {})
257
+ @app = app
258
+ @default_options = self.class::DEFAULT_OPTIONS.merge(options)
259
+ @key = @default_options.delete(:key)
260
+ @cookie_only = @default_options.delete(:cookie_only)
261
+ @same_site = @default_options.delete(:same_site)
262
+ initialize_sid
263
+ end
264
+
265
+ def call(env)
266
+ context(env)
267
+ end
268
+
269
+ def context(env, app = @app)
270
+ req = make_request env
271
+ prepare_session(req)
272
+ status, headers, body = app.call(req.env)
273
+ res = Rack::Response::Raw.new status, headers
274
+ commit_session(req, res)
275
+ [status, headers, body]
276
+ end
277
+
278
+ private
279
+
280
+ def make_request(env)
281
+ Rack::Request.new env
282
+ end
283
+
284
+ def initialize_sid
285
+ @sidbits = @default_options[:sidbits]
286
+ @sid_secure = @default_options[:secure_random]
287
+ @sid_length = @sidbits / 4
288
+ end
289
+
290
+ # Generate a new session id using Ruby #rand. The size of the
291
+ # session id is controlled by the :sidbits option.
292
+ # Monkey patch this to use custom methods for session id generation.
293
+
294
+ def generate_sid(secure = @sid_secure)
295
+ if secure
296
+ secure.hex(@sid_length)
297
+ else
298
+ "%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
299
+ end
300
+ rescue NotImplementedError
301
+ generate_sid(false)
302
+ end
303
+
304
+ # Sets the lazy session at 'rack.session' and places options and session
305
+ # metadata into 'rack.session.options'.
306
+
307
+ def prepare_session(req)
308
+ session_was = req.get_header RACK_SESSION
309
+ session = session_class.new(self, req)
310
+ req.set_header RACK_SESSION, session
311
+ req.set_header RACK_SESSION_OPTIONS, @default_options.dup
312
+ session.merge! session_was if session_was
313
+ end
314
+
315
+ # Extracts the session id from provided cookies and passes it and the
316
+ # environment to #find_session.
317
+
318
+ def load_session(req)
319
+ sid = current_session_id(req)
320
+ sid, session = find_session(req, sid)
321
+ [sid, session || {}]
322
+ end
323
+
324
+ # Extract session id from request object.
325
+
326
+ def extract_session_id(request)
327
+ sid = request.cookies[@key]
328
+ sid ||= request.params[@key] unless @cookie_only
329
+ sid
330
+ end
331
+
332
+ # Returns the current session id from the SessionHash.
333
+
334
+ def current_session_id(req)
335
+ req.get_header(RACK_SESSION).id
336
+ end
337
+
338
+ # Check if the session exists or not.
339
+
340
+ def session_exists?(req)
341
+ value = current_session_id(req)
342
+ value && !value.empty?
343
+ end
344
+
345
+ # Session should be committed if it was loaded, any of specific options like :renew, :drop
346
+ # or :expire_after was given and the security permissions match. Skips if skip is given.
347
+
348
+ def commit_session?(req, session, options)
349
+ if options[:skip]
350
+ false
351
+ else
352
+ has_session = loaded_session?(session) || forced_session_update?(session, options)
353
+ has_session && security_matches?(req, options)
354
+ end
355
+ end
356
+
357
+ def loaded_session?(session)
358
+ !session.is_a?(session_class) || session.loaded?
359
+ end
360
+
361
+ def forced_session_update?(session, options)
362
+ force_options?(options) && session && !session.empty?
363
+ end
364
+
365
+ def force_options?(options)
366
+ options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
367
+ end
368
+
369
+ def security_matches?(request, options)
370
+ return true unless options[:secure]
371
+ request.ssl?
372
+ end
373
+
374
+ # Acquires the session from the environment and the session id from
375
+ # the session options and passes them to #write_session. If successful
376
+ # and the :defer option is not true, a cookie will be added to the
377
+ # response with the session's id.
378
+
379
+ def commit_session(req, res)
380
+ session = req.get_header RACK_SESSION
381
+ options = session.options
382
+
383
+ if options[:drop] || options[:renew]
384
+ session_id = delete_session(req, session.id || generate_sid, options)
385
+ return unless session_id
386
+ end
387
+
388
+ return unless commit_session?(req, session, options)
389
+
390
+ session.send(:load!) unless loaded_session?(session)
391
+ session_id ||= session.id
392
+ session_data = session.to_hash.delete_if { |k, v| v.nil? }
393
+
394
+ if not data = write_session(req, session_id, session_data, options)
395
+ req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
396
+ elsif options[:defer] and not options[:renew]
397
+ req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
398
+ else
399
+ cookie = Hash.new
400
+ cookie[:value] = cookie_value(data)
401
+ cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
402
+ cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
403
+
404
+ if @same_site.respond_to? :call
405
+ cookie[:same_site] = @same_site.call(req, res)
406
+ else
407
+ cookie[:same_site] = @same_site
408
+ end
409
+ set_cookie(req, res, cookie.merge!(options))
410
+ end
411
+ end
412
+ public :commit_session
413
+
414
+ def cookie_value(data)
415
+ data
416
+ end
417
+
418
+ # Sets the cookie back to the client with session id. We skip the cookie
419
+ # setting if the value didn't change (sid is the same) or expires was given.
420
+
421
+ def set_cookie(request, response, cookie)
422
+ if request.cookies[@key] != cookie[:value] || cookie[:expires]
423
+ response.set_cookie(@key, cookie)
424
+ end
425
+ end
426
+
427
+ # Allow subclasses to prepare_session for different Session classes
428
+
429
+ def session_class
430
+ SessionHash
431
+ end
432
+
433
+ # All thread safety and session retrieval procedures should occur here.
434
+ # Should return [session_id, session].
435
+ # If nil is provided as the session id, generation of a new valid id
436
+ # should occur within.
437
+
438
+ def find_session(env, sid)
439
+ raise '#find_session not implemented.'
440
+ end
441
+
442
+ # All thread safety and session storage procedures should occur here.
443
+ # Must return the session id if the session was saved successfully, or
444
+ # false if the session could not be saved.
445
+
446
+ def write_session(req, sid, session, options)
447
+ raise '#write_session not implemented.'
448
+ end
449
+
450
+ # All thread safety and session destroy procedures should occur here.
451
+ # Should return a new session id or nil if options[:drop]
452
+
453
+ def delete_session(req, sid, options)
454
+ raise '#delete_session not implemented'
455
+ end
456
+ end
457
+
458
+ class PersistedSecure < Persisted
459
+ class SecureSessionHash < SessionHash
460
+ def [](key)
461
+ if key == "session_id"
462
+ load_for_read!
463
+ case id
464
+ when SessionId
465
+ id.public_id
466
+ else
467
+ id
468
+ end
469
+ else
470
+ super
471
+ end
472
+ end
473
+ end
474
+
475
+ def generate_sid(*)
476
+ public_id = super
477
+
478
+ SessionId.new(public_id)
479
+ end
480
+
481
+ def extract_session_id(*)
482
+ public_id = super
483
+ public_id && SessionId.new(public_id)
484
+ end
485
+
486
+ private
487
+
488
+ def session_class
489
+ SecureSessionHash
490
+ end
491
+
492
+ def cookie_value(data)
493
+ data.cookie_value
494
+ end
495
+ end
496
+
497
+ class ID < Persisted
498
+ def self.inherited(klass)
499
+ k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID }
500
+ unless k.instance_variable_defined?(:"@_rack_warned")
501
+ warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE
502
+ k.instance_variable_set(:"@_rack_warned", true)
503
+ end
504
+ super
505
+ end
506
+
507
+ # All thread safety and session retrieval procedures should occur here.
508
+ # Should return [session_id, session].
509
+ # If nil is provided as the session id, generation of a new valid id
510
+ # should occur within.
511
+
512
+ def find_session(req, sid)
513
+ get_session req.env, sid
514
+ end
515
+
516
+ # All thread safety and session storage procedures should occur here.
517
+ # Must return the session id if the session was saved successfully, or
518
+ # false if the session could not be saved.
519
+
520
+ def write_session(req, sid, session, options)
521
+ set_session req.env, sid, session, options
522
+ end
523
+
524
+ # All thread safety and session destroy procedures should occur here.
525
+ # Should return a new session id or nil if options[:drop]
526
+
527
+ def delete_session(req, sid, options)
528
+ destroy_session req.env, sid, options
529
+ end
530
+ end
531
+ end
532
+ end
533
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
+ # Copyright, 2022, by Jeremy Evans.
6
+
7
+ module Rack
8
+ module Session
9
+ RACK_SESSION = 'rack.session'
10
+ RACK_SESSION_OPTIONS = 'rack.session.options'
11
+ RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data'
12
+ end
13
+ end
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
+ # Copyright, 2022, by Jeremy Evans.
6
+ # Copyright, 2022, by Jon Dufresne.
7
+
8
+ require 'openssl'
9
+ require 'zlib'
10
+ require 'json'
11
+ require 'base64'
12
+ require 'delegate'
13
+
14
+ require 'rack/constants'
15
+ require 'rack/utils'
16
+
17
+ require_relative 'abstract/id'
18
+ require_relative 'encryptor'
19
+ require_relative 'constants'
20
+
21
+ module Rack
22
+
23
+ module Session
24
+
25
+ # Rack::Session::Cookie provides simple cookie based session management.
26
+ # By default, the session is a Ruby Hash that is serialized and encoded as
27
+ # a cookie set to :key (default: rack.session).
28
+ #
29
+ # This middleware accepts a :secrets option which enables encryption of
30
+ # session cookies. This option should be one or more random "secret keys"
31
+ # that are each at least 64 bytes in length. Multiple secret keys can be
32
+ # supplied in an Array, which is useful when rotating secrets.
33
+ #
34
+ # Several options are also accepted that are passed to Rack::Session::Encryptor.
35
+ # These options include:
36
+ # * :serialize_json
37
+ # Use JSON for message serialization instead of Marshal. This can be
38
+ # viewed as a security enhancement.
39
+ # * :gzip_over
40
+ # For message data over this many bytes, compress it with the deflate
41
+ # algorithm.
42
+ #
43
+ # Refer to Rack::Session::Encryptor for more details on these options.
44
+ #
45
+ # Prior to version TODO, the session hash was stored as base64 encoded
46
+ # marshalled data. When a :secret option was supplied, the integrity of the
47
+ # encoded data was protected with HMAC-SHA1. This functionality is still
48
+ # supported using a set of a legacy options.
49
+ #
50
+ # Lastly, a :coder option is also accepted. When used, both encryption and
51
+ # the legacy HMAC will be skipped. This option could create security issues
52
+ # in your application!
53
+ #
54
+ # Example:
55
+ #
56
+ # use Rack::Session::Cookie, {
57
+ # key: 'rack.session',
58
+ # domain: 'foo.com',
59
+ # path: '/',
60
+ # expire_after: 2592000,
61
+ # secrets: 'a randomly generated, raw binary string 64 bytes in size',
62
+ # }
63
+ #
64
+ # Example using legacy HMAC options:
65
+ #
66
+ # Rack::Session:Cookie.new(application, {
67
+ # # The secret used for legacy HMAC cookies, this enables the functionality
68
+ # legacy_hmac_secret: 'legacy secret',
69
+ # # legacy_hmac_coder will default to Rack::Session::Cookie::Base64::Marshal
70
+ # legacy_hmac_coder: Rack::Session::Cookie::Identity.new,
71
+ # # legacy_hmac will default to OpenSSL::Digest::SHA1
72
+ # legacy_hmac: OpenSSL::Digest::SHA256
73
+ # })
74
+ #
75
+ # Example of a cookie with no encoding:
76
+ #
77
+ # Rack::Session::Cookie.new(application, {
78
+ # :coder => Rack::Session::Cookie::Identity.new
79
+ # })
80
+ #
81
+ # Example of a cookie with custom encoding:
82
+ #
83
+ # Rack::Session::Cookie.new(application, {
84
+ # :coder => Class.new {
85
+ # def encode(str); str.reverse; end
86
+ # def decode(str); str.reverse; end
87
+ # }.new
88
+ # })
89
+ #
90
+
91
+ class Cookie < Abstract::PersistedSecure
92
+ # Encode session cookies as Base64
93
+ class Base64
94
+ def encode(str)
95
+ ::Base64.strict_encode64(str)
96
+ end
97
+
98
+ def decode(str)
99
+ ::Base64.decode64(str)
100
+ end
101
+
102
+ # Encode session cookies as Marshaled Base64 data
103
+ class Marshal < Base64
104
+ def encode(str)
105
+ super(::Marshal.dump(str))
106
+ end
107
+
108
+ def decode(str)
109
+ return unless str
110
+ ::Marshal.load(super(str)) rescue nil
111
+ end
112
+ end
113
+
114
+ # N.B. Unlike other encoding methods, the contained objects must be a
115
+ # valid JSON composite type, either a Hash or an Array.
116
+ class JSON < Base64
117
+ def encode(obj)
118
+ super(::JSON.dump(obj))
119
+ end
120
+
121
+ def decode(str)
122
+ return unless str
123
+ ::JSON.parse(super(str)) rescue nil
124
+ end
125
+ end
126
+
127
+ class ZipJSON < Base64
128
+ def encode(obj)
129
+ super(Zlib::Deflate.deflate(::JSON.dump(obj)))
130
+ end
131
+
132
+ def decode(str)
133
+ return unless str
134
+ ::JSON.parse(Zlib::Inflate.inflate(super(str)))
135
+ rescue
136
+ nil
137
+ end
138
+ end
139
+ end
140
+
141
+ # Use no encoding for session cookies
142
+ class Identity
143
+ def encode(str); str; end
144
+ def decode(str); str; end
145
+ end
146
+
147
+ class Marshal
148
+ def encode(str)
149
+ ::Marshal.dump(str)
150
+ end
151
+
152
+ def decode(str)
153
+ ::Marshal.load(str) if str
154
+ end
155
+ end
156
+
157
+ attr_reader :coder, :encryptors
158
+
159
+ def initialize(app, options = {})
160
+ # support both :secrets and :secret for backwards compatibility
161
+ secrets = [*(options[:secrets] || options[:secret])]
162
+
163
+ encryptor_opts = {
164
+ purpose: options[:key], serialize_json: options[:serialize_json]
165
+ }
166
+
167
+ # For each secret, create an Encryptor. We have iterate this Array at
168
+ # decryption time to achieve key rotation.
169
+ @encryptors = secrets.map do |secret|
170
+ Rack::Session::Encryptor.new secret, encryptor_opts
171
+ end
172
+
173
+ # If a legacy HMAC secret is present, initialize those features.
174
+ # Fallback to :secret for backwards compatibility.
175
+ if options.has_key?(:legacy_hmac_secret) || options.has_key?(:secret)
176
+ @legacy_hmac = options.fetch(:legacy_hmac, 'SHA1')
177
+
178
+ @legacy_hmac_secret = options[:legacy_hmac_secret] || options[:secret]
179
+ @legacy_hmac_coder = options.fetch(:legacy_hmac_coder, Base64::Marshal.new)
180
+ else
181
+ @legacy_hmac = false
182
+ end
183
+
184
+ warn <<-MSG unless secure?(options)
185
+ SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
186
+ This poses a security threat. It is strongly recommended that you
187
+ provide a secret to prevent exploits that may be possible from crafted
188
+ cookies. This will not be supported in future versions of Rack, and
189
+ future versions will even invalidate your existing user cookies.
190
+
191
+ Called from: #{caller[0]}.
192
+ MSG
193
+
194
+ # Potential danger ahead! Marshal without verification and/or
195
+ # encryption could present a major security issue.
196
+ @coder = options[:coder] ||= Base64::Marshal.new
197
+
198
+ super(app, options.merge!(cookie_only: true))
199
+ end
200
+
201
+ private
202
+
203
+ def find_session(req, sid)
204
+ data = unpacked_cookie_data(req)
205
+ data = persistent_session_id!(data)
206
+ [data["session_id"], data]
207
+ end
208
+
209
+ def extract_session_id(request)
210
+ unpacked_cookie_data(request)&.[]("session_id")
211
+ end
212
+
213
+ def unpacked_cookie_data(request)
214
+ request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
215
+ if cookie_data = request.cookies[@key]
216
+ session_data = nil
217
+
218
+ # Try to decrypt the session data with our encryptors
219
+ encryptors.each do |encryptor|
220
+ begin
221
+ session_data = encryptor.decrypt(cookie_data)
222
+ break
223
+ rescue Rack::Session::Encryptor::Error => error
224
+ request.env[Rack::RACK_ERRORS].puts "Session cookie encryptor error: #{error.message}"
225
+
226
+ next
227
+ end
228
+ end
229
+
230
+ # If session decryption fails but there is @legacy_hmac_secret
231
+ # defined, attempt legacy HMAC verification
232
+ if !session_data && @legacy_hmac_secret
233
+ # Parse and verify legacy HMAC session cookie
234
+ session_data, _, digest = cookie_data.rpartition('--')
235
+ session_data = nil unless legacy_digest_match?(session_data, digest)
236
+
237
+ # Decode using legacy HMAC decoder
238
+ session_data = @legacy_hmac_coder.decode(session_data)
239
+
240
+ elsif !session_data && coder
241
+ # Use the coder option, which has the potential to be very unsafe
242
+ session_data = coder.decode(cookie_data)
243
+ end
244
+ end
245
+
246
+ request.set_header(k, session_data || {})
247
+ end
248
+ end
249
+
250
+ def persistent_session_id!(data, sid = nil)
251
+ data ||= {}
252
+ data["session_id"] ||= sid || generate_sid
253
+ data
254
+ end
255
+
256
+ class SessionId < DelegateClass(Session::SessionId)
257
+ attr_reader :cookie_value
258
+
259
+ def initialize(session_id, cookie_value)
260
+ super(session_id)
261
+ @cookie_value = cookie_value
262
+ end
263
+ end
264
+
265
+ def write_session(req, session_id, session, options)
266
+ session = session.merge("session_id" => session_id)
267
+ session_data = encode_session_data(session)
268
+
269
+ if session_data.size > (4096 - @key.size)
270
+ req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
271
+ nil
272
+ else
273
+ SessionId.new(session_id, session_data)
274
+ end
275
+ end
276
+
277
+ def delete_session(req, session_id, options)
278
+ # Nothing to do here, data is in the client
279
+ generate_sid unless options[:drop]
280
+ end
281
+
282
+ def legacy_digest_match?(data, digest)
283
+ return false unless data && digest
284
+
285
+ Rack::Utils.secure_compare(digest, legacy_generate_hmac(data))
286
+ end
287
+
288
+ def legacy_generate_hmac(data)
289
+ OpenSSL::HMAC.hexdigest(@legacy_hmac, @legacy_hmac_secret, data)
290
+ end
291
+
292
+ def encode_session_data(session)
293
+ if encryptors.empty?
294
+ coder.encode(session)
295
+ else
296
+ encryptors.first.encrypt(session)
297
+ end
298
+ end
299
+
300
+ # Were consider "secure" if:
301
+ # * Encrypted cookies are enabled and one or more encryptor is
302
+ # initialized
303
+ # * The legacy HMAC option is enabled
304
+ # * Customer :coder is used, with :let_coder_handle_secure_encoding
305
+ # set to true
306
+ def secure?(options)
307
+ !@encryptors.empty? ||
308
+ @legacy_hmac ||
309
+ (options[:coder] && options[:let_coder_handle_secure_encoding])
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
+ # Copyright, 2022, by Philip Arndt.
6
+
7
+ require 'base64'
8
+ require 'openssl'
9
+ require 'securerandom'
10
+ require 'zlib'
11
+
12
+ require 'rack/utils'
13
+
14
+ module Rack
15
+ module Session
16
+ class Encryptor
17
+ class Error < StandardError
18
+ end
19
+
20
+ class InvalidSignature < Error
21
+ end
22
+
23
+ class InvalidMessage < Error
24
+ end
25
+
26
+ # The secret String must be at least 64 bytes in size. The first 32 bytes
27
+ # will be used for the encryption cipher key. The remainder will be used
28
+ # for an HMAC key.
29
+ #
30
+ # Options may include:
31
+ # * :serialize_json
32
+ # Use JSON for message serialization instead of Marshal. This can be
33
+ # viewed as a security enhancement.
34
+ # * :pad_size
35
+ # Pad encrypted message data, to a multiple of this many bytes
36
+ # (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
37
+ # padding.
38
+ # * :purpose
39
+ # Limit messages to a specific purpose. This can be viewed as a
40
+ # security enhancement to prevent message reuse from different contexts
41
+ # if keys are reused.
42
+ #
43
+ # Cryptography and Output Format:
44
+ #
45
+ # urlsafe_encode64(version + random_data + IV + encrypted data + HMAC)
46
+ #
47
+ # Where:
48
+ # * version - 1 byte and is currently always 0x01
49
+ # * random_data - 32 bytes used for generating the per-message secret
50
+ # * IV - 16 bytes random initialization vector
51
+ # * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
52
+ # value
53
+ def initialize(secret, opts = {})
54
+ raise ArgumentError, "secret must be a String" unless String === secret
55
+ raise ArgumentError, "invalid secret: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
56
+
57
+ case opts[:pad_size]
58
+ when nil
59
+ # padding is disabled
60
+ when Integer
61
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size]
62
+ else
63
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil"
64
+ end
65
+
66
+ @options = {
67
+ serialize_json: false, pad_size: 32, purpose: nil
68
+ }.update(opts)
69
+
70
+ @hmac_secret = secret.dup.force_encoding('BINARY')
71
+ @cipher_secret = @hmac_secret.slice!(0, 32)
72
+
73
+ @hmac_secret.freeze
74
+ @cipher_secret.freeze
75
+ end
76
+
77
+ def decrypt(base64_data)
78
+ data = Base64.urlsafe_decode64(base64_data)
79
+
80
+ signature = data.slice!(-32..-1)
81
+
82
+ verify_authenticity! data, signature
83
+
84
+ # The version is reserved for future
85
+ _version = data.slice!(0, 1)
86
+ message_secret = data.slice!(0, 32)
87
+ cipher_iv = data.slice!(0, 16)
88
+
89
+ cipher = new_cipher
90
+ cipher.decrypt
91
+
92
+ set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
93
+
94
+ cipher.iv = cipher_iv
95
+ data = cipher.update(data) << cipher.final
96
+
97
+ deserialized_message data
98
+ rescue ArgumentError
99
+ raise InvalidSignature, 'Message invalid'
100
+ end
101
+
102
+ def encrypt(message)
103
+ version = "\1"
104
+
105
+ serialized_payload = serialize_payload(message)
106
+ message_secret, cipher_secret = new_message_and_cipher_secret
107
+
108
+ cipher = new_cipher
109
+ cipher.encrypt
110
+
111
+ set_cipher_key(cipher, cipher_secret)
112
+
113
+ cipher_iv = cipher.random_iv
114
+
115
+ encrypted_data = cipher.update(serialized_payload) << cipher.final
116
+
117
+ data = String.new
118
+ data << version
119
+ data << message_secret
120
+ data << cipher_iv
121
+ data << encrypted_data
122
+ data << compute_signature(data)
123
+
124
+ Base64.urlsafe_encode64(data)
125
+ end
126
+
127
+ private
128
+
129
+ def new_cipher
130
+ OpenSSL::Cipher.new('aes-256-ctr')
131
+ end
132
+
133
+ def new_message_and_cipher_secret
134
+ message_secret = SecureRandom.random_bytes(32)
135
+
136
+ [message_secret, cipher_secret_from_message_secret(message_secret)]
137
+ end
138
+
139
+ def cipher_secret_from_message_secret(message_secret)
140
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @cipher_secret, message_secret)
141
+ end
142
+
143
+ def set_cipher_key(cipher, key)
144
+ cipher.key = key
145
+ end
146
+
147
+ def serializer
148
+ @serializer ||= @options[:serialize_json] ? JSON : Marshal
149
+ end
150
+
151
+ def compute_signature(data)
152
+ signing_data = data
153
+ signing_data += @options[:purpose] if @options[:purpose]
154
+
155
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @hmac_secret, signing_data)
156
+ end
157
+
158
+ def verify_authenticity!(data, signature)
159
+ raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
160
+
161
+ unless Rack::Utils.secure_compare(signature, compute_signature(data))
162
+ raise InvalidSignature, 'HMAC is invalid'
163
+ end
164
+ end
165
+
166
+ # Returns a serialized payload of the message. If a :pad_size is supplied,
167
+ # the message will be padded. The first 2 bytes of the returned string will
168
+ # indicating the amount of padding.
169
+ def serialize_payload(message)
170
+ serialized_data = serializer.dump(message)
171
+
172
+ return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
173
+
174
+ padding_bytes = @options[:pad_size] - (2 + serialized_data.size) % @options[:pad_size]
175
+ padding_data = SecureRandom.random_bytes(padding_bytes)
176
+
177
+ "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
178
+ end
179
+
180
+ # Return the deserialized message. The first 2 bytes will be read as the
181
+ # amount of padding.
182
+ def deserialized_message(data)
183
+ # Read the first 2 bytes as the padding_bytes size
184
+ padding_bytes, = data.unpack('v')
185
+
186
+ # Slice out the serialized_data and deserialize it
187
+ serialized_data = data.slice(2 + padding_bytes, data.bytesize)
188
+ serializer.load serialized_data
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
+
6
+ require_relative 'abstract/id'
7
+
8
+ module Rack
9
+ module Session
10
+ # Rack::Session::Pool provides simple cookie based session management.
11
+ # Session data is stored in a hash held by @pool.
12
+ # In the context of a multithreaded environment, sessions being
13
+ # committed to the pool is done in a merging manner.
14
+ #
15
+ # The :drop option is available in rack.session.options if you wish to
16
+ # explicitly remove the session from the session cache.
17
+ #
18
+ # Example:
19
+ # myapp = MyRackApp.new
20
+ # sessioned = Rack::Session::Pool.new(myapp,
21
+ # :domain => 'foo.com',
22
+ # :expire_after => 2592000
23
+ # )
24
+ # Rack::Handler::WEBrick.run sessioned
25
+
26
+ class Pool < Abstract::PersistedSecure
27
+ attr_reader :mutex, :pool
28
+ DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge(drop: false, allow_fallback: true)
29
+
30
+ def initialize(app, options = {})
31
+ super
32
+ @pool = Hash.new
33
+ @mutex = Mutex.new
34
+ @allow_fallback = @default_options.delete(:allow_fallback)
35
+ end
36
+
37
+ def generate_sid(*args, use_mutex: true)
38
+ loop do
39
+ sid = super(*args)
40
+ break sid unless use_mutex ? @mutex.synchronize { @pool.key? sid.private_id } : @pool.key?(sid.private_id)
41
+ end
42
+ end
43
+
44
+ def find_session(req, sid)
45
+ @mutex.synchronize do
46
+ unless sid and session = get_session_with_fallback(sid)
47
+ sid, session = generate_sid(use_mutex: false), {}
48
+ @pool.store sid.private_id, session
49
+ end
50
+ [sid, session]
51
+ end
52
+ end
53
+
54
+ def write_session(req, session_id, new_session, options)
55
+ @mutex.synchronize do
56
+ @pool.store session_id.private_id, new_session
57
+ session_id
58
+ end
59
+ end
60
+
61
+ def delete_session(req, session_id, options)
62
+ @mutex.synchronize do
63
+ @pool.delete(session_id.public_id)
64
+ @pool.delete(session_id.private_id)
65
+ generate_sid(use_mutex: false) unless options[:drop]
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def get_session_with_fallback(sid)
72
+ @pool[sid.private_id] || (@pool[sid.public_id] if @allow_fallback)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Rack
7
7
  module Session
8
- VERSION = "1.0.0"
8
+ VERSION = "2.0.0"
9
9
  end
10
10
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
+ # Copyright, 2022, by Jeremy Evans.
6
+
7
+ module Rack
8
+ module Session
9
+ autoload :Cookie, "rack/session/cookie"
10
+ autoload :Pool, "rack/session/pool"
11
+ autoload :Memcache, "rack/session/memcache"
12
+ end
13
+ end
data/license.md CHANGED
@@ -1,7 +1,55 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2022-2023, by Samuel Williams.
4
- Copyright, 2022, by Jeremy Evans.
3
+ Copyright, 2007-2008, by Leah Neukirchen.
4
+ Copyright, 2007-2009, by Scytrin dai Kinthra.
5
+ Copyright, 2008, by Daniel Roethlisberger.
6
+ Copyright, 2009, by Joshua Peek.
7
+ Copyright, 2009, by Mickaël Riga.
8
+ Copyright, 2010, by Simon Chiang.
9
+ Copyright, 2010-2011, by José Valim.
10
+ Copyright, 2010-2013, by James Tucker.
11
+ Copyright, 2010-2019, by Aaron Patterson.
12
+ Copyright, 2011, by Max Cantor.
13
+ Copyright, 2011-2012, by Konstantin Haase.
14
+ Copyright, 2011, by Will Leinweber.
15
+ Copyright, 2011, by John Manoogian III.
16
+ Copyright, 2012, by Yun Huang Yong.
17
+ Copyright, 2012, by Ravil Bayramgalin.
18
+ Copyright, 2012, by Timothy Elliott.
19
+ Copyright, 2012, by Jamie Macey.
20
+ Copyright, 2012-2015, by Santiago Pastorino.
21
+ Copyright, 2013, by Andrew Cole.
22
+ Copyright, 2013, by Postmodern.
23
+ Copyright, 2013, by Vipul A M.
24
+ Copyright, 2013, by Charles Hornberger.
25
+ Copyright, 2014, by Michal Bryxí.
26
+ Copyright, 2015, by deepj.
27
+ Copyright, 2015, by Doug McInnes.
28
+ Copyright, 2015, by David Runger.
29
+ Copyright, 2015, by Francesco Rodríguez.
30
+ Copyright, 2015, by Yuichiro Kaneko.
31
+ Copyright, 2015, by Michael Sauter.
32
+ Copyright, 2016, by Kir Shatrov.
33
+ Copyright, 2016, by Yann Vanhalewyn.
34
+ Copyright, 2016, by Jian Weihang.
35
+ Copyright, 2017, by Jordan Raine.
36
+ Copyright, 2018, by Dillon Welch.
37
+ Copyright, 2018, by Yoshiyuki Hirano.
38
+ Copyright, 2019, by Krzysztof Rybka.
39
+ Copyright, 2019, by Frederick Cheung.
40
+ Copyright, 2019, by Adrian Setyadi.
41
+ Copyright, 2019, by Rafael Mendonça França.
42
+ Copyright, 2019-2020, by Pavel Rosicky.
43
+ Copyright, 2019, by Dima Fatko.
44
+ Copyright, 2019, by Oleh Demianiuk.
45
+ Copyright, 2020-2023, by Samuel Williams.
46
+ Copyright, 2020-2022, by Jeremy Evans.
47
+ Copyright, 2020, by Alex Speller.
48
+ Copyright, 2020, by Ryuta Kamizono.
49
+ Copyright, 2020, by Yudai Suzuki.
50
+ Copyright, 2020, by Bart de Water.
51
+ Copyright, 2020, by Alec Clarke.
52
+ Copyright, 2021, by Michael Coyne.
5
53
  Copyright, 2022, by Philip Arndt.
6
54
  Copyright, 2022, by Jon Dufresne.
7
55
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-session
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -17,16 +17,16 @@ dependencies:
17
17
  name: rack
18
18
  requirement: !ruby/object:Gem::Requirement
19
19
  requirements:
20
- - - "<="
20
+ - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: '3'
22
+ version: 3.0.0
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - "<="
27
+ - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '3'
29
+ version: 3.0.0
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: bundler
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -103,6 +103,12 @@ executables: []
103
103
  extensions: []
104
104
  extra_rdoc_files: []
105
105
  files:
106
+ - lib/rack/session.rb
107
+ - lib/rack/session/abstract/id.rb
108
+ - lib/rack/session/constants.rb
109
+ - lib/rack/session/cookie.rb
110
+ - lib/rack/session/encryptor.rb
111
+ - lib/rack/session/pool.rb
106
112
  - lib/rack/session/version.rb
107
113
  - license.md
108
114
  - readme.md