rack-session 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []