rack-session 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ca669bfeeda8e9b9d563ae88f899301acc7d8df1bfebb6768762edc9e99fb895
4
+ data.tar.gz: 16170181b6d0eb480807ccff0cbd04ad4d8208a33251317ecdd1da202ed1076c
5
+ SHA512:
6
+ metadata.gz: fc97739262aca4442a56ff5ad029adeb20f461637fd8730f819f0c8c5faa0e0aef736d64366031312393e04cfce44ec9b44c5a6bb4548b75be85c2c89a8f6d3d
7
+ data.tar.gz: dbcbf050c12610a82a37a02fa1f2e75b46cb5d2d0644b6a1e55175a76feca1afe7780746f04dd91575d54665ff2d0983eb262c37ed6291eac8a214b2e8071db3
@@ -0,0 +1,531 @@
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
+ module Rack
15
+
16
+ module Session
17
+
18
+ class SessionId
19
+ ID_VERSION = 2
20
+
21
+ attr_reader :public_id
22
+
23
+ def initialize(public_id)
24
+ @public_id = public_id
25
+ end
26
+
27
+ def private_id
28
+ "#{ID_VERSION}::#{hash_sid(public_id)}"
29
+ end
30
+
31
+ alias :cookie_value :public_id
32
+ alias :to_s :public_id
33
+
34
+ def empty?; false; end
35
+ def inspect; public_id.inspect; end
36
+
37
+ private
38
+
39
+ def hash_sid(sid)
40
+ Digest::SHA256.hexdigest(sid)
41
+ end
42
+ end
43
+
44
+ module Abstract
45
+ # SessionHash is responsible to lazily load the session from store.
46
+
47
+ class SessionHash
48
+ include Enumerable
49
+ attr_writer :id
50
+
51
+ Unspecified = Object.new
52
+
53
+ def self.find(req)
54
+ req.get_header RACK_SESSION
55
+ end
56
+
57
+ def self.set(req, session)
58
+ req.set_header RACK_SESSION, session
59
+ end
60
+
61
+ def self.set_options(req, options)
62
+ req.set_header RACK_SESSION_OPTIONS, options.dup
63
+ end
64
+
65
+ def initialize(store, req)
66
+ @store = store
67
+ @req = req
68
+ @loaded = false
69
+ end
70
+
71
+ def id
72
+ return @id if @loaded or instance_variable_defined?(:@id)
73
+ @id = @store.send(:extract_session_id, @req)
74
+ end
75
+
76
+ def options
77
+ @req.session_options
78
+ end
79
+
80
+ def each(&block)
81
+ load_for_read!
82
+ @data.each(&block)
83
+ end
84
+
85
+ def [](key)
86
+ load_for_read!
87
+ @data[key.to_s]
88
+ end
89
+
90
+ def dig(key, *keys)
91
+ load_for_read!
92
+ @data.dig(key.to_s, *keys)
93
+ end
94
+
95
+ def fetch(key, default = Unspecified, &block)
96
+ load_for_read!
97
+ if default == Unspecified
98
+ @data.fetch(key.to_s, &block)
99
+ else
100
+ @data.fetch(key.to_s, default, &block)
101
+ end
102
+ end
103
+
104
+ def has_key?(key)
105
+ load_for_read!
106
+ @data.has_key?(key.to_s)
107
+ end
108
+ alias :key? :has_key?
109
+ alias :include? :has_key?
110
+
111
+ def []=(key, value)
112
+ load_for_write!
113
+ @data[key.to_s] = value
114
+ end
115
+ alias :store :[]=
116
+
117
+ def clear
118
+ load_for_write!
119
+ @data.clear
120
+ end
121
+
122
+ def destroy
123
+ clear
124
+ @id = @store.send(:delete_session, @req, id, options)
125
+ end
126
+
127
+ def to_hash
128
+ load_for_read!
129
+ @data.dup
130
+ end
131
+
132
+ def update(hash)
133
+ load_for_write!
134
+ @data.update(stringify_keys(hash))
135
+ end
136
+ alias :merge! :update
137
+
138
+ def replace(hash)
139
+ load_for_write!
140
+ @data.replace(stringify_keys(hash))
141
+ end
142
+
143
+ def delete(key)
144
+ load_for_write!
145
+ @data.delete(key.to_s)
146
+ end
147
+
148
+ def inspect
149
+ if loaded?
150
+ @data.inspect
151
+ else
152
+ "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
153
+ end
154
+ end
155
+
156
+ def exists?
157
+ return @exists if instance_variable_defined?(:@exists)
158
+ @data = {}
159
+ @exists = @store.send(:session_exists?, @req)
160
+ end
161
+
162
+ def loaded?
163
+ @loaded
164
+ end
165
+
166
+ def empty?
167
+ load_for_read!
168
+ @data.empty?
169
+ end
170
+
171
+ def keys
172
+ load_for_read!
173
+ @data.keys
174
+ end
175
+
176
+ def values
177
+ load_for_read!
178
+ @data.values
179
+ end
180
+
181
+ private
182
+
183
+ def load_for_read!
184
+ load! if !loaded? && exists?
185
+ end
186
+
187
+ def load_for_write!
188
+ load! unless loaded?
189
+ end
190
+
191
+ def load!
192
+ @id, session = @store.send(:load_session, @req)
193
+ @data = stringify_keys(session)
194
+ @loaded = true
195
+ end
196
+
197
+ def stringify_keys(other)
198
+ # Use transform_keys after dropping Ruby 2.4 support
199
+ hash = {}
200
+ other.to_hash.each do |key, value|
201
+ hash[key.to_s] = value
202
+ end
203
+ hash
204
+ end
205
+ end
206
+
207
+ # ID sets up a basic framework for implementing an id based sessioning
208
+ # service. Cookies sent to the client for maintaining sessions will only
209
+ # contain an id reference. Only #find_session, #write_session and
210
+ # #delete_session are required to be overwritten.
211
+ #
212
+ # All parameters are optional.
213
+ # * :key determines the name of the cookie, by default it is
214
+ # 'rack.session'
215
+ # * :path, :domain, :expire_after, :secure, :httponly, and :same_site set
216
+ # the related cookie options as by Rack::Response#set_cookie
217
+ # * :skip will not a set a cookie in the response nor update the session state
218
+ # * :defer will not set a cookie in the response but still update the session
219
+ # state if it is used with a backend
220
+ # * :renew (implementation dependent) will prompt the generation of a new
221
+ # session id, and migration of data to be referenced at the new id. If
222
+ # :defer is set, it will be overridden and the cookie will be set.
223
+ # * :sidbits sets the number of bits in length that a generated session
224
+ # id will be.
225
+ #
226
+ # These options can be set on a per request basis, at the location of
227
+ # <tt>env['rack.session.options']</tt>. Additionally the id of the
228
+ # session can be found within the options hash at the key :id. It is
229
+ # highly not recommended to change its value.
230
+ #
231
+ # Is Rack::Utils::Context compatible.
232
+ #
233
+ # Not included by default; you must require 'rack/session/abstract/id'
234
+ # to use.
235
+
236
+ class Persisted
237
+ DEFAULT_OPTIONS = {
238
+ key: RACK_SESSION,
239
+ path: '/',
240
+ domain: nil,
241
+ expire_after: nil,
242
+ secure: false,
243
+ httponly: true,
244
+ defer: false,
245
+ renew: false,
246
+ sidbits: 128,
247
+ cookie_only: true,
248
+ secure_random: ::SecureRandom
249
+ }.freeze
250
+
251
+ attr_reader :key, :default_options, :sid_secure
252
+
253
+ def initialize(app, options = {})
254
+ @app = app
255
+ @default_options = self.class::DEFAULT_OPTIONS.merge(options)
256
+ @key = @default_options.delete(:key)
257
+ @cookie_only = @default_options.delete(:cookie_only)
258
+ @same_site = @default_options.delete(:same_site)
259
+ initialize_sid
260
+ end
261
+
262
+ def call(env)
263
+ context(env)
264
+ end
265
+
266
+ def context(env, app = @app)
267
+ req = make_request env
268
+ prepare_session(req)
269
+ status, headers, body = app.call(req.env)
270
+ res = Rack::Response::Raw.new status, headers
271
+ commit_session(req, res)
272
+ [status, headers, body]
273
+ end
274
+
275
+ private
276
+
277
+ def make_request(env)
278
+ Rack::Request.new env
279
+ end
280
+
281
+ def initialize_sid
282
+ @sidbits = @default_options[:sidbits]
283
+ @sid_secure = @default_options[:secure_random]
284
+ @sid_length = @sidbits / 4
285
+ end
286
+
287
+ # Generate a new session id using Ruby #rand. The size of the
288
+ # session id is controlled by the :sidbits option.
289
+ # Monkey patch this to use custom methods for session id generation.
290
+
291
+ def generate_sid(secure = @sid_secure)
292
+ if secure
293
+ secure.hex(@sid_length)
294
+ else
295
+ "%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
296
+ end
297
+ rescue NotImplementedError
298
+ generate_sid(false)
299
+ end
300
+
301
+ # Sets the lazy session at 'rack.session' and places options and session
302
+ # metadata into 'rack.session.options'.
303
+
304
+ def prepare_session(req)
305
+ session_was = req.get_header RACK_SESSION
306
+ session = session_class.new(self, req)
307
+ req.set_header RACK_SESSION, session
308
+ req.set_header RACK_SESSION_OPTIONS, @default_options.dup
309
+ session.merge! session_was if session_was
310
+ end
311
+
312
+ # Extracts the session id from provided cookies and passes it and the
313
+ # environment to #find_session.
314
+
315
+ def load_session(req)
316
+ sid = current_session_id(req)
317
+ sid, session = find_session(req, sid)
318
+ [sid, session || {}]
319
+ end
320
+
321
+ # Extract session id from request object.
322
+
323
+ def extract_session_id(request)
324
+ sid = request.cookies[@key]
325
+ sid ||= request.params[@key] unless @cookie_only
326
+ sid
327
+ end
328
+
329
+ # Returns the current session id from the SessionHash.
330
+
331
+ def current_session_id(req)
332
+ req.get_header(RACK_SESSION).id
333
+ end
334
+
335
+ # Check if the session exists or not.
336
+
337
+ def session_exists?(req)
338
+ value = current_session_id(req)
339
+ value && !value.empty?
340
+ end
341
+
342
+ # Session should be committed if it was loaded, any of specific options like :renew, :drop
343
+ # or :expire_after was given and the security permissions match. Skips if skip is given.
344
+
345
+ def commit_session?(req, session, options)
346
+ if options[:skip]
347
+ false
348
+ else
349
+ has_session = loaded_session?(session) || forced_session_update?(session, options)
350
+ has_session && security_matches?(req, options)
351
+ end
352
+ end
353
+
354
+ def loaded_session?(session)
355
+ !session.is_a?(session_class) || session.loaded?
356
+ end
357
+
358
+ def forced_session_update?(session, options)
359
+ force_options?(options) && session && !session.empty?
360
+ end
361
+
362
+ def force_options?(options)
363
+ options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
364
+ end
365
+
366
+ def security_matches?(request, options)
367
+ return true unless options[:secure]
368
+ request.ssl?
369
+ end
370
+
371
+ # Acquires the session from the environment and the session id from
372
+ # the session options and passes them to #write_session. If successful
373
+ # and the :defer option is not true, a cookie will be added to the
374
+ # response with the session's id.
375
+
376
+ def commit_session(req, res)
377
+ session = req.get_header RACK_SESSION
378
+ options = session.options
379
+
380
+ if options[:drop] || options[:renew]
381
+ session_id = delete_session(req, session.id || generate_sid, options)
382
+ return unless session_id
383
+ end
384
+
385
+ return unless commit_session?(req, session, options)
386
+
387
+ session.send(:load!) unless loaded_session?(session)
388
+ session_id ||= session.id
389
+ session_data = session.to_hash.delete_if { |k, v| v.nil? }
390
+
391
+ if not data = write_session(req, session_id, session_data, options)
392
+ req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
393
+ elsif options[:defer] and not options[:renew]
394
+ req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
395
+ else
396
+ cookie = Hash.new
397
+ cookie[:value] = cookie_value(data)
398
+ cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
399
+ cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
400
+
401
+ if @same_site.respond_to? :call
402
+ cookie[:same_site] = @same_site.call(req, res)
403
+ else
404
+ cookie[:same_site] = @same_site
405
+ end
406
+ set_cookie(req, res, cookie.merge!(options))
407
+ end
408
+ end
409
+ public :commit_session
410
+
411
+ def cookie_value(data)
412
+ data
413
+ end
414
+
415
+ # Sets the cookie back to the client with session id. We skip the cookie
416
+ # setting if the value didn't change (sid is the same) or expires was given.
417
+
418
+ def set_cookie(request, res, cookie)
419
+ if request.cookies[@key] != cookie[:value] || cookie[:expires]
420
+ res.set_cookie_header =
421
+ Utils.add_cookie_to_header(res.set_cookie_header, @key, cookie)
422
+ end
423
+ end
424
+
425
+ # Allow subclasses to prepare_session for different Session classes
426
+
427
+ def session_class
428
+ SessionHash
429
+ end
430
+
431
+ # All thread safety and session retrieval procedures should occur here.
432
+ # Should return [session_id, session].
433
+ # If nil is provided as the session id, generation of a new valid id
434
+ # should occur within.
435
+
436
+ def find_session(env, sid)
437
+ raise '#find_session not implemented.'
438
+ end
439
+
440
+ # All thread safety and session storage procedures should occur here.
441
+ # Must return the session id if the session was saved successfully, or
442
+ # false if the session could not be saved.
443
+
444
+ def write_session(req, sid, session, options)
445
+ raise '#write_session not implemented.'
446
+ end
447
+
448
+ # All thread safety and session destroy procedures should occur here.
449
+ # Should return a new session id or nil if options[:drop]
450
+
451
+ def delete_session(req, sid, options)
452
+ raise '#delete_session not implemented'
453
+ end
454
+ end
455
+
456
+ class PersistedSecure < Persisted
457
+ class SecureSessionHash < SessionHash
458
+ def [](key)
459
+ if key == "session_id"
460
+ load_for_read!
461
+ case id
462
+ when SessionId
463
+ id.public_id
464
+ else
465
+ id
466
+ end
467
+ else
468
+ super
469
+ end
470
+ end
471
+ end
472
+
473
+ def generate_sid(*)
474
+ public_id = super
475
+
476
+ SessionId.new(public_id)
477
+ end
478
+
479
+ def extract_session_id(*)
480
+ public_id = super
481
+ public_id && SessionId.new(public_id)
482
+ end
483
+
484
+ private
485
+
486
+ def session_class
487
+ SecureSessionHash
488
+ end
489
+
490
+ def cookie_value(data)
491
+ data.cookie_value
492
+ end
493
+ end
494
+
495
+ class ID < Persisted
496
+ def self.inherited(klass)
497
+ k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID }
498
+ unless k.instance_variable_defined?(:"@_rack_warned")
499
+ warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE
500
+ k.instance_variable_set(:"@_rack_warned", true)
501
+ end
502
+ super
503
+ end
504
+
505
+ # All thread safety and session retrieval procedures should occur here.
506
+ # Should return [session_id, session].
507
+ # If nil is provided as the session id, generation of a new valid id
508
+ # should occur within.
509
+
510
+ def find_session(req, sid)
511
+ get_session req.env, sid
512
+ end
513
+
514
+ # All thread safety and session storage procedures should occur here.
515
+ # Must return the session id if the session was saved successfully, or
516
+ # false if the session could not be saved.
517
+
518
+ def write_session(req, sid, session, options)
519
+ set_session req.env, sid, session, options
520
+ end
521
+
522
+ # All thread safety and session destroy procedures should occur here.
523
+ # Should return a new session id or nil if options[:drop]
524
+
525
+ def delete_session(req, sid, options)
526
+ destroy_session req.env, sid, options
527
+ end
528
+ end
529
+ end
530
+ end
531
+ end
@@ -0,0 +1,6 @@
1
+
2
+ module Rack
3
+ module Session
4
+ RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data'
5
+ end
6
+ end
@@ -0,0 +1,304 @@
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
+
15
+ module Rack
16
+
17
+ module Session
18
+
19
+ # Rack::Session::Cookie provides simple cookie based session management.
20
+ # By default, the session is a Ruby Hash that is serialized and encoded as
21
+ # a cookie set to :key (default: rack.session).
22
+ #
23
+ # This middleware accepts a :secrets option which enables encryption of
24
+ # session cookies. This option should be one or more random "secret keys"
25
+ # that are each at least 64 bytes in length. Multiple secret keys can be
26
+ # supplied in an Array, which is useful when rotating secrets.
27
+ #
28
+ # Several options are also accepted that are passed to Rack::Session::Encryptor.
29
+ # These options include:
30
+ # * :serialize_json
31
+ # Use JSON for message serialization instead of Marshal. This can be
32
+ # viewed as a security ehancement.
33
+ # * :gzip_over
34
+ # For message data over this many bytes, compress it with the deflate
35
+ # algorithm.
36
+ #
37
+ # Refer to Rack::Session::Encryptor for more details on these options.
38
+ #
39
+ # Prior to version TODO, the session hash was stored as base64 encoded
40
+ # marshalled data. When a :secret option was supplied, the integrity of the
41
+ # encoded data was protected with HMAC-SHA1. This functionality is still
42
+ # supported using a set of a legacy options.
43
+ #
44
+ # Lastly, a :coder option is also accepted. When used, both encryption and
45
+ # the legacy HMAC will be skipped. This option could create security issues
46
+ # in your application!
47
+ #
48
+ # Example:
49
+ #
50
+ # use Rack::Session::Cookie, {
51
+ # key: 'rack.session',
52
+ # domain: 'foo.com',
53
+ # path: '/',
54
+ # expire_after: 2592000,
55
+ # secrets: 'a randomly generated, raw binary string 64 bytes in size',
56
+ # }
57
+ #
58
+ # Example using legacy HMAC options:
59
+ #
60
+ # Rack::Session:Cookie.new(application, {
61
+ # # The secret used for legacy HMAC cookies, this enables the functionality
62
+ # legacy_hmac_secret: 'legacy secret',
63
+ # # legacy_hmac_coder will default to Rack::Session::Cookie::Base64::Marshal
64
+ # legacy_hmac_coder: Rack::Session::Cookie::Identity.new,
65
+ # # legacy_hmac will default to OpenSSL::Digest::SHA1
66
+ # legacy_hmac: OpenSSL::Digest::SHA256
67
+ # })
68
+ #
69
+ # Example of a cookie with no encoding:
70
+ #
71
+ # Rack::Session::Cookie.new(application, {
72
+ # :coder => Rack::Session::Cookie::Identity.new
73
+ # })
74
+ #
75
+ # Example of a cookie with custom encoding:
76
+ #
77
+ # Rack::Session::Cookie.new(application, {
78
+ # :coder => Class.new {
79
+ # def encode(str); str.reverse; end
80
+ # def decode(str); str.reverse; end
81
+ # }.new
82
+ # })
83
+ #
84
+
85
+ class Cookie < Abstract::PersistedSecure
86
+ # Encode session cookies as Base64
87
+ class Base64
88
+ def encode(str)
89
+ ::Base64.strict_encode64(str)
90
+ end
91
+
92
+ def decode(str)
93
+ ::Base64.decode64(str)
94
+ end
95
+
96
+ # Encode session cookies as Marshaled Base64 data
97
+ class Marshal < Base64
98
+ def encode(str)
99
+ super(::Marshal.dump(str))
100
+ end
101
+
102
+ def decode(str)
103
+ return unless str
104
+ ::Marshal.load(super(str)) rescue nil
105
+ end
106
+ end
107
+
108
+ # N.B. Unlike other encoding methods, the contained objects must be a
109
+ # valid JSON composite type, either a Hash or an Array.
110
+ class JSON < Base64
111
+ def encode(obj)
112
+ super(::JSON.dump(obj))
113
+ end
114
+
115
+ def decode(str)
116
+ return unless str
117
+ ::JSON.parse(super(str)) rescue nil
118
+ end
119
+ end
120
+
121
+ class ZipJSON < Base64
122
+ def encode(obj)
123
+ super(Zlib::Deflate.deflate(::JSON.dump(obj)))
124
+ end
125
+
126
+ def decode(str)
127
+ return unless str
128
+ ::JSON.parse(Zlib::Inflate.inflate(super(str)))
129
+ rescue
130
+ nil
131
+ end
132
+ end
133
+ end
134
+
135
+ # Use no encoding for session cookies
136
+ class Identity
137
+ def encode(str); str; end
138
+ def decode(str); str; end
139
+ end
140
+
141
+ class Marshal
142
+ def encode(str)
143
+ ::Marshal.dump(str)
144
+ end
145
+
146
+ def decode(str)
147
+ ::Marshal.load(str) if str
148
+ end
149
+ end
150
+
151
+ attr_reader :coder, :encryptors
152
+
153
+ def initialize(app, options = {})
154
+ secrets = [*options[:secrets]]
155
+
156
+ encryptor_opts = {
157
+ purpose: options[:key], serialize_json: options[:serialize_json]
158
+ }
159
+
160
+ # For each secret, create an Encryptor. We have iterate this Array at
161
+ # decryption time to achieve key rotation.
162
+ @encryptors = secrets.map do |secret|
163
+ Rack::Session::Encryptor.new secret, encryptor_opts
164
+ end
165
+
166
+ # If a legacy HMAC secret is present, initialize those features
167
+ if options.has_key?(:legacy_hmac_secret)
168
+ @legacy_hmac = options.fetch(:legacy_hmac, 'SHA1')
169
+
170
+ @legacy_hmac_secret = options[:legacy_hmac_secret]
171
+ @legacy_hmac_coder = options.fetch(:legacy_hmac_coder, Base64::Marshal.new)
172
+ else
173
+ @legacy_hmac = false
174
+ end
175
+
176
+ warn <<-MSG unless secure?(options)
177
+ SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
178
+ This poses a security threat. It is strongly recommended that you
179
+ provide a secret to prevent exploits that may be possible from crafted
180
+ cookies. This will not be supported in future versions of Rack, and
181
+ future versions will even invalidate your existing user cookies.
182
+
183
+ Called from: #{caller[0]}.
184
+ MSG
185
+
186
+ # Potential danger ahead! Marshal without verification and/or
187
+ # encryption could present a major security issue.
188
+ @coder = options[:coder] ||= Base64::Marshal.new
189
+
190
+ super(app, options.merge!(cookie_only: true))
191
+ end
192
+
193
+ private
194
+
195
+ def find_session(req, sid)
196
+ data = unpacked_cookie_data(req)
197
+ data = persistent_session_id!(data)
198
+ [data["session_id"], data]
199
+ end
200
+
201
+ def extract_session_id(request)
202
+ unpacked_cookie_data(request)["session_id"]
203
+ end
204
+
205
+ def unpacked_cookie_data(request)
206
+ request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
207
+ cookie_data = request.cookies[@key]
208
+ session_data = nil
209
+
210
+ # Try to decrypt the session data with our encryptors
211
+ encryptors.each do |encryptor|
212
+ begin
213
+ session_data = encryptor.decrypt(cookie_data) if cookie_data
214
+ break
215
+ rescue Rack::Session::Encryptor::Error => error
216
+ request.env[Rack::RACK_ERRORS].puts "Session cookie encryptor error: #{error.message}"
217
+
218
+ next
219
+ end
220
+ end
221
+
222
+ # If session decryption fails but there is @legacy_hmac_secret
223
+ # defined, attempt legacy HMAC verification
224
+ if !session_data && @legacy_hmac_secret
225
+ # Parse and verify legacy HMAC session cookie
226
+ session_data, _, digest = cookie_data.rpartition('--')
227
+ session_data = nil unless legacy_digest_match?(session_data, digest)
228
+
229
+ # Decode using legacy HMAC decoder
230
+ session_data = @legacy_hmac_coder.decode(session_data)
231
+
232
+ elsif !session_data && coder
233
+ # Use the coder option, which has the potential to be very unsafe
234
+ session_data = coder.decode(cookie_data)
235
+ end
236
+
237
+ request.set_header(k, session_data || {})
238
+ end
239
+ end
240
+
241
+ def persistent_session_id!(data, sid = nil)
242
+ data ||= {}
243
+ data["session_id"] ||= sid || generate_sid
244
+ data
245
+ end
246
+
247
+ class SessionId < DelegateClass(Session::SessionId)
248
+ attr_reader :cookie_value
249
+
250
+ def initialize(session_id, cookie_value)
251
+ super(session_id)
252
+ @cookie_value = cookie_value
253
+ end
254
+ end
255
+
256
+ def write_session(req, session_id, session, options)
257
+ session = session.merge("session_id" => session_id)
258
+ session_data = encode_session_data(session)
259
+
260
+ if session_data.size > (4096 - @key.size)
261
+ req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
262
+ nil
263
+ else
264
+ SessionId.new(session_id, session_data)
265
+ end
266
+ end
267
+
268
+ def delete_session(req, session_id, options)
269
+ # Nothing to do here, data is in the client
270
+ generate_sid unless options[:drop]
271
+ end
272
+
273
+ def legacy_digest_match?(data, digest)
274
+ return false unless data && digest
275
+
276
+ Rack::Utils.secure_compare(digest, legacy_generate_hmac(data))
277
+ end
278
+
279
+ def legacy_generate_hmac(data)
280
+ OpenSSL::HMAC.hexdigest(@legacy_hmac, @legacy_hmac_secret, data)
281
+ end
282
+
283
+ def encode_session_data(session)
284
+ if encryptors.empty?
285
+ coder.encode(session)
286
+ else
287
+ encryptors.first.encrypt(session)
288
+ end
289
+ end
290
+
291
+ # Were consider "secure" if:
292
+ # * Encrypted cookies are enabled and one or more encryptor is
293
+ # initialized
294
+ # * The legacy HMAC option is enabled
295
+ # * Customer :coder is used, with :let_coder_handle_secure_encoding
296
+ # set to true
297
+ def secure?(options)
298
+ !@encryptors.empty? ||
299
+ @legacy_hmac ||
300
+ (options[:coder] && options[:let_coder_handle_secure_encoding])
301
+ end
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,188 @@
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 ehancement.
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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/session/dalli'
4
+
5
+ module Rack
6
+ module Session
7
+ warn "Rack::Session::Memcache is deprecated, please use Rack::Session::Dalli from 'dalli' gem instead."
8
+ Memcache = Dalli
9
+ end
10
+ end
@@ -0,0 +1,78 @@
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
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
4
+ #
5
+ # Rack is freely distributable under the terms of an MIT-style license.
6
+ # See MIT-LICENSE or https://opensource.org/licenses/MIT.
7
+
8
+ # The Rack main module, serving as a namespace for all core Rack
9
+ # modules and classes.
10
+ #
11
+ # All modules meant for use in your application are <tt>autoload</tt>ed here,
12
+ # so it should be enough just to <tt>require 'rack'</tt> in your code.
13
+
14
+ module Rack
15
+ module Session
16
+ VERSION = "0.1.0"
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+
2
+ module Rack
3
+ RACK_SESSION = 'rack.session'
4
+ RACK_SESSION_OPTIONS = 'rack.session.options'
5
+ RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data'
6
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-session
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rack Contributors
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-02-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest-sprint
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest-global_expectations
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - lib/rack/session.rb
90
+ - lib/rack/session/abstract/id.rb
91
+ - lib/rack/session/constants.rb
92
+ - lib/rack/session/cookie.rb
93
+ - lib/rack/session/encryptor.rb
94
+ - lib/rack/session/memcache.rb
95
+ - lib/rack/session/pool.rb
96
+ - lib/rack/session/version.rb
97
+ homepage: https://github.com/rack/rack-session
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 2.4.0
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 3.4.0.dev
117
+ signing_key:
118
+ specification_version: 4
119
+ summary: A session implementation for Rack.
120
+ test_files: []