rack-session 1.0.2 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11a5c280b017199d82d7ac8f5518048cedb48f1e0e01ee162cefe4a4751b1931
4
- data.tar.gz: 3e38b13932869caacf14361cb9921b6b084feb2393f6c945cf9884f9647ac445
3
+ metadata.gz: 82ef73dda808550f0838e5fa7d75d90a15d1b524831c58a5e082349143637357
4
+ data.tar.gz: 4214302b0ec644f8b905d575d870a7fc4de5f4c8e1450a104e7193eb117b70f2
5
5
  SHA512:
6
- metadata.gz: e5c519939f66107a12bc1d561329fd3c4df2ea085d099395b190a3dd7dde0455677ff1b31c55e09528abd7223e3058d27d8a7ef12a0a25cba2dc18bec6b80910
7
- data.tar.gz: 789a2af79f74672cac563fbc2e0d00163393499d491a02107390cca3fffa3d4d1f53e6d35a494b0b0b057a7ba3b2aee70d5d5c4d3b249b1f1c0e116ea98f372c
6
+ metadata.gz: 17b3713d1ba66fd9c40f825826f05be73d60681b51b06e8df561efa4a146c537b323ff2fc436cc8ddf75314a26f5839e89d98bcc270746ad70e18af441e1a7ae
7
+ data.tar.gz: 5a06e7eec1398684eaca507d0c69063cfcfae2bb8478255d43c49ce01d9db5bc551e1cb593bed6872718ad925ea5145ea4acb95cf6f0eae26182b7af5d37a83a
@@ -0,0 +1,535 @@
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, :partitioned 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
+ partitioned: false,
248
+ defer: false,
249
+ renew: false,
250
+ sidbits: 128,
251
+ cookie_only: true,
252
+ secure_random: ::SecureRandom
253
+ }.freeze
254
+
255
+ attr_reader :key, :default_options, :sid_secure, :same_site
256
+
257
+ def initialize(app, options = {})
258
+ @app = app
259
+ @default_options = self.class::DEFAULT_OPTIONS.merge(options)
260
+ @key = @default_options.delete(:key)
261
+ @assume_ssl = @default_options.delete(:assume_ssl)
262
+ @cookie_only = @default_options.delete(:cookie_only)
263
+ @same_site = @default_options.delete(:same_site)
264
+ initialize_sid
265
+ end
266
+
267
+ def call(env)
268
+ context(env)
269
+ end
270
+
271
+ def context(env, app = @app)
272
+ req = make_request env
273
+ prepare_session(req)
274
+ status, headers, body = app.call(req.env)
275
+ res = Rack::Response::Raw.new status, headers
276
+ commit_session(req, res)
277
+ [status, headers, body]
278
+ end
279
+
280
+ private
281
+
282
+ def make_request(env)
283
+ Rack::Request.new env
284
+ end
285
+
286
+ def initialize_sid
287
+ @sidbits = @default_options[:sidbits]
288
+ @sid_secure = @default_options[:secure_random]
289
+ @sid_length = @sidbits / 4
290
+ end
291
+
292
+ # Generate a new session id using Ruby #rand. The size of the
293
+ # session id is controlled by the :sidbits option.
294
+ # Monkey patch this to use custom methods for session id generation.
295
+
296
+ def generate_sid(secure = @sid_secure)
297
+ if secure
298
+ secure.hex(@sid_length)
299
+ else
300
+ "%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
301
+ end
302
+ rescue NotImplementedError
303
+ generate_sid(false)
304
+ end
305
+
306
+ # Sets the lazy session at 'rack.session' and places options and session
307
+ # metadata into 'rack.session.options'.
308
+
309
+ def prepare_session(req)
310
+ session_was = req.get_header RACK_SESSION
311
+ session = session_class.new(self, req)
312
+ req.set_header RACK_SESSION, session
313
+ req.set_header RACK_SESSION_OPTIONS, @default_options.dup
314
+ session.merge! session_was if session_was
315
+ end
316
+
317
+ # Extracts the session id from provided cookies and passes it and the
318
+ # environment to #find_session.
319
+
320
+ def load_session(req)
321
+ sid = current_session_id(req)
322
+ sid, session = find_session(req, sid)
323
+ [sid, session || {}]
324
+ end
325
+
326
+ # Extract session id from request object.
327
+
328
+ def extract_session_id(request)
329
+ sid = request.cookies[@key]
330
+ sid ||= request.params[@key] unless @cookie_only
331
+ sid
332
+ end
333
+
334
+ # Returns the current session id from the SessionHash.
335
+
336
+ def current_session_id(req)
337
+ req.get_header(RACK_SESSION).id
338
+ end
339
+
340
+ # Check if the session exists or not.
341
+
342
+ def session_exists?(req)
343
+ value = current_session_id(req)
344
+ value && !value.empty?
345
+ end
346
+
347
+ # Session should be committed if it was loaded, any of specific options like :renew, :drop
348
+ # or :expire_after was given and the security permissions match. Skips if skip is given.
349
+
350
+ def commit_session?(req, session, options)
351
+ if options[:skip]
352
+ false
353
+ else
354
+ has_session = loaded_session?(session) || forced_session_update?(session, options)
355
+ has_session && security_matches?(req, options)
356
+ end
357
+ end
358
+
359
+ def loaded_session?(session)
360
+ !session.is_a?(session_class) || session.loaded?
361
+ end
362
+
363
+ def forced_session_update?(session, options)
364
+ force_options?(options) && session && !session.empty?
365
+ end
366
+
367
+ def force_options?(options)
368
+ options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
369
+ end
370
+
371
+ def security_matches?(request, options)
372
+ return true unless options[:secure]
373
+ request.ssl? || @assume_ssl == true
374
+ end
375
+
376
+ # Acquires the session from the environment and the session id from
377
+ # the session options and passes them to #write_session. If successful
378
+ # and the :defer option is not true, a cookie will be added to the
379
+ # response with the session's id.
380
+
381
+ def commit_session(req, res)
382
+ session = req.get_header RACK_SESSION
383
+ options = session.options
384
+
385
+ if options[:drop] || options[:renew]
386
+ session_id = delete_session(req, session.id || generate_sid, options)
387
+ return unless session_id
388
+ end
389
+
390
+ return unless commit_session?(req, session, options)
391
+
392
+ session.send(:load!) unless loaded_session?(session)
393
+ session_id ||= session.id
394
+ session_data = session.to_hash.delete_if { |k, v| v.nil? }
395
+
396
+ if not data = write_session(req, session_id, session_data, options)
397
+ req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
398
+ elsif options[:defer] and not options[:renew]
399
+ req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
400
+ else
401
+ cookie = Hash.new
402
+ cookie[:value] = cookie_value(data)
403
+ cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
404
+ cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
405
+
406
+ if @same_site.respond_to? :call
407
+ cookie[:same_site] = @same_site.call(req, res)
408
+ else
409
+ cookie[:same_site] = @same_site
410
+ end
411
+ set_cookie(req, res, cookie.merge!(options))
412
+ end
413
+ end
414
+ public :commit_session
415
+
416
+ def cookie_value(data)
417
+ data
418
+ end
419
+
420
+ # Sets the cookie back to the client with session id. We skip the cookie
421
+ # setting if the value didn't change (sid is the same) or expires was given.
422
+
423
+ def set_cookie(request, response, cookie)
424
+ if request.cookies[@key] != cookie[:value] || cookie[:expires]
425
+ response.set_cookie(@key, cookie)
426
+ end
427
+ end
428
+
429
+ # Allow subclasses to prepare_session for different Session classes
430
+
431
+ def session_class
432
+ SessionHash
433
+ end
434
+
435
+ # All thread safety and session retrieval procedures should occur here.
436
+ # Should return [session_id, session].
437
+ # If nil is provided as the session id, generation of a new valid id
438
+ # should occur within.
439
+
440
+ def find_session(env, sid)
441
+ raise '#find_session not implemented.'
442
+ end
443
+
444
+ # All thread safety and session storage procedures should occur here.
445
+ # Must return the session id if the session was saved successfully, or
446
+ # false if the session could not be saved.
447
+
448
+ def write_session(req, sid, session, options)
449
+ raise '#write_session not implemented.'
450
+ end
451
+
452
+ # All thread safety and session destroy procedures should occur here.
453
+ # Should return a new session id or nil if options[:drop]
454
+
455
+ def delete_session(req, sid, options)
456
+ raise '#delete_session not implemented'
457
+ end
458
+ end
459
+
460
+ class PersistedSecure < Persisted
461
+ class SecureSessionHash < SessionHash
462
+ def [](key)
463
+ if key == "session_id"
464
+ load_for_read!
465
+ case id
466
+ when SessionId
467
+ id.public_id
468
+ else
469
+ id
470
+ end
471
+ else
472
+ super
473
+ end
474
+ end
475
+ end
476
+
477
+ def generate_sid(*)
478
+ public_id = super
479
+
480
+ SessionId.new(public_id)
481
+ end
482
+
483
+ def extract_session_id(*)
484
+ public_id = super
485
+ public_id && SessionId.new(public_id)
486
+ end
487
+
488
+ private
489
+
490
+ def session_class
491
+ SecureSessionHash
492
+ end
493
+
494
+ def cookie_value(data)
495
+ data.cookie_value
496
+ end
497
+ end
498
+
499
+ class ID < Persisted
500
+ def self.inherited(klass)
501
+ k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID }
502
+ unless k.instance_variable_defined?(:"@_rack_warned")
503
+ warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE
504
+ k.instance_variable_set(:"@_rack_warned", true)
505
+ end
506
+ super
507
+ end
508
+
509
+ # All thread safety and session retrieval procedures should occur here.
510
+ # Should return [session_id, session].
511
+ # If nil is provided as the session id, generation of a new valid id
512
+ # should occur within.
513
+
514
+ def find_session(req, sid)
515
+ get_session req.env, sid
516
+ end
517
+
518
+ # All thread safety and session storage procedures should occur here.
519
+ # Must return the session id if the session was saved successfully, or
520
+ # false if the session could not be saved.
521
+
522
+ def write_session(req, sid, session, options)
523
+ set_session req.env, sid, session, options
524
+ end
525
+
526
+ # All thread safety and session destroy procedures should occur here.
527
+ # Should return a new session id or nil if options[:drop]
528
+
529
+ def delete_session(req, sid, options)
530
+ destroy_session req.env, sid, options
531
+ end
532
+ end
533
+ end
534
+ end
535
+ 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