diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware')
12 files changed, 272 insertions, 233 deletions
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index cf4f654ed6..b653e4eacd 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,55 +1,55 @@ 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'.freeze] ||= Cookies::CookieJar.build(self, cookies) + fetch_header('action_dispatch.cookies'.freeze) do + self.cookie_jar = Cookies::CookieJar.build(self, cookies) + end end # :stopdoc: def have_cookie_jar? - env.key? 'action_dispatch.cookies'.freeze + has_header? 'action_dispatch.cookies'.freeze end def cookie_jar=(jar) - env['action_dispatch.cookies'.freeze] = jar + set_header 'action_dispatch.cookies'.freeze, jar end def key_generator - env[Cookies::GENERATOR_KEY] + get_header Cookies::GENERATOR_KEY end def signed_cookie_salt - env[Cookies::SIGNED_COOKIE_SALT] + get_header Cookies::SIGNED_COOKIE_SALT end def encrypted_cookie_salt - env[Cookies::ENCRYPTED_COOKIE_SALT] + get_header Cookies::ENCRYPTED_COOKIE_SALT end def encrypted_signed_cookie_salt - env[Cookies::ENCRYPTED_SIGNED_COOKIE_SALT] + get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT end def secret_token - env[Cookies::SECRET_TOKEN] + get_header Cookies::SECRET_TOKEN end def secret_key_base - env[Cookies::SECRET_KEY_BASE] + get_header Cookies::SECRET_KEY_BASE end def cookies_serializer - env[Cookies::COOKIES_SERIALIZER] + get_header Cookies::COOKIES_SERIALIZER end def cookies_digest - env[Cookies::COOKIES_DIGEST] + get_header Cookies::COOKIES_DIGEST end # :startdoc: end @@ -221,19 +221,11 @@ module ActionDispatch end end - protected - - def request; @parent_jar.request; end - private def upgrade_legacy_signed_cookies? request.secret_token.present? && request.secret_key_base.present? end - - def key_generator - request.key_generator - end end # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream @@ -253,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: @@ -319,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 @@ -405,7 +409,7 @@ module ActionDispatch end end - class PermanentCookieJar #:nodoc: + class AbstractCookieJar # :nodoc: include ChainedCookieJars def initialize(parent_jar) @@ -413,19 +417,35 @@ module ActionDispatch 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: @@ -477,45 +497,30 @@ module ActionDispatch def digest 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) - @parent_jar = 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 @@ -525,20 +530,13 @@ 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) - @parent_jar = parent_jar + super if ActiveSupport::LegacyKeyGenerator === key_generator raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + @@ -550,35 +548,18 @@ module ActionDispatch @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 @@ -587,12 +568,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) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 226a688fe2..66bb74b9c5 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -55,18 +55,17 @@ module ActionDispatch response rescue Exception => exception raise exception unless request.show_exceptions? - render_exception(env, exception) + render_exception(request, exception) end private - def render_exception(env, exception) - backtrace_cleaner = env['action_dispatch.backtrace_cleaner'] + def render_exception(request, exception) + backtrace_cleaner = request.get_header('action_dispatch.backtrace_cleaner') wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) - log_error(env, wrapper) + 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' @@ -108,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 @@ -125,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/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 23da169b22..c482b1c5e7 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,20 +1,22 @@ require 'active_support/core_ext/hash/keys' module ActionDispatch - class Request < Rack::Request + class Request # Access the contents of the flash. Use <tt>flash["notice"]</tt> to # read a notice you put there or <tt>flash["notice"] = "hello"</tt> # to put a new one. def flash - @env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"]) + flash = flash_hash + return flash if flash + self.flash = Flash::FlashHash.from_session_value(session["flash"]) end def flash=(flash) - @env[Flash::KEY] = flash + set_header Flash::KEY, flash end def flash_hash # :nodoc: - @env[Flash::KEY] + get_header Flash::KEY end end @@ -274,7 +276,7 @@ module ActionDispatch req = ActionDispatch::Request.new env @app.call(env) ensure - session = Request::Session.find(env) || {} + session = Request::Session.find(req) || {} flash_hash = req.flash_hash if flash_hash && (flash_hash.present? || session.key?('flash')) diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index e65279a285..9cde9c9b98 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 @@ -21,6 +26,10 @@ module ActionDispatch } } + # 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 initialize(app, parsers = {}) @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) end @@ -28,7 +37,9 @@ module ActionDispatch def call(env) request = Request.new(env) - request.request_parameters = parse_formatted_parameters(request, @parsers) + parse_formatted_parameters(request, @parsers) do |params| + request.request_parameters = params + end @app.call(env) end @@ -39,7 +50,7 @@ module ActionDispatch strategy = parsers.fetch(request.content_mime_type) { return nil } - strategy.call(request.raw_post) + yield strategy.call(request.raw_post) rescue => e # JSON or Ruby code block errors logger(request).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" 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/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..02b6cfe727 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,7 +62,7 @@ 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 + class CookieStore < Rack::Session::Abstract::Persisted include Compatibility include StaleSessionCheck include SessionObject @@ -71,16 +71,16 @@ module ActionDispatch 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 +88,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 +112,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 12d8dab8eb..64695f9738 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -31,7 +31,7 @@ module ActionDispatch @app.call(env) rescue Exception => exception if request.show_exceptions? - render_exception(env, exception) + render_exception(request, exception) else raise exception end @@ -39,14 +39,14 @@ module ActionDispatch private - def render_exception(env, exception) - backtrace_cleaner = env['action_dispatch.backtrace_cleaner'] + 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 9462ae4278..c4344c9609 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -28,7 +28,7 @@ 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 @@ -43,7 +43,7 @@ module ActionDispatch end } - return ::Rack::Utils.escape(match) + return ::Rack::Utils.escape_path(match) end end @@ -90,7 +90,7 @@ module ActionDispatch 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 |