Session 扫盲

以下源码摘自 /Users/cbd/.rvm/rubies/ruby-2.3.0/lib/ruby/2.3.0/cgi/session.rb

# frozen_string_literal: false
#
# cgi/session.rb - session support for cgi scripts
#
# Copyright (C) 2001  Yukihiro "Matz" Matsumoto
# Copyright (C) 2000  Network Applied Communication Laboratory, Inc.
# Copyright (C) 2000  Information-technology Promotion Agency, Japan
#
# 简体中文翻译    2016 Cbd-Focus, Shanghai
#
# Author: Yukihiro "Matz" Matsumoto
#
# Documentation: William Webber ([email protected])

require 'cgi'
require 'tmpdir'

class CGI

  # == 概述
  #
  # 本文件构建CGI::Session类,为CGI脚本提供session支持.
  # session是关联单一客户端的一连串相互链接的HTTP请求和响应。
  # 在各个请求间,与session关联的信息存储在服务器上。
  # 客户端和服务器在每个请求和响应之间传递session id.
  # 由此,为无状态的HTTP请求和响应协议加入了状态信息。
  #
  # == 生命周期
  #
  # CGI::Session实例由CGI对象创建.
  # 默认情况下,若该客户端的CGI::Session实例已存在,则沿用现有session,若不存在就新实例化一个.
  # 通过new_session 选项可以来设置'总是'或者'永不'创建新的session.
  # 详见#new() .
  #
  # #delete() 从session库中删除一个session,然而它并没有从客户端删除session id.
  # 如果客户端在另一个请求里使用与之相同的session id,则新建的session会沿用这个老的 session id.
  #
  # == session 数据的赋值取值
  #
  # Session 类采用键值对儿来维护session数据.
  # 可以用'[]'方法,通过索引来给session实例赋值和取值,这很像哈希(并没有支持哈希的其他方法).
  #
  # 对于这个请求,当session处理完成后,需要调用close()方法关闭session.这会持久存储session的状态.
  # 如果你想持久存储一个还未完结的session处理,调用update()方法.
  #
  # == session状态存储
  # 调用者可以在CGI::Session::new 用 database_manager 来指定session数据的存储样式.
  # 下面是标准库里提供的几种存储类型:
  # CGI::Session::Filestore:: 在文件中存储纯文本.只用于字符串数据.也是默认的存储类型.
  # CGI::Session::MemoryStore:: 存在内存哈希中.数据只在当前的ruby解释器运行时存在.
  # CGI::Session::PStore:: 以整理后的格式存储.功能由cgi/session/pstore.rb实现.支持任意格式数据,并提供文件锁和事务.
  #
  # 当然也可DIY一种存储方式,只需定义类实现下面方法:
  #   new(session,options)
  #   restore #返回session数据的哈希
  #   update
  #   close
  #   delete
  #
  # Changing storage type mid-session does not work.  Note in particular
  # that by default the FileStore and PStore session data files have the
  # same name.  If your application switches from one to the other without
  # making sure that filenames will be different
  # and clients still have old sessions lying around in cookies, then
  # things will break nastily!
  #
  # == 维护session id
  # 大多数session状态在服务器上维护,而session id必须在客户端和服务端来回传递.
  # 最简单的方式是使用cookies.
  # 当客户端启用cookies,CGI::Session类 会通过cookies来传递session id.
  # 当客户端禁用cookies,session id就必须作为参数包在请求里发送给服务器.
  # CGI::Session类 协同 CGI类,将会在所有生成的表单里添加包含sessionid的隐藏域(通过 CGI#form()方法生成).
  #
  # 为其他机制提供非内建支持,例如 URL重写.
  # 当使用其他机制时,调用者需要通过session_id属性来提取session id,并手工编码URL,手工在HTML表单中加入隐藏域.
  # == 使用样例
  #
  # === Setting the user's name
  #
  #   require 'cgi'
  #   require 'cgi/session'
  #   require 'cgi/session/pstore'     # provides CGI::Session::PStore
  #
  #   cgi = CGI.new("html4")
  #
  #   session = CGI::Session.new(cgi,
  #       'database_manager' => CGI::Session::PStore,  # use PStore
  #       'session_key' => '_rb_sess_id',              # custom session key
  #       'session_expires' => Time.now + 30 * 60,     # 30 minute timeout
  #       'prefix' => 'pstore_sid_')                   # PStore option
  #   if cgi.has_key?('user_name') and cgi['user_name'] != ''
  #       # coerce to String: cgi[] returns the
  #       # string-like CGI::QueryExtension::Value
  #       session['user_name'] = cgi['user_name'].to_s
  #   elsif !session['user_name']
  #       session['user_name'] = "guest"
  #   end
  #   session.close
  #
  # === Creating a new session safely
  #
  #   require 'cgi'
  #   require 'cgi/session'
  #
  #   cgi = CGI.new("html4")
  #
  #   # We make sure to delete an old session if one exists,
  #   # not just to free resources, but to prevent the session
  #   # from being maliciously hijacked later on.
  #   begin
  #       session = CGI::Session.new(cgi, 'new_session' => false)
  #       session.delete
  #   rescue ArgumentError  # if no old session
  #   end
  #   session = CGI::Session.new(cgi, 'new_session' => true)
  #   session.close
  #
  class Session

    class NoSession < RuntimeError #:nodoc:
    end

    # The id of this session.
    attr_reader :session_id, :new_session

    def Session::callback(dbman)  #:nodoc:
      Proc.new{
        dbman[0].close unless dbman.empty?
      }
    end

    # Create a new session id.
    #
    # The session id is a secure random number by SecureRandom
    # if possible, otherwise an SHA512 hash based upon the time,
    # a random number, and a constant string.  This routine is
    # used internally for automatically generated session ids.
    def create_new_id
      require 'securerandom'
      begin
        # 通过 OpenSSL生成,或者 系统提供的内核熵池
        session_id = SecureRandom.hex(16)
      rescue NotImplementedError
        # 在现代操作系统上永远不会出现这个异常
        # 如果系统不支持SecureRandom的随机串儿生成方式,就用SHA512
        require 'digest'
        d = Digest('SHA512').new
        now = Time::now
        d.update(now.to_s)
        d.update(String(now.usec))
        d.update(String(rand(0)))
        d.update(String($$))
        d.update('foobar')
        session_id = d.hexdigest[0, 32]
      end
      session_id
    end
    private :create_new_id

    # 为+request+新实例化一个 CGI::Session 对象
    #
    # +request+ 是一个+CGI+类的实例(见 cgi.rb).
    # +option+ is a hash of options for initialising this
    # +option+ 是初始化这个 CGI:Session 实例的选项,哈希结构.
    # 可识别下列选项:
    #
    # session_key:: the parameter name used for the session id.
    #               Defaults to '_session_id'.
    # session_id:: the session id to use.  If not provided, then
    #              it is retrieved from the +session_key+ parameter
    #              of the request, or automatically generated for
    #              a new session.
    # new_session:: if true, force creation of a new session.  If not set,
    #               a new session is only created if none currently
    #               exists.  If false, a new session is never created,
    #               and if none currently exists and the +session_id+
    #               option is not set, an ArgumentError is raised.
    # database_manager:: the name of the class providing storage facilities
    #                    for session state persistence.  Built-in support
    #                    is provided for +FileStore+ (the default),
    #                    +MemoryStore+, and +PStore+ (from
    #                    cgi/session/pstore.rb).  See the documentation for
    #                    these classes for more details.
    #
    # The following options are also recognised, but only apply if the
    # session id is stored in a cookie.
    #
    # session_expires:: the time the current session expires, as a
    #                   +Time+ object.  If not set, the session will terminate
    #                   when the user's browser is closed.
    # session_domain:: the hostname domain for which this session is valid.
    #                  If not set, defaults to the hostname of the server.
    # session_secure:: if +true+, this session will only work over HTTPS.
    # session_path:: the path for which this session applies.  Defaults
    #                to the directory of the CGI script.
    #
    # +option+ is also passed on to the session storage class initializer; see
    # the documentation for each session storage class for the options
    # they support.
    #
    # The retrieved or created session is automatically added to +request+
    # as a cookie, and also to its +output_hidden+ table, which is used
    # to add hidden input elements to forms.
    #
    # *WARNING* the +output_hidden+
    # fields are surrounded by a 
tag in HTML 4 generation, which # is _not_ invisible on many browsers; you may wish to disable the # use of fieldsets with code similar to the following # (see http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/37805) # # cgi = CGI.new("html4") # class << cgi # undef_method :fieldset # end # def initialize(request, option={}) @new_session = false session_key = option['session_key'] || '_session_id' session_id = option['session_id'] unless session_id if option['new_session'] session_id = create_new_id @new_session = true end end unless session_id if request.key?(session_key) session_id = request[session_key] session_id = session_id.read if session_id.respond_to?(:read) end unless session_id session_id, = request.cookies[session_key] end unless session_id unless option.fetch('new_session', true) raise ArgumentError, "session_key `%s' should be supplied"%session_key end session_id = create_new_id @new_session = true end end @session_id = session_id dbman = option['database_manager'] || FileStore begin @dbman = dbman::new(self, option) rescue NoSession unless option.fetch('new_session', true) raise ArgumentError, "invalid session_id `%s'"%session_id end session_id = @session_id = create_new_id unless session_id @new_session=true retry end request.instance_eval do @output_hidden = {session_key => session_id} unless option['no_hidden'] @output_cookies = [ Cookie::new("name" => session_key, "value" => session_id, "expires" => option['session_expires'], "domain" => option['session_domain'], "secure" => option['session_secure'], "path" => if option['session_path'] option['session_path'] elsif ENV["SCRIPT_NAME"] File::dirname(ENV["SCRIPT_NAME"]) else "" end) ] unless option['no_cookies'] end @dbprot = [@dbman] ObjectSpace::define_finalizer(self, Session::callback(@dbprot)) end # Retrieve the session data for key +key+. def [](key) @data ||= @dbman.restore @data[key] end # Set the session data for key +key+. def []=(key, val) @write_lock ||= true @data ||= @dbman.restore @data[key] = val end # Store session data on the server. For some session storage types, # this is a no-op. def update @dbman.update end # Store session data on the server and close the session storage. # For some session storage types, this is a no-op. def close @dbman.close @dbprot.clear end # Delete the session from storage. Also closes the storage. # # Note that the session's data is _not_ automatically deleted # upon the session expiring. def delete @dbman.delete @dbprot.clear end # File-based session storage class. # # Implements session storage as a flat file of 'key=value' values. # This storage type only works directly with String values; the # user is responsible for converting other types to Strings when # storing and from Strings when retrieving. class FileStore # Create a new FileStore 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. # # +option+ is a hash of options for the initializer. The # following options are recognised: # # tmpdir:: the directory to use for storing the FileStore # file. Defaults to Dir::tmpdir (generally "/tmp" # on Unix systems). # prefix:: the prefix to add to the session id when generating # the filename for this session's FileStore file. # Defaults to "cgi_sid_". # suffix:: the prefix to add to the session id when generating # the filename for this session's FileStore file. # Defaults to the empty string. # # This session's FileStore file will be created if it does # not exist, or opened if it does. def initialize(session, option={}) dir = option['tmpdir'] || Dir::tmpdir prefix = option['prefix'] || 'cgi_sid_' suffix = option['suffix'] || '' id = session.session_id require 'digest/md5' md5 = Digest::MD5.hexdigest(id)[0,16] @path = dir+"/"+prefix+md5+suffix if File::exist? @path @hash = nil else unless session.new_session raise CGI::Session::NoSession, "uninitialized session" end @hash = {} end end # Restore session state from the session's FileStore file. # # Returns the session state as a hash. def restore unless @hash @hash = {} begin lockf = File.open(@path+".lock", "r") lockf.flock File::LOCK_SH f = File.open(@path, 'r') for line in f line.chomp! k, v = line.split('=',2) @hash[CGI::unescape(k)] = Marshal.restore(CGI::unescape(v)) end ensure f.close unless f.nil? lockf.close if lockf end end @hash end # Save session state to the session's FileStore file. def update return unless @hash begin lockf = File.open(@path+".lock", File::CREAT|File::RDWR, 0600) lockf.flock File::LOCK_EX f = File.open(@path+".new", File::CREAT|File::TRUNC|File::WRONLY, 0600) for k,v in @hash f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(Marshal.dump(v))) end f.close File.rename @path+".new", @path ensure f.close if f and !f.closed? lockf.close if lockf end end # Update and close the session's FileStore file. def close update end # Close and delete the session's FileStore file. def delete File::unlink @path+".lock" rescue nil File::unlink @path+".new" rescue nil File::unlink @path rescue nil end end # In-memory session storage class. # # Implements session storage as a global in-memory hash. Session # data will only persist for as long as the Ruby interpreter # instance does. class MemoryStore GLOBAL_HASH_TABLE = {} #:nodoc: # Create a new MemoryStore instance. # # +session+ is the session this instance is associated with. # +option+ is a list of initialisation options. None are # currently recognized. def initialize(session, option=nil) @session_id = session.session_id unless GLOBAL_HASH_TABLE.key?(@session_id) unless session.new_session raise CGI::Session::NoSession, "uninitialized session" end GLOBAL_HASH_TABLE[@session_id] = {} end end # Restore session state. # # Returns session data as a hash. def restore GLOBAL_HASH_TABLE[@session_id] end # Update session state. # # A no-op. def update # don't need to update; hash is shared end # Close session storage. # # A no-op. def close # don't need to close end # Delete the session state. def delete GLOBAL_HASH_TABLE.delete(@session_id) end end # Dummy session storage class. # # Implements session storage place holder. No actual storage # will be done. class NullStore # Create a new NullStore instance. # # +session+ is the session this instance is associated with. # +option+ is a list of initialisation options. None are # currently recognised. def initialize(session, option=nil) end # Restore (empty) session state. def restore {} end # Update session state. # # A no-op. def update end # Close session storage. # # A no-op. def close end # Delete the session state. # # A no-op. def delete end end end end

你可能感兴趣的:(Session 扫盲)