summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSamuel Williams <samuel.williams@oriontransfer.co.nz>2022-02-15 18:41:48 +1300
committerSamuel Williams <samuel.williams@oriontransfer.co.nz>2022-02-22 20:38:59 +1300
commit1085a3e97162a8b1675ae896f7c2158e6f00d1fc (patch)
tree165ea958c510ca5be072200e0ee876bd516bbe75
parentc394c4d645cdc574c18f4a8ed3f162e28cb04d6d (diff)
downloadrack-1085a3e97162a8b1675ae896f7c2158e6f00d1fc.tar.gz
Move `Rack::Session` module and related tests into a separate gem.
-rw-r--r--CHANGELOG.md1
-rw-r--r--lib/rack.rb6
-rw-r--r--lib/rack/constants.rb1
-rw-r--r--lib/rack/encryptor.rb184
-rw-r--r--lib/rack/session/abstract/id.rb531
-rw-r--r--lib/rack/session/cookie.rb303
-rw-r--r--lib/rack/session/memcache.rb10
-rw-r--r--lib/rack/session/pool.rb78
-rw-r--r--rack.gemspec2
-rw-r--r--test/spec_encryptor.rb166
-rw-r--r--test/spec_session_abstract_id.rb85
-rw-r--r--test/spec_session_abstract_persisted.rb70
-rw-r--r--test/spec_session_abstract_persisted_secure_secure_session_hash.rb86
-rw-r--r--test/spec_session_abstract_session_hash.rb104
-rw-r--r--test/spec_session_cookie.rb591
-rw-r--r--test/spec_session_pool.rb288
16 files changed, 3 insertions, 2503 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c447f182..2f379888 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,7 @@ All notable changes to this project will be documented in this file. For info on
- `Rack::Request#POST` now caches an empty hash if input content type is not parseable. ([#749](https://github.com/rack/rack/pull/749), [@jeremyevans](https://github.com/jeremyevans))
- BREAKING CHANGE: Updated `trusted_proxy?` to match full 127.0.0.0/8 network. ([#1781](https://github.com/rack/rack/pull/1781), [@snbloch](https://github.com/snbloch))
- Explicitly deprecate `Rack::File` which was an alias for `Rack::Files`. ([#1811](https://github.com/rack/rack/pull/1720), [@ioquatix](https://github.com/ioquatix)).
+- Moved `Rack::Session` into [separate gem](https://github.com/rack/rack-session). ([#1805](https://github.com/rack/rack/pull/1805), [@ioquatix](https://github.com/ioquatix))
### Fixed
diff --git a/lib/rack.rb b/lib/rack.rb
index 6ec8bad7..a05c93a7 100644
--- a/lib/rack.rb
+++ b/lib/rack.rb
@@ -72,10 +72,4 @@ module Rack
autoload :Request, "rack/auth/digest/request"
end
end
-
- module Session
- autoload :Cookie, "rack/session/cookie"
- autoload :Pool, "rack/session/pool"
- autoload :Memcache, "rack/session/memcache"
- end
end
diff --git a/lib/rack/constants.rb b/lib/rack/constants.rb
index 55cfd9fe..6d1becb7 100644
--- a/lib/rack/constants.rb
+++ b/lib/rack/constants.rb
@@ -57,5 +57,4 @@ module Rack
RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'
RACK_REQUEST_QUERY_STRING = 'rack.request.query_string'
RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method'
- RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data'
end
diff --git a/lib/rack/encryptor.rb b/lib/rack/encryptor.rb
deleted file mode 100644
index 86ad4e05..00000000
--- a/lib/rack/encryptor.rb
+++ /dev/null
@@ -1,184 +0,0 @@
-# frozen_string_literal: true
-
-require 'base64'
-require 'openssl'
-require 'securerandom'
-require 'zlib'
-
-module Rack
- class Encryptor
- class Error < StandardError
- end
-
- class InvalidSignature < Error
- end
-
- class InvalidMessage < Error
- end
-
- # The secret String must be at least 64 bytes in size. The first 32 bytes
- # will be used for the encryption cipher key. The remainder will be used
- # for an HMAC key.
- #
- # Options may include:
- # * :serialize_json
- # Use JSON for message serialization instead of Marshal. This can be
- # viewed as a security ehancement.
- # * :pad_size
- # Pad encrypted message data, to a multiple of this many bytes
- # (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
- # padding.
- # * :purpose
- # Limit messages to a specific purpose. This can be viewed as a
- # security enhancement to prevent message reuse from different contexts
- # if keys are reused.
- #
- # Cryptography and Output Format:
- #
- # urlsafe_encode64(version + random_data + IV + encrypted data + HMAC)
- #
- # Where:
- # * version - 1 byte and is currently always 0x01
- # * random_data - 32 bytes used for generating the per-message secret
- # * IV - 16 bytes random initialization vector
- # * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
- # value
- def initialize(secret, opts = {})
- raise ArgumentError, "secret must be a String" unless String === secret
- raise ArgumentError, "invalid secret: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
-
- case opts[:pad_size]
- when nil
- # padding is disabled
- when Integer
- raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size]
- else
- raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil"
- end
-
- @options = {
- serialize_json: false, pad_size: 32, purpose: nil
- }.update(opts)
-
- @hmac_secret = secret.dup.force_encoding('BINARY')
- @cipher_secret = @hmac_secret.slice!(0, 32)
-
- @hmac_secret.freeze
- @cipher_secret.freeze
- end
-
- def decrypt(base64_data)
- data = Base64.urlsafe_decode64(base64_data)
-
- signature = data.slice!(-32..-1)
-
- verify_authenticity! data, signature
-
- # The version is reserved for future
- _version = data.slice!(0, 1)
- message_secret = data.slice!(0, 32)
- cipher_iv = data.slice!(0, 16)
-
- cipher = new_cipher
- cipher.decrypt
-
- set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
-
- cipher.iv = cipher_iv
- data = cipher.update(data) << cipher.final
-
- deserialized_message data
- rescue ArgumentError
- raise InvalidSignature, 'Message invalid'
- end
-
- def encrypt(message)
- version = "\1"
-
- serialized_payload = serialize_payload(message)
- message_secret, cipher_secret = new_message_and_cipher_secret
-
- cipher = new_cipher
- cipher.encrypt
-
- set_cipher_key(cipher, cipher_secret)
-
- cipher_iv = cipher.random_iv
-
- encrypted_data = cipher.update(serialized_payload) << cipher.final
-
- data = String.new
- data << version
- data << message_secret
- data << cipher_iv
- data << encrypted_data
- data << compute_signature(data)
-
- Base64.urlsafe_encode64(data)
- end
-
- private
-
- def new_cipher
- OpenSSL::Cipher.new('aes-256-ctr')
- end
-
- def new_message_and_cipher_secret
- message_secret = SecureRandom.random_bytes(32)
-
- [message_secret, cipher_secret_from_message_secret(message_secret)]
- end
-
- def cipher_secret_from_message_secret(message_secret)
- OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @cipher_secret, message_secret)
- end
-
- def set_cipher_key(cipher, key)
- cipher.key = key
- end
-
- def serializer
- @serializer ||= @options[:serialize_json] ? JSON : Marshal
- end
-
- def compute_signature(data)
- signing_data = data
- signing_data += @options[:purpose] if @options[:purpose]
-
- OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @hmac_secret, signing_data)
- end
-
- def verify_authenticity!(data, signature)
- raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
-
- unless Rack::Utils.secure_compare(signature, compute_signature(data))
- raise InvalidSignature, 'HMAC is invalid'
- end
- end
-
- # Returns a serialized payload of the message. If a :pad_size is supplied,
- # the message will be padded. The first 2 bytes of the returned string will
- # indicating the amount of padding.
- def serialize_payload(message)
- serialized_data = serializer.dump(message)
-
- return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
-
- padding_bytes = @options[:pad_size] - (2 + serialized_data.size) % @options[:pad_size]
- padding_data = SecureRandom.random_bytes(padding_bytes)
-
- "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
- end
-
- # Return the deserialized message. The first 2 bytes will be read as the
- # amount of padding.
- def deserialized_message(data)
- # Read the first 2 bytes as the padding_bytes size
- padding_bytes, = data.unpack('v')
-
- # Slice out the serialized_data and deserialize it
- serialized_data = data.slice(2 + padding_bytes, data.bytesize)
- serializer.load serialized_data
- end
- end
-end
diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb
deleted file mode 100644
index 65b93d69..00000000
--- a/lib/rack/session/abstract/id.rb
+++ /dev/null
@@ -1,531 +0,0 @@
-# frozen_string_literal: true
-
-# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
-# bugrep: Andreas Zehnder
-
-require 'time'
-require 'securerandom'
-require 'digest/sha2'
-
-require_relative '../../constants'
-require_relative '../../request'
-require_relative '../../response'
-
-module Rack
-
- module Session
-
- class SessionId
- ID_VERSION = 2
-
- attr_reader :public_id
-
- def initialize(public_id)
- @public_id = public_id
- end
-
- def private_id
- "#{ID_VERSION}::#{hash_sid(public_id)}"
- end
-
- alias :cookie_value :public_id
- alias :to_s :public_id
-
- def empty?; false; end
- def inspect; public_id.inspect; end
-
- private
-
- def hash_sid(sid)
- Digest::SHA256.hexdigest(sid)
- end
- end
-
- module Abstract
- # SessionHash is responsible to lazily load the session from store.
-
- class SessionHash
- include Enumerable
- attr_writer :id
-
- Unspecified = Object.new
-
- def self.find(req)
- req.get_header RACK_SESSION
- end
-
- def self.set(req, session)
- req.set_header RACK_SESSION, session
- end
-
- def self.set_options(req, options)
- req.set_header RACK_SESSION_OPTIONS, options.dup
- end
-
- def initialize(store, req)
- @store = store
- @req = req
- @loaded = false
- end
-
- def id
- return @id if @loaded or instance_variable_defined?(:@id)
- @id = @store.send(:extract_session_id, @req)
- end
-
- def options
- @req.session_options
- end
-
- def each(&block)
- load_for_read!
- @data.each(&block)
- end
-
- def [](key)
- load_for_read!
- @data[key.to_s]
- end
-
- def dig(key, *keys)
- load_for_read!
- @data.dig(key.to_s, *keys)
- end
-
- def fetch(key, default = Unspecified, &block)
- load_for_read!
- if default == Unspecified
- @data.fetch(key.to_s, &block)
- else
- @data.fetch(key.to_s, default, &block)
- end
- end
-
- def has_key?(key)
- load_for_read!
- @data.has_key?(key.to_s)
- end
- alias :key? :has_key?
- alias :include? :has_key?
-
- def []=(key, value)
- load_for_write!
- @data[key.to_s] = value
- end
- alias :store :[]=
-
- def clear
- load_for_write!
- @data.clear
- end
-
- def destroy
- clear
- @id = @store.send(:delete_session, @req, id, options)
- end
-
- def to_hash
- load_for_read!
- @data.dup
- end
-
- def update(hash)
- load_for_write!
- @data.update(stringify_keys(hash))
- end
- alias :merge! :update
-
- def replace(hash)
- load_for_write!
- @data.replace(stringify_keys(hash))
- end
-
- def delete(key)
- load_for_write!
- @data.delete(key.to_s)
- end
-
- def inspect
- if loaded?
- @data.inspect
- else
- "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
- end
- end
-
- def exists?
- return @exists if instance_variable_defined?(:@exists)
- @data = {}
- @exists = @store.send(:session_exists?, @req)
- end
-
- def loaded?
- @loaded
- end
-
- def empty?
- load_for_read!
- @data.empty?
- end
-
- def keys
- load_for_read!
- @data.keys
- end
-
- def values
- load_for_read!
- @data.values
- end
-
- private
-
- def load_for_read!
- load! if !loaded? && exists?
- end
-
- def load_for_write!
- load! unless loaded?
- end
-
- def load!
- @id, session = @store.send(:load_session, @req)
- @data = stringify_keys(session)
- @loaded = true
- end
-
- def stringify_keys(other)
- # Use transform_keys after dropping Ruby 2.4 support
- hash = {}
- other.to_hash.each do |key, value|
- hash[key.to_s] = value
- end
- hash
- end
- end
-
- # ID sets up a basic framework for implementing an id based sessioning
- # service. Cookies sent to the client for maintaining sessions will only
- # contain an id reference. Only #find_session, #write_session and
- # #delete_session are required to be overwritten.
- #
- # All parameters are optional.
- # * :key determines the name of the cookie, by default it is
- # 'rack.session'
- # * :path, :domain, :expire_after, :secure, :httponly, and :same_site set
- # the related cookie options as by Rack::Response#set_cookie
- # * :skip will not a set a cookie in the response nor update the session state
- # * :defer will not set a cookie in the response but still update the session
- # state if it is used with a backend
- # * :renew (implementation dependent) will prompt the generation of a new
- # session id, and migration of data to be referenced at the new id. If
- # :defer is set, it will be overridden and the cookie will be set.
- # * :sidbits sets the number of bits in length that a generated session
- # id will be.
- #
- # These options can be set on a per request basis, at the location of
- # <tt>env['rack.session.options']</tt>. Additionally the id of the
- # session can be found within the options hash at the key :id. It is
- # highly not recommended to change its value.
- #
- # Is Rack::Utils::Context compatible.
- #
- # Not included by default; you must require 'rack/session/abstract/id'
- # to use.
-
- class Persisted
- DEFAULT_OPTIONS = {
- key: RACK_SESSION,
- path: '/',
- domain: nil,
- expire_after: nil,
- secure: false,
- httponly: true,
- defer: false,
- renew: false,
- sidbits: 128,
- cookie_only: true,
- secure_random: ::SecureRandom
- }.freeze
-
- attr_reader :key, :default_options, :sid_secure
-
- def initialize(app, options = {})
- @app = app
- @default_options = self.class::DEFAULT_OPTIONS.merge(options)
- @key = @default_options.delete(:key)
- @cookie_only = @default_options.delete(:cookie_only)
- @same_site = @default_options.delete(:same_site)
- initialize_sid
- end
-
- def call(env)
- context(env)
- end
-
- def context(env, app = @app)
- req = make_request env
- prepare_session(req)
- status, headers, body = app.call(req.env)
- res = Rack::Response::Raw.new status, headers
- commit_session(req, res)
- [status, headers, body]
- end
-
- private
-
- def make_request(env)
- Rack::Request.new env
- end
-
- def initialize_sid
- @sidbits = @default_options[:sidbits]
- @sid_secure = @default_options[:secure_random]
- @sid_length = @sidbits / 4
- end
-
- # Generate a new session id using Ruby #rand. The size of the
- # session id is controlled by the :sidbits option.
- # Monkey patch this to use custom methods for session id generation.
-
- def generate_sid(secure = @sid_secure)
- if secure
- secure.hex(@sid_length)
- else
- "%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
- end
- rescue NotImplementedError
- generate_sid(false)
- end
-
- # Sets the lazy session at 'rack.session' and places options and session
- # metadata into 'rack.session.options'.
-
- def prepare_session(req)
- session_was = req.get_header RACK_SESSION
- session = session_class.new(self, req)
- req.set_header RACK_SESSION, session
- req.set_header RACK_SESSION_OPTIONS, @default_options.dup
- session.merge! session_was if session_was
- end
-
- # Extracts the session id from provided cookies and passes it and the
- # environment to #find_session.
-
- def load_session(req)
- sid = current_session_id(req)
- sid, session = find_session(req, sid)
- [sid, session || {}]
- end
-
- # Extract session id from request object.
-
- def extract_session_id(request)
- sid = request.cookies[@key]
- sid ||= request.params[@key] unless @cookie_only
- sid
- end
-
- # Returns the current session id from the SessionHash.
-
- def current_session_id(req)
- req.get_header(RACK_SESSION).id
- end
-
- # Check if the session exists or not.
-
- def session_exists?(req)
- value = current_session_id(req)
- value && !value.empty?
- end
-
- # Session should be committed if it was loaded, any of specific options like :renew, :drop
- # or :expire_after was given and the security permissions match. Skips if skip is given.
-
- def commit_session?(req, session, options)
- if options[:skip]
- false
- else
- has_session = loaded_session?(session) || forced_session_update?(session, options)
- has_session && security_matches?(req, options)
- end
- end
-
- def loaded_session?(session)
- !session.is_a?(session_class) || session.loaded?
- end
-
- def forced_session_update?(session, options)
- force_options?(options) && session && !session.empty?
- end
-
- def force_options?(options)
- options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
- end
-
- def security_matches?(request, options)
- return true unless options[:secure]
- request.ssl?
- end
-
- # Acquires the session from the environment and the session id from
- # the session options and passes them to #write_session. If successful
- # and the :defer option is not true, a cookie will be added to the
- # response with the session's id.
-
- def commit_session(req, res)
- session = req.get_header RACK_SESSION
- options = session.options
-
- if options[:drop] || options[:renew]
- session_id = delete_session(req, session.id || generate_sid, options)
- return unless session_id
- end
-
- return unless commit_session?(req, session, options)
-
- session.send(:load!) unless loaded_session?(session)
- session_id ||= session.id
- session_data = session.to_hash.delete_if { |k, v| v.nil? }
-
- if not data = write_session(req, session_id, session_data, options)
- req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
- elsif options[:defer] and not options[:renew]
- req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
- else
- cookie = Hash.new
- cookie[:value] = cookie_value(data)
- cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
- cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
-
- if @same_site.respond_to? :call
- cookie[:same_site] = @same_site.call(req, res)
- else
- cookie[:same_site] = @same_site
- end
- set_cookie(req, res, cookie.merge!(options))
- end
- end
- public :commit_session
-
- def cookie_value(data)
- data
- end
-
- # Sets the cookie back to the client with session id. We skip the cookie
- # setting if the value didn't change (sid is the same) or expires was given.
-
- def set_cookie(request, res, cookie)
- if request.cookies[@key] != cookie[:value] || cookie[:expires]
- res.set_cookie_header =
- Utils.add_cookie_to_header(res.set_cookie_header, @key, cookie)
- end
- end
-
- # Allow subclasses to prepare_session for different Session classes
-
- def session_class
- SessionHash
- end
-
- # All thread safety and session retrieval procedures should occur here.
- # Should return [session_id, session].
- # If nil is provided as the session id, generation of a new valid id
- # should occur within.
-
- def find_session(env, sid)
- raise '#find_session not implemented.'
- end
-
- # All thread safety and session storage procedures should occur here.
- # Must return the session id if the session was saved successfully, or
- # false if the session could not be saved.
-
- def write_session(req, sid, session, options)
- raise '#write_session not implemented.'
- end
-
- # All thread safety and session destroy procedures should occur here.
- # Should return a new session id or nil if options[:drop]
-
- def delete_session(req, sid, options)
- raise '#delete_session not implemented'
- end
- end
-
- class PersistedSecure < Persisted
- class SecureSessionHash < SessionHash
- def [](key)
- if key == "session_id"
- load_for_read!
- case id
- when SessionId
- id.public_id
- else
- id
- end
- else
- super
- end
- end
- end
-
- def generate_sid(*)
- public_id = super
-
- SessionId.new(public_id)
- end
-
- def extract_session_id(*)
- public_id = super
- public_id && SessionId.new(public_id)
- end
-
- private
-
- def session_class
- SecureSessionHash
- end
-
- def cookie_value(data)
- data.cookie_value
- end
- end
-
- class ID < Persisted
- def self.inherited(klass)
- k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID }
- unless k.instance_variable_defined?(:"@_rack_warned")
- warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE
- k.instance_variable_set(:"@_rack_warned", true)
- end
- super
- end
-
- # All thread safety and session retrieval procedures should occur here.
- # Should return [session_id, session].
- # If nil is provided as the session id, generation of a new valid id
- # should occur within.
-
- def find_session(req, sid)
- get_session req.env, sid
- end
-
- # All thread safety and session storage procedures should occur here.
- # Must return the session id if the session was saved successfully, or
- # false if the session could not be saved.
-
- def write_session(req, sid, session, options)
- set_session req.env, sid, session, options
- end
-
- # All thread safety and session destroy procedures should occur here.
- # Should return a new session id or nil if options[:drop]
-
- def delete_session(req, sid, options)
- destroy_session req.env, sid, options
- end
- end
- end
- end
-end
diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb
deleted file mode 100644
index e4b91f1f..00000000
--- a/lib/rack/session/cookie.rb
+++ /dev/null
@@ -1,303 +0,0 @@
-# frozen_string_literal: true
-
-require 'openssl'
-require 'zlib'
-require 'json'
-require 'base64'
-require 'delegate'
-
-require_relative '../constants'
-require_relative '../utils'
-require_relative 'abstract/id'
-require_relative '../encryptor'
-
-module Rack
-
- module Session
-
- # Rack::Session::Cookie provides simple cookie based session management.
- # By default, the session is a Ruby Hash that is serialized and encoded as
- # a cookie set to :key (default: rack.session).
- #
- # This middleware accepts a :secrets option which enables encryption of
- # session cookies. This option should be one or more random "secret keys"
- # that are each at least 64 bytes in length. Multiple secret keys can be
- # supplied in an Array, which is useful when rotating secrets.
- #
- # Several options are also accepted that are passed to Rack::Encryptor.
- # These options include:
- # * :serialize_json
- # Use JSON for message serialization instead of Marshal. This can be
- # viewed as a security ehancement.
- # * :gzip_over
- # For message data over this many bytes, compress it with the deflate
- # algorithm.
- #
- # Refer to Rack::Encryptor for more details on these options.
- #
- # Prior to version TODO, the session hash was stored as base64 encoded
- # marshalled data. When a :secret option was supplied, the integrity of the
- # encoded data was protected with HMAC-SHA1. This functionality is still
- # supported using a set of a legacy options.
- #
- # Lastly, a :coder option is also accepted. When used, both encryption and
- # the legacy HMAC will be skipped. This option could create security issues
- # in your application!
- #
- # Example:
- #
- # use Rack::Session::Cookie, {
- # key: 'rack.session',
- # domain: 'foo.com',
- # path: '/',
- # expire_after: 2592000,
- # secrets: 'a randomly generated, raw binary string 64 bytes in size',
- # }
- #
- # Example using legacy HMAC options:
- #
- # Rack::Session:Cookie.new(application, {
- # # The secret used for legacy HMAC cookies, this enables the functionality
- # legacy_hmac_secret: 'legacy secret',
- # # legacy_hmac_coder will default to Rack::Session::Cookie::Base64::Marshal
- # legacy_hmac_coder: Rack::Session::Cookie::Identity.new,
- # # legacy_hmac will default to OpenSSL::Digest::SHA1
- # legacy_hmac: OpenSSL::Digest::SHA256
- # })
- #
- # Example of a cookie with no encoding:
- #
- # Rack::Session::Cookie.new(application, {
- # :coder => Rack::Session::Cookie::Identity.new
- # })
- #
- # Example of a cookie with custom encoding:
- #
- # Rack::Session::Cookie.new(application, {
- # :coder => Class.new {
- # def encode(str); str.reverse; end
- # def decode(str); str.reverse; end
- # }.new
- # })
- #
-
- class Cookie < Abstract::PersistedSecure
- # Encode session cookies as Base64
- class Base64
- def encode(str)
- ::Base64.strict_encode64(str)
- end
-
- def decode(str)
- ::Base64.decode64(str)
- end
-
- # Encode session cookies as Marshaled Base64 data
- class Marshal < Base64
- def encode(str)
- super(::Marshal.dump(str))
- end
-
- def decode(str)
- return unless str
- ::Marshal.load(super(str)) rescue nil
- end
- end
-
- # N.B. Unlike other encoding methods, the contained objects must be a
- # valid JSON composite type, either a Hash or an Array.
- class JSON < Base64
- def encode(obj)
- super(::JSON.dump(obj))
- end
-
- def decode(str)
- return unless str
- ::JSON.parse(super(str)) rescue nil
- end
- end
-
- class ZipJSON < Base64
- def encode(obj)
- super(Zlib::Deflate.deflate(::JSON.dump(obj)))
- end
-
- def decode(str)
- return unless str
- ::JSON.parse(Zlib::Inflate.inflate(super(str)))
- rescue
- nil
- end
- end
- end
-
- # Use no encoding for session cookies
- class Identity
- def encode(str); str; end
- def decode(str); str; end
- end
-
- class Marshal
- def encode(str)
- ::Marshal.dump(str)
- end
-
- def decode(str)
- ::Marshal.load(str) if str
- end
- end
-
- attr_reader :coder, :encryptors
-
- def initialize(app, options = {})
- secrets = [*options[:secrets]]
-
- encryptor_opts = {
- purpose: options[:key], serialize_json: options[:serialize_json]
- }
-
- # For each secret, create an Encryptor. We have iterate this Array at
- # decryption time to achieve key rotation.
- @encryptors = secrets.map do |secret|
- Rack::Encryptor.new secret, encryptor_opts
- end
-
- # If a legacy HMAC secret is present, initialize those features
- if options.has_key?(:legacy_hmac_secret)
- @legacy_hmac = options.fetch(:legacy_hmac, 'SHA1')
-
- @legacy_hmac_secret = options[:legacy_hmac_secret]
- @legacy_hmac_coder = options.fetch(:legacy_hmac_coder, Base64::Marshal.new)
- else
- @legacy_hmac = false
- end
-
- warn <<-MSG unless secure?(options)
- SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
- This poses a security threat. It is strongly recommended that you
- provide a secret to prevent exploits that may be possible from crafted
- cookies. This will not be supported in future versions of Rack, and
- future versions will even invalidate your existing user cookies.
-
- Called from: #{caller[0]}.
- MSG
-
- # Potential danger ahead! Marshal without verification and/or
- # encryption could present a major security issue.
- @coder = options[:coder] ||= Base64::Marshal.new
-
- super(app, options.merge!(cookie_only: true))
- end
-
- private
-
- def find_session(req, sid)
- data = unpacked_cookie_data(req)
- data = persistent_session_id!(data)
- [data["session_id"], data]
- end
-
- def extract_session_id(request)
- unpacked_cookie_data(request)["session_id"]
- end
-
- def unpacked_cookie_data(request)
- request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
- cookie_data = request.cookies[@key]
- session_data = nil
-
- # Try to decrypt the session data with our encryptors
- encryptors.each do |encryptor|
- begin
- session_data = encryptor.decrypt(cookie_data) if cookie_data
- break
- rescue Rack::Encryptor::Error => error
- request.env[Rack::RACK_ERRORS].puts "Session cookie encryptor error: #{error.message}"
-
- next
- end
- end
-
- # If session decryption fails but there is @legacy_hmac_secret
- # defined, attempt legacy HMAC verification
- if !session_data && @legacy_hmac_secret
- # Parse and verify legacy HMAC session cookie
- session_data, _, digest = cookie_data.rpartition('--')
- session_data = nil unless legacy_digest_match?(session_data, digest)
-
- # Decode using legacy HMAC decoder
- session_data = @legacy_hmac_coder.decode(session_data)
-
- elsif !session_data && coder
- # Use the coder option, which has the potential to be very unsafe
- session_data = coder.decode(cookie_data)
- end
-
- request.set_header(k, session_data || {})
- end
- end
-
- def persistent_session_id!(data, sid = nil)
- data ||= {}
- data["session_id"] ||= sid || generate_sid
- data
- end
-
- class SessionId < DelegateClass(Session::SessionId)
- attr_reader :cookie_value
-
- def initialize(session_id, cookie_value)
- super(session_id)
- @cookie_value = cookie_value
- end
- end
-
- def write_session(req, session_id, session, options)
- session = session.merge("session_id" => session_id)
- session_data = encode_session_data(session)
-
- if session_data.size > (4096 - @key.size)
- req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
- nil
- else
- SessionId.new(session_id, session_data)
- end
- end
-
- def delete_session(req, session_id, options)
- # Nothing to do here, data is in the client
- generate_sid unless options[:drop]
- end
-
- def legacy_digest_match?(data, digest)
- return false unless data && digest
-
- Rack::Utils.secure_compare(digest, legacy_generate_hmac(data))
- end
-
- def legacy_generate_hmac(data)
- OpenSSL::HMAC.hexdigest(@legacy_hmac, @legacy_hmac_secret, data)
- end
-
- def encode_session_data(session)
- if encryptors.empty?
- coder.encode(session)
- else
- encryptors.first.encrypt(session)
- end
- end
-
- # Were consider "secure" if:
- # * Encrypted cookies are enabled and one or more encryptor is
- # initialized
- # * The legacy HMAC option is enabled
- # * Customer :coder is used, with :let_coder_handle_secure_encoding
- # set to true
- def secure?(options)
- !@encryptors.empty? ||
- @legacy_hmac ||
- (options[:coder] && options[:let_coder_handle_secure_encoding])
- end
- end
- end
-end
diff --git a/lib/rack/session/memcache.rb b/lib/rack/session/memcache.rb
deleted file mode 100644
index 6a601174..00000000
--- a/lib/rack/session/memcache.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-require 'rack/session/dalli'
-
-module Rack
- module Session
- warn "Rack::Session::Memcache is deprecated, please use Rack::Session::Dalli from 'dalli' gem instead."
- Memcache = Dalli
- end
-end
diff --git a/lib/rack/session/pool.rb b/lib/rack/session/pool.rb
deleted file mode 100644
index 05c567d0..00000000
--- a/lib/rack/session/pool.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
-# THANKS:
-# apeiros, for session id generation, expiry setup, and threadiness
-# sergio, threadiness and bugreps
-
-require_relative 'abstract/id'
-
-module Rack
- module Session
- # Rack::Session::Pool provides simple cookie based session management.
- # Session data is stored in a hash held by @pool.
- # In the context of a multithreaded environment, sessions being
- # committed to the pool is done in a merging manner.
- #
- # The :drop option is available in rack.session.options if you wish to
- # explicitly remove the session from the session cache.
- #
- # Example:
- # myapp = MyRackApp.new
- # sessioned = Rack::Session::Pool.new(myapp,
- # :domain => 'foo.com',
- # :expire_after => 2592000
- # )
- # Rack::Handler::WEBrick.run sessioned
-
- class Pool < Abstract::PersistedSecure
- attr_reader :mutex, :pool
- DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge(drop: false, allow_fallback: true)
-
- def initialize(app, options = {})
- super
- @pool = Hash.new
- @mutex = Mutex.new
- @allow_fallback = @default_options.delete(:allow_fallback)
- end
-
- def generate_sid(*args, use_mutex: true)
- loop do
- sid = super(*args)
- break sid unless use_mutex ? @mutex.synchronize { @pool.key? sid.private_id } : @pool.key?(sid.private_id)
- end
- end
-
- def find_session(req, sid)
- @mutex.synchronize do
- unless sid and session = get_session_with_fallback(sid)
- sid, session = generate_sid(use_mutex: false), {}
- @pool.store sid.private_id, session
- end
- [sid, session]
- end
- end
-
- def write_session(req, session_id, new_session, options)
- @mutex.synchronize do
- @pool.store session_id.private_id, new_session
- session_id
- end
- end
-
- def delete_session(req, session_id, options)
- @mutex.synchronize do
- @pool.delete(session_id.public_id)
- @pool.delete(session_id.private_id)
- generate_sid(use_mutex: false) unless options[:drop]
- end
- end
-
- private
-
- def get_session_with_fallback(sid)
- @pool[sid.private_id] || (@pool[sid.public_id] if @allow_fallback)
- end
- end
- end
-end
diff --git a/rack.gemspec b/rack.gemspec
index 999a69a0..44c405d7 100644
--- a/rack.gemspec
+++ b/rack.gemspec
@@ -39,6 +39,8 @@ Gem::Specification.new do |s|
"source_code_uri" => "https://github.com/rack/rack"
}
+ s.add_dependency 'rack-session'
+
s.add_development_dependency 'minitest', "~> 5.0"
s.add_development_dependency 'minitest-sprint'
s.add_development_dependency 'minitest-global_expectations'
diff --git a/test/spec_encryptor.rb b/test/spec_encryptor.rb
deleted file mode 100644
index 7f88661c..00000000
--- a/test/spec_encryptor.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-require 'rack/encryptor'
-require 'rack/utils'
-
-describe Rack::Encryptor do
- def setup
- @secret = SecureRandom.random_bytes(64)
- end
-
- it 'initialize does not destroy key string' do
- encryptor = Rack::Encryptor.new(@secret)
-
- @secret.size.must_equal 64
- end
-
- it 'initialize raises ArgumentError on invalid key' do
- lambda { Rack::Encryptor.new [:foo] }.must_raise ArgumentError
- end
-
- it 'initialize raises ArgumentError on short key' do
- lambda { Rack::Encryptor.new 'key' }.must_raise ArgumentError
- end
-
- it 'decrypts an encrypted message' do
- encryptor = Rack::Encryptor.new(@secret)
-
- message = encryptor.encrypt(foo: 'bar')
-
- encryptor.decrypt(message).must_equal foo: 'bar'
- end
-
- it 'decrypt raises InvalidSignature for tampered messages' do
- encryptor = Rack::Encryptor.new(@secret)
-
- message = encryptor.encrypt(foo: 'bar')
-
- decoded_message = Base64.urlsafe_decode64(message)
- tampered_message = Base64.urlsafe_encode64(decoded_message.tap { |m|
- m[m.size - 1] = (m[m.size - 1].unpack('C')[0] ^ 1).chr
- })
-
- lambda {
- encryptor.decrypt(tampered_message)
- }.must_raise Rack::Encryptor::InvalidSignature
- end
-
- it 'decrypts an encrypted message with purpose' do
- encryptor = Rack::Encryptor.new(@secret, purpose: 'testing')
-
- message = encryptor.encrypt(foo: 'bar')
-
- encryptor.decrypt(message).must_equal foo: 'bar'
- end
-
- it 'decrypts raises InvalidSignature without purpose' do
- encryptor = Rack::Encryptor.new(@secret, purpose: 'testing')
- other_encryptor = Rack::Encryptor.new(@secret)
-
- message = other_encryptor.encrypt(foo: 'bar')
-
- lambda { encryptor.decrypt(message) }.must_raise Rack::Encryptor::InvalidSignature
- end
-
- it 'decrypts raises InvalidSignature with different purpose' do
- encryptor = Rack::Encryptor.new(@secret, purpose: 'testing')
- other_encryptor = Rack::Encryptor.new(@secret, purpose: 'other')
-
- message = other_encryptor.encrypt(foo: 'bar')
-
- lambda { encryptor.decrypt(message) }.must_raise Rack::Encryptor::InvalidSignature
- end
-
- it 'initialize raises ArgumentError on invalid pad_size' do
- lambda { Rack::Encryptor.new @secret, pad_size: :bar }.must_raise ArgumentError
- end
-
- it 'initialize raises ArgumentError on to short pad_size' do
- lambda { Rack::Encryptor.new @secret, pad_size: 1 }.must_raise ArgumentError
- end
-
- it 'initialize raises ArgumentError on to long pad_size' do
- lambda { Rack::Encryptor.new @secret, pad_size: 8023 }.must_raise ArgumentError
- end
-
- it 'decrypts an encrypted message without pad_size' do
- encryptor = Rack::Encryptor.new(@secret, purpose: 'testing', pad_size: nil)
-
- message = encryptor.encrypt(foo: 'bar')
-
- encryptor.decrypt(message).must_equal foo: 'bar'
- end
-
- it 'encryptor with pad_size increases message size' do
- no_pad_encryptor = Rack::Encryptor.new(@secret, purpose: 'testing', pad_size: nil)
- pad_encryptor = Rack::Encryptor.new(@secret, purpose: 'testing', pad_size: 64)
-
- message_without = Base64.urlsafe_decode64(no_pad_encryptor.encrypt(''))
- message_with = Base64.urlsafe_decode64(pad_encryptor.encrypt(''))
- message_size_diff = message_with.bytesize - message_without.bytesize
-
- message_with.bytesize.must_be :>, message_without.bytesize
- message_size_diff.must_equal 64 - Marshal.dump('').bytesize - 2
- end
-
- it 'encryptor with pad_size has message payload size to multiple of pad_size' do
- encryptor = Rack::Encryptor.new(@secret, purpose: 'testing', pad_size: 24)
- message = encryptor.encrypt(foo: 'bar' * 4)
-
- decoded_message = Base64.urlsafe_decode64(message)
-
- # slice 1 byte for version, 32 bytes for cipher_secret, 16 bytes for IV
- # from the start of the string and 32 bytes at the end of the string
- encrypted_payload = decoded_message[(1 + 32 + 16)..-33]
-
- (encrypted_payload.bytesize % 24).must_equal 0
- end
-
- # This test checks the one-time message key IS NOT used as the cipher key.
- # Doing so would remove the confidentiality assurances as the key is
- # essentially included in plaintext then.
- it 'uses a secret cipher key for encryption and decryption' do
- cipher = OpenSSL::Cipher.new('aes-256-ctr')
- encryptor = Rack::Encryptor.new(@secret)
-
- message = encryptor.encrypt(foo: 'bar')
- raw_message = Base64.urlsafe_decode64(message)
-
- ver = raw_message.slice!(0, 1)
- key = raw_message.slice!(0, 32)
- iv = raw_message.slice!(0, 16)
-
- cipher.decrypt
- cipher.key = key
- cipher.iv = iv
-
- data = cipher.update(raw_message) << cipher.final
-
- # "data" should now be random bytes because we tried to decrypt a message
- # with the wrong key
-
- padding_bytes, = data.unpack('v') # likely a large number
- serialized_data = data.slice(2 + padding_bytes, data.bytesize) # likely nil
-
- lambda { Marshal.load serialized_data }.must_raise TypeError
- end
-
- it 'it calls set_cipher_key with the correct key' do
- encryptor = Rack::Encryptor.new(@secret, purpose: 'testing', pad_size: 24)
- message = encryptor.encrypt(foo: 'bar')
-
- message_key = Base64.urlsafe_decode64(message).slice(1, 32)
-
- callable = proc do |cipher, key|
- key.wont_equal @secret
- key.wont_equal message_key
-
- cipher.key = key
- end
-
- encryptor.stub :set_cipher_key, callable do
- encryptor.decrypt message
- end
- end
-end
diff --git a/test/spec_session_abstract_id.rb b/test/spec_session_abstract_id.rb
deleted file mode 100644
index 9812b530..00000000
--- a/test/spec_session_abstract_id.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-### WARNING: there be hax in this file.
-
-require_relative '../lib/rack/session/abstract/id'
-separate_testing do
- require_relative '../lib/rack/request'
-end
-
-describe Rack::Session::Abstract::ID do
- attr_reader :id
-
- def setup
- super
- @id = Rack::Session::Abstract::ID
- end
-
- it "use securerandom" do
- assert_equal ::SecureRandom, id::DEFAULT_OPTIONS[:secure_random]
-
- id = @id.new nil
- assert_equal ::SecureRandom, id.sid_secure
- end
-
- it "allow to use another securerandom provider" do
- secure_random = Class.new do
- def hex(*args)
- 'fake_hex'
- end
- end
- id = Rack::Session::Abstract::ID.new nil, secure_random: secure_random.new
- id.send(:generate_sid).must_equal 'fake_hex'
- end
-
- it "should warn when subclassing" do
- verbose = $VERBOSE
- begin
- $VERBOSE = true
- warn_arg = nil
- @id.define_singleton_method(:warn) do |arg|
- warn_arg = arg
- end
- c = Class.new(@id)
- regexp = /is inheriting from Rack::Session::Abstract::ID. Inheriting from Rack::Session::Abstract::ID is deprecated, please inherit from Rack::Session::Abstract::Persisted instead/
- warn_arg.must_match(regexp)
-
- warn_arg = nil
- c = Class.new(c)
- warn_arg.must_be_nil
- ensure
- $VERBOSE = verbose
- @id.singleton_class.send(:remove_method, :warn)
- end
- end
-
- it "#find_session should find session in request" do
- id = @id.new(nil)
- def id.get_session(env, sid)
- [env['rack.session'], generate_sid]
- end
- req = Rack::Request.new('rack.session' => {})
- session, sid = id.find_session(req, nil)
- session.must_equal({})
- sid.must_match(/\A\h+\z/)
- end
-
- it "#write_session should write session to request" do
- id = @id.new(nil)
- def id.set_session(env, sid, session, options)
- [env, sid, session, options]
- end
- req = Rack::Request.new({})
- id.write_session(req, 1, 2, 3).must_equal [{}, 1, 2, 3]
- end
-
- it "#delete_session should remove session from request" do
- id = @id.new(nil)
- def id.destroy_session(env, sid, options)
- [env, sid, options]
- end
- req = Rack::Request.new({})
- id.delete_session(req, 1, 2).must_equal [{}, 1, 2]
- end
-end
diff --git a/test/spec_session_abstract_persisted.rb b/test/spec_session_abstract_persisted.rb
deleted file mode 100644
index 1b1fce86..00000000
--- a/test/spec_session_abstract_persisted.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-
-require_relative '../lib/rack/session/abstract/id'
-separate_testing do
- require_relative '../lib/rack/request'
-end
-
-describe Rack::Session::Abstract::Persisted do
- def setup
- @class = Rack::Session::Abstract::Persisted
- @pers = @class.new(nil)
- end
-
- it "#generated_sid generates a session identifier" do
- @pers.send(:generate_sid).must_match(/\A\h+\z/)
- @pers.send(:generate_sid, nil).must_match(/\A\h+\z/)
-
- obj = Object.new
- def obj.hex(_); raise NotImplementedError end
- @pers.send(:generate_sid, obj).must_match(/\A\h+\z/)
- end
-
- it "#commit_session? returns false if :skip option is given" do
- @pers.send(:commit_session?, Rack::Request.new({}), {}, skip: true).must_equal false
- end
-
- it "#commit_session writes to rack.errors if session cannot be written" do
- @pers = @class.new(nil)
- def @pers.write_session(*) end
- errors = StringIO.new
- env = { 'rack.errors' => errors }
- req = Rack::Request.new(env)
- store = Class.new do
- def load_session(req)
- ["id", {}]
- end
- def session_exists?(req)
- true
- end
- end
- session = env['rack.session'] = Rack::Session::Abstract::SessionHash.new(store.new, req)
- session['foo'] = 'bar'
- @pers.send(:commit_session, req, Rack::Response.new)
- errors.rewind
- errors.read.must_equal "Warning! Rack::Session::Abstract::Persisted failed to save session. Content dropped.\n"
- end
-
- it "#cookie_value returns its argument" do
- obj = Object.new
- @pers.send(:cookie_value, obj).must_equal(obj)
- end
-
- it "#session_class returns the default session class" do
- @pers.send(:session_class).must_equal Rack::Session::Abstract::SessionHash
- end
-
- it "#find_session raises" do
- proc { @pers.send(:find_session, nil, nil) }.must_raise RuntimeError
- end
-
- it "#write_session raises" do
- proc { @pers.send(:write_session, nil, nil, nil, nil) }.must_raise RuntimeError
- end
-
- it "#delete_session raises" do
- proc { @pers.send(:delete_session, nil, nil, nil) }.must_raise RuntimeError
- end
-end
diff --git a/test/spec_session_abstract_persisted_secure_secure_session_hash.rb b/test/spec_session_abstract_persisted_secure_secure_session_hash.rb
deleted file mode 100644
index d6d18860..00000000
--- a/test/spec_session_abstract_persisted_secure_secure_session_hash.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-require_relative '../lib/rack/session/abstract/id'
-
-separate_testing do
- require_relative '../lib/rack/request'
-end
-
-describe Rack::Session::Abstract::PersistedSecure::SecureSessionHash do
- attr_reader :hash
-
- def setup
- super
- @store = Class.new do
- def load_session(req)
- [Rack::Session::SessionId.new("id"), { foo: :bar, baz: :qux }]
- end
- def session_exists?(req)
- true
- end
- end
- @hash = Rack::Session::Abstract::PersistedSecure::SecureSessionHash.new(@store.new, nil)
- end
-
- it "returns keys" do
- assert_equal ["foo", "baz"], hash.keys
- end
-
- it "returns values" do
- assert_equal [:bar, :qux], hash.values
- end
-
- describe "#[]" do
- it "returns value for a matching key" do
- assert_equal :bar, hash[:foo]
- end
-
- it "returns value for a 'session_id' key" do
- assert_equal "id", hash['session_id']
- end
-
- it "returns nil value for missing 'session_id' key" do
- store = @store.new
- def store.load_session(req)
- [nil, {}]
- end
- @hash = Rack::Session::Abstract::PersistedSecure::SecureSessionHash.new(store, nil)
- assert_nil hash['session_id']
- end
-
- it "returns value for non SessionId 'session_id' key" do
- store = @store.new
- def store.load_session(req)
- ["id", {}]
- end
- @hash = Rack::Session::Abstract::PersistedSecure::SecureSessionHash.new(store, nil)
- assert_equal "id", hash['session_id']
- end
- end
-
- describe "#fetch" do
- it "returns value for a matching key" do
- assert_equal :bar, hash.fetch(:foo)
- end
-
- it "works with a default value" do
- assert_equal :default, hash.fetch(:unknown, :default)
- end
-
- it "works with a block" do
- assert_equal :default, hash.fetch(:unknown) { :default }
- end
-
- it "it raises when fetching unknown keys without defaults" do
- lambda { hash.fetch(:unknown) }.must_raise KeyError
- end
- end
-
- describe "#stringify_keys" do
- it "returns hash or session hash with keys stringified" do
- assert_equal({ "foo" => :bar, "baz" => :qux }, hash.send(:stringify_keys, hash).to_h)
- end
- end
-end
-
diff --git a/test/spec_session_abstract_session_hash.rb b/test/spec_session_abstract_session_hash.rb
deleted file mode 100644
index 2a832c1c..00000000
--- a/test/spec_session_abstract_session_hash.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-
-require 'rack/session/abstract/id'
-separate_testing do
- require_relative '../lib/rack/request'
-end
-
-describe Rack::Session::Abstract::SessionHash do
- attr_reader :hash
-
- def setup
- super
- store = Class.new do
- def load_session(req)
- ["id", { foo: :bar, baz: :qux, x: { y: 1 } }]
- end
- def session_exists?(req)
- true
- end
- end
- @class = Rack::Session::Abstract::SessionHash
- @hash = @class.new(store.new, nil)
- end
-
- it ".find finds entry in request" do
- assert_equal({}, @class.find(Rack::Request.new('rack.session' => {})))
- end
-
- it ".set sets session in request" do
- req = Rack::Request.new({})
- @class.set(req, {})
- req.env['rack.session'].must_equal({})
- end
-
- it ".set_options sets session options in request" do
- req = Rack::Request.new({})
- h = {}
- @class.set_options(req, h)
- opts = req.env['rack.session.options']
- opts.must_equal(h)
- opts.wont_be_same_as(h)
- end
-
- it "#keys returns keys" do
- assert_equal ["foo", "baz", "x"], hash.keys
- end
-
- it "#values returns values" do
- assert_equal [:bar, :qux, { y: 1 }], hash.values
- end
-
- it "#dig operates like Hash#dig" do
- assert_equal({ y: 1 }, hash.dig("x"))
- assert_equal(1, hash.dig(:x, :y))
- assert_nil(hash.dig(:z))
- assert_nil(hash.dig(:x, :z))
- lambda { hash.dig(:x, :y, :z) }.must_raise TypeError
- lambda { hash.dig }.must_raise ArgumentError
- end
-
- it "#each iterates over entries" do
- a = []
- @hash.each do |k, v|
- a << [k, v]
- end
- a.must_equal [["foo", :bar], ["baz", :qux], ["x", { y: 1 }]]
- end
-
- it "#has_key returns whether the key is in the hash" do
- assert_equal true, hash.has_key?("foo")
- assert_equal true, hash.has_key?(:foo)
- assert_equal false, hash.has_key?("food")
- assert_equal false, hash.has_key?(:food)
- end
-
- it "#replace replaces hash" do
- hash.replace({ bar: "foo" })
- assert_equal "foo", hash["bar"]
- end
-
- describe "#fetch" do
- it "returns value for a matching key" do
- assert_equal :bar, hash.fetch(:foo)
- end
-
- it "works with a default value" do
- assert_equal :default, hash.fetch(:unknown, :default)
- end
-
- it "works with a block" do
- assert_equal :default, hash.fetch(:unknown) { :default }
- end
-
- it "it raises when fetching unknown keys without defaults" do
- lambda { hash.fetch(:unknown) }.must_raise KeyError
- end
- end
-
- it "#stringify_keys returns hash or session hash with keys stringified" do
- assert_equal({ "foo" => :bar, "baz" => :qux, "x" => { y: 1 } }, hash.send(:stringify_keys, hash).to_h)
- end
-end
diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb
deleted file mode 100644
index dcb981d3..00000000
--- a/test/spec_session_cookie.rb
+++ /dev/null
@@ -1,591 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-
-separate_testing do
- require_relative '../lib/rack/session/cookie'
- require_relative '../lib/rack/response'
- require_relative '../lib/rack/lint'
- require_relative '../lib/rack/mock'
-end
-
-describe Rack::Session::Cookie do
- incrementor = lambda do |env|
- env["rack.session"]["counter"] ||= 0
- env["rack.session"]["counter"] += 1
- hash = env["rack.session"].dup
- hash.delete("session_id")
- Rack::Response.new(hash.inspect).to_a
- end
-
- session_id = lambda do |env|
- Rack::Response.new(env["rack.session"].to_hash.inspect).to_a
- end
-
- session_option = lambda do |opt|
- lambda do |env|
- Rack::Response.new(env["rack.session.options"][opt].inspect).to_a
- end
- end
-
- nothing = lambda do |env|
- Rack::Response.new("Nothing").to_a
- end
-
- renewer = lambda do |env|
- env["rack.session.options"][:renew] = true
- Rack::Response.new("Nothing").to_a
- end
-
- only_session_id = lambda do |env|
- Rack::Response.new(env["rack.session"]["session_id"].to_s).to_a
- end
-
- bigcookie = lambda do |env|
- env["rack.session"]["cookie"] = "big" * 3000
- Rack::Response.new(env["rack.session"].inspect).to_a
- end
-
- destroy_session = lambda do |env|
- env["rack.session"].destroy
- Rack::Response.new("Nothing").to_a
- end
-
- def response_for(options = {})
- request_options = options.fetch(:request, {})
- cookie = if options[:cookie].is_a?(Rack::Response)
- options[:cookie]["Set-Cookie"]
- else
- options[:cookie]
- end
- request_options["HTTP_COOKIE"] = cookie || ""
-
- app_with_cookie = Rack::Session::Cookie.new(*options[:app])
- app_with_cookie = Rack::Lint.new(app_with_cookie)
- Rack::MockRequest.new(app_with_cookie).get("/", request_options)
- end
-
- def random_encryptor_secret
- SecureRandom.random_bytes(64)
- end
-
- before do
- # Random key, as a hex string
- @secret = random_encryptor_secret
-
- @warnings = warnings = []
- Rack::Session::Cookie.class_eval do
- define_method(:warn) { |m| warnings << m }
- end
- end
-
- after do
- Rack::Session::Cookie.class_eval { remove_method :warn }
- end
-
- describe 'Base64' do
- it 'uses base64 to encode' do
- coder = Rack::Session::Cookie::Base64.new
- str = 'fuuuuu'
- coder.encode(str).must_equal [str].pack('m0')
- end
-
- it 'uses base64 to decode' do
- coder = Rack::Session::Cookie::Base64.new
- str = ['fuuuuu'].pack('m0')
- coder.decode(str).must_equal str.unpack('m0').first
- end
-
- it 'handles non-strict base64 encoding' do
- coder = Rack::Session::Cookie::Base64.new
- str = ['A' * 256].pack('m')
- coder.decode(str).must_equal 'A' * 256
- end
-
- describe 'Marshal' do
- it 'marshals and base64 encodes' do
- coder = Rack::Session::Cookie::Base64::Marshal.new
- str = 'fuuuuu'
- coder.encode(str).must_equal [::Marshal.dump(str)].pack('m0')
- end
-
- it 'marshals and base64 decodes' do
- coder = Rack::Session::Cookie::Base64::Marshal.new
- str = [::Marshal.dump('fuuuuu')].pack('m0')
- coder.decode(str).must_equal ::Marshal.load(str.unpack('m0').first)
- end
-
- it 'rescues failures on decode' do
- coder = Rack::Session::Cookie::Base64::Marshal.new
- coder.decode('lulz').must_be_nil
- end
- end
-
- describe 'JSON' do
- it 'JSON and base64 encodes' do
- coder = Rack::Session::Cookie::Base64::JSON.new
- obj = %w[fuuuuu]
- coder.encode(obj).must_equal [::JSON.dump(obj)].pack('m0')
- end
-
- it 'JSON and base64 decodes' do
- coder = Rack::Session::Cookie::Base64::JSON.new
- str = [::JSON.dump(%w[fuuuuu])].pack('m0')
- coder.decode(str).must_equal ::JSON.parse(str.unpack('m0').first)
- end
-
- it 'rescues failures on decode' do
- coder = Rack::Session::Cookie::Base64::JSON.new
- coder.decode('lulz').must_be_nil
- end
- end
-
- describe 'ZipJSON' do
- it 'jsons, deflates, and base64 encodes' do
- coder = Rack::Session::Cookie::Base64::ZipJSON.new
- obj = %w[fuuuuu]
- json = JSON.dump(obj)
- coder.encode(obj).must_equal [Zlib::Deflate.deflate(json)].pack('m0')
- end
-
- it 'base64 decodes, inflates, and decodes json' do
- coder = Rack::Session::Cookie::Base64::ZipJSON.new
- obj = %w[fuuuuu]
- json = JSON.dump(obj)
- b64 = [Zlib::Deflate.deflate(json)].pack('m0')
- coder.decode(b64).must_equal obj
- end
-
- it 'rescues failures on decode' do
- coder = Rack::Session::Cookie::Base64::ZipJSON.new
- coder.decode('lulz').must_be_nil
- end
- end
- end
-
- it "warns if no secret is given" do
- Rack::Session::Cookie.new(incrementor)
- @warnings.first.must_match(/no secret/i)
- @warnings.clear
- Rack::Session::Cookie.new(incrementor, secrets: @secret)
- @warnings.must_be :empty?
- end
-
- it 'abort if secret is too short' do
- lambda {
- Rack::Session::Cookie.new(incrementor, secrets: @secret[0, 16])
- }.must_raise ArgumentError
- end
-
- it "doesn't warn if coder is configured to handle encoding" do
- Rack::Session::Cookie.new(
- incrementor,
- coder: Object.new,
- let_coder_handle_secure_encoding: true)
- @warnings.must_be :empty?
- end
-
- it "still warns if coder is not set" do
- Rack::Session::Cookie.new(
- incrementor,
- let_coder_handle_secure_encoding: true)
- @warnings.first.must_match(/no secret/i)
- end
-
- it 'uses a coder' do
- identity = Class.new {
- attr_reader :calls
-
- def initialize
- @calls = []
- end
-
- def encode(str); @calls << :encode; str; end
- def decode(str); @calls << :decode; str; end
- }.new
- response = response_for(app: [incrementor, { coder: identity }])
-
- response["Set-Cookie"].must_include "rack.session="
- response.body.must_equal '{"counter"=>1}'
- identity.calls.must_equal [:decode, :encode]
- end
-
- it "creates a new cookie" do
- response = response_for(app: incrementor)
- response["Set-Cookie"].must_include "rack.session="
- response.body.must_equal '{"counter"=>1}'
- end
-
- it "passes through same_site option to session cookie" do
- response = response_for(app: [incrementor, same_site: :none])
- response["Set-Cookie"].must_include "SameSite=None"
- end
-
- it "allows using a lambda to specify same_site option, because some browsers require different settings" do
- # Details of why this might need to be set dynamically:
- # https://www.chromium.org/updates/same-site/incompatible-clients
- # https://gist.github.com/bnorton/7dee72023787f367c48b3f5c2d71540f
-
- response = response_for(app: [incrementor, same_site: lambda { |req, res| :none }])
- response["Set-Cookie"].must_include "SameSite=None"
-
- response = response_for(app: [incrementor, same_site: lambda { |req, res| :lax }])
- response["Set-Cookie"].must_include "SameSite=Lax"
- end
-
- it "loads from a cookie" do
- response = response_for(app: incrementor)
-
- response = response_for(app: incrementor, cookie: response)
- response.body.must_equal '{"counter"=>2}'
-
- response = response_for(app: incrementor, cookie: response)
- response.body.must_equal '{"counter"=>3}'
- end
-
- it "renew session id" do
- response = response_for(app: incrementor)
- cookie = response['Set-Cookie']
- response = response_for(app: only_session_id, cookie: cookie)
- cookie = response['Set-Cookie'] if response['Set-Cookie']
-
- response.body.wont_equal ""
- old_session_id = response.body
-
- response = response_for(app: renewer, cookie: cookie)
- cookie = response['Set-Cookie'] if response['Set-Cookie']
- response = response_for(app: only_session_id, cookie: cookie)
-
- response.body.wont_equal ""
- response.body.wont_equal old_session_id
- end
-
- it "destroys session" do
- response = response_for(app: incrementor)
- response = response_for(app: only_session_id, cookie: response)
-
- response.body.wont_equal ""
- old_session_id = response.body
-
- response = response_for(app: destroy_session, cookie: response)
- response = response_for(app: only_session_id, cookie: response)
-
- response.body.wont_equal ""
- response.body.wont_equal old_session_id
- end
-
- it "survives broken cookies" do
- response = response_for(
- app: incrementor,
- cookie: "rack.session=blarghfasel"
- )
- response.body.must_equal '{"counter"=>1}'
-
- response = response_for(
- app: [incrementor, { secrets: @secret }],
- cookie: "rack.session="
- )
- response.body.must_equal '{"counter"=>1}'
- end
-
- it "barks on too big cookies" do
- lambda{
- response_for(app: bigcookie, request: { fatal: true })
- }.must_raise Rack::MockRequest::FatalWarning
- end
-
- it "loads from a cookie with encryption" do
- app = [incrementor, { secrets: @secret }]
-
- response = response_for(app: app)
- response = response_for(app: app, cookie: response)
- response.body.must_equal '{"counter"=>2}'
-
- response = response_for(app: app, cookie: response)
- response.body.must_equal '{"counter"=>3}'
-
- app = [incrementor, { secrets: random_encryptor_secret }]
-
- response = response_for(app: app, cookie: response)
- response.body.must_equal '{"counter"=>1}'
- end
-
- it "loads from a cookie with accept-only integrity hash for graceful key rotation" do
- response = response_for(app: [incrementor, { secrets: @secret }])
-
- new_secret = random_encryptor_secret
-
- app = [incrementor, { secrets: [new_secret, @secret] }]
- response = response_for(app: app, cookie: response)
- response.body.must_equal '{"counter"=>2}'
-
- newer_secret = random_encryptor_secret
-
- app = [incrementor, { secrets: [newer_secret, new_secret] }]
- response = response_for(app: app, cookie: response)
-
- response.body.must_equal '{"counter"=>3}'
- end
-
- it 'loads from a legacy hmac cookie' do
- legacy_session = Rack::Session::Cookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' })
- legacy_secret = 'test legacy secret'
- legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, legacy_secret, legacy_session)
-
- legacy_cookie = "rack.session=#{legacy_session}--#{legacy_digest}; path=/; HttpOnly"
-
- app = [incrementor, { secrets: @secret, legacy_hmac_secret: legacy_secret }]
- response = response_for(app: app, cookie: legacy_cookie)
- response.body.must_equal '{"counter"=>2}'
- end
-
- it "ignores tampered session cookies" do
- app = [incrementor, { secrets: @secret }]
-
- response = response_for(app: app)
- response.body.must_equal '{"counter"=>1}'
-
- response = response_for(app: app, cookie: response)
- response.body.must_equal '{"counter"=>2}'
-
- encoded_cookie = response["Set-Cookie"].split('=', 2).last.split(';').first
- decoded_cookie = Base64.urlsafe_decode64(Rack::Utils.unescape(encoded_cookie))
-
- tampered_cookie = "rack.session=#{Base64.urlsafe_encode64(decoded_cookie.tap { |m|
- m[m.size - 1] = (m[m.size - 1].unpack('C')[0] ^ 1).chr
- })}"
-
- response = response_for(app: app, cookie: tampered_cookie)
- response.body.must_equal '{"counter"=>1}'
- end
-
- it 'rejects session cookie with different purpose' do
- app = [incrementor, { secrets: @secrets }]
- other_app = [incrementor, { secrets: @secrets, key: 'other' }]
-
- response = response_for(app: app)
- response.body.must_equal '{"counter"=>1}'
-
- response = response_for(app: app, cookie: response)
- response.body.must_equal '{"counter"=>2}'
-
- response = response_for(app: other_app, cookie: response)
- response.body.must_equal '{"counter"=>1}'
- end
-
- it 'adds to RACK_ERRORS on encryptor errors' do
- echo_rack_errors = lambda do |env|
- env["rack.session"]["counter"] ||= 0
- env["rack.session"]["counter"] += 1
- Rack::Response.new(env[Rack::RACK_ERRORS].flush.tap(&:rewind).read).to_a
- end
-
- app = [incrementor, { secrets: @secret }]
- err_app = [echo_rack_errors, { secrets: @secret }]
-
- response = response_for(app: app)
- response.body.must_equal '{"counter"=>1}'
-
- encoded_cookie = response["Set-Cookie"].split('=', 2).last.split(';').first
- decoded_cookie = Base64.urlsafe_decode64(Rack::Utils.unescape(encoded_cookie))
-
- tampered_cookie = "rack.session=#{Base64.urlsafe_encode64(decoded_cookie.tap { |m|
- m[m.size - 1] = "\0"
- })}"
-
- response = response_for(app: err_app, cookie: tampered_cookie)
- response.body.must_equal "Session cookie encryptor error: HMAC is invalid\n"
- end
-
- it 'ignores tampered with legacy hmac cookie' do
- legacy_session = Rack::Session::Cookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' })
- legacy_secret = 'test legacy secret'
- legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, legacy_secret, legacy_session).reverse
-
- legacy_cookie = "rack.session=#{legacy_session}--#{legacy_digest}; path=/; HttpOnly"
-
- app = [incrementor, { secret: @secret, legacy_hmac_secret: legacy_secret }]
- response = response_for(app: app, cookie: legacy_cookie)
- response.body.must_equal '{"counter"=>1}'
- end
-
- it "supports custom digest instance for legacy hmac cookie" do
- legacy_hmac = 'SHA256'
- legacy_session = Rack::Session::Cookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' })
- legacy_secret = 'test legacy secret'
- legacy_digest = OpenSSL::HMAC.hexdigest(legacy_hmac, legacy_secret, legacy_session)
- legacy_cookie = "rack.session=#{legacy_session}--#{legacy_digest}; path=/; HttpOnly"
-
- app = [incrementor, {
- secrets: @secret, legacy_hmac_secret: legacy_secret, legacy_hmac: legacy_hmac
- }]
-
- response = response_for(app: app, cookie: legacy_cookie)
- response.body.must_equal '{"counter"=>2}'
-
- response = response_for(app: app, cookie: response)
- response.body.must_equal '{"counter"=>3}'
- end
-
- it "can handle Rack::Lint middleware" do
- response = response_for(app: incrementor)
-
- lint = Rack::Lint.new(session_id)
- response = response_for(app: lint, cookie: response)
- response.body.wont_be :nil?
- end
-
- it "can handle middleware that inspects the env" do
- class TestEnvInspector
- def initialize(app)
- @app = app
- end
- def call(env)
- env.inspect
- @app.call(env)
- end
- end
-
- response = response_for(app: incrementor)
-
- inspector = TestEnvInspector.new(session_id)
- response = response_for(app: inspector, cookie: response)
- response.body.wont_be :nil?
- end
-
- it "returns the session id in the session hash" do
- response = response_for(app: incrementor)
- response.body.must_equal '{"counter"=>1}'
-
- response = response_for(app: session_id, cookie: response)
- response.body.must_match(/"session_id"=>/)
- response.body.must_match(/"counter"=>1/)
- end
-
- it "does not return a cookie if set to secure but not using ssl" do
- app = [incrementor, { secure: true }]
-
- response = response_for(app: app)
- response["Set-Cookie"].must_be_nil
-
- response = response_for(app: app, request: { "HTTPS" => "on" })
- response["Set-Cookie"].wont_be :nil?
- response["Set-Cookie"].must_match(/secure/)
- end
-
- it "does not return a cookie if cookie was not read/written" do
- response = response_for(app: nothing)
- response["Set-Cookie"].must_be_nil
- end
-
- it "does not return a cookie if cookie was not written (only read)" do
- response = response_for(app: session_id)
- response["Set-Cookie"].must_be_nil
- end
-
- it "returns even if not read/written if :expire_after is set" do
- app = [nothing, { expire_after: 3600 }]
- request = { "rack.session" => { "not" => "empty" } }
- response = response_for(app: app, request: request)
- response["Set-Cookie"].wont_be :nil?
- end
-
- it "returns no cookie if no data was written and no session was created previously, even if :expire_after is set" do
- app = [nothing, { expire_after: 3600 }]
- response = response_for(app: app)
- response["Set-Cookie"].must_be_nil
- end
-
- it "exposes :secrets in env['rack.session.option']" do
- response = response_for(app: [session_option[:secrets], { secrets: @secret }])
- response.body.must_equal @secret.inspect
- end
-
- it "exposes :coder in env['rack.session.option']" do
- response = response_for(app: session_option[:coder])
- response.body.must_match(/Base64::Marshal/)
- end
-
- it 'exposes correct :coder when a secrets is used' do
- response = response_for(app: session_option[:coder], secrets: @secret)
- response.body.must_match(/Marshal/)
- end
-
- it "allows passing in a hash with session data from middleware in front" do
- request = { 'rack.session' => { foo: 'bar' } }
- response = response_for(app: session_id, request: request)
- response.body.must_match(/foo/)
- end
-
- it "allows modifying session data with session data from middleware in front" do
- request = { 'rack.session' => { foo: 'bar' } }
- response = response_for(app: incrementor, request: request)
- response.body.must_match(/counter/)
- response.body.must_match(/foo/)
- end
-
- it "allows more than one '--' in the cookie when calculating legacy digests" do
- @counter = 0
- app = lambda do |env|
- env["rack.session"]["message"] ||= ""
- env["rack.session"]["message"] += "#{(@counter += 1).to_s}--"
- hash = env["rack.session"].dup
- hash.delete("session_id")
- Rack::Response.new(hash["message"]).to_a
- end
-
- # another example of an unsafe coder is Base64.urlsafe_encode64
- unsafe_coder = Class.new {
- def encode(hash); hash.inspect end
- def decode(str); eval(str) if str; end
- }.new
-
- legacy_session = unsafe_coder.encode('message' => "#{@counter += 1}--#{@counter += 1}--", 'session_id' => 'abcdef')
- legacy_secret = 'test legacy secret'
- legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, legacy_secret, legacy_session)
- legacy_cookie = "rack.session=#{Rack::Utils.escape legacy_session}--#{legacy_digest}; path=/; HttpOnly"
-
- _app = [ app, {
- secrets: @secret,
- legacy_hmac_secret: legacy_secret,
- legacy_hmac_coder: unsafe_coder
- }]
-
- response = response_for(app: _app, cookie: legacy_cookie)
- response.body.must_equal "1--2--3--"
- end
-
- it 'allows for non-strict encoded cookie' do
- long_session_app = lambda do |env|
- env['rack.session']['value'] = 'A' * 256
- env['rack.session']['counter'] = 1
- hash = env["rack.session"].dup
- hash.delete("session_id")
- Rack::Response.new(hash.inspect).to_a
- end
-
- non_strict_coder = Class.new {
- def encode(str)
- [Marshal.dump(str)].pack('m')
- end
-
- def decode(str)
- return unless str
-
- Marshal.load(str.unpack('m').first)
- end
- }.new
-
- non_strict_response = response_for(app: [
- long_session_app, { coder: non_strict_coder }
- ])
-
- response = response_for(app: [
- incrementor
- ], cookie: non_strict_response)
-
- response.body.must_match %Q["value"=>"#{'A' * 256}"]
- response.body.must_match '"counter"=>2'
- response.body.must_match(/\A{[^}]+}\z/)
- end
-end
diff --git a/test/spec_session_pool.rb b/test/spec_session_pool.rb
deleted file mode 100644
index 02b196d3..00000000
--- a/test/spec_session_pool.rb
+++ /dev/null
@@ -1,288 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-
-separate_testing do
- require_relative '../lib/rack/session/pool'
- require_relative '../lib/rack/response'
- require_relative '../lib/rack/lint'
- require_relative '../lib/rack/mock'
- require_relative '../lib/rack/utils'
-end
-
-describe Rack::Session::Pool do
- session_key = Rack::Session::Pool::DEFAULT_OPTIONS[:key]
- session_match = /#{session_key}=([0-9a-fA-F]+);/
-
- incrementor = lambda do |env|
- env["rack.session"]["counter"] ||= 0
- env["rack.session"]["counter"] += 1
- Rack::Response.new(env["rack.session"].inspect).to_a
- end
-
- get_session_id = Rack::Lint.new(lambda do |env|
- Rack::Response.new(env["rack.session"].inspect).to_a
- end)
-
- nothing = Rack::Lint.new(lambda do |env|
- Rack::Response.new("Nothing").to_a
- end)
-
- drop_session = Rack::Lint.new(lambda do |env|
- env['rack.session.options'][:drop] = true
- incrementor.call(env)
- end)
-
- renew_session = Rack::Lint.new(lambda do |env|
- env['rack.session.options'][:renew] = true
- incrementor.call(env)
- end)
-
- defer_session = Rack::Lint.new(lambda do |env|
- env['rack.session.options'][:defer] = true
- incrementor.call(env)
- end)
-
- incrementor = Rack::Lint.new(incrementor)
-
- it "creates a new cookie" do
- pool = Rack::Session::Pool.new(incrementor)
- res = Rack::MockRequest.new(pool).get("/")
- res["Set-Cookie"].must_match(session_match)
- res.body.must_equal '{"counter"=>1}'
- end
-
- it "determines session from a cookie" do
- pool = Rack::Session::Pool.new(incrementor)
- req = Rack::MockRequest.new(pool)
- cookie = req.get("/")["Set-Cookie"]
- req.get("/", "HTTP_COOKIE" => cookie).
- body.must_equal '{"counter"=>2}'
- req.get("/", "HTTP_COOKIE" => cookie).
- body.must_equal '{"counter"=>3}'
- end
-
- it "survives nonexistent cookies" do
- pool = Rack::Session::Pool.new(incrementor)
- res = Rack::MockRequest.new(pool).
- get("/", "HTTP_COOKIE" => "#{session_key}=blarghfasel")
- res.body.must_equal '{"counter"=>1}'
- end
-
- it "does not send the same session id if it did not change" do
- pool = Rack::Session::Pool.new(incrementor)
- req = Rack::MockRequest.new(pool)
-
- res0 = req.get("/")
- cookie = res0["Set-Cookie"][session_match]
- res0.body.must_equal '{"counter"=>1}'
- pool.pool.size.must_equal 1
-
- res1 = req.get("/", "HTTP_COOKIE" => cookie)
- res1["Set-Cookie"].must_be_nil
- res1.body.must_equal '{"counter"=>2}'
- pool.pool.size.must_equal 1
-
- res2 = req.get("/", "HTTP_COOKIE" => cookie)
- res2["Set-Cookie"].must_be_nil
- res2.body.must_equal '{"counter"=>3}'
- pool.pool.size.must_equal 1
- end
-
- it "deletes cookies with :drop option" do
- pool = Rack::Session::Pool.new(incrementor)
- req = Rack::MockRequest.new(pool)
- drop = Rack::Utils::Context.new(pool, drop_session)
- dreq = Rack::MockRequest.new(drop)
-
- res1 = req.get("/")
- session = (cookie = res1["Set-Cookie"])[session_match]
- res1.body.must_equal '{"counter"=>1}'
- pool.pool.size.must_equal 1
-
- res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
- res2["Set-Cookie"].must_be_nil
- res2.body.must_equal '{"counter"=>2}'
- pool.pool.size.must_equal 0
-
- res3 = req.get("/", "HTTP_COOKIE" => cookie)
- res3["Set-Cookie"][session_match].wont_equal session
- res3.body.must_equal '{"counter"=>1}'
- pool.pool.size.must_equal 1
- end
-
- it "provides new session id with :renew option" do
- pool = Rack::Session::Pool.new(incrementor)
- req = Rack::MockRequest.new(pool)
- renew = Rack::Utils::Context.new(pool, renew_session)
- rreq = Rack::MockRequest.new(renew)
-
- res1 = req.get("/")
- session = (cookie = res1["Set-Cookie"])[session_match]
- res1.body.must_equal '{"counter"=>1}'
- pool.pool.size.must_equal 1
-
- res2 = rreq.get("/", "HTTP_COOKIE" => cookie)
- new_cookie = res2["Set-Cookie"]
- new_session = new_cookie[session_match]
- new_session.wont_equal session
- res2.body.must_equal '{"counter"=>2}'
- pool.pool.size.must_equal 1
-
- res3 = req.get("/", "HTTP_COOKIE" => new_cookie)
- res3.body.must_equal '{"counter"=>3}'
- pool.pool.size.must_equal 1
-
- res4 = req.get("/", "HTTP_COOKIE" => cookie)
- res4.body.must_equal '{"counter"=>1}'
- pool.pool.size.must_equal 2
- end
-
- it "omits cookie with :defer option" do
- pool = Rack::Session::Pool.new(incrementor)
- defer = Rack::Utils::Context.new(pool, defer_session)
- dreq = Rack::MockRequest.new(defer)
-
- res1 = dreq.get("/")
- res1["Set-Cookie"].must_be_nil
- res1.body.must_equal '{"counter"=>1}'
- pool.pool.size.must_equal 1
- end
-
- it "can read the session with the legacy id" do
- pool = Rack::Session::Pool.new(incrementor)
- req = Rack::MockRequest.new(pool)
-
- res0 = req.get("/")
- cookie = res0["Set-Cookie"]
- session_id = Rack::Session::SessionId.new cookie[session_match, 1]
- ses0 = pool.pool[session_id.private_id]
- pool.pool[session_id.public_id] = ses0
- pool.pool.delete(session_id.private_id)
-
- res1 = req.get("/", "HTTP_COOKIE" => cookie)
- res1["Set-Cookie"].must_be_nil
- res1.body.must_equal '{"counter"=>2}'
- pool.pool[session_id.private_id].wont_be_nil
- end
-
- it "cannot read the session with the legacy id if allow_fallback: false option is used" do
- pool = Rack::Session::Pool.new(incrementor, allow_fallback: false)
- req = Rack::MockRequest.new(pool)
-
- res0 = req.get("/")
- cookie = res0["Set-Cookie"]
- session_id = Rack::Session::SessionId.new cookie[session_match, 1]
- ses0 = pool.pool[session_id.private_id]
- pool.pool[session_id.public_id] = ses0
- pool.pool.delete(session_id.private_id)
-
- res1 = req.get("/", "HTTP_COOKIE" => cookie)
- res1["Set-Cookie"].wont_be_nil
- res1.body.must_equal '{"counter"=>1}'
- end
-
- it "drops the session in the legacy id as well" do
- pool = Rack::Session::Pool.new(incrementor)
- req = Rack::MockRequest.new(pool)
- drop = Rack::Utils::Context.new(pool, drop_session)
- dreq = Rack::MockRequest.new(drop)
-
- res0 = req.get("/")
- cookie = res0["Set-Cookie"]
- session_id = Rack::Session::SessionId.new cookie[session_match, 1]
- ses0 = pool.pool[session_id.private_id]
- pool.pool[session_id.public_id] = ses0
- pool.pool.delete(session_id.private_id)
-
- res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
- res2["Set-Cookie"].must_be_nil
- res2.body.must_equal '{"counter"=>2}'
- pool.pool[session_id.private_id].must_be_nil
- pool.pool[session_id.public_id].must_be_nil
- end
-
- it "passes through same_site option to session pool" do
- pool = Rack::Session::Pool.new(incrementor, same_site: :none)
- req = Rack::MockRequest.new(pool)
- res = req.get("/")
- res["Set-Cookie"].must_include "SameSite=None"
- end
-
- it "allows using a lambda to specify same_site option, because some browsers require different settings" do
- pool = Rack::Session::Pool.new(incrementor, same_site: lambda { |req, res| :none })
- req = Rack::MockRequest.new(pool)
- res = req.get("/")
- res["Set-Cookie"].must_include "SameSite=None"
-
- pool = Rack::Session::Pool.new(incrementor, same_site: lambda { |req, res| :lax })
- req = Rack::MockRequest.new(pool)
- res = req.get("/")
- res["Set-Cookie"].must_include "SameSite=Lax"
- end
-
- # anyone know how to do this better?
- it "should merge sessions when multithreaded" do
- unless $DEBUG
- 1.must_equal 1
- next
- end
-
- warn 'Running multithread tests for Session::Pool'
- pool = Rack::Session::Pool.new(incrementor)
- req = Rack::MockRequest.new(pool)
-
- res = req.get('/')
- res.body.must_equal '{"counter"=>1}'
- cookie = res["Set-Cookie"]
- sess_id = cookie[/#{pool.key}=([^,;]+)/, 1]
-
- delta_incrementor = lambda do |env|
- # emulate disconjoinment of threading
- env['rack.session'] = env['rack.session'].dup
- Thread.stop
- env['rack.session'][(Time.now.usec * rand).to_i] = true
- incrementor.call(env)
- end
- tses = Rack::Utils::Context.new pool, delta_incrementor
- treq = Rack::MockRequest.new(tses)
- tnum = rand(7).to_i + 5
- r = Array.new(tnum) do
- Thread.new(treq) do |run|
- run.get('/', "HTTP_COOKIE" => cookie)
- end
- end.reverse.map{|t| t.run.join.value }
- r.each do |resp|
- resp['Set-Cookie'].must_equal cookie
- resp.body.must_include '"counter"=>2'
- end
-
- session = pool.pool[sess_id]
- session.size.must_equal tnum + 1 # counter
- session['counter'].must_equal 2 # meeeh
- end
-
- it "does not return a cookie if cookie was not read/written" do
- app = Rack::Session::Pool.new(nothing)
- res = Rack::MockRequest.new(app).get("/")
- res["Set-Cookie"].must_be_nil
- end
-
- it "does not return a cookie if cookie was not written (only read)" do
- app = Rack::Session::Pool.new(get_session_id)
- res = Rack::MockRequest.new(app).get("/")
- res["Set-Cookie"].must_be_nil
- end
-
- it "returns even if not read/written if :expire_after is set" do
- app = Rack::Session::Pool.new(nothing, expire_after: 3600)
- res = Rack::MockRequest.new(app).get("/", 'rack.session' => { 'not' => 'empty' })
- res["Set-Cookie"].wont_be :nil?
- end
-
- it "returns no cookie if no data was written and no session was created previously, even if :expire_after is set" do
- app = Rack::Session::Pool.new(nothing, expire_after: 3600)
- res = Rack::MockRequest.new(app).get("/")
- res["Set-Cookie"].must_be_nil
- end
-end