aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/middleware
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware')
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb287
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb23
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb66
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb53
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb22
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb14
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb42
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb20
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb129
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb87
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb65
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