Skip to content

Latest commit

 

History

History
1231 lines (1028 loc) · 40.3 KB

03.middlewares-2.md

File metadata and controls

1231 lines (1028 loc) · 40.3 KB

An Http Request Through Rails

03. Middlewares - 2

续 02. Middlewares - 1

下一个Middleware是Cookie相关的,ActionDispatch::Cookies,定义在actionpack-3.2.13/lib/action_dispatch/middleware/cookies.rb中:

  def initialize(app)
    @app = app
  end

  def call(env)
    cookie_jar = nil
    status, headers, body = @app.call(env)

    if cookie_jar = env['action_dispatch.cookies']
      cookie_jar.write(headers)
      if headers[HTTP_HEADER].respond_to?(:join)
        headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
      end
    end

    [status, headers, body]
  end

这个Middleware负责裆CookieJar对象存在时的write操作,以及当Set-Cookie实现了join方法时,调用该方法的功能。

其中env['action_dispatch.cookies']在调用request.cookie_jar的时候就已经被调用了:

module ActionDispatch
  class Request
    def cookie_jar
      env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self)
    end
  end
end

这里的Cookies::CookieJar封装了所有对于Cookie的基本操作。

然后是一个Middleware是与Session相关的,具体实现取决于config.session_store的实现。这里以默认实现ActionDispatch::Session::CookieStore为例,实现在actionpack-3.2.13/lib/action_dispatch/middleware/session/cookie_store.rb中,需要注意的是,CookieStore Session的实现较长,而且还跨三个类的层次(还有两个Rails实现的模块),因此主要讲解这三层实现的不同功能:

require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/object/blank'
require 'action_dispatch/middleware/session/abstract_store'
require 'rack/session/cookie'

module ActionDispatch
  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. 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.
    #
    #     :secret => '449fe2e7daee471bffae2fd8dc02313d'
    #
    # * <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/initializers/secret_token.rb.
    #
    # Note that changing digest or secret invalidates all existing sessions!
    class CookieStore < Rack::Session::Cookie
      include Compatibility
      include StaleSessionCheck

      private

      def unpacked_cookie_data(env)
        env["action_dispatch.request.unsigned_session_cookie"] ||= begin
          stale_session_check! do
            request = ActionDispatch::Request.new(env)
            if data = request.cookie_jar.signed[@key]
              data.stringify_keys!
            end
            data || {}
          end
        end
      end

      def set_session(env, sid, session_data, options)
        session_data.merge!("session_id" => sid)
      end

      def set_cookie(env, session_id, cookie)
        request = ActionDispatch::Request.new(env)
        request.cookie_jar.signed[@key] = cookie
      end
    end
  end
end

这层类是Rails实现的,负责在设置cookie时使用SignedCookieJar而不是普通的CookieJar以保障安全性。SignedCookieJar会在Cookie set时增加验证功能,而在get时增加一个验证的token,实现在actionpack-3.2.13/lib/action_dispatch/middleware/cookies.rb中:

class SignedCookieJar < CookieJar
  MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes.
  SECRET_MIN_LENGTH = 30 # Characters

  def initialize(parent_jar, secret)
    ensure_secret_secure(secret)
    @parent_jar = parent_jar
    @verifier   = ActiveSupport::MessageVerifier.new(secret)
  end

  def [](name)
    if signed_message = @parent_jar[name]
      @verifier.verify(signed_message)
    end
  rescue ActiveSupport::MessageVerifier::InvalidSignature
    nil
  end

  def []=(key, options)
    if options.is_a?(Hash)
      options.symbolize_keys!
      options[:value] = @verifier.generate(options[:value])
    else
      options = { :value => @verifier.generate(options) }
    end

    raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
    @parent_jar[key] = options
  end

  def method_missing(method, *arguments, &block)
    @parent_jar.send(method, *arguments, &block)
  end

protected

  # 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)
    if secret.blank?
      raise ArgumentError, "A secret is required to generate an " +
        "integrity hash for cookie session data. Use " +
        "config.secret_token = \"some secret phrase of at " +
        "least #{SECRET_MIN_LENGTH} characters\"" +
        "in config/initializers/secret_token.rb"
    end

    if secret.length < SECRET_MIN_LENGTH
      raise ArgumentError, "Secret should be something secure, " +
        "like \"#{SecureRandom.hex(16)}\". The value you " +
        "provided, \"#{secret}\", is shorter than the minimum length " +
        "of #{SECRET_MIN_LENGTH} characters"
    end
  end
end

其中验证和验证码生成均用到了ActiveSupport::MessageVerifier实用类。这个类定义在activesupport-3.2.13/lib/active_support/message_verifier.rb中:

require 'active_support/base64'
require 'active_support/deprecation'
require 'active_support/core_ext/object/blank'

module ActiveSupport
  # +MessageVerifier+ makes it easy to generate and verify messages which are signed
  # to prevent tampering.
  #
  # This is useful for cases like remember-me tokens and auto-unsubscribe links where the
  # session store isn't suitable or available.
  #
  # Remember Me:
  #   cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now])
  #
  # In the authentication filter:
  #
  #   id, time = @verifier.verify(cookies[:remember_me])
  #   if time < Time.now
  #     self.current_user = User.find(id)
  #   end
  #
  # By default it uses Marshal to serialize the message. If you want to use another 
  # serialization method, you can set the serializer attribute to something that responds
  # to dump and load, e.g.:
  #
  #   @verifier.serializer = YAML
  class MessageVerifier
    class InvalidSignature < StandardError; end

    def initialize(secret, options = {})
      unless options.is_a?(Hash)
        ActiveSupport::Deprecation.warn "The second parameter should be an options hash. Use :digest => 'algorithm' to specify the digest algorithm."
        options = { :digest => options }
      end

      @secret = secret
      @digest = options[:digest] || 'SHA1'
      @serializer = options[:serializer] || Marshal
    end

    def verify(signed_message)
      raise InvalidSignature if signed_message.blank?

      data, digest = signed_message.split("--")
      if data.present? && digest.present? && secure_compare(digest, generate_digest(data))
        @serializer.load(::Base64.decode64(data))
      else
        raise InvalidSignature
      end
    end

    def generate(value)
      data = ::Base64.strict_encode64(@serializer.dump(value))
      "#{data}--#{generate_digest(data)}"
    end

    private
      # constant-time comparison algorithm to prevent timing attacks
      def secure_compare(a, b)
        return false unless a.bytesize == b.bytesize

        l = a.unpack "C#{a.bytesize}"

        res = 0
        b.each_byte { |byte| res |= byte ^ l.shift }
        res == 0
      end

      def generate_digest(data)
        require 'openssl' unless defined?(OpenSSL)
        OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
      end
  end
end

可以看到用以验证的数据跟在数据的后面一起被写入到session中。

具体的Cookie的装载和设定功能由基类Rack::Session::Cookie负责,实现在rack-1.4.5/lib/rack/session/cookie.rb中:

require 'openssl'
require 'rack/request'
require 'rack/response'
require 'rack/session/abstract/id'

module Rack

  module Session

    # Rack::Session::Cookie provides simple cookie based session management.
    # By default, the session is a Ruby Hash stored as base64 encoded marshalled
    # data set to :key (default: rack.session).  The object that encodes the
    # session data is configurable and must respond to +encode+ and +decode+.
    # Both methods must take a string and return a string.
    #
    # When the secret key is set, cookie data is checked for data integrity.
    # The old secret key is also accepted and allows graceful secret rotation.
    #
    # Example:
    #
    #     use Rack::Session::Cookie, :key => 'rack.session',
    #                                :domain => 'foo.com',
    #                                :path => '/',
    #                                :expire_after => 2592000,
    #                                :secret => 'change_me',
    #                                :old_secret => 'also_change_me'
    #
    #     All parameters are optional.
    #
    # 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::ID
      # Encode session cookies as Base64
      class Base64
        def encode(str)
          [str].pack('m')
        end

        def decode(str)
          str.unpack('m').first
        end

        # Encode session cookies as Marshaled Base64 data
        class Marshal < Base64
          def encode(str)
            super(::Marshal.dump(str))
          end

          def decode(str)
            ::Marshal.load(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

      # Reverse string encoding. (trollface)
      class Reverse
        def encode(str); str.reverse; end
        def decode(str); str.reverse; end
      end

      attr_reader :coder

      def initialize(app, options={})
        @secrets = options.values_at(:secret, :old_secret).compact
        warn <<-MSG unless @secrets.size >= 1
        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
        @coder  = options[:coder] ||= Base64::Marshal.new
        super(app, options.merge!(:cookie_only => true))
      end

      private

      def load_session(env)
        data = unpacked_cookie_data(env)
        data = persistent_session_id!(data)
        [data["session_id"], data]
      end

      def extract_session_id(env)
        unpacked_cookie_data(env)["session_id"]
      end

      def unpacked_cookie_data(env)
        env["rack.session.unpacked_cookie_data"] ||= begin
          request = Rack::Request.new(env)
          session_data = request.cookies[@key]

          if @secrets.size > 0 && session_data
            session_data, digest = session_data.split("--")

            if session_data && digest
              ok = @secrets.any? do |secret|
                secret && Rack::Utils.secure_compare(digest, generate_hmac(session_data, secret))
              end
            end

            session_data = nil unless ok
          end

          coder.decode(session_data) || {}
        end
      end

      def persistent_session_id!(data, sid=nil)
        data ||= {}
        data["session_id"] ||= sid || generate_sid
        data
      end

      # Overwrite set cookie to bypass content equality and always stream the cookie.

      def set_cookie(env, headers, cookie)
        Utils.set_cookie_header!(headers, @key, cookie)
      end

      def set_session(env, session_id, session, options)
        session = session.merge("session_id" => session_id)
        session_data = coder.encode(session)

        if @secrets.first
          session_data = "#{session_data}--#{generate_hmac(session_data, @secrets.first)}"
        end

        if session_data.size > (4096 - @key.size)
          env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
          nil
        else
          session_data
        end
      end

      def destroy_session(env, session_id, options)
        # Nothing to do here, data is in the client
        generate_sid unless options[:drop]
      end

      def generate_hmac(data, secret)
        OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data)
      end

    end
  end
end

这层由Rack实现,Rack::Session::Cookie基本实现了Cookie存储Session所需要的全部逻辑,包括各种编码方法,以及简单的SHA1验证。其中Rails在继承后并没有使用它的SHA1验证功能,这可能是因为Rails要求CookieStore使用Rails自己实现的CookieJar类,并且需要对付一些安全攻击。

至于Session底层机制的实现由它的基类Rack::Session::Abstract::ID负责,其实现在rack-1.4.5/lib/rack/session/abstract/id.rb中:

# 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 #get_session and #set_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, and :httponly set the related
#   cookie options as by Rack::Response#add_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
# env['rack.session.options']. 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 ID
  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 rescue false)
  }

  attr_reader :key, :default_options

  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)
    initialize_sid
  end

  def call(env)
    context(env)
  end

  def context(env, app=@app)
    prepare_session(env)
    status, headers, body = app.call(env)
    commit_session(env, status, headers, body)
  end

  private

  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
      SecureRandom.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(env)
    session_was                  = env[ENV_SESSION_KEY]
    env[ENV_SESSION_KEY]         = SessionHash.new(self, env)
    env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
    env[ENV_SESSION_KEY].merge! session_was if session_was
  end

  # Extracts the session id from provided cookies and passes it and the
  # environment to #get_session.

  def load_session(env)
    sid = current_session_id(env)
    sid, session = get_session(env, sid)
    [sid, session || {}]
  end

  # Extract session id from request object.

  def extract_session_id(env)
    request = Rack::Request.new(env)
    sid = request.cookies[@key]
    sid ||= request.params[@key] unless @cookie_only
    sid
  end

  # Returns the current session id from the OptionsHash.

  def current_session_id(env)
    env[ENV_SESSION_OPTIONS_KEY][:id]
  end

  # Check if the session exists or not.

  def session_exists?(env)
    value = current_session_id(env)
    value && !value.empty?
  end

  # Session should be commited 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?(env, session, options)
    if options[:skip]
      false
    else
      has_session = loaded_session?(session) || forced_session_update?(session, options)
      has_session && security_matches?(env, options)
    end
  end

  def loaded_session?(session)
    !session.is_a?(SessionHash) || session.loaded?
  end

  def forced_session_update?(session, options)
    force_options?(options) && session && !session.empty?
  end

  def force_options?(options)
    options.values_at(:renew, :drop, :defer, :expire_after).any?
  end

  def security_matches?(env, options)
    return true unless options[:secure]
    request = Rack::Request.new(env)
    request.ssl?
  end

  # Acquires the session from the environment and the session id from
  # the session options and passes them to #set_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(env, status, headers, body)
    session = env['rack.session']
    options = env['rack.session.options']

    if options[:drop] || options[:renew]
      session_id = destroy_session(env, options[:id] || generate_sid, options)
      return [status, headers, body] unless session_id
    end

    return [status, headers, body] unless commit_session?(env, session, options)

    session.send(:load!) unless loaded_session?(session)
    session = session.to_hash
    session_id ||= options[:id] || generate_sid

    if not data = set_session(env, session_id, session, options)
      env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
    elsif options[:defer] and not options[:renew]
      env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
    else
      cookie = Hash.new
      cookie[:value] = data
      cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
      set_cookie(env, headers, cookie.merge!(options))
    end

    [status, headers, body]
  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(env, headers, cookie)
    request = Rack::Request.new(env)
    if request.cookies[@key] != cookie[:value] || cookie[:expires]
      Utils.set_cookie_header!(headers, @key, cookie)
    end
  end

  # All thread safety and session retrival proceedures 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 get_session(env, sid)
    raise '#get_session not implemented.'
  end

  # All thread safety and session storage proceedures should occur here.
  # Should return true or false dependant on whether or not the session
  # was saved or not.

  def set_session(env, sid, session, options)
    raise '#set_session not implemented.'
  end

  # All thread safety and session destroy proceedures should occur here.
  # Should return a new session id or nil if options[:drop]

  def destroy_session(env, sid, options)
    raise '#destroy_session not implemented'
  end
end

Rack::Session::Abstract::ID实现了一套抽象的以ID作为标示方法的Session取出和存储的机制,主要负责提供Session在内存中的存储结构SessionHash,Session相关选项的存储结构OptionHash,Middleware接口,初始化Session和请求结束后对Session的写入功能。注意其Middleware主要实现请求处理前上一次请求的Session的装载以及SessionHashOptionHash的初始化,请求处理后将Session写入同时Cookie记录Session ID的功能或者丢弃Session的功能。

下一个Middleware和Session相关,那就是ActionDispatch::Flash,定义在actionpack-3.2.13/lib/action_dispatch/middleware/flash.rb

module ActionDispatch
  class Request
    # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
    # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
    # to put a new one.
    def flash
      @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new)
    end
  end

  # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
  # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
  # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
  # then expose the flash to its template. Actually, that exposure is automatically done. Example:
  #
  #   class PostsController < ActionController::Base
  #     def create
  #       # save post
  #       flash[:notice] = "Post successfully created"
  #       redirect_to posts_path(@post)
  #     end
  #
  #     def show
  #       # doesn't need to assign the flash notice to the template, that's done automatically
  #     end
  #   end
  #
  #   show.html.erb
  #     <% if flash[:notice] %>
  #       <div class="notice"><%= flash[:notice] %></div>
  #     <% end %>
  #
  # Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
  #
  #   flash.alert = "You must be logged in"
  #   flash.notice = "Post successfully created"
  #
  # This example just places a string in the flash, but you can put any object in there. And of course, you can put as
  # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
  #
  # See docs on the FlashHash class for more details about the flash.
  class Flash
    KEY = 'action_dispatch.request.flash_hash'.freeze

    class FlashNow
      attr_accessor :flash

      def initialize(flash)
        @flash = flash
      end

      def []=(k, v)
        @flash[k] = v
        @flash.discard(k)
        v
      end

      def [](k)
        @flash[k]
      end

      # Convenience accessor for flash.now[:alert]=
      def alert=(message)
        self[:alert] = message
      end

      # Convenience accessor for flash.now[:notice]=
      def notice=(message)
        self[:notice] = message
      end
    end

    # Implementation detail: please do not change the signature of the
    # FlashHash class. Doing that will likely affect all Rails apps in
    # production as the FlashHash currently stored in their sessions will
    # become invalid.
    class FlashHash
      include Enumerable

      def initialize
        @used    = Set.new
        @closed  = false
        @flashes = {}
        @now     = nil
      end

      def initialize_copy(other)
        if other.now_is_loaded?
          @now = other.now.dup
          @now.flash = self
        end
        super
      end

      def []=(k, v)
        keep(k)
        @flashes[k] = v
      end

      def [](k)
        @flashes[k]
      end

      def update(h)
        h.keys.each { |k| keep(k) }
        @flashes.update h
        self
      end

      def keys
        @flashes.keys
      end

      def key?(name)
        @flashes.key? name
      end

      def delete(key)
        @flashes.delete key
        self
      end

      def to_hash
        @flashes.dup
      end

      def empty?
        @flashes.empty?
      end

      def clear
        @flashes.clear
      end

      def each(&block)
        @flashes.each(&block)
      end

      alias :merge! :update

      def replace(h)
        @used = Set.new
        @flashes.replace h
        self
      end

      # Sets a flash that will not be available to the next action, only to the current.
      #
      #     flash.now[:message] = "Hello current action"
      #
      # This method enables you to use the flash as a central messaging system in your app.
      # When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
      # When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
      # vanish when the current action is done.
      #
      # Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
      def now
        @now ||= FlashNow.new(self)
      end

      # Keeps either the entire current flash or a specific flash entry available for the next action:
      #
      #    flash.keep            # keeps the entire flash
      #    flash.keep(:notice)   # keeps only the "notice" entry, the rest of the flash is discarded
      def keep(k = nil)
        use(k, false)
      end

      # Marks the entire flash or a single flash entry to be discarded by the end of the current action:
      #
      #     flash.discard              # discard the entire flash at the end of the current action
      #     flash.discard(:warning)    # discard only the "warning" entry at the end of the current action
      def discard(k = nil)
        use(k)
      end

      # Mark for removal entries that were kept, and delete unkept ones.
      #
      # This method is called automatically by filters, so you generally don't need to care about it.
      def sweep
        keys.each do |k|
          unless @used.include?(k)
            @used << k
          else
            delete(k)
            @used.delete(k)
          end
        end

        # clean up after keys that could have been left over by calling reject! or shift on the flash
        (@used - keys).each{ |k| @used.delete(k) }
      end

      # Convenience accessor for flash[:alert]
      def alert
        self[:alert]
      end

      # Convenience accessor for flash[:alert]=
      def alert=(message)
        self[:alert] = message
      end

      # Convenience accessor for flash[:notice]
      def notice
        self[:notice]
      end

      # Convenience accessor for flash[:notice]=
      def notice=(message)
        self[:notice] = message
      end

      protected

        def now_is_loaded?
          !!@now
        end

        # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
        #     use()               # marks the entire flash as used
        #     use('msg')          # marks the "msg" entry as used
        #     use(nil, false)     # marks the entire flash as unused (keeps it around for one more action)
        #     use('msg', false)   # marks the "msg" entry as unused (keeps it around for one more action)
        # Returns the single value for the key you asked to be marked (un)used or the FlashHash itself
        # if no key is passed.
        def use(key = nil, used = true)
          Array(key || keys).each { |k| used ? @used << k : @used.delete(k) }
          return key ? self[key] : self
        end
    end

    def initialize(app)
      @app = app
    end

    def call(env)
      if (session = env['rack.session']) && (flash = session['flash'])
        flash.sweep
      end

      @app.call(env)
    ensure
      session    = env['rack.session'] || {}
      flash_hash = env[KEY]

      if flash_hash
        if !flash_hash.empty? || session.key?('flash')
          session["flash"] = flash_hash
          new_hash = flash_hash.dup
        else
          new_hash = flash_hash
        end

        env[KEY] = new_hash
      end

      if session.key?('flash') && session['flash'].empty?
        session.delete('flash')
      end
    end
  end
end

Rails的Flash是完全基于Session存储的特殊对象。在处理请求前,从Rails env中取出session,然后从session中取出flash,调用sweep方法,将还没有使用过的flash标记为已经使用,已经使用过的flash则删除(对于flash.now而言,任何flash在设置时就已经被标记为使用过,这是它和flash的最大区别)。在action和view执行的过程中,对于flash的操作会被写入到Rails env中。在处理请求后,将flash从Rails env中拷贝一份并且放置到session中持久化。

下一个Middleware是ActionDispatch::ParamsParser,定义在actionpack-3.2.13/lib/action_dispatch/middleware/params_parser.rb中:

require 'active_support/core_ext/hash/conversions'
require 'action_dispatch/http/request'
require 'active_support/core_ext/hash/indifferent_access'

module ActionDispatch
  class ParamsParser
    DEFAULT_PARSERS = {
      Mime::XML => :xml_simple,
      Mime::JSON => :json
    }

    def initialize(app, parsers = {})
      @app, @parsers = app, DEFAULT_PARSERS.merge(parsers)
    end

    def call(env)
      if params = parse_formatted_parameters(env)
        env["action_dispatch.request.request_parameters"] = params
      end

      @app.call(env)
    end

    private
      def parse_formatted_parameters(env)
        request = Request.new(env)

        return false if request.content_length.zero?

        mime_type = content_type_from_legacy_post_data_format_header(env) ||
          request.content_mime_type

        strategy = @parsers[mime_type]

        return false unless strategy

        case strategy
        when Proc
          strategy.call(request.raw_post)
        when :xml_simple, :xml_node
          data = request.deep_munge(Hash.from_xml(request.body.read) || {})
          request.body.rewind if request.body.respond_to?(:rewind)
          data.with_indifferent_access
        when :yaml
          YAML.load(request.raw_post)
        when :json
          data = ActiveSupport::JSON.decode(request.body)
          request.body.rewind if request.body.respond_to?(:rewind)
          data = {:_json => data} unless data.is_a?(Hash)
          request.deep_munge(data).with_indifferent_access
        else
          false
        end
      rescue Exception => e # YAML, XML or Ruby code block errors
        logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"

        raise e
      end

      def content_type_from_legacy_post_data_format_header(env)
        if x_post_format = env['HTTP_X_POST_DATA_FORMAT']
          case x_post_format.to_s.downcase
          when 'yaml' then return Mime::YAML
          when 'xml'  then return Mime::XML
          end
        end

        nil
      end

      def logger(env)
        env['action_dispatch.logger'] || Logger.new($stderr)
      end
  end
end

由于Rails同样支持使用XML,JSON或者YAML作为参数上传,因此这个Middlware负责将这些参数全部转换为标准的Hash类型。这里识别MIME类型使用到了ActionDispatch::Http::MimeNegotiation模块的content_mime_type方法,定义在actionpack-3.2.13/lib/action_dispatch/http/mime_negotiation.rb

# The MIME type of the HTTP request, such as Mime::XML.
#
# For backward compatibility, the post \format is extracted from the
# X-Post-Data-Format HTTP header if present.
def content_mime_type
  @env["action_dispatch.request.content_type"] ||= begin
    if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
      Mime::Type.lookup($1.strip.downcase)
    else
      nil
    end
  end
end

实际搜索用得是Mime::Type.lookup方法,在所有注册过的Mime类型中搜索出匹配的Mime::Type类,不过由于parse_formatted_parameters本身就实现了三种类型的支持,因此实际上也只能支持三种类型。如果能够找到,解析结果将放在Rails env的action_dispatch.request.request_parameters项内。

下一个Middleware是ActionDispatch::Head,定义在actionpack-3.2.13/lib/action_dispatch/middleware/head.rb中:

module ActionDispatch
  class Head
    def initialize(app)
      @app = app
    end

    def call(env)
      if env["REQUEST_METHOD"] == "HEAD"
        env["REQUEST_METHOD"] = "GET"
        env["rack.methodoverride.original_method"] = "HEAD"
        status, headers, _ = @app.call(env)
        [status, headers, []]
      else
        @app.call(env)
      end
    end
  end
end

Rails本身不直接支持HEAD请求,但是通过这个Middleware可以提供简单支持。这里先把Request Method改成GET请求,然后把原来的方法写入Rails env的rack.methodoverride.original_method项,接着按照GET请求的方法继续响应请求。请求结束后,将body去除,只留下header和status code。

下一个Middleware是Rack::ConditionalGet,定义在rack-1.4.5/lib/rack/conditionalget.rb

require 'rack/utils'

module Rack

  # Middleware that enables conditional GET using If-None-Match and
  # If-Modified-Since. The application should set either or both of the
  # Last-Modified or Etag response headers according to RFC 2616. When
  # either of the conditions is met, the response body is set to be zero
  # length and the response status is set to 304 Not Modified.
  #
  # Applications that defer response body generation until the body's each
  # message is received will avoid response body generation completely when
  # a conditional GET matches.
  #
  # Adapted from Michael Klishin's Merb implementation:
  # http://github.com/wycats/merb-core/tree/master/lib/merb-core/rack/middleware/conditional_get.rb
  class ConditionalGet
    def initialize(app)
      @app = app
    end

    def call(env)
      case env['REQUEST_METHOD']
      when "GET", "HEAD"
        status, headers, body = @app.call(env)
        headers = Utils::HeaderHash.new(headers)
        if status == 200 && fresh?(env, headers)
          status = 304
          headers.delete('Content-Type')
          headers.delete('Content-Length')
          body = []
        end
        [status, headers, body]
      else
        @app.call(env)
      end
    end

  private

    def fresh?(env, headers)
      modified_since = env['HTTP_IF_MODIFIED_SINCE']
      none_match     = env['HTTP_IF_NONE_MATCH']

      return false unless modified_since || none_match

      success = true
      success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
      success &&= etag_matches?(none_match, headers) if none_match
      success
    end

    def etag_matches?(none_match, headers)
      etag = headers['ETag'] and etag == none_match
    end

    def modified_since?(modified_since, headers)
      last_modified = to_rfc2822(headers['Last-Modified']) and
        modified_since and
        modified_since >= last_modified
    end

    def to_rfc2822(since)
      Time.rfc2822(since) rescue nil
    end
  end
end

这个Middleware能简单处理请求中包含HTTP_IF_MODIFIED_SINCEHTTP_IF_NONE_MATCH的情况。当返回的Response的Header中也包含Last-ModifiedETag项时,并且Last-Modified小于等于HTTP_IF_MODIFIED_SINCE,且ETag等于HTTP_IF_NONE_MATCH时,将返回304请求而不是200请求,同时消除返回的body内容,这个方法虽然不能节省服务器的性能损耗,但是可以提高响应速度。

下一个请求是Rack::ETag,定义在rack-1.4.5/lib/rack/etag.rb中:

require 'digest/md5'

module Rack
  # Automatically sets the ETag header on all String bodies.
  #
  # The ETag header is skipped if ETag or Last-Modified headers are sent or if
  # a sendfile body (body.responds_to :to_path) is given (since such cases
  # should be handled by apache/nginx).
  #
  # On initialization, you can pass two parameters: a Cache-Control directive
  # used when Etag is absent and a directive when it is present. The first
  # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
  class ETag
    DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze

    def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
      @app = app
      @cache_control = cache_control
      @no_cache_control = no_cache_control
    end

    def call(env)
      status, headers, body = @app.call(env)

      if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
        digest, body = digest_body(body)
        headers['ETag'] = %("#{digest}") if digest
      end

      unless headers['Cache-Control']
        if digest
          headers['Cache-Control'] = @cache_control if @cache_control
        else
          headers['Cache-Control'] = @no_cache_control if @no_cache_control
        end
      end

      [status, headers, body]
    end

    private

      def etag_status?(status)
        status == 200 || status == 201
      end

      def etag_body?(body)
        !body.respond_to?(:to_path)
      end

      def skip_caching?(headers)
        (headers['Cache-Control'] && headers['Cache-Control'].include?('no-cache')) ||
          headers.key?('ETag') || headers.key?('Last-Modified')
      end

      def digest_body(body)
        parts = []
        body.each { |part| parts << part }
        string_body = parts.join
        digest = Digest::MD5.hexdigest(string_body) unless string_body.empty?
        [digest, parts]
      end
  end
end

这个Middleware试图为每个有需求的Response添加ETag项和Cache-Control项。添加ETag的条件是,Status码在200或201,body没有实现to_path方法(在Rails中代表这是sendfile的Header),并且Cache-Control项中没有包含no-cache,Header中也没有ETag或者是Last-Modified。生成ETag的方法是对body的内容做MD5运算,结果前后各增加一个双引号。如果没有Cache-Control,那么如果之前生成了ETag,这里设定的Cache-Control是默认的"max-age=0, private, must-revalidate",如果没有生成过则使用Rails在声明这个Middleware时定义的'no-cache'。

最后一个Middleware是ActionDispatch::BestStandardsSupport,定义在actionpack-3.2.13/lib/action_dispatch/middleware/best_standards_support.rb

module ActionDispatch
  class BestStandardsSupport
    def initialize(app, type = true)
      @app = app

      @header = case type
      when true
        "IE=Edge,chrome=1"
      when :builtin
        "IE=Edge"
      when false
        nil
      end
    end

    def call(env)
      status, headers, body = @app.call(env)

      if headers["X-UA-Compatible"] && @header
        unless headers["X-UA-Compatible"][@header]
          headers["X-UA-Compatible"] << "," << @header.to_s
        end
      else
        headers["X-UA-Compatible"] = @header
      end

      [status, headers, body]
    end
  end
end

这个Middleware完全为了Internet Explorer而存在,为了尽可能使Internet Explorer使用最高版本的内核解析HTML,如果安装了Google Chrome Frame,则启用它来让Internet Explorer使用Webkit核心来解析HTML。可以使用config.action_dispatch.best_standards_support选项来对这一功能进行配置。

至此,这一阶段的Middleware已经结束,下一章将开始解析Rails Router的源代码。