diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware')
15 files changed, 451 insertions, 383 deletions
| diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 07d97bd6bd..2889acaeb8 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,15 +1,57 @@  require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/object/blank'  require 'active_support/key_generator'  require 'active_support/message_verifier'  require 'active_support/json'  module ActionDispatch -  class Request < Rack::Request +  class Request      def cookie_jar -      env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(env, host, ssl?, cookies) +      fetch_header('action_dispatch.cookies'.freeze) do +        self.cookie_jar = Cookies::CookieJar.build(self, cookies) +      end +    end + +    # :stopdoc: +    def have_cookie_jar? +      has_header? 'action_dispatch.cookies'.freeze +    end + +    def cookie_jar=(jar) +      set_header 'action_dispatch.cookies'.freeze, jar +    end + +    def key_generator +      get_header Cookies::GENERATOR_KEY +    end + +    def signed_cookie_salt +      get_header Cookies::SIGNED_COOKIE_SALT +    end + +    def encrypted_cookie_salt +      get_header Cookies::ENCRYPTED_COOKIE_SALT +    end + +    def encrypted_signed_cookie_salt +      get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT +    end + +    def secret_token +      get_header Cookies::SECRET_TOKEN +    end + +    def secret_key_base +      get_header Cookies::SECRET_KEY_BASE +    end + +    def cookies_serializer +      get_header Cookies::COOKIES_SERIALIZER +    end + +    def cookies_digest +      get_header Cookies::COOKIES_DIGEST      end +    # :startdoc:    end    # \Cookies are read and written through ActionController#cookies. @@ -118,7 +160,7 @@ module ActionDispatch        #   cookies.permanent.signed[:remember_me] = current_user.id        #   # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT        def permanent -        @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) +        @permanent ||= PermanentCookieJar.new(self)        end        # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from @@ -138,10 +180,10 @@ module ActionDispatch        #   cookies.signed[:discount] # => 45        def signed          @signed ||= -          if @options[:upgrade_legacy_signed_cookies] -            UpgradeLegacySignedCookieJar.new(self, @key_generator, @options) +          if upgrade_legacy_signed_cookies? +            UpgradeLegacySignedCookieJar.new(self)            else -            SignedCookieJar.new(self, @key_generator, @options) +            SignedCookieJar.new(self)            end        end @@ -161,10 +203,10 @@ module ActionDispatch        #   cookies.encrypted[:discount] # => 45        def encrypted          @encrypted ||= -          if @options[:upgrade_legacy_signed_cookies] -            UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options) +          if upgrade_legacy_signed_cookies? +            UpgradeLegacyEncryptedCookieJar.new(self)            else -            EncryptedCookieJar.new(self, @key_generator, @options) +            EncryptedCookieJar.new(self)            end        end @@ -172,12 +214,18 @@ module ActionDispatch        # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.        def signed_or_encrypted          @signed_or_encrypted ||= -          if @options[:secret_key_base].present? +          if request.secret_key_base.present?              encrypted            else              signed            end        end + +      private + +      def upgrade_legacy_signed_cookies? +        request.secret_token.present? && request.secret_key_base.present? +      end      end      # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream @@ -187,7 +235,7 @@ module ActionDispatch      module VerifyAndUpgradeLegacySignedMessage # :nodoc:        def initialize(*args)          super -        @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) +        @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer)        end        def verify_and_upgrade_legacy_signed_message(name, signed_message) @@ -197,6 +245,11 @@ module ActionDispatch        rescue ActiveSupport::MessageVerifier::InvalidSignature          nil        end + +      private +        def parse(name, signed_message) +          super || verify_and_upgrade_legacy_signed_message(name, signed_message) +        end      end      class CookieJar #:nodoc: @@ -216,34 +269,18 @@ module ActionDispatch        # $& => example.local        DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ -      def self.options_for_env(env) #:nodoc: -        { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', -          encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', -          encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', -          secret_token: env[SECRET_TOKEN], -          secret_key_base: env[SECRET_KEY_BASE], -          upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, -          serializer: env[COOKIES_SERIALIZER], -          digest: env[COOKIES_DIGEST] -        } -      end - -      def self.build(env, host, secure, cookies) -        key_generator = env[GENERATOR_KEY] -        options = options_for_env env - -        new(key_generator, host, secure, options).tap do |hash| +      def self.build(req, cookies) +        new(req).tap do |hash|            hash.update(cookies)          end        end -      def initialize(key_generator, host = nil, secure = false, options = {}) -        @key_generator = key_generator +      attr_reader :request + +      def initialize(request)          @set_cookies = {}          @delete_cookies = {} -        @host = host -        @secure = secure -        @options = options +        @request = request          @cookies = {}          @committed = false        end @@ -279,6 +316,13 @@ module ActionDispatch          self        end +      def update_cookies_from_jar +        request_jar = @request.cookie_jar.instance_variable_get(:@cookies) +        set_cookies = request_jar.reject { |k,_| @delete_cookies.key?(k) } + +        @cookies.update set_cookies if set_cookies +      end +        def to_header          @cookies.map { |k,v| "#{k}=#{v}" }.join ';'        end @@ -292,12 +336,12 @@ module ActionDispatch            # if host is not ip and matches domain regexp            # (ip confirms to domain regexp so we explicitly check for ip) -          options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp) +          options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)              ".#{$&}"            end          elsif options[:domain].is_a? Array            # if host matches one of the supplied domains without a dot in front of it -          options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') } +          options[:domain] = options[:domain].find {|domain| request.host.include? domain.sub(/^\./, '') }          end        end @@ -352,47 +396,71 @@ module ActionDispatch        end        def write(headers) -        @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } -        @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } -      end - -      def recycle! #:nodoc: -        @set_cookies = {} -        @delete_cookies = {} +        if header = make_set_cookie_header(headers[HTTP_HEADER]) +          headers[HTTP_HEADER] = header +        end        end        mattr_accessor :always_write_cookie        self.always_write_cookie = false        private -        def write_cookie?(cookie) -          @secure || !cookie[:secure] || always_write_cookie -        end + +      def make_set_cookie_header(header) +        header = @set_cookies.inject(header) { |m, (k, v)| +          if write_cookie?(v) +            ::Rack::Utils.add_cookie_to_header(m, k, v) +          else +            m +          end +        } +        @delete_cookies.inject(header) { |m, (k, v)| +          ::Rack::Utils.add_remove_cookie_to_header(m, k, v) +        } +      end + +      def write_cookie?(cookie) +        request.ssl? || !cookie[:secure] || always_write_cookie +      end      end -    class PermanentCookieJar #:nodoc: +    class AbstractCookieJar # :nodoc:        include ChainedCookieJars -      def initialize(parent_jar, key_generator, options = {}) +      def initialize(parent_jar)          @parent_jar = parent_jar -        @key_generator = key_generator -        @options = options        end        def [](name) -        @parent_jar[name.to_s] +        if data = @parent_jar[name.to_s] +          parse name, data +        end        end        def []=(name, options)          if options.is_a?(Hash)            options.symbolize_keys!          else -          options = { :value => options } +          options = { value: options }          end -        options[:expires] = 20.years.from_now +        commit(options)          @parent_jar[name] = options        end + +      protected +        def request; @parent_jar.request; end + +      private +        def parse(name, data); data; end +        def commit(options); end +    end + +    class PermanentCookieJar < AbstractCookieJar # :nodoc: +      private +        def commit(options) +          options[:expires] = 20.years.from_now +        end      end      class JsonSerializer # :nodoc: @@ -410,7 +478,7 @@ module ActionDispatch        protected          def needs_migration?(value) -          @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) +          request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)          end          def serialize(value) @@ -430,7 +498,7 @@ module ActionDispatch          end          def serializer -          serializer = @options[:serializer] || :marshal +          serializer = request.cookies_serializer || :marshal            case serializer            when :marshal              Marshal @@ -442,48 +510,32 @@ module ActionDispatch          end          def digest -          @options[:digest] || 'SHA1' +          request.cookies_digest || 'SHA1' +        end + +        def key_generator +          request.key_generator          end      end -    class SignedCookieJar #:nodoc: -      include ChainedCookieJars +    class SignedCookieJar < AbstractCookieJar # :nodoc:        include SerializedCookieJars -      def initialize(parent_jar, key_generator, options = {}) -        @parent_jar = parent_jar -        @options = options -        secret = key_generator.generate_key(@options[:signed_cookie_salt]) +      def initialize(parent_jar) +        super +        secret = key_generator.generate_key(request.signed_cookie_salt)          @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)        end -      # Returns the value of the cookie by +name+ if it is untampered, -      # returns +nil+ otherwise or if no such cookie exists. -      def [](name) -        if signed_message = @parent_jar[name] -          deserialize name, verify(signed_message) +      private +        def parse(name, signed_message) +          deserialize name, @verifier.verified(signed_message)          end -      end -      # Signs and sets the cookie named +name+. The second argument may be the cookie's -      # value or a hash of options as documented above. -      def []=(name, options) -        if options.is_a?(Hash) -          options.symbolize_keys! +        def commit(options)            options[:value] = @verifier.generate(serialize(options[:value])) -        else -          options = { :value => @verifier.generate(serialize(options)) } -        end - -        raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE -        @parent_jar[name] = options -      end -      private -        def verify(signed_message) -          @verifier.verify(signed_message) -        rescue ActiveSupport::MessageVerifier::InvalidSignature -          nil +          raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE          end      end @@ -493,60 +545,36 @@ module ActionDispatch      # re-saves them using the new key generator to provide a smooth upgrade path.      class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:        include VerifyAndUpgradeLegacySignedMessage - -      def [](name) -        if signed_message = @parent_jar[name] -          deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message) -        end -      end      end -    class EncryptedCookieJar #:nodoc: -      include ChainedCookieJars +    class EncryptedCookieJar < AbstractCookieJar # :nodoc:        include SerializedCookieJars -      def initialize(parent_jar, key_generator, options = {}) +      def initialize(parent_jar) +        super +          if ActiveSupport::LegacyKeyGenerator === key_generator            raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +              "Read the upgrade documentation to learn more about this new config option."          end -        @parent_jar = parent_jar -        @options = options -        secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) -        sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) +        secret = key_generator.generate_key(request.encrypted_cookie_salt || '') +        sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '')          @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)        end -      # Returns the value of the cookie by +name+ if it is untampered, -      # returns +nil+ otherwise or if no such cookie exists. -      def [](name) -        if encrypted_message = @parent_jar[name] -          deserialize name, decrypt_and_verify(encrypted_message) -        end -      end - -      # Encrypts and sets the cookie named +name+. The second argument may be the cookie's -      # value or a hash of options as documented above. -      def []=(name, options) -        if options.is_a?(Hash) -          options.symbolize_keys! -        else -          options = { :value => options } -        end - -        options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) - -        raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE -        @parent_jar[name] = options -      end -        private -        def decrypt_and_verify(encrypted_message) -          @encryptor.decrypt_and_verify(encrypted_message) +        def parse(name, encrypted_message) +          deserialize name, @encryptor.decrypt_and_verify(encrypted_message)          rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage            nil          end + +        def commit(options) +          options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) + +          raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE +        end      end      # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore @@ -555,12 +583,6 @@ module ActionDispatch      # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.      class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:        include VerifyAndUpgradeLegacySignedMessage - -      def [](name) -        if encrypted_or_signed_message = @parent_jar[name] -          deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) -        end -      end      end      def initialize(app) @@ -568,9 +590,12 @@ module ActionDispatch      end      def call(env) +      request = ActionDispatch::Request.new env +        status, headers, body = @app.call(env) -      if cookie_jar = env['action_dispatch.cookies'] +      if request.have_cookie_jar? +        cookie_jar = request.cookie_jar          unless cookie_jar.committed?            cookie_jar.write(headers)            if headers[HTTP_HEADER].respond_to?(:join) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 9082aac271..66bb74b9c5 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -44,6 +44,7 @@ module ActionDispatch      end      def call(env) +      request = ActionDispatch::Request.new env        _, headers, body = response = @app.call(env)        if headers['X-Cascade'] == 'pass' @@ -53,18 +54,18 @@ module ActionDispatch        response      rescue Exception => exception -      raise exception if env['action_dispatch.show_exceptions'] == false -      render_exception(env, exception) +      raise exception unless request.show_exceptions? +      render_exception(request, exception)      end      private -    def render_exception(env, exception) -      wrapper = ExceptionWrapper.new(env, exception) -      log_error(env, wrapper) +    def render_exception(request, exception) +      backtrace_cleaner = request.get_header('action_dispatch.backtrace_cleaner') +      wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) +      log_error(request, wrapper) -      if env['action_dispatch.show_detailed_exceptions'] -        request = Request.new(env) +      if request.get_header('action_dispatch.show_detailed_exceptions')          traces = wrapper.traces          trace_to_show = 'Application Trace' @@ -106,8 +107,8 @@ module ActionDispatch        [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]      end -    def log_error(env, wrapper) -      logger = logger(env) +    def log_error(request, wrapper) +      logger = logger(request)        return unless logger        exception = wrapper.exception @@ -123,8 +124,8 @@ module ActionDispatch        end      end -    def logger(env) -      env['action_dispatch.logger'] || stderr_logger +    def logger(request) +      request.logger || stderr_logger      end      def stderr_logger diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 8c3d45584d..5fd984cd07 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -31,10 +31,10 @@ module ActionDispatch        'ActionView::Template::Error'         => 'template_error'      ) -    attr_reader :env, :exception, :line_number, :file +    attr_reader :backtrace_cleaner, :exception, :line_number, :file -    def initialize(env, exception) -      @env = env +    def initialize(backtrace_cleaner, exception) +      @backtrace_cleaner = backtrace_cleaner        @exception = original_exception(exception)        expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError) @@ -61,7 +61,7 @@ module ActionDispatch      end      def traces -      appplication_trace_with_ids = [] +      application_trace_with_ids = []        framework_trace_with_ids = []        full_trace_with_ids = [] @@ -69,7 +69,7 @@ module ActionDispatch          trace_with_id = { id: idx, trace: trace }          if application_trace.include?(trace) -          appplication_trace_with_ids << trace_with_id +          application_trace_with_ids << trace_with_id          else            framework_trace_with_ids << trace_with_id          end @@ -78,7 +78,7 @@ module ActionDispatch        end        { -        "Application Trace" => appplication_trace_with_ids, +        "Application Trace" => application_trace_with_ids,          "Framework Trace" => framework_trace_with_ids,          "Full Trace" => full_trace_with_ids        } @@ -125,10 +125,6 @@ module ActionDispatch        end      end -    def backtrace_cleaner -      @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner'] -    end -      def source_fragment(path, line)        return unless Rails.respond_to?(:root) && Rails.root        full_path = Rails.root.join(path) diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 59639a010e..c51dcd542a 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,15 +1,6 @@  require 'active_support/core_ext/hash/keys'  module ActionDispatch -  class Request < Rack::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] ||= Flash::FlashHash.from_session_value(session["flash"]) -    end -  end -    # The flash provides a way to pass temporary primitive-types (String, Array, Hash) 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 @@ -47,6 +38,40 @@ module ActionDispatch    class Flash      KEY = 'action_dispatch.request.flash_hash'.freeze +    module RequestMethods +      # 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 +        flash = flash_hash +        return flash if flash +        self.flash = Flash::FlashHash.from_session_value(session["flash"]) +      end + +      def flash=(flash) +        set_header Flash::KEY, flash +      end + +      def flash_hash # :nodoc: +        get_header Flash::KEY +      end + +      def commit_flash # :nodoc: +        session    = self.session || {} +        flash_hash = self.flash_hash + +        if flash_hash && (flash_hash.present? || session.key?('flash')) +          session["flash"] = flash_hash.to_session_value +          self.flash = flash_hash.dup +        end + +        if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) +            session.key?('flash') && session['flash'].nil? +          session.delete('flash') +        end +      end +    end +      class FlashNow #:nodoc:        attr_accessor :flash @@ -258,25 +283,10 @@ module ActionDispatch        end      end -    def initialize(app) -      @app = app -    end - -    def call(env) -      @app.call(env) -    ensure -      session    = Request::Session.find(env) || {} -      flash_hash = env[KEY] - -      if flash_hash && (flash_hash.present? || session.key?('flash')) -        session["flash"] = flash_hash.to_session_value -        env[KEY] = flash_hash.dup -      end +    def self.new(app) app; end +  end -      if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) -        session.key?('flash') && session['flash'].nil? -        session.delete('flash') -      end -    end +  class Request +    prepend Flash::RequestMethods    end  end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index e2b3b06fd8..18af0a583a 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -1,9 +1,14 @@ -require 'active_support/core_ext/hash/conversions'  require 'action_dispatch/http/request' -require 'active_support/core_ext/hash/indifferent_access'  module ActionDispatch +  # ActionDispatch::ParamsParser works for all the requests having any Content-Length +  # (like POST). It takes raw data from the request and puts it through the parser +  # that is picked based on Content-Type header. +  # +  # In case of any error while parsing data ParamsParser::ParseError is raised.    class ParamsParser +    # Raised when raw data from the request cannot be parsed by the parser +    # defined for request's content mime type.      class ParseError < StandardError        attr_reader :original_exception @@ -13,43 +18,13 @@ module ActionDispatch        end      end -    DEFAULT_PARSERS = { -      Mime::JSON => lambda { |raw_post| -        data = ActiveSupport::JSON.decode(raw_post) -        data = {:_json => data} unless data.is_a?(Hash) -        Request::Utils.deep_munge(data).with_indifferent_access -      } -    } - -    def initialize(app, parsers = {}) -      @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) -    end - -    def call(env) -      default = env["action_dispatch.request.request_parameters"] -      env["action_dispatch.request.request_parameters"] = parse_formatted_parameters(env, @parsers, default) - -      @app.call(env) +    # Create a new +ParamsParser+ middleware instance. +    # +    # The +parsers+ argument can take Hash of parsers where key is identifying +    # content mime type, and value is a lambda that is going to process data. +    def self.new(app, parsers = {}) +      ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers) +      app      end - -    private -      def parse_formatted_parameters(env, parsers, default) -        request = Request.new(env) - -        return default if request.content_length.zero? - -        strategy = parsers.fetch(request.content_mime_type) { return default } - -        strategy.call(request.raw_post) - -      rescue => e # JSON or Ruby code block errors -        logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" - -        raise ParseError.new(e.message, e) -      end - -      def logger(env) -        env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr) -      end    end  end diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index 7cde76b30e..0f27984550 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -17,8 +17,8 @@ module ActionDispatch      end      def call(env) -      status       = env["PATH_INFO"][1..-1].to_i        request      = ActionDispatch::Request.new(env) +      status       = request.path_info[1..-1].to_i        content_type = request.formats.first        body         = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) } diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 6c7fba00cb..af9a29eb07 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation/reporting' -  module ActionDispatch    # ActionDispatch::Reloader provides prepare and cleanup callbacks,    # intended to assist with code reloading during development. diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 9f894e2ec6..aee2334da9 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -74,16 +74,17 @@ module ActionDispatch      # requests. For those requests that do need to know the IP, the      # GetIp#calculate_ip method will calculate the memoized client IP address.      def call(env) -      env["action_dispatch.remote_ip"] = GetIp.new(env, check_ip, proxies) -      @app.call(env) +      req = ActionDispatch::Request.new env +      req.remote_ip = GetIp.new(req, check_ip, proxies) +      @app.call(req.env)      end      # The GetIp class exists as a way to defer processing of the request data      # into an actual IP address. If the ActionDispatch::Request#remote_ip method      # is called, this class will calculate the value and then memoize it.      class GetIp -      def initialize(env, check_ip, proxies) -        @env      = env +      def initialize(req, check_ip, proxies) +        @req      = req          @check_ip = check_ip          @proxies  = proxies        end @@ -108,11 +109,11 @@ module ActionDispatch        # the last address left, which was presumably set by one of those proxies.        def calculate_ip          # Set by the Rack web server, this is a single value. -        remote_addr = ips_from('REMOTE_ADDR').last +        remote_addr = ips_from(@req.remote_addr).last          # Could be a CSV list and/or repeated headers that were concatenated. -        client_ips    = ips_from('HTTP_CLIENT_IP').reverse -        forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse +        client_ips    = ips_from(@req.client_ip).reverse +        forwarded_ips = ips_from(@req.x_forwarded_for).reverse          # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set.          # If they are both set, it means that this request passed through two @@ -123,8 +124,8 @@ module ActionDispatch          if should_check_ip && !forwarded_ips.include?(client_ips.last)            # We don't know which came from the proxy, and which from the user            raise IpSpoofAttackError, "IP spoofing attack?! " + -            "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} " + -            "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" +            "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " + +            "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"          end          # We assume these things about the IP headers: @@ -147,8 +148,9 @@ module ActionDispatch      protected        def ips_from(header) +        return [] unless header          # Split the comma-separated list into an array of strings -        ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : [] +        ips = header.strip.split(/[,\s]+/)          ips.select do |ip|            begin              # Only return IPs that are valid according to the IPAddr#new method diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 84df55fd5a..9e50fea3fc 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -36,6 +36,11 @@ module ActionDispatch          @default_options.delete(:sidbits)          @default_options.delete(:secure_random)        end + +      private +      def make_request(env) +        ActionDispatch::Request.new env +      end      end      module StaleSessionCheck @@ -65,8 +70,8 @@ module ActionDispatch      end      module SessionObject # :nodoc: -      def prepare_session(env) -        Request::Session.create(self, env, @default_options) +      def prepare_session(req) +        Request::Session.create(self, req, @default_options)        end        def loaded_session?(session) @@ -74,15 +79,14 @@ module ActionDispatch        end      end -    class AbstractStore < Rack::Session::Abstract::ID +    class AbstractStore < Rack::Session::Abstract::Persisted        include Compatibility        include StaleSessionCheck        include SessionObject        private -      def set_cookie(env, session_id, cookie) -        request = ActionDispatch::Request.new(env) +      def set_cookie(request, session_id, cookie)          request.cookie_jar[key] = cookie        end      end diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb index 857e49a682..589ae46e38 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -18,7 +18,7 @@ module ActionDispatch        end        # Get a session from the cache. -      def get_session(env, sid) +      def find_session(env, sid)          unless sid and session = @cache.read(cache_key(sid))            sid, session = generate_sid, {}          end @@ -26,7 +26,7 @@ module ActionDispatch        end        # Set a session in the cache. -      def set_session(env, sid, session, options) +      def write_session(env, sid, session, options)          key = cache_key(sid)          if session            @cache.write(key, session, :expires_in => options[:expire_after]) @@ -37,7 +37,7 @@ module ActionDispatch        end        # Remove a session from the cache. -      def destroy_session(env, sid, options) +      def delete_session(env, sid, options)          @cache.delete(cache_key(sid))          generate_sid        end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index d8f9614904..0e636b8257 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -53,7 +53,7 @@ module ActionDispatch      #      # Note that changing the secret key will invalidate all existing sessions!      # -    # Because CookieStore extends Rack::Session::Abstract::ID, many of the +    # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the      # options described there can be used to customize the session cookie that      # is generated. For example:      # @@ -62,25 +62,21 @@ module ActionDispatch      # would set the session cookie to expire automatically 14 days after creation.      # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and      # <tt>:httponly</tt>. -    class CookieStore < Rack::Session::Abstract::ID -      include Compatibility -      include StaleSessionCheck -      include SessionObject - +    class CookieStore < AbstractStore        def initialize(app, options={})          super(app, options.merge!(:cookie_only => true))        end -      def destroy_session(env, session_id, options) +      def delete_session(req, session_id, options)          new_sid = generate_sid unless options[:drop]          # Reset hash and Assign the new session id -        env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {} +        req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})          new_sid        end -      def load_session(env) +      def load_session(req)          stale_session_check! do -          data = unpacked_cookie_data(env) +          data = unpacked_cookie_data(req)            data = persistent_session_id!(data)            [data["session_id"], data]          end @@ -88,20 +84,21 @@ module ActionDispatch        private -      def extract_session_id(env) +      def extract_session_id(req)          stale_session_check! do -          unpacked_cookie_data(env)["session_id"] +          unpacked_cookie_data(req)["session_id"]          end        end -      def unpacked_cookie_data(env) -        env["action_dispatch.request.unsigned_session_cookie"] ||= begin -          stale_session_check! do -            if data = get_cookie(env) +      def unpacked_cookie_data(req) +        req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k| +          v = stale_session_check! do +            if data = get_cookie(req)                data.stringify_keys!              end              data || {}            end +          req.set_header k, v          end        end @@ -111,21 +108,20 @@ module ActionDispatch          data        end -      def set_session(env, sid, session_data, options) +      def write_session(req, sid, session_data, options)          session_data["session_id"] = sid          session_data        end -      def set_cookie(env, session_id, cookie) -        cookie_jar(env)[@key] = cookie +      def set_cookie(request, session_id, cookie) +        cookie_jar(request)[@key] = cookie        end -      def get_cookie(env) -        cookie_jar(env)[@key] +      def get_cookie(req) +        cookie_jar(req)[@key]        end -      def cookie_jar(env) -        request = ActionDispatch::Request.new(env) +      def cookie_jar(request)          request.cookie_jar.signed_or_encrypted        end      end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index f0779279c1..64695f9738 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -27,24 +27,26 @@ module ActionDispatch      end      def call(env) +      request = ActionDispatch::Request.new env        @app.call(env)      rescue Exception => exception -      if env['action_dispatch.show_exceptions'] == false -        raise exception +      if request.show_exceptions? +        render_exception(request, exception)        else -        render_exception(env, exception) +        raise exception        end      end      private -    def render_exception(env, exception) -      wrapper = ExceptionWrapper.new(env, exception) +    def render_exception(request, exception) +      backtrace_cleaner = request.get_header 'action_dispatch.backtrace_cleaner' +      wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)        status  = wrapper.status_code -      env["action_dispatch.exception"] = wrapper.exception -      env["action_dispatch.original_path"] = env["PATH_INFO"] -      env["PATH_INFO"] = "/#{status}" -      response = @exceptions_app.call(env) +      request.set_header "action_dispatch.exception", wrapper.exception +      request.set_header "action_dispatch.original_path", request.path_info +      request.path_info = "/#{status}" +      response = @exceptions_app.call(request.env)        response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response      rescue Exception => failsafe_error        $stderr.puts "Error during failsafe response: #{failsafe_error}\n  #{failsafe_error.backtrace * "\n  "}" diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 7b3d8bcc5b..47f475559a 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -1,72 +1,129 @@  module ActionDispatch +  # This middleware is added to the stack when `config.force_ssl = true`. +  # It does three jobs to enforce secure HTTP requests: +  # +  #   1. TLS redirect. http:// requests are permanently redirected to https:// +  #      with the same URL host, path, etc. Pass `:host` and/or `:port` to +  #      modify the destination URL. This is always enabled. +  # +  #   2. Secure cookies. Sets the `secure` flag on cookies to tell browsers they +  #      mustn't be sent along with http:// requests. This is always enabled. +  # +  #   3. HTTP Strict Transport Security (HSTS). Tells the browser to remember +  #      this site as TLS-only and automatically redirect non-TLS requests. +  #      Enabled by default. Pass `hsts: false` to disable. +  # +  # Configure HSTS with `hsts: { … }`: +  #   * `expires`: How long, in seconds, these settings will stick. Defaults to +  #     `180.days` (recommended). The minimum required to qualify for browser +  #     preload lists is `18.weeks`. +  #   * `subdomains`: Set to `true` to tell the browser to apply these settings +  #     to all subdomains. This protects your cookies from interception by a +  #     vulnerable site on a subdomain. Defaults to `false`. +  #   * `preload`: Advertise that this site may be included in browsers' +  #     preloaded HSTS lists. HSTS protects your site on every visit *except the +  #     first visit* since it hasn't seen your HSTS header yet. To close this +  #     gap, browser vendors include a baked-in list of HSTS-enabled sites. +  #     Go to https://hstspreload.appspot.com to submit your site for inclusion. +  # +  # Disabling HSTS: To turn off HSTS, omitting the header is not enough. +  # Browsers will remember the original HSTS directive until it expires. +  # Instead, use the header to tell browsers to expire HSTS immediately. +  # Setting `hsts: false` is a shortcut for `hsts: { expires: 0 }`.    class SSL -    YEAR = 31536000 +    # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/ +    # and greater than the 18-week requirement for browser preload lists. +    HSTS_EXPIRES_IN = 15552000      def self.default_hsts_options -      { :expires => YEAR, :subdomains => false } +      { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false }      end -    def initialize(app, options = {}) +    def initialize(app, redirect: {}, hsts: {}, **options)        @app = app -      @hsts = options.fetch(:hsts, {}) -      @hsts = {} if @hsts == true -      @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts +      if options[:host] || options[:port] +        ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc +          The `:host` and `:port` options are moving within `:redirect`: +          `config.ssl_options = { redirect: { host: …, port: … }}`. +        end_warning +        @redirect = options.slice(:host, :port) +      else +        @redirect = redirect +      end -      @host    = options[:host] -      @port    = options[:port] +      @hsts_header = build_hsts_header(normalize_hsts_options(hsts))      end      def call(env) -      request = Request.new(env) +      request = Request.new env        if request.ssl? -        status, headers, body = @app.call(env) -        headers.reverse_merge!(hsts_headers) -        flag_cookies_as_secure!(headers) -        [status, headers, body] +        @app.call(env).tap do |status, headers, body| +          set_hsts_header! headers +          flag_cookies_as_secure! headers +        end        else -        redirect_to_https(request) +        redirect_to_https request        end      end      private -      def redirect_to_https(request) -        host = @host || request.host -        port = @port || request.port - -        location = "https://#{host}" -        location << ":#{port}" if port != 80 -        location << request.fullpath - -        headers = { 'Content-Type' => 'text/html', 'Location' => location } - -        [301, headers, []] +      def set_hsts_header!(headers) +        headers['Strict-Transport-Security'.freeze] ||= @hsts_header        end -      # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02 -      def hsts_headers -        if @hsts -          value = "max-age=#{@hsts[:expires].to_i}" -          value += "; includeSubDomains" if @hsts[:subdomains] -          { 'Strict-Transport-Security' => value } +      def normalize_hsts_options(options) +        case options +        # Explicitly disabling HSTS clears the existing setting from browsers +        # by setting expiry to 0. +        when false +          self.class.default_hsts_options.merge(expires: 0) +        # Default to enabled, with default options. +        when nil, true +          self.class.default_hsts_options          else -          {} +          self.class.default_hsts_options.merge(options)          end        end +      # http://tools.ietf.org/html/rfc6797#section-6.1 +      def build_hsts_header(hsts) +        value = "max-age=#{hsts[:expires].to_i}" +        value << "; includeSubDomains" if hsts[:subdomains] +        value << "; preload" if hsts[:preload] +        value +      end +        def flag_cookies_as_secure!(headers) -        if cookies = headers['Set-Cookie'] -          cookies = cookies.split("\n") +        if cookies = headers['Set-Cookie'.freeze] +          cookies = cookies.split("\n".freeze) -          headers['Set-Cookie'] = cookies.map { |cookie| +          headers['Set-Cookie'.freeze] = cookies.map { |cookie|              if cookie !~ /;\s*secure\s*(;|$)/i                "#{cookie}; secure"              else                cookie              end -          }.join("\n") +          }.join("\n".freeze)          end        end + +      def redirect_to_https(request) +        [ @redirect.fetch(:status, 301), +          { 'Content-Type' => 'text/html', +            'Location' => https_location_for(request) }, +          @redirect.fetch(:body, []) ] +      end + +      def https_location_for(request) +        host = @redirect[:host] || request.host +        port = @redirect[:port] || request.port + +        location = "https://#{host}" +        location << ":#{port}" if port != 80 && port != 443 +        location << request.fullpath +        location +      end    end  end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index bbf734f103..90e2ae6802 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -4,36 +4,15 @@ require "active_support/dependencies"  module ActionDispatch    class MiddlewareStack      class Middleware -      attr_reader :args, :block, :name, :classcache +      attr_reader :args, :block, :klass -      def initialize(klass_or_name, *args, &block) -        @klass = nil - -        if klass_or_name.respond_to?(:name) -          @klass = klass_or_name -          @name  = @klass.name -        else -          @name  = klass_or_name.to_s -        end - -        @classcache = ActiveSupport::Dependencies::Reference -        @args, @block = args, block +      def initialize(klass, args, block) +        @klass = klass +        @args  = args +        @block = block        end -      def klass -        @klass || classcache[@name] -      end - -      def ==(middleware) -        case middleware -        when Middleware -          klass == middleware.klass -        when Class -          klass == middleware -        else -          normalize(@name) == normalize(middleware) -        end -      end +      def name; klass.name; end        def inspect          klass.to_s @@ -42,12 +21,6 @@ module ActionDispatch        def build(app)          klass.new(app, *args, &block)        end - -    private - -      def normalize(object) -        object.to_s.strip.sub(/^::/, '') -      end      end      include Enumerable @@ -75,19 +48,17 @@ module ActionDispatch        middlewares[i]      end -    def unshift(*args, &block) -      middleware = self.class::Middleware.new(*args, &block) -      middlewares.unshift(middleware) +    def unshift(klass, *args, &block) +      middlewares.unshift(build_middleware(klass, args, block))      end      def initialize_copy(other)        self.middlewares = other.middlewares.dup      end -    def insert(index, *args, &block) +    def insert(index, klass, *args, &block)        index = assert_index(index, :before) -      middleware = self.class::Middleware.new(*args, &block) -      middlewares.insert(index, middleware) +      middlewares.insert(index, build_middleware(klass, args, block))      end      alias_method :insert_before, :insert @@ -104,26 +75,46 @@ module ActionDispatch      end      def delete(target) -      middlewares.delete target +      target = get_class target +      middlewares.delete_if { |m| m.klass == target }      end -    def use(*args, &block) -      middleware = self.class::Middleware.new(*args, &block) -      middlewares.push(middleware) +    def use(klass, *args, &block) +      middlewares.push(build_middleware(klass, args, block))      end -    def build(app = nil, &block) -      app ||= block -      raise "MiddlewareStack#build requires an app" unless app +    def build(app = Proc.new)        middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }      end -  protected +    private      def assert_index(index, where) -      i = index.is_a?(Integer) ? index : middlewares.index(index) +      index = get_class index +      i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }        raise "No such middleware to insert #{where}: #{index.inspect}" unless i        i      end + +    def get_class(klass) +      if klass.is_a?(String) || klass.is_a?(Symbol) +        classcache = ActiveSupport::Dependencies::Reference +        converted_klass = classcache[klass.to_s] +        ActiveSupport::Deprecation.warn <<-eowarn +Passing strings or symbols to the middleware builder is deprecated, please change +them to actual class references.  For example: + +  "#{klass}" => #{converted_klass} + +        eowarn +        converted_klass +      else +        klass +      end +    end + +    def build_middleware(klass, args, block) +      Middleware.new(get_class(klass), args, block) +    end    end  end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index b098ea389f..75f8e05a3f 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -3,8 +3,8 @@ require 'active_support/core_ext/uri'  module ActionDispatch    # This middleware returns a file's contents from disk in the body response. -  # When initialized, it can accept an optional 'Cache-Control' header, which -  # will be set when a response containing a file's contents is delivered. +  # When initialized, it can accept optional HTTP headers, which will be set +  # when a response containing a file's contents is delivered.    #    # This middleware will render the file specified in `env["PATH_INFO"]`    # where the base path is in the +root+ directory. For example, if the +root+ @@ -13,12 +13,11 @@ module ActionDispatch    # located at `public/assets/application.js` if the file exists. If the file    # does not exist, a 404 "File not Found" response will be returned.    class FileHandler -    def initialize(root, cache_control, index: 'index') +    def initialize(root, index: 'index', headers: {})        @root          = root.chomp('/')        @compiled_root = /^#{Regexp.escape(root)}/ -      headers        = cache_control && { 'Cache-Control' => cache_control } -      @file_server = ::Rack::File.new(@root, headers) -      @index = index +      @file_server   = ::Rack::File.new(@root, headers) +      @index         = index      end      # Takes a path to a file. If the file is found, has valid encoding, and has @@ -28,14 +27,14 @@ module ActionDispatch      # Used by the `Static` class to check the existence of a valid file      # in the server's `public/` directory (see Static#call).      def match?(path) -      path = URI.parser.unescape(path) +      path = ::Rack::Utils.unescape_path path        return false unless path.valid_encoding?        path = Rack::Utils.clean_path_info path        paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]        if match = paths.detect { |p| -        path = File.join(@root, p.force_encoding('UTF-8')) +        path = File.join(@root, p.force_encoding('UTF-8'.freeze))          begin            File.file?(path) && File.readable?(path)          rescue SystemCallError @@ -43,31 +42,35 @@ module ActionDispatch          end        } -        return ::Rack::Utils.escape(match) +        return ::Rack::Utils.escape_path(match)        end      end      def call(env) -      path      = env['PATH_INFO'] +      serve ActionDispatch::Request.new env +    end + +    def serve(request) +      path      = request.path_info        gzip_path = gzip_file_path(path) -      if gzip_path && gzip_encoding_accepted?(env) -        env['PATH_INFO']            = gzip_path -        status, headers, body       = @file_server.call(env) +      if gzip_path && gzip_encoding_accepted?(request) +        request.path_info           = gzip_path +        status, headers, body       = @file_server.call(request.env)          if status == 304            return [status, headers, body]          end          headers['Content-Encoding'] = 'gzip'          headers['Content-Type']     = content_type(path)        else -        status, headers, body = @file_server.call(env) +        status, headers, body = @file_server.call(request.env)        end        headers['Vary'] = 'Accept-Encoding' if gzip_path        return [status, headers, body]      ensure -      env['PATH_INFO'] = path +      request.path_info = path      end      private @@ -76,17 +79,17 @@ module ActionDispatch        end        def content_type(path) -        ::Rack::Mime.mime_type(::File.extname(path), 'text/plain') +        ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze)        end -      def gzip_encoding_accepted?(env) -        env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i +      def gzip_encoding_accepted?(request) +        request.accept_encoding =~ /\bgzip\b/i        end        def gzip_file_path(path)          can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/          gzip_path     = "#{path}.gz" -        if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path))) +        if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))            gzip_path          else            false @@ -104,22 +107,30 @@ module ActionDispatch    # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'    # requests will result in a file being returned.    class Static -    def initialize(app, path, cache_control = nil, index: 'index') +    def initialize(app, path, deprecated_cache_control = :not_set, index: 'index', headers: {}) +      if deprecated_cache_control != :not_set +        ActiveSupport::Deprecation.warn("The `cache_control` argument is deprecated," \ +                                        "replaced by `headers: { 'Cache-Control' => #{deprecated_cache_control} }`, " \ +                                        " and will be removed in Rails 5.1.") +        headers['Cache-Control'.freeze] = deprecated_cache_control +      end +        @app = app -      @file_handler = FileHandler.new(path, cache_control, index: index) +      @file_handler = FileHandler.new(path, index: index, headers: headers)      end      def call(env) -      case env['REQUEST_METHOD'] -      when 'GET', 'HEAD' -        path = env['PATH_INFO'].chomp('/') +      req = ActionDispatch::Request.new env + +      if req.get? || req.head? +        path = req.path_info.chomp('/'.freeze)          if match = @file_handler.match?(path) -          env['PATH_INFO'] = match -          return @file_handler.call(env) +          req.path_info = match +          return @file_handler.serve(req)          end        end -      @app.call(env) +      @app.call(req.env)      end    end  end | 
