Instiki 0.16.3: Rails 2.3.0
Instiki now runs on the Rails 2.3.0 Candidate Release. Among other improvements, this means that it now automagically selects between WEBrick and Mongrel. Just run ./instiki --daemon
This commit is contained in:
parent
43aadecc99
commit
4e14ccc74d
893 changed files with 71965 additions and 28511 deletions
168
vendor/rails/actionpack/lib/action_controller/session/abstract_store.rb
vendored
Normal file
168
vendor/rails/actionpack/lib/action_controller/session/abstract_store.rb
vendored
Normal file
|
@ -0,0 +1,168 @@
|
|||
require 'rack/utils'
|
||||
|
||||
module ActionController
|
||||
module Session
|
||||
class AbstractStore
|
||||
ENV_SESSION_KEY = 'rack.session'.freeze
|
||||
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
|
||||
|
||||
HTTP_COOKIE = 'HTTP_COOKIE'.freeze
|
||||
SET_COOKIE = 'Set-Cookie'.freeze
|
||||
|
||||
class SessionHash < Hash
|
||||
def initialize(by, env)
|
||||
super()
|
||||
@by = by
|
||||
@env = env
|
||||
@loaded = false
|
||||
end
|
||||
|
||||
def id
|
||||
load! unless @loaded
|
||||
@id
|
||||
end
|
||||
|
||||
def session_id
|
||||
ActiveSupport::Deprecation.warn(
|
||||
"ActionController::Session::AbstractStore::SessionHash#session_id" +
|
||||
"has been deprecated.Please use #id instead.", caller)
|
||||
id
|
||||
end
|
||||
|
||||
def [](key)
|
||||
load! unless @loaded
|
||||
super
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
load! unless @loaded
|
||||
super
|
||||
end
|
||||
|
||||
def to_hash
|
||||
h = {}.replace(self)
|
||||
h.delete_if { |k,v| v.nil? }
|
||||
h
|
||||
end
|
||||
|
||||
def data
|
||||
ActiveSupport::Deprecation.warn(
|
||||
"ActionController::Session::AbstractStore::SessionHash#data" +
|
||||
"has been deprecated.Please use #to_hash instead.", caller)
|
||||
to_hash
|
||||
end
|
||||
|
||||
private
|
||||
def loaded?
|
||||
@loaded
|
||||
end
|
||||
|
||||
def load!
|
||||
@id, session = @by.send(:load_session, @env)
|
||||
replace(session)
|
||||
@loaded = true
|
||||
end
|
||||
end
|
||||
|
||||
DEFAULT_OPTIONS = {
|
||||
:key => '_session_id',
|
||||
:path => '/',
|
||||
:domain => nil,
|
||||
:expire_after => nil,
|
||||
:secure => false,
|
||||
:httponly => true,
|
||||
:cookie_only => true
|
||||
}
|
||||
|
||||
def initialize(app, options = {})
|
||||
# Process legacy CGI options
|
||||
options = options.symbolize_keys
|
||||
if options.has_key?(:session_path)
|
||||
options[:path] = options.delete(:session_path)
|
||||
end
|
||||
if options.has_key?(:session_key)
|
||||
options[:key] = options.delete(:session_key)
|
||||
end
|
||||
if options.has_key?(:session_http_only)
|
||||
options[:httponly] = options.delete(:session_http_only)
|
||||
end
|
||||
|
||||
@app = app
|
||||
@default_options = DEFAULT_OPTIONS.merge(options)
|
||||
@key = @default_options[:key]
|
||||
@cookie_only = @default_options[:cookie_only]
|
||||
end
|
||||
|
||||
def call(env)
|
||||
session = SessionHash.new(self, env)
|
||||
|
||||
env[ENV_SESSION_KEY] = session
|
||||
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
|
||||
|
||||
response = @app.call(env)
|
||||
|
||||
session_data = env[ENV_SESSION_KEY]
|
||||
options = env[ENV_SESSION_OPTIONS_KEY]
|
||||
|
||||
if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
|
||||
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
|
||||
|
||||
if session_data.is_a?(AbstractStore::SessionHash)
|
||||
sid = session_data.id
|
||||
else
|
||||
sid = generate_sid
|
||||
end
|
||||
|
||||
unless set_session(env, sid, session_data.to_hash)
|
||||
return response
|
||||
end
|
||||
|
||||
cookie = Rack::Utils.escape(@key) + '=' + Rack::Utils.escape(sid)
|
||||
cookie << "; domain=#{options[:domain]}" if options[:domain]
|
||||
cookie << "; path=#{options[:path]}" if options[:path]
|
||||
if options[:expire_after]
|
||||
expiry = Time.now + options[:expire_after]
|
||||
cookie << "; expires=#{expiry.httpdate}"
|
||||
end
|
||||
cookie << "; Secure" if options[:secure]
|
||||
cookie << "; HttpOnly" if options[:httponly]
|
||||
|
||||
headers = response[1]
|
||||
case a = headers[SET_COOKIE]
|
||||
when Array
|
||||
a << cookie
|
||||
when String
|
||||
headers[SET_COOKIE] = [a, cookie]
|
||||
when nil
|
||||
headers[SET_COOKIE] = cookie
|
||||
end
|
||||
end
|
||||
|
||||
response
|
||||
end
|
||||
|
||||
private
|
||||
def generate_sid
|
||||
ActiveSupport::SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
def load_session(env)
|
||||
request = Rack::Request.new(env)
|
||||
sid = request.cookies[@key]
|
||||
unless @cookie_only
|
||||
sid ||= request.params[@key]
|
||||
end
|
||||
sid, session = get_session(env, sid)
|
||||
[sid, session]
|
||||
end
|
||||
|
||||
def get_session(env, sid)
|
||||
raise '#get_session needs to be implemented.'
|
||||
end
|
||||
|
||||
def set_session(env, sid, session_data)
|
||||
raise '#set_session needs to be implemented.'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,340 +0,0 @@
|
|||
require 'cgi'
|
||||
require 'cgi/session'
|
||||
require 'digest/md5'
|
||||
|
||||
class CGI
|
||||
class Session
|
||||
attr_reader :data
|
||||
|
||||
# Return this session's underlying Session instance. Useful for the DB-backed session stores.
|
||||
def model
|
||||
@dbman.model if @dbman
|
||||
end
|
||||
|
||||
|
||||
# A session store backed by an Active Record class. A default class is
|
||||
# provided, but any object duck-typing to an Active Record Session class
|
||||
# with text +session_id+ and +data+ attributes is sufficient.
|
||||
#
|
||||
# The default assumes a +sessions+ tables with columns:
|
||||
# +id+ (numeric primary key),
|
||||
# +session_id+ (text, or longtext if your session data exceeds 65K), and
|
||||
# +data+ (text or longtext; careful if your session data exceeds 65KB).
|
||||
# The +session_id+ column should always be indexed for speedy lookups.
|
||||
# Session data is marshaled to the +data+ column in Base64 format.
|
||||
# If the data you write is larger than the column's size limit,
|
||||
# ActionController::SessionOverflowError will be raised.
|
||||
#
|
||||
# You may configure the table name, primary key, and data column.
|
||||
# For example, at the end of <tt>config/environment.rb</tt>:
|
||||
# CGI::Session::ActiveRecordStore::Session.table_name = 'legacy_session_table'
|
||||
# CGI::Session::ActiveRecordStore::Session.primary_key = 'session_id'
|
||||
# CGI::Session::ActiveRecordStore::Session.data_column_name = 'legacy_session_data'
|
||||
# Note that setting the primary key to the +session_id+ frees you from
|
||||
# having a separate +id+ column if you don't want it. However, you must
|
||||
# set <tt>session.model.id = session.session_id</tt> by hand! A before filter
|
||||
# on ApplicationController is a good place.
|
||||
#
|
||||
# Since the default class is a simple Active Record, you get timestamps
|
||||
# for free if you add +created_at+ and +updated_at+ datetime columns to
|
||||
# the +sessions+ table, making periodic session expiration a snap.
|
||||
#
|
||||
# You may provide your own session class implementation, whether a
|
||||
# feature-packed Active Record or a bare-metal high-performance SQL
|
||||
# store, by setting
|
||||
# CGI::Session::ActiveRecordStore.session_class = MySessionClass
|
||||
# You must implement these methods:
|
||||
# self.find_by_session_id(session_id)
|
||||
# initialize(hash_of_session_id_and_data)
|
||||
# attr_reader :session_id
|
||||
# attr_accessor :data
|
||||
# save
|
||||
# destroy
|
||||
#
|
||||
# The example SqlBypass class is a generic SQL session store. You may
|
||||
# use it as a basis for high-performance database-specific stores.
|
||||
class ActiveRecordStore
|
||||
# The default Active Record class.
|
||||
class Session < ActiveRecord::Base
|
||||
# Customizable data column name. Defaults to 'data'.
|
||||
cattr_accessor :data_column_name
|
||||
self.data_column_name = 'data'
|
||||
|
||||
before_save :marshal_data!
|
||||
before_save :raise_on_session_data_overflow!
|
||||
|
||||
class << self
|
||||
# Don't try to reload ARStore::Session in dev mode.
|
||||
def reloadable? #:nodoc:
|
||||
false
|
||||
end
|
||||
|
||||
def data_column_size_limit
|
||||
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
|
||||
end
|
||||
|
||||
# Hook to set up sessid compatibility.
|
||||
def find_by_session_id(session_id)
|
||||
setup_sessid_compatibility!
|
||||
find_by_session_id(session_id)
|
||||
end
|
||||
|
||||
def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
|
||||
def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
|
||||
|
||||
def create_table!
|
||||
connection.execute <<-end_sql
|
||||
CREATE TABLE #{table_name} (
|
||||
id INTEGER PRIMARY KEY,
|
||||
#{connection.quote_column_name('session_id')} TEXT UNIQUE,
|
||||
#{connection.quote_column_name(@@data_column_name)} TEXT(255)
|
||||
)
|
||||
end_sql
|
||||
end
|
||||
|
||||
def drop_table!
|
||||
connection.execute "DROP TABLE #{table_name}"
|
||||
end
|
||||
|
||||
private
|
||||
# Compatibility with tables using sessid instead of session_id.
|
||||
def setup_sessid_compatibility!
|
||||
# Reset column info since it may be stale.
|
||||
reset_column_information
|
||||
if columns_hash['sessid']
|
||||
def self.find_by_session_id(*args)
|
||||
find_by_sessid(*args)
|
||||
end
|
||||
|
||||
define_method(:session_id) { sessid }
|
||||
define_method(:session_id=) { |session_id| self.sessid = session_id }
|
||||
else
|
||||
def self.find_by_session_id(session_id)
|
||||
find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Lazy-unmarshal session state.
|
||||
def data
|
||||
@data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
|
||||
end
|
||||
|
||||
attr_writer :data
|
||||
|
||||
# Has the session been loaded yet?
|
||||
def loaded?
|
||||
!! @data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def marshal_data!
|
||||
return false if !loaded?
|
||||
write_attribute(@@data_column_name, self.class.marshal(self.data))
|
||||
end
|
||||
|
||||
# Ensures that the data about to be stored in the database is not
|
||||
# larger than the data storage column. Raises
|
||||
# ActionController::SessionOverflowError.
|
||||
def raise_on_session_data_overflow!
|
||||
return false if !loaded?
|
||||
limit = self.class.data_column_size_limit
|
||||
if loaded? and limit and read_attribute(@@data_column_name).size > limit
|
||||
raise ActionController::SessionOverflowError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A barebones session store which duck-types with the default session
|
||||
# store but bypasses Active Record and issues SQL directly. This is
|
||||
# an example session model class meant as a basis for your own classes.
|
||||
#
|
||||
# The database connection, table name, and session id and data columns
|
||||
# are configurable class attributes. Marshaling and unmarshaling
|
||||
# are implemented as class methods that you may override. By default,
|
||||
# marshaling data is
|
||||
#
|
||||
# ActiveSupport::Base64.encode64(Marshal.dump(data))
|
||||
#
|
||||
# and unmarshaling data is
|
||||
#
|
||||
# Marshal.load(ActiveSupport::Base64.decode64(data))
|
||||
#
|
||||
# This marshaling behavior is intended to store the widest range of
|
||||
# binary session data in a +text+ column. For higher performance,
|
||||
# store in a +blob+ column instead and forgo the Base64 encoding.
|
||||
class SqlBypass
|
||||
# Use the ActiveRecord::Base.connection by default.
|
||||
cattr_accessor :connection
|
||||
|
||||
# The table name defaults to 'sessions'.
|
||||
cattr_accessor :table_name
|
||||
@@table_name = 'sessions'
|
||||
|
||||
# The session id field defaults to 'session_id'.
|
||||
cattr_accessor :session_id_column
|
||||
@@session_id_column = 'session_id'
|
||||
|
||||
# The data field defaults to 'data'.
|
||||
cattr_accessor :data_column
|
||||
@@data_column = 'data'
|
||||
|
||||
class << self
|
||||
|
||||
def connection
|
||||
@@connection ||= ActiveRecord::Base.connection
|
||||
end
|
||||
|
||||
# Look up a session by id and unmarshal its data if found.
|
||||
def find_by_session_id(session_id)
|
||||
if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
|
||||
new(:session_id => session_id, :marshaled_data => record['data'])
|
||||
end
|
||||
end
|
||||
|
||||
def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
|
||||
def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
|
||||
|
||||
def create_table!
|
||||
@@connection.execute <<-end_sql
|
||||
CREATE TABLE #{table_name} (
|
||||
id INTEGER PRIMARY KEY,
|
||||
#{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
|
||||
#{@@connection.quote_column_name(data_column)} TEXT
|
||||
)
|
||||
end_sql
|
||||
end
|
||||
|
||||
def drop_table!
|
||||
@@connection.execute "DROP TABLE #{table_name}"
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :session_id
|
||||
attr_writer :data
|
||||
|
||||
# Look for normal and marshaled data, self.find_by_session_id's way of
|
||||
# telling us to postpone unmarshaling until the data is requested.
|
||||
# We need to handle a normal data attribute in case of a new record.
|
||||
def initialize(attributes)
|
||||
@session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
|
||||
@new_record = @marshaled_data.nil?
|
||||
end
|
||||
|
||||
def new_record?
|
||||
@new_record
|
||||
end
|
||||
|
||||
# Lazy-unmarshal session state.
|
||||
def data
|
||||
unless @data
|
||||
if @marshaled_data
|
||||
@data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
|
||||
else
|
||||
@data = {}
|
||||
end
|
||||
end
|
||||
@data
|
||||
end
|
||||
|
||||
def loaded?
|
||||
!! @data
|
||||
end
|
||||
|
||||
def save
|
||||
return false if !loaded?
|
||||
marshaled_data = self.class.marshal(data)
|
||||
|
||||
if @new_record
|
||||
@new_record = false
|
||||
@@connection.update <<-end_sql, 'Create session'
|
||||
INSERT INTO #{@@table_name} (
|
||||
#{@@connection.quote_column_name(@@session_id_column)},
|
||||
#{@@connection.quote_column_name(@@data_column)} )
|
||||
VALUES (
|
||||
#{@@connection.quote(session_id)},
|
||||
#{@@connection.quote(marshaled_data)} )
|
||||
end_sql
|
||||
else
|
||||
@@connection.update <<-end_sql, 'Update session'
|
||||
UPDATE #{@@table_name}
|
||||
SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
|
||||
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
|
||||
end_sql
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
unless @new_record
|
||||
@@connection.delete <<-end_sql, 'Destroy session'
|
||||
DELETE FROM #{@@table_name}
|
||||
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
|
||||
end_sql
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# The class used for session storage. Defaults to
|
||||
# CGI::Session::ActiveRecordStore::Session.
|
||||
cattr_accessor :session_class
|
||||
self.session_class = Session
|
||||
|
||||
# Find or instantiate a session given a CGI::Session.
|
||||
def initialize(session, option = nil)
|
||||
session_id = session.session_id
|
||||
unless @session = ActiveRecord::Base.silence { @@session_class.find_by_session_id(session_id) }
|
||||
unless session.new_session
|
||||
raise CGI::Session::NoSession, 'uninitialized session'
|
||||
end
|
||||
@session = @@session_class.new(:session_id => session_id, :data => {})
|
||||
# session saving can be lazy again, because of improved component implementation
|
||||
# therefore next line gets commented out:
|
||||
# @session.save
|
||||
end
|
||||
end
|
||||
|
||||
# Access the underlying session model.
|
||||
def model
|
||||
@session
|
||||
end
|
||||
|
||||
# Restore session state. The session model handles unmarshaling.
|
||||
def restore
|
||||
if @session
|
||||
@session.data
|
||||
end
|
||||
end
|
||||
|
||||
# Save session store.
|
||||
def update
|
||||
if @session
|
||||
ActiveRecord::Base.silence { @session.save }
|
||||
end
|
||||
end
|
||||
|
||||
# Save and close the session store.
|
||||
def close
|
||||
if @session
|
||||
update
|
||||
@session = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Delete and close the session store.
|
||||
def delete
|
||||
if @session
|
||||
ActiveRecord::Base.silence { @session.destroy }
|
||||
@session = nil
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def logger
|
||||
ActionController::Base.logger rescue nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,167 +1,224 @@
|
|||
require 'cgi'
|
||||
require 'cgi/session'
|
||||
require 'openssl' # to generate the HMAC message digest
|
||||
module ActionController
|
||||
module Session
|
||||
# This cookie-based session store is the Rails default. Sessions typically
|
||||
# contain at most a user_id and flash message; both fit within the 4K cookie
|
||||
# size limit. Cookie-based sessions are dramatically faster than the
|
||||
# alternatives.
|
||||
#
|
||||
# If you have more than 4K of session data or don't want your data to be
|
||||
# visible to the user, pick another session store.
|
||||
#
|
||||
# CookieOverflow is raised if you attempt to store more than 4K of data.
|
||||
#
|
||||
# A message digest is included with the cookie to ensure data integrity:
|
||||
# a user cannot alter his +user_id+ without knowing the secret key
|
||||
# included in the hash. New apps are generated with a pregenerated secret
|
||||
# in config/environment.rb. Set your own for old apps you're upgrading.
|
||||
#
|
||||
# Session options:
|
||||
#
|
||||
# * <tt>:secret</tt>: An application-wide key string or block returning a
|
||||
# string called per generated digest. The block is called with the
|
||||
# CGI::Session instance as an argument. It's important that the secret
|
||||
# is not vulnerable to a dictionary attack. Therefore, you should choose
|
||||
# a secret consisting of random numbers and letters and more than 30
|
||||
# characters. Examples:
|
||||
#
|
||||
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
|
||||
# :secret => Proc.new { User.current_user.secret_key }
|
||||
#
|
||||
# * <tt>:digest</tt>: The message digest algorithm used to verify session
|
||||
# integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
|
||||
# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
|
||||
#
|
||||
# To generate a secret key for an existing application, run
|
||||
# "rake secret" and set the key in config/environment.rb.
|
||||
#
|
||||
# Note that changing digest or secret invalidates all existing sessions!
|
||||
class CookieStore
|
||||
# Cookies can typically store 4096 bytes.
|
||||
MAX = 4096
|
||||
SECRET_MIN_LENGTH = 30 # characters
|
||||
|
||||
# This cookie-based session store is the Rails default. Sessions typically
|
||||
# contain at most a user_id and flash message; both fit within the 4K cookie
|
||||
# size limit. Cookie-based sessions are dramatically faster than the
|
||||
# alternatives.
|
||||
#
|
||||
# If you have more than 4K of session data or don't want your data to be
|
||||
# visible to the user, pick another session store.
|
||||
#
|
||||
# CookieOverflow is raised if you attempt to store more than 4K of data.
|
||||
# TamperedWithCookie is raised if the data integrity check fails.
|
||||
#
|
||||
# A message digest is included with the cookie to ensure data integrity:
|
||||
# a user cannot alter his +user_id+ without knowing the secret key included in
|
||||
# the hash. New apps are generated with a pregenerated secret in
|
||||
# config/environment.rb. Set your own for old apps you're upgrading.
|
||||
#
|
||||
# Session options:
|
||||
#
|
||||
# * <tt>:secret</tt>: An application-wide key string or block returning a string
|
||||
# called per generated digest. The block is called with the CGI::Session
|
||||
# instance as an argument. It's important that the secret is not vulnerable to
|
||||
# a dictionary attack. Therefore, you should choose a secret consisting of
|
||||
# random numbers and letters and more than 30 characters. Examples:
|
||||
#
|
||||
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
|
||||
# :secret => Proc.new { User.current_user.secret_key }
|
||||
#
|
||||
# * <tt>:digest</tt>: The message digest algorithm used to verify session
|
||||
# integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
|
||||
# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
|
||||
#
|
||||
# To generate a secret key for an existing application, run
|
||||
# "rake secret" and set the key in config/environment.rb.
|
||||
#
|
||||
# Note that changing digest or secret invalidates all existing sessions!
|
||||
class CGI::Session::CookieStore
|
||||
# Cookies can typically store 4096 bytes.
|
||||
MAX = 4096
|
||||
SECRET_MIN_LENGTH = 30 # characters
|
||||
DEFAULT_OPTIONS = {
|
||||
:key => '_session_id',
|
||||
:domain => nil,
|
||||
:path => "/",
|
||||
:expire_after => nil,
|
||||
:httponly => true
|
||||
}.freeze
|
||||
|
||||
# Raised when storing more than 4K of session data.
|
||||
class CookieOverflow < StandardError; end
|
||||
ENV_SESSION_KEY = "rack.session".freeze
|
||||
ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
|
||||
HTTP_SET_COOKIE = "Set-Cookie".freeze
|
||||
|
||||
# Raised when the cookie fails its integrity check.
|
||||
class TamperedWithCookie < StandardError; end
|
||||
# Raised when storing more than 4K of session data.
|
||||
class CookieOverflow < StandardError; end
|
||||
|
||||
# Called from CGI::Session only.
|
||||
def initialize(session, options = {})
|
||||
# The session_key option is required.
|
||||
if options['session_key'].blank?
|
||||
raise ArgumentError, 'A session_key is required to write a cookie containing the session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb'
|
||||
end
|
||||
|
||||
# The secret option is required.
|
||||
ensure_secret_secure(options['secret'])
|
||||
|
||||
# Keep the session and its secret on hand so we can read and write cookies.
|
||||
@session, @secret = session, options['secret']
|
||||
|
||||
# Message digest defaults to SHA1.
|
||||
@digest = options['digest'] || 'SHA1'
|
||||
|
||||
# Default cookie options derived from session settings.
|
||||
@cookie_options = {
|
||||
'name' => options['session_key'],
|
||||
'path' => options['session_path'],
|
||||
'domain' => options['session_domain'],
|
||||
'expires' => options['session_expires'],
|
||||
'secure' => options['session_secure'],
|
||||
'http_only' => options['session_http_only']
|
||||
}
|
||||
|
||||
# Set no_hidden and no_cookies since the session id is unused and we
|
||||
# set our own data cookie.
|
||||
options['no_hidden'] = true
|
||||
options['no_cookies'] = true
|
||||
end
|
||||
|
||||
# To prevent users from using something insecure like "Password" we make sure that the
|
||||
# secret they've provided is at least 30 characters in length.
|
||||
def ensure_secret_secure(secret)
|
||||
# There's no way we can do this check if they've provided a proc for the
|
||||
# secret.
|
||||
return true if secret.is_a?(Proc)
|
||||
|
||||
if secret.blank?
|
||||
raise ArgumentError, %Q{A secret is required to generate an integrity hash for cookie session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase of at least #{SECRET_MIN_LENGTH} characters" } in config/environment.rb}
|
||||
end
|
||||
|
||||
if secret.length < SECRET_MIN_LENGTH
|
||||
raise ArgumentError, %Q{Secret should be something secure, like "#{CGI::Session.generate_unique_id}". The value you provided, "#{secret}", is shorter than the minimum length of #{SECRET_MIN_LENGTH} characters}
|
||||
end
|
||||
end
|
||||
|
||||
# Restore session data from the cookie.
|
||||
def restore
|
||||
@original = read_cookie
|
||||
@data = unmarshal(@original) || {}
|
||||
end
|
||||
|
||||
# Wait until close to write the session data cookie.
|
||||
def update; end
|
||||
|
||||
# Write the session data cookie if it was loaded and has changed.
|
||||
def close
|
||||
if defined?(@data) && !@data.blank?
|
||||
updated = marshal(@data)
|
||||
raise CookieOverflow if updated.size > MAX
|
||||
write_cookie('value' => updated) unless updated == @original
|
||||
end
|
||||
end
|
||||
|
||||
# Delete the session data by setting an expired cookie with no data.
|
||||
def delete
|
||||
@data = nil
|
||||
clear_old_cookie_value
|
||||
write_cookie('value' => nil, 'expires' => 1.year.ago)
|
||||
end
|
||||
|
||||
# Generate the HMAC keyed message digest. Uses SHA1 by default.
|
||||
def generate_digest(data)
|
||||
key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret
|
||||
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data)
|
||||
end
|
||||
|
||||
private
|
||||
# Marshal a session hash into safe cookie data. Include an integrity hash.
|
||||
def marshal(session)
|
||||
data = ActiveSupport::Base64.encode64s(Marshal.dump(session))
|
||||
"#{data}--#{generate_digest(data)}"
|
||||
end
|
||||
|
||||
# Unmarshal cookie data to a hash and verify its integrity.
|
||||
def unmarshal(cookie)
|
||||
if cookie
|
||||
data, digest = cookie.split('--')
|
||||
|
||||
# Do two checks to transparently support old double-escaped data.
|
||||
unless digest == generate_digest(data) || digest == generate_digest(data = CGI.unescape(data))
|
||||
delete
|
||||
raise TamperedWithCookie
|
||||
def initialize(app, options = {})
|
||||
# Process legacy CGI options
|
||||
options = options.symbolize_keys
|
||||
if options.has_key?(:session_path)
|
||||
options[:path] = options.delete(:session_path)
|
||||
end
|
||||
if options.has_key?(:session_key)
|
||||
options[:key] = options.delete(:session_key)
|
||||
end
|
||||
if options.has_key?(:session_http_only)
|
||||
options[:httponly] = options.delete(:session_http_only)
|
||||
end
|
||||
|
||||
Marshal.load(ActiveSupport::Base64.decode64(data))
|
||||
@app = app
|
||||
|
||||
# The session_key option is required.
|
||||
ensure_session_key(options[:key])
|
||||
@key = options.delete(:key).freeze
|
||||
|
||||
# The secret option is required.
|
||||
ensure_secret_secure(options[:secret])
|
||||
@secret = options.delete(:secret).freeze
|
||||
|
||||
@digest = options.delete(:digest) || 'SHA1'
|
||||
@verifier = verifier_for(@secret, @digest)
|
||||
|
||||
@default_options = DEFAULT_OPTIONS.merge(options).freeze
|
||||
|
||||
freeze
|
||||
end
|
||||
end
|
||||
|
||||
# Read the session data cookie.
|
||||
def read_cookie
|
||||
@session.cgi.cookies[@cookie_options['name']].first
|
||||
end
|
||||
def call(env)
|
||||
env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
|
||||
env[ENV_SESSION_OPTIONS_KEY] = @default_options
|
||||
|
||||
# CGI likes to make you hack.
|
||||
def write_cookie(options)
|
||||
cookie = CGI::Cookie.new(@cookie_options.merge(options))
|
||||
@session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
|
||||
end
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
# Clear cookie value so subsequent new_session doesn't reload old data.
|
||||
def clear_old_cookie_value
|
||||
@session.cgi.cookies[@cookie_options['name']].clear
|
||||
session_data = env[ENV_SESSION_KEY]
|
||||
options = env[ENV_SESSION_OPTIONS_KEY]
|
||||
|
||||
if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
|
||||
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
|
||||
session_data = marshal(session_data.to_hash)
|
||||
|
||||
raise CookieOverflow if session_data.size > MAX
|
||||
|
||||
cookie = Hash.new
|
||||
cookie[:value] = session_data
|
||||
unless options[:expire_after].nil?
|
||||
cookie[:expires] = Time.now + options[:expire_after]
|
||||
end
|
||||
|
||||
cookie = build_cookie(@key, cookie.merge(options))
|
||||
case headers[HTTP_SET_COOKIE]
|
||||
when Array
|
||||
headers[HTTP_SET_COOKIE] << cookie
|
||||
when String
|
||||
headers[HTTP_SET_COOKIE] = [headers[HTTP_SET_COOKIE], cookie]
|
||||
when nil
|
||||
headers[HTTP_SET_COOKIE] = cookie
|
||||
end
|
||||
end
|
||||
|
||||
[status, headers, body]
|
||||
end
|
||||
|
||||
private
|
||||
# Should be in Rack::Utils soon
|
||||
def build_cookie(key, value)
|
||||
case value
|
||||
when Hash
|
||||
domain = "; domain=" + value[:domain] if value[:domain]
|
||||
path = "; path=" + value[:path] if value[:path]
|
||||
# According to RFC 2109, we need dashes here.
|
||||
# N.B.: cgi.rb uses spaces...
|
||||
expires = "; expires=" + value[:expires].clone.gmtime.
|
||||
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
|
||||
secure = "; secure" if value[:secure]
|
||||
httponly = "; httponly" if value[:httponly]
|
||||
value = value[:value]
|
||||
end
|
||||
value = [value] unless Array === value
|
||||
cookie = Rack::Utils.escape(key) + "=" +
|
||||
value.map { |v| Rack::Utils.escape(v) }.join("&") +
|
||||
"#{domain}#{path}#{expires}#{secure}#{httponly}"
|
||||
end
|
||||
|
||||
def load_session(env)
|
||||
request = Rack::Request.new(env)
|
||||
session_data = request.cookies[@key]
|
||||
data = unmarshal(session_data) || persistent_session_id!({})
|
||||
[data[:session_id], data]
|
||||
end
|
||||
|
||||
# Marshal a session hash into safe cookie data. Include an integrity hash.
|
||||
def marshal(session)
|
||||
@verifier.generate(persistent_session_id!(session))
|
||||
end
|
||||
|
||||
# Unmarshal cookie data to a hash and verify its integrity.
|
||||
def unmarshal(cookie)
|
||||
persistent_session_id!(@verifier.verify(cookie)) if cookie
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||
nil
|
||||
end
|
||||
|
||||
def ensure_session_key(key)
|
||||
if key.blank?
|
||||
raise ArgumentError, 'A key is required to write a ' +
|
||||
'cookie containing the session data. Use ' +
|
||||
'config.action_controller.session = { :key => ' +
|
||||
'"_myapp_session", :secret => "some secret phrase" } in ' +
|
||||
'config/environment.rb'
|
||||
end
|
||||
end
|
||||
|
||||
# To prevent users from using something insecure like "Password" we make sure that the
|
||||
# secret they've provided is at least 30 characters in length.
|
||||
def ensure_secret_secure(secret)
|
||||
# There's no way we can do this check if they've provided a proc for the
|
||||
# secret.
|
||||
return true if secret.is_a?(Proc)
|
||||
|
||||
if secret.blank?
|
||||
raise ArgumentError, "A secret is required to generate an " +
|
||||
"integrity hash for cookie session data. Use " +
|
||||
"config.action_controller.session = { :key => " +
|
||||
"\"_myapp_session\", :secret => \"some secret phrase of at " +
|
||||
"least #{SECRET_MIN_LENGTH} characters\" } " +
|
||||
"in config/environment.rb"
|
||||
end
|
||||
|
||||
if secret.length < SECRET_MIN_LENGTH
|
||||
raise ArgumentError, "Secret should be something secure, " +
|
||||
"like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
|
||||
"provided, \"#{secret}\", is shorter than the minimum length " +
|
||||
"of #{SECRET_MIN_LENGTH} characters"
|
||||
end
|
||||
end
|
||||
|
||||
def verifier_for(secret, digest)
|
||||
key = secret.respond_to?(:call) ? secret.call : secret
|
||||
ActiveSupport::MessageVerifier.new(key, digest)
|
||||
end
|
||||
|
||||
def generate_sid
|
||||
ActiveSupport::SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
def persistent_session_id!(data)
|
||||
(data ||= {}).merge!(inject_persistent_session_id(data))
|
||||
end
|
||||
|
||||
def inject_persistent_session_id(data)
|
||||
requires_session_id?(data) ? { :session_id => generate_sid } : {}
|
||||
end
|
||||
|
||||
def requires_session_id?(data)
|
||||
if data
|
||||
data.respond_to?(:key?) && !data.key?(:session_id)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
# This is a really simple session storage daemon, basically just a hash,
|
||||
# which is enabled for DRb access.
|
||||
|
||||
require 'drb'
|
||||
|
||||
session_hash = Hash.new
|
||||
session_hash.instance_eval { @mutex = Mutex.new }
|
||||
|
||||
class <<session_hash
|
||||
def []=(key, value)
|
||||
@mutex.synchronize do
|
||||
super(key, value)
|
||||
end
|
||||
end
|
||||
|
||||
def [](key)
|
||||
@mutex.synchronize do
|
||||
super(key)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
@mutex.synchronize do
|
||||
super(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
DRb.start_service('druby://127.0.0.1:9192', session_hash)
|
||||
DRb.thread.join
|
|
@ -1,35 +0,0 @@
|
|||
require 'cgi'
|
||||
require 'cgi/session'
|
||||
require 'drb'
|
||||
|
||||
class CGI #:nodoc:all
|
||||
class Session
|
||||
class DRbStore
|
||||
@@session_data = DRbObject.new(nil, 'druby://localhost:9192')
|
||||
|
||||
def initialize(session, option=nil)
|
||||
@session_id = session.session_id
|
||||
end
|
||||
|
||||
def restore
|
||||
@h = @@session_data[@session_id] || {}
|
||||
end
|
||||
|
||||
def update
|
||||
@@session_data[@session_id] = @h
|
||||
end
|
||||
|
||||
def close
|
||||
update
|
||||
end
|
||||
|
||||
def delete
|
||||
@@session_data.delete(@session_id)
|
||||
end
|
||||
|
||||
def data
|
||||
@@session_data[@session_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,95 +1,48 @@
|
|||
# cgi/session/memcached.rb - persistent storage of marshalled session data
|
||||
#
|
||||
# == Overview
|
||||
#
|
||||
# This file provides the CGI::Session::MemCache class, which builds
|
||||
# persistence of storage data on top of the MemCache library. See
|
||||
# cgi/session.rb for more details on session storage managers.
|
||||
#
|
||||
|
||||
begin
|
||||
require 'cgi/session'
|
||||
require_library_or_gem 'memcache'
|
||||
|
||||
class CGI
|
||||
class Session
|
||||
# MemCache-based session storage class.
|
||||
#
|
||||
# This builds upon the top-level MemCache class provided by the
|
||||
# library file memcache.rb. Session data is marshalled and stored
|
||||
# in a memcached cache.
|
||||
class MemCacheStore
|
||||
def check_id(id) #:nodoc:#
|
||||
/[^0-9a-zA-Z]+/ =~ id.to_s ? false : true
|
||||
end
|
||||
module ActionController
|
||||
module Session
|
||||
class MemCacheStore < AbstractStore
|
||||
def initialize(app, options = {})
|
||||
# Support old :expires option
|
||||
options[:expire_after] ||= options[:expires]
|
||||
|
||||
# Create a new CGI::Session::MemCache instance
|
||||
#
|
||||
# This constructor is used internally by CGI::Session. The
|
||||
# user does not generally need to call it directly.
|
||||
#
|
||||
# +session+ is the session for which this instance is being
|
||||
# created. The session id must only contain alphanumeric
|
||||
# characters; automatically generated session ids observe
|
||||
# this requirement.
|
||||
#
|
||||
# +options+ is a hash of options for the initializer. The
|
||||
# following options are recognized:
|
||||
#
|
||||
# cache:: an instance of a MemCache client to use as the
|
||||
# session cache.
|
||||
#
|
||||
# expires:: an expiry time value to use for session entries in
|
||||
# the session cache. +expires+ is interpreted in seconds
|
||||
# relative to the current time if it’s less than 60*60*24*30
|
||||
# (30 days), or as an absolute Unix time (e.g., Time#to_i) if
|
||||
# greater. If +expires+ is +0+, or not passed on +options+,
|
||||
# the entry will never expire.
|
||||
#
|
||||
# This session's memcache entry will be created if it does
|
||||
# not exist, or retrieved if it does.
|
||||
def initialize(session, options = {})
|
||||
id = session.session_id
|
||||
unless check_id(id)
|
||||
raise ArgumentError, "session_id '%s' is invalid" % id
|
||||
super
|
||||
|
||||
@default_options = {
|
||||
:namespace => 'rack:session',
|
||||
:memcache_server => 'localhost:11211'
|
||||
}.merge(@default_options)
|
||||
|
||||
@pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options)
|
||||
unless @pool.servers.any? { |s| s.alive? }
|
||||
raise "#{self} unable to find server during initialization."
|
||||
end
|
||||
@cache = options['cache'] || MemCache.new('localhost')
|
||||
@expires = options['expires'] || 0
|
||||
@session_key = "session:#{id}"
|
||||
@session_data = {}
|
||||
# Add this key to the store if haven't done so yet
|
||||
unless @cache.get(@session_key)
|
||||
@cache.add(@session_key, @session_data, @expires)
|
||||
@mutex = Mutex.new
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
def get_session(env, sid)
|
||||
sid ||= generate_sid
|
||||
begin
|
||||
session = @pool.get(sid) || {}
|
||||
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
|
||||
session = {}
|
||||
end
|
||||
[sid, session]
|
||||
end
|
||||
end
|
||||
|
||||
# Restore session state from the session's memcache entry.
|
||||
#
|
||||
# Returns the session state as a hash.
|
||||
def restore
|
||||
@session_data = @cache[@session_key] || {}
|
||||
end
|
||||
|
||||
# Save session state to the session's memcache entry.
|
||||
def update
|
||||
@cache.set(@session_key, @session_data, @expires)
|
||||
end
|
||||
|
||||
# Update and close the session's memcache entry.
|
||||
def close
|
||||
update
|
||||
end
|
||||
|
||||
# Delete the session's memcache entry.
|
||||
def delete
|
||||
@cache.delete(@session_key)
|
||||
@session_data = {}
|
||||
end
|
||||
|
||||
def data
|
||||
@session_data
|
||||
end
|
||||
|
||||
def set_session(env, sid, session_data)
|
||||
options = env['rack.session.options']
|
||||
expiry = options[:expire_after] || 0
|
||||
@pool.set(sid, session_data, expiry)
|
||||
return true
|
||||
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue