diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware')
24 files changed, 799 insertions, 699 deletions
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index c782779b34..5b2ad36dd5 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActionDispatch # Provides callbacks to be executed before and after dispatching the request. @@ -7,17 +8,6 @@ module ActionDispatch define_callbacks :call class << self - def to_prepare(*args, &block) - ActiveSupport::Reloader.to_prepare(*args, &block) - end - - def to_cleanup(*args, &block) - ActiveSupport::Reloader.to_complete(*args, &block) - end - - deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead', - to_cleanup: 'use ActiveSupport::Reloader.to_complete instead' - def before(*args, &block) set_callback(:call, :before, *args, &block) end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index f2f3150b56..ea4156c972 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,13 +1,15 @@ -require 'active_support/core_ext/hash/keys' -require 'active_support/key_generator' -require 'active_support/message_verifier' -require 'active_support/json' -require 'rack/utils' +# frozen_string_literal: true + +require "active_support/core_ext/hash/keys" +require "active_support/key_generator" +require "active_support/message_verifier" +require "active_support/json" +require "rack/utils" module ActionDispatch class Request def cookie_jar - fetch_header('action_dispatch.cookies'.freeze) do + fetch_header("action_dispatch.cookies".freeze) do self.cookie_jar = Cookies::CookieJar.build(self, cookies) end end @@ -20,11 +22,11 @@ module ActionDispatch } def have_cookie_jar? - has_header? 'action_dispatch.cookies'.freeze + has_header? "action_dispatch.cookies".freeze end def cookie_jar=(jar) - set_header 'action_dispatch.cookies'.freeze, jar + set_header "action_dispatch.cookies".freeze, jar end def key_generator @@ -43,6 +45,22 @@ module ActionDispatch get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT end + def authenticated_encrypted_cookie_salt + get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT + end + + def use_authenticated_cookie_encryption + get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION + end + + def encrypted_cookie_cipher + get_header Cookies::ENCRYPTED_COOKIE_CIPHER + end + + def signed_cookie_digest + get_header Cookies::SIGNED_COOKIE_DIGEST + end + def secret_token get_header Cookies::SECRET_TOKEN end @@ -58,6 +76,11 @@ module ActionDispatch def cookies_digest get_header Cookies::COOKIES_DIGEST end + + def cookies_rotations + get_header Cookies::COOKIES_ROTATIONS + end + # :startdoc: end @@ -77,16 +100,17 @@ module ActionDispatch # cookies[:lat_lon] = JSON.generate([47.68, -122.37]) # # # Sets a cookie that expires in 1 hour. - # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } + # cookies[:login] = { value: "XJ-122", expires: 1.hour } + # + # # Sets a cookie that expires at a specific time. + # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) } # # # Sets a signed cookie, which prevents users from tampering with its value. - # # The cookie is signed by your app's `secrets.secret_key_base` value. # # It can be read using the signed method `cookies.signed[:name]` # cookies.signed[:user_id] = current_user.id # # # Sets an encrypted cookie value before sending it to the client which # # prevent users from reading and tampering with its value. - # # The cookie is signed by your app's `secrets.secret_key_base` value. # # It can be read using the encrypted method `cookies.encrypted[:name]` # cookies.encrypted[:discount] = 45 # @@ -94,7 +118,7 @@ module ActionDispatch # cookies.permanent[:login] = "XJ-122" # # # You can also chain these methods: - # cookies.permanent.signed[:login] = "XJ-122" + # cookies.signed.permanent[:login] = "XJ-122" # # Examples of reading: # @@ -112,7 +136,7 @@ module ActionDispatch # # cookies[:name] = { # value: 'a yummy cookie', - # expires: 1.year.from_now, + # expires: 1.year, # domain: 'domain.com' # } # @@ -137,8 +161,8 @@ module ActionDispatch # # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD. - # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 1. - # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. + # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2. + # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object. # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers. # Default is +false+. # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or @@ -149,10 +173,15 @@ module ActionDispatch SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze + AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze + USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze + ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze + SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze + COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -160,7 +189,7 @@ module ActionDispatch # Raised when storing more than 4K of session data. CookieOverflow = Class.new StandardError - # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed + # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed. module ChainedCookieJars # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example: # @@ -179,12 +208,12 @@ module ActionDispatch # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed - # cookie was tampered with by the user (or a 3rd party), nil will be returned. + # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned. # - # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, + # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. + # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+. # # Example: # @@ -193,35 +222,28 @@ module ActionDispatch # # cookies.signed[:discount] # => 45 def signed - @signed ||= - if upgrade_legacy_signed_cookies? - UpgradeLegacySignedCookieJar.new(self) - else - SignedCookieJar.new(self) - end + @signed ||= SignedKeyRotatingCookieJar.new(self) end # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. - # If the cookie was tampered with by the user (or a 3rd party), nil will be returned. + # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned. # - # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, + # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. + # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+ + # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded. + # + # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+. # # Example: # # cookies.encrypted[:discount] = 45 - # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/ + # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/ # # cookies.encrypted[:discount] # => 45 def encrypted - @encrypted ||= - if upgrade_legacy_signed_cookies? - UpgradeLegacyEncryptedCookieJar.new(self) - else - EncryptedCookieJar.new(self) - end + @encrypted ||= EncryptedKeyRotatingCookieJar.new(self) end # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set. @@ -237,32 +259,23 @@ module ActionDispatch private - def upgrade_legacy_signed_cookies? - request.secret_token.present? && request.secret_key_base.present? - end - end + def upgrade_legacy_signed_cookies? + request.secret_token.present? && request.secret_key_base.present? + end - # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream - # to the Message{Encryptor,Verifier} allows us to handle the - # (de)serialization step within the cookie jar, which gives us the - # opportunity to detect and migrate legacy cookies. - module VerifyAndUpgradeLegacySignedMessage # :nodoc: - def initialize(*args) - super - @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer) - end + def upgrade_legacy_hmac_aes_cbc_cookies? + request.secret_key_base.present? && + request.encrypted_signed_cookie_salt.present? && + request.encrypted_cookie_salt.present? && + request.use_authenticated_cookie_encryption + end - def verify_and_upgrade_legacy_signed_message(name, signed_message) - deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value| - self[name] = { value: value } + def encrypted_cookie_cipher + request.encrypted_cookie_cipher || "aes-256-gcm" end - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil - end - private - def parse(name, signed_message) - super || verify_and_upgrade_legacy_signed_message(name, signed_message) + def signed_cookie_digest + request.signed_cookie_digest || "SHA1" end end @@ -332,30 +345,34 @@ module ActionDispatch def update_cookies_from_jar request_jar = @request.cookie_jar.instance_variable_get(:@cookies) - set_cookies = request_jar.reject { |k,_| @delete_cookies.key?(k) } + 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| "#{escape(k)}=#{escape(v)}" }.join '; ' + @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; " end - def handle_options(options) #:nodoc: + def handle_options(options) # :nodoc: + if options[:expires].respond_to?(:from_now) + options[:expires] = options[:expires].from_now + end + options[:path] ||= "/" - if options[:domain] == :all || options[:domain] == 'all' - # if there is a provided tld length then we use it otherwise default domain regexp + if options[:domain] == :all || options[:domain] == "all" + # If there is a provided tld length then we use it otherwise default domain regexp. domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP - # if host is not ip and matches domain regexp + # If host is not ip and matches domain regexp. # (ip confirms to domain regexp so we explicitly check for ip) 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| request.host.include? domain.sub(/^\./, '') } + # If host matches one of the supplied domains without a dot in front of it. + options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") } end end @@ -367,12 +384,12 @@ module ActionDispatch value = options[:value] else value = options - options = { :value => value } + options = { value: value } end handle_options(options) - if @cookies[name.to_s] != value or options[:expires] + if @cookies[name.to_s] != value || options[:expires] @cookies[name.to_s] = value @set_cookies[name.to_s] = options @delete_cookies.delete(name.to_s) @@ -404,9 +421,9 @@ module ActionDispatch @delete_cookies[name.to_s] == options end - # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie + # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie. def clear(options = {}) - @cookies.each_key{ |k| delete(k, options) } + @cookies.each_key { |k| delete(k, options) } end def write(headers) @@ -415,31 +432,30 @@ module ActionDispatch end end - mattr_accessor :always_write_cookie - self.always_write_cookie = false + mattr_accessor :always_write_cookie, default: false private - def escape(string) - ::Rack::Utils.escape(string) - end + def escape(string) + ::Rack::Utils.escape(string) + 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 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 + def write_cookie?(cookie) + request.ssl? || !cookie[:secure] || always_write_cookie + end end class AbstractCookieJar # :nodoc: @@ -470,6 +486,14 @@ module ActionDispatch def request; @parent_jar.request; end private + def expiry_options(options) + if options[:expires].respond_to?(:from_now) + { expires_in: options[:expires] } + else + { expires_at: options[:expires] } + end + end + def parse(name, data); data; end def commit(options); end end @@ -493,6 +517,7 @@ module ActionDispatch module SerializedCookieJars # :nodoc: MARSHAL_SIGNATURE = "\x04\x08".freeze + SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer protected def needs_migration?(value) @@ -503,12 +528,16 @@ module ActionDispatch serializer.dump(value) end - def deserialize(name, value) + def deserialize(name) + rotate = false + value = yield -> { rotate = true } + if value - if needs_migration?(value) - Marshal.load(value).tap do |v| - self[name] = { value: v } - end + case + when needs_migration?(value) + self[name] = Marshal.load(value) + when rotate + self[name] = serializer.load(value) else serializer.load(value) end @@ -528,79 +557,100 @@ module ActionDispatch end def digest - request.cookies_digest || 'SHA1' - end - - def key_generator - request.key_generator + request.cookies_digest || "SHA1" end end - class SignedCookieJar < AbstractCookieJar # :nodoc: + class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars 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) + + secret = request.key_generator.generate_key(request.signed_cookie_salt) + @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER) + + request.cookies_rotations.signed.each do |*secrets, **options| + @verifier.rotate(*secrets, serializer: SERIALIZER, **options) + end + + if upgrade_legacy_signed_cookies? + @verifier.rotate request.secret_token, serializer: SERIALIZER + end end private def parse(name, signed_message) - deserialize name, @verifier.verified(signed_message) + deserialize(name) do |rotate| + @verifier.verified(signed_message, on_rotation: rotate) + end end def commit(options) - options[:value] = @verifier.generate(serialize(options[:value])) + options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options)) raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end end - # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if - # secrets.secret_token and secrets.secret_key_base are both set. It reads - # legacy cookies signed with the old dummy key generator and signs and - # re-saves them using the new key generator to provide a smooth upgrade path. - class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: - include VerifyAndUpgradeLegacySignedMessage - end - - class EncryptedCookieJar < AbstractCookieJar # :nodoc: + class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars 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." + if request.use_authenticated_cookie_encryption + key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher) + secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER) + else + key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc") + secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len) + sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER) + end + + request.cookies_rotations.encrypted.each do |*secrets, **options| + @encryptor.rotate(*secrets, serializer: SERIALIZER, **options) + end + + if upgrade_legacy_hmac_aes_cbc_cookies? + legacy_cipher = "aes-256-cbc" + secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher)) + sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt) + + @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER) end - 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) + if upgrade_legacy_signed_cookies? + @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER) + end end private def parse(name, encrypted_message) - deserialize name, @encryptor.decrypt_and_verify(encrypted_message) - rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage - nil + deserialize(name) do |rotate| + @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate) + end + rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature + parse_legacy_signed_message(name, encrypted_message) end def commit(options) - options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) + options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options)) raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end - end - # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore - # instead of EncryptedCookieJar if secrets.secret_token and secrets.secret_key_base - # are both set. It reads legacy cookies signed with the old dummy key generator and - # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. - class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: - include VerifyAndUpgradeLegacySignedMessage + def parse_legacy_signed_message(name, legacy_signed_message) + if defined?(@legacy_verifier) + deserialize(name) do |rotate| + rotate.call + + @legacy_verifier.verified(legacy_signed_message) + end + 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 51a471fb23..511306eb0e 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -1,16 +1,18 @@ -require 'action_dispatch/http/request' -require 'action_dispatch/middleware/exception_wrapper' -require 'action_dispatch/routing/inspector' -require 'action_view' -require 'action_view/base' +# frozen_string_literal: true -require 'pp' +require "action_dispatch/http/request" +require "action_dispatch/middleware/exception_wrapper" +require "action_dispatch/routing/inspector" +require "action_view" +require "action_view/base" + +require "pp" module ActionDispatch # This middleware is responsible for logging exceptions and # showing a debugging page in case the request is local. class DebugExceptions - RESCUES_TEMPLATE_PATH = File.expand_path('../templates', __FILE__) + RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__) class DebugView < ActionView::Base def debug_params(params) @@ -19,23 +21,33 @@ module ActionDispatch clean_params.delete("controller") if clean_params.empty? - 'None' + "None" else - PP.pp(clean_params, "", 200) + PP.pp(clean_params, "".dup, 200) end end def debug_headers(headers) if headers.present? - headers.inspect.gsub(',', ",\n") + headers.inspect.gsub(",", ",\n") else - 'None' + "None" end end def debug_hash(object) object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") end + + def render(*) + logger = ActionView::Base.logger + + if logger && logger.respond_to?(:silence) + logger.silence { super } + else + super + end + end end def initialize(app, routes_app = nil, response_format = :default) @@ -48,7 +60,7 @@ module ActionDispatch request = ActionDispatch::Request.new env _, headers, body = response = @app.call(env) - if headers['X-Cascade'] == 'pass' + if headers["X-Cascade"] == "pass" body.close if body.respond_to?(:close) raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}" end @@ -61,125 +73,133 @@ module ActionDispatch private - 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 request.get_header('action_dispatch.show_detailed_exceptions') - case @response_format - when :api - render_for_api_application(request, wrapper) - when :default - render_for_default_application(request, 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 request.get_header("action_dispatch.show_detailed_exceptions") + content_type = request.formats.first + + if api_request?(content_type) + render_for_api_request(content_type, wrapper) + else + render_for_browser_request(request, wrapper) + end + else + raise exception end - else - raise exception end - end - def render_for_default_application(request, wrapper) - template = create_template(request, wrapper) - file = "rescues/#{wrapper.rescue_template}" + def render_for_browser_request(request, wrapper) + template = create_template(request, wrapper) + file = "rescues/#{wrapper.rescue_template}" - if request.xhr? - body = template.render(template: file, layout: false, formats: [:text]) - format = "text/plain" - else - body = template.render(template: file, layout: 'rescues/layout') - format = "text/html" + if request.xhr? + body = template.render(template: file, layout: false, formats: [:text]) + format = "text/plain" + else + body = template.render(template: file, layout: "rescues/layout") + format = "text/html" + end + render(wrapper.status_code, body, format) end - render(wrapper.status_code, body, format) - end - - def render_for_api_application(request, wrapper) - body = { - status: wrapper.status_code, - error: Rack::Utils::HTTP_STATUS_CODES.fetch( - wrapper.status_code, - Rack::Utils::HTTP_STATUS_CODES[500] - ), - exception: wrapper.exception.inspect, - traces: wrapper.traces - } - - content_type = request.formats.first - to_format = "to_#{content_type.to_sym}" - - if content_type && body.respond_to?(to_format) - formatted_body = body.public_send(to_format) - format = content_type - else - formatted_body = body.to_json - format = Mime[:json] - end - - render(wrapper.status_code, formatted_body, format) - end - def create_template(request, wrapper) - traces = wrapper.traces + def render_for_api_request(content_type, wrapper) + body = { + status: wrapper.status_code, + error: Rack::Utils::HTTP_STATUS_CODES.fetch( + wrapper.status_code, + Rack::Utils::HTTP_STATUS_CODES[500] + ), + exception: wrapper.exception.inspect, + traces: wrapper.traces + } + + to_format = "to_#{content_type.to_sym}" + + if content_type && body.respond_to?(to_format) + formatted_body = body.public_send(to_format) + format = content_type + else + formatted_body = body.to_json + format = Mime[:json] + end - trace_to_show = 'Application Trace' - if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error' - trace_to_show = 'Full Trace' + render(wrapper.status_code, formatted_body, format) end - if source_to_show = traces[trace_to_show].first - source_to_show_id = source_to_show[:id] + def create_template(request, wrapper) + traces = wrapper.traces + + trace_to_show = "Application Trace" + if traces[trace_to_show].empty? && wrapper.rescue_template != "routing_error" + trace_to_show = "Full Trace" + end + + if source_to_show = traces[trace_to_show].first + source_to_show_id = source_to_show[:id] + end + + DebugView.new([RESCUES_TEMPLATE_PATH], + request: request, + exception: wrapper.exception, + traces: traces, + show_source_idx: source_to_show_id, + trace_to_show: trace_to_show, + routes_inspector: routes_inspector(wrapper.exception), + source_extracts: wrapper.source_extracts, + line_number: wrapper.line_number, + file: wrapper.file + ) end - DebugView.new([RESCUES_TEMPLATE_PATH], - request: request, - exception: wrapper.exception, - traces: traces, - show_source_idx: source_to_show_id, - trace_to_show: trace_to_show, - routes_inspector: routes_inspector(wrapper.exception), - source_extracts: wrapper.source_extracts, - line_number: wrapper.line_number, - file: wrapper.file - ) - end + def render(status, body, format) + [status, { "Content-Type" => "#{format}; charset=#{Response.default_charset}", "Content-Length" => body.bytesize.to_s }, [body]] + end - def render(status, body, format) - [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] - end + def log_error(request, wrapper) + logger = logger(request) + return unless logger - def log_error(request, wrapper) - logger = logger(request) - return unless logger + exception = wrapper.exception - exception = wrapper.exception + trace = wrapper.application_trace + trace = wrapper.framework_trace if trace.empty? - trace = wrapper.application_trace - trace = wrapper.framework_trace if trace.empty? + ActiveSupport::Deprecation.silence do + logger.fatal " " + logger.fatal "#{exception.class} (#{exception.message}):" + log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code) + logger.fatal " " + log_array logger, trace + end + end - ActiveSupport::Deprecation.silence do - logger.fatal " " - logger.fatal "#{exception.class} (#{exception.message}):" - log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code) - logger.fatal " " - log_array logger, trace + def log_array(logger, array) + if logger.formatter && logger.formatter.respond_to?(:tags_text) + logger.fatal array.join("\n#{logger.formatter.tags_text}") + else + logger.fatal array.join("\n") + end end - end - def log_array(logger, array) - array.map { |line| logger.fatal line } - end + def logger(request) + request.logger || ActionView::Base.logger || stderr_logger + end - def logger(request) - request.logger || ActionView::Base.logger || stderr_logger - end + def stderr_logger + @stderr_logger ||= ActiveSupport::Logger.new($stderr) + end - def stderr_logger - @stderr_logger ||= ActiveSupport::Logger.new($stderr) - end + def routes_inspector(exception) + if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error)) + ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes) + end + end - def routes_inspector(exception) - if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error)) - ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes) + def api_request?(content_type) + @response_format == :api && !content_type.html? end - end end end diff --git a/actionpack/lib/action_dispatch/middleware/debug_locks.rb b/actionpack/lib/action_dispatch/middleware/debug_locks.rb new file mode 100644 index 0000000000..03760438f7 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/debug_locks.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +module ActionDispatch + # This middleware can be used to diagnose deadlocks in the autoload interlock. + # + # To use it, insert it near the top of the middleware stack, using + # <tt>config/application.rb</tt>: + # + # config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks + # + # After restarting the application and re-triggering the deadlock condition, + # <tt>/rails/locks</tt> will show a summary of all threads currently known to + # the interlock, which lock level they are holding or awaiting, and their + # current backtrace. + # + # Generally a deadlock will be caused by the interlock conflicting with some + # other external lock or blocking I/O call. These cannot be automatically + # identified, but should be visible in the displayed backtraces. + # + # NOTE: The formatting and content of this middleware's output is intended for + # human consumption, and should be expected to change between releases. + # + # This middleware exposes operational details of the server, with no access + # control. It should only be enabled when in use, and removed thereafter. + class DebugLocks + def initialize(app, path = "/rails/locks") + @app = app + @path = path + end + + def call(env) + req = ActionDispatch::Request.new env + + if req.get? + path = req.path_info.chomp("/".freeze) + if path == @path + return render_details(req) + end + end + + @app.call(env) + end + + private + def render_details(req) + threads = ActiveSupport::Dependencies.interlock.raw_state do |raw_threads| + # The Interlock itself comes to a complete halt as long as this block + # is executing. That gives us a more consistent picture of everything, + # but creates a pretty strong Observer Effect. + # + # Most directly, that means we need to do as little as possible in + # this block. More widely, it means this middleware should remain a + # strictly diagnostic tool (to be used when something has gone wrong), + # and not for any sort of general monitoring. + + raw_threads.each.with_index do |(thread, info), idx| + info[:index] = idx + info[:backtrace] = thread.backtrace + end + + raw_threads + end + + str = threads.map do |thread, info| + if info[:exclusive] + lock_state = "Exclusive".dup + elsif info[:sharing] > 0 + lock_state = "Sharing".dup + lock_state << " x#{info[:sharing]}" if info[:sharing] > 1 + else + lock_state = "No lock".dup + end + + if info[:waiting] + lock_state << " (yielded share)" + end + + msg = "Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n".dup + + if info[:sleeper] + msg << " Waiting in #{info[:sleeper]}" + msg << " to #{info[:purpose].to_s.inspect}" unless info[:purpose].nil? + msg << "\n" + + if info[:compatible] + compat = info[:compatible].map { |c| c == false ? "share" : c.to_s.inspect } + msg << " may be pre-empted for: #{compat.join(', ')}\n" + end + + blockers = threads.values.select { |binfo| blocked_by?(info, binfo, threads.values) } + msg << " blocked by: #{blockers.map { |i| i[:index] }.join(', ')}\n" if blockers.any? + end + + blockees = threads.values.select { |binfo| blocked_by?(binfo, info, threads.values) } + msg << " blocking: #{blockees.map { |i| i[:index] }.join(', ')}\n" if blockees.any? + + msg << "\n#{info[:backtrace].join("\n")}\n" if info[:backtrace] + end.join("\n\n---\n\n\n") + + [200, { "Content-Type" => "text/plain", "Content-Length" => str.size }, [str]] + end + + def blocked_by?(victim, blocker, all_threads) + return false if victim.equal?(blocker) + + case victim[:sleeper] + when :start_sharing + blocker[:exclusive] || + (!victim[:waiting] && blocker[:compatible] && !blocker[:compatible].include?(false)) + when :start_exclusive + blocker[:sharing] > 0 || + blocker[:exclusive] || + (blocker[:compatible] && !blocker[:compatible].include?(victim[:purpose])) + when :yield_shares + blocker[:exclusive] + when :stop_exclusive + blocker[:exclusive] || + victim[:compatible] && + victim[:compatible].include?(blocker[:purpose]) && + all_threads.all? { |other| !other[:compatible] || blocker.equal?(other) || other[:compatible].include?(blocker[:purpose]) } + end + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 59edc66086..d1b4508378 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,33 +1,32 @@ -require 'active_support/core_ext/module/attribute_accessors' -require 'rack/utils' +# frozen_string_literal: true + +require "active_support/core_ext/module/attribute_accessors" +require "rack/utils" module ActionDispatch class ExceptionWrapper - cattr_accessor :rescue_responses - @@rescue_responses = Hash.new(:internal_server_error) - @@rescue_responses.merge!( - 'ActionController::RoutingError' => :not_found, - 'AbstractController::ActionNotFound' => :not_found, - 'ActionController::MethodNotAllowed' => :method_not_allowed, - 'ActionController::UnknownHttpMethod' => :method_not_allowed, - 'ActionController::NotImplemented' => :not_implemented, - 'ActionController::UnknownFormat' => :not_acceptable, - 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity, - 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity, - 'ActionDispatch::ParamsParser::ParseError' => :bad_request, - 'ActionController::BadRequest' => :bad_request, - 'ActionController::ParameterMissing' => :bad_request, - 'Rack::Utils::ParameterTypeError' => :bad_request, - 'Rack::Utils::InvalidParameterError' => :bad_request + cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!( + "ActionController::RoutingError" => :not_found, + "AbstractController::ActionNotFound" => :not_found, + "ActionController::MethodNotAllowed" => :method_not_allowed, + "ActionController::UnknownHttpMethod" => :method_not_allowed, + "ActionController::NotImplemented" => :not_implemented, + "ActionController::UnknownFormat" => :not_acceptable, + "ActionController::InvalidAuthenticityToken" => :unprocessable_entity, + "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity, + "ActionDispatch::Http::Parameters::ParseError" => :bad_request, + "ActionController::BadRequest" => :bad_request, + "ActionController::ParameterMissing" => :bad_request, + "Rack::QueryParser::ParameterTypeError" => :bad_request, + "Rack::QueryParser::InvalidParameterError" => :bad_request ) - cattr_accessor :rescue_templates - @@rescue_templates = Hash.new('diagnostics') - @@rescue_templates.merge!( - 'ActionView::MissingTemplate' => 'missing_template', - 'ActionController::RoutingError' => 'routing_error', - 'AbstractController::ActionNotFound' => 'unknown_action', - 'ActionView::Template::Error' => 'template_error' + cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!( + "ActionView::MissingTemplate" => "missing_template", + "ActionController::RoutingError" => "routing_error", + "AbstractController::ActionNotFound" => "unknown_action", + "ActiveRecord::StatementInvalid" => "invalid_statement", + "ActionView::Template::Error" => "template_error" ) attr_reader :backtrace_cleaner, :exception, :line_number, :file @@ -100,49 +99,49 @@ module ActionDispatch private - def backtrace - Array(@exception.backtrace) - end + def backtrace + Array(@exception.backtrace) + end - def original_exception(exception) - if @@rescue_responses.has_key?(exception.cause.class.name) - exception.cause - else - exception + def original_exception(exception) + if @@rescue_responses.has_key?(exception.cause.class.name) + exception.cause + else + exception + end end - end - def clean_backtrace(*args) - if backtrace_cleaner - backtrace_cleaner.clean(backtrace, *args) - else - backtrace + def clean_backtrace(*args) + if backtrace_cleaner + backtrace_cleaner.clean(backtrace, *args) + else + backtrace + end end - end - def source_fragment(path, line) - return unless Rails.respond_to?(:root) && Rails.root - full_path = Rails.root.join(path) - if File.exist?(full_path) - File.open(full_path, "r") do |file| - start = [line - 3, 0].max - lines = file.each_line.drop(start).take(6) - Hash[*(start+1..(lines.count+start)).zip(lines).flatten] + def source_fragment(path, line) + return unless Rails.respond_to?(:root) && Rails.root + full_path = Rails.root.join(path) + if File.exist?(full_path) + File.open(full_path, "r") do |file| + start = [line - 3, 0].max + lines = file.each_line.drop(start).take(6) + Hash[*(start + 1..(lines.count + start)).zip(lines).flatten] + end end end - end - def extract_file_and_line_number(trace) - # Split by the first colon followed by some digits, which works for both - # Windows and Unix path styles. - file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace - [file, line.to_i] - end + def extract_file_and_line_number(trace) + # Split by the first colon followed by some digits, which works for both + # Windows and Unix path styles. + file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace + [file, line.to_i] + end - def expand_backtrace - @exception.backtrace.unshift( - @exception.to_s.split("\n") - ).flatten! - end + def expand_backtrace + @exception.backtrace.unshift( + @exception.to_s.split("\n") + ).flatten! + end end end diff --git a/actionpack/lib/action_dispatch/middleware/executor.rb b/actionpack/lib/action_dispatch/middleware/executor.rb index 06245b403b..129b18d3d9 100644 --- a/actionpack/lib/action_dispatch/middleware/executor.rb +++ b/actionpack/lib/action_dispatch/middleware/executor.rb @@ -1,4 +1,6 @@ -require 'rack/body_proxy' +# frozen_string_literal: true + +require "rack/body_proxy" module ActionDispatch class Executor diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 06038af571..3e11846778 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,4 +1,6 @@ -require 'active_support/core_ext/hash/keys' +# frozen_string_literal: true + +require "active_support/core_ext/hash/keys" module ActionDispatch # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed @@ -36,7 +38,7 @@ module ActionDispatch # # See docs on the FlashHash class for more details about the flash. class Flash - KEY = 'action_dispatch.request.flash_hash'.freeze + KEY = "action_dispatch.request.flash_hash".freeze module RequestMethods # Access the contents of the flash. Use <tt>flash["notice"]</tt> to @@ -60,14 +62,14 @@ module ActionDispatch session = self.session || {} flash_hash = self.flash_hash - if flash_hash && (flash_hash.present? || session.key?('flash')) + 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') + 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 @@ -118,8 +120,8 @@ module ActionDispatch end new(flashes, flashes.keys) when Hash # Rails 4.0 - flashes = value['flashes'] - if discard = value['discard'] + flashes = value["flashes"] + if discard = value["discard"] flashes.except!(*discard) end new(flashes, flashes.keys) @@ -129,11 +131,11 @@ module ActionDispatch end # Builds a hash containing the flashes to keep for the next request. - # If there are none to keep, returns nil. + # If there are none to keep, returns +nil+. def to_session_value #:nodoc: flashes_to_keep = @flashes.except(*@discard) return nil if flashes_to_keep.empty? - {'flashes' => flashes_to_keep} + { "discard" => [], "flashes" => flashes_to_keep } end def initialize(flashes = {}, discard = []) #:nodoc: @@ -277,15 +279,16 @@ module ActionDispatch end protected - def now_is_loaded? - @now - end + def now_is_loaded? + @now + end - def stringify_array(array) - array.map do |item| - item.kind_of?(Symbol) ? item.to_s : item + private + def stringify_array(array) # :doc: + array.map do |item| + item.kind_of?(Symbol) ? item.to_s : item + end end - end end def self.new(app) app; end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb deleted file mode 100644 index faf3262b8f..0000000000 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'action_dispatch/http/request' - -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 - - def initialize(message = nil, original_exception = nil) - if message - ActiveSupport::Deprecation.warn("Passing #message is deprecated and has no effect. " \ - "#{self.class} will automatically capture the message " \ - "of the original exception.", caller) - end - - if original_exception - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - - super($!.message) - end - - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end - end - - # 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 = {}) - ActiveSupport::Deprecation.warn('ActionDispatch::ParamsParser is deprecated and will be removed in Rails 5.1. Configure the parameter parsing in ActionDispatch::Request.parameter_parsers.') - parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key } - ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers) - app - end - end -end diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index 0f27984550..3feb3a19f3 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + module ActionDispatch # When called, this middleware renders an error page. By default if an HTML - # response is expected it will render static error pages from the `/public` + # response is expected it will render static error pages from the <tt>/public</tt> # directory. For example when this middleware receives a 500 response it will - # render the template found in `/public/500.html`. + # render the template found in <tt>/public/500.html</tt>. # If an internationalized locale is set, this middleware will attempt to render - # the template in `/public/500.<locale>.html`. If an internationalized template - # is not found it will fall back on `/public/500.html`. + # the template in <tt>/public/500.<locale>.html</tt>. If an internationalized template + # is not found it will fall back on <tt>/public/500.html</tt>. # # When a request with a content type other than HTML is made, this middleware # will attempt to convert error information into the appropriate response type. @@ -20,36 +22,36 @@ module ActionDispatch 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]) } + body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) } render(status, content_type, body) end private - def render(status, content_type, body) - format = "to_#{content_type.to_sym}" if content_type - if format && body.respond_to?(format) - render_format(status, content_type, body.public_send(format)) - else - render_html(status) + def render(status, content_type, body) + format = "to_#{content_type.to_sym}" if content_type + if format && body.respond_to?(format) + render_format(status, content_type, body.public_send(format)) + else + render_html(status) + end end - end - def render_format(status, content_type, body) - [status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}", - 'Content-Length' => body.bytesize.to_s}, [body]] - end + def render_format(status, content_type, body) + [status, { "Content-Type" => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}", + "Content-Length" => body.bytesize.to_s }, [body]] + end - def render_html(status) - path = "#{public_path}/#{status}.#{I18n.locale}.html" - path = "#{public_path}/#{status}.html" unless (found = File.exist?(path)) + def render_html(status) + path = "#{public_path}/#{status}.#{I18n.locale}.html" + path = "#{public_path}/#{status}.html" unless (found = File.exist?(path)) - if found || File.exist?(path) - render_format(status, 'text/html', File.read(path)) - else - [404, { "X-Cascade" => "pass" }, []] + if found || File.exist?(path) + render_format(status, "text/html", File.read(path)) + else + [404, { "X-Cascade" => "pass" }, []] + end end - end end end diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 112bde6596..8bb3ba7504 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,54 +1,12 @@ +# frozen_string_literal: true + module ActionDispatch - # ActionDispatch::Reloader provides prepare and cleanup callbacks, - # intended to assist with code reloading during development. - # - # Prepare callbacks are run before each request, and cleanup callbacks - # after each request. In this respect they are analogs of ActionDispatch::Callback's - # before and after callbacks. However, cleanup callbacks are not called until the - # request is fully complete -- that is, after #close has been called on - # the response body. This is important for streaming responses such as the - # following: - # - # self.response_body = -> (response, output) do - # # code here which refers to application models - # end - # - # Cleanup callbacks will not be called until after the response_body lambda - # is evaluated, ensuring that it can refer to application models and other - # classes before they are unloaded. + # ActionDispatch::Reloader wraps the request with callbacks provided by ActiveSupport::Reloader + # callbacks, intended to assist with code reloading during development. # # By default, ActionDispatch::Reloader is included in the middleware stack # only in the development environment; specifically, when +config.cache_classes+ - # is false. Callbacks may be registered even when it is not included in the - # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt> - # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually. - # + # is false. class Reloader < Executor - def self.to_prepare(*args, &block) - ActiveSupport::Reloader.to_prepare(*args, &block) - end - - def self.to_cleanup(*args, &block) - ActiveSupport::Reloader.to_complete(*args, &block) - end - - def self.prepare! - default_reloader.prepare! - end - - def self.cleanup! - default_reloader.reload! - end - - class << self - attr_accessor :default_reloader # :nodoc: - - deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead', - to_cleanup: 'use ActiveSupport::Reloader.to_complete instead', - prepare!: 'use Rails.application.reloader.prepare! instead', - cleanup!: 'use Rails.application.reloader.reload! instead of cleanup + prepare' - end - - self.default_reloader = ActiveSupport::Reloader end end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 31b75498b6..35158f9062 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -1,4 +1,6 @@ -require 'ipaddr' +# frozen_string_literal: true + +require "ipaddr" module ActionDispatch # This middleware calculates the IP address of the remote client that is @@ -10,7 +12,7 @@ module ActionDispatch # by @gingerlime. A more detailed explanation of the algorithm is given # at GetIp#calculate_ip. # - # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2] + # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2] # requires. Some Rack servers simply drop preceding headers, and only report # the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers]. # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn) @@ -29,7 +31,7 @@ module ActionDispatch # The default trusted IPs list simply includes IP addresses that are # guaranteed by the IP specification to be private addresses. Those will # not be the ultimate client IP in production, and so are discarded. See - # http://en.wikipedia.org/wiki/Private_network for details. + # https://en.wikipedia.org/wiki/Private_network for details. TRUSTED_PROXIES = [ "127.0.0.1", # localhost IPv4 "::1", # localhost IPv6 @@ -131,8 +133,8 @@ module ActionDispatch should_check_ip = @check_ip && client_ips.last && forwarded_ips.last 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=#{@req.client_ip.inspect} " + + raise IpSpoofAttackError, "IP spoofing attack?! " \ + "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" end @@ -153,17 +155,17 @@ module ActionDispatch @ip ||= calculate_ip end - protected + private - def ips_from(header) + def ips_from(header) # :doc: return [] unless header - # Split the comma-separated list into an array of strings + # Split the comma-separated list into an array of strings. ips = header.strip.split(/[,\s]+/) ips.select do |ip| begin - # Only return IPs that are valid according to the IPAddr#new method + # Only return IPs that are valid according to the IPAddr#new method. range = IPAddr.new(ip).to_range - # we want to make sure nobody is sneaking a netmask in + # We want to make sure nobody is sneaking a netmask in. range.begin == range.end rescue ArgumentError nil @@ -171,13 +173,11 @@ module ActionDispatch end end - def filter_proxies(ips) + def filter_proxies(ips) # :doc: ips.reject do |ip| @proxies.any? { |proxy| proxy === ip } end end - end - end end diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 1555ff72af..805d3f2148 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -1,9 +1,12 @@ -require 'securerandom' -require 'active_support/core_ext/string/access' +# frozen_string_literal: true + +require "securerandom" +require "active_support/core_ext/string/access" module ActionDispatch - # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through - # ActionDispatch::Request#uuid or the alias ActionDispatch::Request#request_id) and sends the same id to the client via the X-Request-Id header. + # Makes a unique request id available to the +action_dispatch.request_id+ env variable (which is then accessible + # through <tt>ActionDispatch::Request#request_id</tt> or the alias <tt>ActionDispatch::Request#uuid</tt>) and sends + # the same id to the client via the X-Request-Id header. # # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the @@ -12,7 +15,7 @@ module ActionDispatch # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files # from multiple pieces of the stack. class RequestId - X_REQUEST_ID = "X-Request-Id".freeze # :nodoc: + X_REQUEST_ID = "X-Request-Id".freeze #:nodoc: def initialize(app) @app = app diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 5fb5953811..5b0be96223 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -1,34 +1,25 @@ -require 'rack/utils' -require 'rack/request' -require 'rack/session/abstract/id' -require 'action_dispatch/middleware/cookies' -require 'action_dispatch/request/session' +# frozen_string_literal: true + +require "rack/utils" +require "rack/request" +require "rack/session/abstract/id" +require "action_dispatch/middleware/cookies" +require "action_dispatch/request/session" module ActionDispatch module Session class SessionRestoreError < StandardError #:nodoc: - - def initialize(const_error = nil) - if const_error - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - - super("Session contains objects whose class definition isn't available.\n" + - "Remember to require the classes for all objects kept in the session.\n" + + def initialize + super("Session contains objects whose class definition isn't available.\n" \ + "Remember to require the classes for all objects kept in the session.\n" \ "(Original exception: #{$!.message} [#{$!.class}])\n") set_backtrace $!.backtrace end - - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end end module Compatibility def initialize(app, options = {}) - options[:key] ||= '_session_id' + options[:key] ||= "_session_id" super end @@ -38,14 +29,13 @@ module ActionDispatch sid end - protected + private - def initialize_sid + def initialize_sid # :doc: @default_options.delete(:sidbits) @default_options.delete(:secure_random) end - private def make_request(env) ActionDispatch::Request.new env end @@ -65,7 +55,7 @@ module ActionDispatch rescue ArgumentError => argument_error if argument_error.message =~ %r{undefined class/module ([\w:]*\w)} begin - # Note that the regexp does not allow $1 to end with a ':' + # Note that the regexp does not allow $1 to end with a ':'. $1.constantize rescue LoadError, NameError raise ActionDispatch::Session::SessionRestoreError @@ -94,9 +84,9 @@ module ActionDispatch private - def set_cookie(request, session_id, cookie) - request.cookie_jar[key] = cookie - end + def set_cookie(request, session_id, cookie) + request.cookie_jar[key] = cookie + end end 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 589ae46e38..a6d965a644 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -1,4 +1,6 @@ -require 'action_dispatch/middleware/session/abstract_store' +# frozen_string_literal: true + +require "action_dispatch/middleware/session/abstract_store" module ActionDispatch module Session @@ -19,7 +21,7 @@ module ActionDispatch # Get a session from the cache. def find_session(env, sid) - unless sid and session = @cache.read(cache_key(sid)) + unless sid && (session = @cache.read(cache_key(sid))) sid, session = generate_sid, {} end [sid, session] @@ -29,7 +31,7 @@ module ActionDispatch def write_session(env, sid, session, options) key = cache_key(sid) if session - @cache.write(key, session, :expires_in => options[:expire_after]) + @cache.write(key, session, expires_in: options[:expire_after]) else @cache.delete(key) end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index dec9c60ef2..4ea96196d3 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -1,6 +1,8 @@ -require 'active_support/core_ext/hash/keys' -require 'action_dispatch/middleware/session/abstract_store' -require 'rack/session/cookie' +# frozen_string_literal: true + +require "active_support/core_ext/hash/keys" +require "action_dispatch/middleware/session/abstract_store" +require "rack/session/cookie" module ActionDispatch module Session @@ -19,39 +21,25 @@ module ActionDispatch # knowing your app's secret key, but can easily read their +user_id+. This # was the default for Rails 3 apps. # - # If you have secret_key_base set, your cookies will be encrypted. This + # Your cookies will be encrypted using your apps secret_key_base. This # goes a step further than signed cookies in that encrypted cookies cannot # be altered or read by users. This is the default starting in Rails 4. # - # If you have both secret_token and secret_key_base set, your cookies will - # be encrypted, and signed cookies generated by Rails 3 will be - # transparently read and encrypted to provide a smooth upgrade path. - # - # Configure your session store in config/initializers/session_store.rb: + # Configure your session store in <tt>config/initializers/session_store.rb</tt>: # # Rails.application.config.session_store :cookie_store, key: '_your_app_session' # - # Configure your secret key in config/secrets.yml: - # - # development: - # secret_key_base: 'secret key' + # By default, your secret key base is derived from your application name in + # the test and development environments. In all other environments, it is stored + # encrypted in the <tt>config/credentials.yml.enc</tt> file. # - # To generate a secret key for an existing application, run `rails secret`. + # If your application was not updated to Rails 5.2 defaults, the secret_key_base + # will be found in the old <tt>config/secrets.yml</tt> file. # - # If you are upgrading an existing Rails 3 app, you should leave your - # existing secret_token in place and simply add the new secret_key_base. - # Note that you should wait to set secret_key_base until you have 100% of - # your userbase on Rails 4 and are reasonably sure you will not need to - # rollback to Rails 3. This is because cookies signed based on the new - # secret_key_base in Rails 4 are not backwards compatible with Rails 3. - # You are free to leave your existing secret_token in place, not set the - # new secret_key_base, and ignore the deprecation warnings until you are - # reasonably sure that your upgrade is otherwise complete. Additionally, - # you should take care to make sure you are not relying on the ability to - # decode signed cookies generated by your app in external applications or - # JavaScript before upgrading. - # - # Note that changing the secret key will invalidate all existing sessions! + # Note that changing your secret_key_base will invalidate all existing session. + # Additionally, you should take care to make sure you are not relying on the + # ability to decode signed cookies generated by your app in external + # applications or JavaScript before changing it. # # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the # options described there can be used to customize the session cookie that @@ -63,8 +51,8 @@ module ActionDispatch # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and # <tt>:httponly</tt>. class CookieStore < AbstractStore - def initialize(app, options={}) - super(app, options.merge!(:cookie_only => true)) + def initialize(app, options = {}) + super(app, options.merge!(cookie_only: true)) end def delete_session(req, session_id, options) @@ -84,46 +72,46 @@ module ActionDispatch private - def extract_session_id(req) - stale_session_check! do - unpacked_cookie_data(req)["session_id"] + def extract_session_id(req) + stale_session_check! do + unpacked_cookie_data(req)["session_id"] + end end - end - 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! + 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 - data || {} + req.set_header k, v end - req.set_header k, v end - end - def persistent_session_id!(data, sid=nil) - data ||= {} - data["session_id"] ||= sid || generate_sid - data - end + def persistent_session_id!(data, sid = nil) + data ||= {} + data["session_id"] ||= sid || generate_sid + data + end - def write_session(req, sid, session_data, options) - session_data["session_id"] = sid - session_data - end + def write_session(req, sid, session_data, options) + session_data["session_id"] = sid + session_data + end - def set_cookie(request, session_id, cookie) - cookie_jar(request)[@key] = cookie - end + def set_cookie(request, session_id, cookie) + cookie_jar(request)[@key] = cookie + end - def get_cookie(req) - cookie_jar(req)[@key] - end + def get_cookie(req) + cookie_jar(req)[@key] + end - def cookie_jar(request) - request.cookie_jar.signed_or_encrypted - end + def cookie_jar(request) + request.cookie_jar.signed_or_encrypted + end end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index cb19786f0b..914df3a2b1 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -1,6 +1,8 @@ -require 'action_dispatch/middleware/session/abstract_store' +# frozen_string_literal: true + +require "action_dispatch/middleware/session/abstract_store" begin - require 'rack/session/dalli' + require "rack/session/dalli" rescue LoadError => e $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install" raise e diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 64695f9738..3c88afd4d3 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -1,5 +1,7 @@ -require 'action_dispatch/http/request' -require 'action_dispatch/middleware/exception_wrapper' +# frozen_string_literal: true + +require "action_dispatch/http/request" +require "action_dispatch/middleware/exception_wrapper" module ActionDispatch # This middleware rescues any exception returned by the application @@ -8,14 +10,14 @@ module ActionDispatch # The exceptions app should be passed as parameter on initialization # of ShowExceptions. Every time there is an exception, ShowExceptions will # store the exception in env["action_dispatch.exception"], rewrite the - # PATH_INFO to the exception status code and call the rack app. + # PATH_INFO to the exception status code and call the Rack app. # # If the application returns a "X-Cascade" pass response, this middleware # will send an empty response as result with the correct status code. # If any exception happens inside the exceptions app, this middleware # catches the exceptions and returns a FAILSAFE_RESPONSE. class ShowExceptions - FAILSAFE_RESPONSE = [500, { 'Content-Type' => 'text/plain' }, + FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" }, ["500 Internal Server Error\n" \ "If you are the administrator of this website, then please read this web " \ "application's log file and/or the web server's log file to find out what " \ @@ -39,22 +41,22 @@ module ActionDispatch private - def render_exception(request, exception) - backtrace_cleaner = request.get_header 'action_dispatch.backtrace_cleaner' - wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) - status = wrapper.status_code - 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 "}" - FAILSAFE_RESPONSE - end + def render_exception(request, exception) + backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner" + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) + status = wrapper.status_code + 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 "}" + FAILSAFE_RESPONSE + end - def pass_response(status) - [status, {"Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0"}, []] - end + def pass_response(status) + [status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []] + end end end diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index ab3077b308..ef633aadc6 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -1,78 +1,68 @@ +# frozen_string_literal: true + module ActionDispatch - # This middleware is added to the stack when `config.force_ssl = true`, and is passed - # the options set in `config.ssl_options`. It does three jobs to enforce secure HTTP + # This middleware is added to the stack when <tt>config.force_ssl = true</tt>, and is passed + # the options set in +config.ssl_options+. It does three jobs to enforce secure HTTP # requests: # - # 1. TLS redirect: Permanently redirects http:// requests to https:// - # with the same URL host, path, etc. Enabled by default. Set `config.ssl_options` - # to modify the destination URL - # (e.g. `redirect: { host: "secure.widgets.com", port: 8080 }`), or set - # `redirect: false` to disable this feature. + # 1. <b>TLS redirect</b>: Permanently redirects +http://+ requests to +https://+ + # with the same URL host, path, etc. Enabled by default. Set +config.ssl_options+ + # to modify the destination URL + # (e.g. <tt>redirect: { host: "secure.widgets.com", port: 8080 }</tt>), or set + # <tt>redirect: false</tt> to disable this feature. + # + # Requests can opt-out of redirection with +exclude+: + # + # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } } + # + # 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they + # must not be sent along with +http://+ requests. Enabled by default. Set + # +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature. # - # 2. Secure cookies: Sets the `secure` flag on cookies to tell browsers they - # mustn't be sent along with http:// requests. Enabled by default. Set - # `config.ssl_options` with `secure_cookies: false` to disable this feature. + # 3. <b>HTTP Strict Transport Security (HSTS)</b>: Tells the browser to remember + # this site as TLS-only and automatically redirect non-TLS requests. + # Enabled by default. Configure +config.ssl_options+ with <tt>hsts: false</tt> to disable. # - # 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. Configure `config.ssl_options` with `hsts: false` to disable. + # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS: # - # Set `config.ssl_options` with `hsts: { … }` to configure 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 `true`. - # * `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. + # * +expires+: How long, in seconds, these settings will stick. The minimum + # required to qualify for browser preload lists is 18 weeks. Defaults to + # 180 days (recommended). # - # 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 }`. + # * +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 +true+. # - # Requests can opt-out of redirection with `exclude`: + # * +preload+: Advertise that this site may be included in browsers' + # preloaded HSTS lists. HSTS protects your site on every visit <i>except the + # first visit</i> 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.org to submit your site for inclusion. + # Defaults to +false+. # - # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } } + # 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 <tt>hsts: false</tt> is a shortcut for + # <tt>hsts: { expires: 0 }</tt>. class SSL + # :stopdoc: + # 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: HSTS_EXPIRES_IN, subdomains: false, preload: false } + { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false } end - def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, **options) + def initialize(app, redirect: {}, hsts: {}, secure_cookies: true) @app = app - 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 + @redirect = redirect @exclude = @redirect && @redirect[:exclude] || proc { !@redirect } @secure_cookies = secure_cookies - if hsts != true && hsts != false && hsts[:subdomains].nil? - hsts[:subdomains] = false - - ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc - In Rails 5.1, The `:subdomains` option of HSTS config will be treated as true if - unspecified. Set `config.ssl_options = { hsts: { subdomains: false } }` to opt out - of this behavior. - end_warning - end - @hsts_header = build_hsts_header(normalize_hsts_options(hsts)) end @@ -92,7 +82,7 @@ module ActionDispatch private def set_hsts_header!(headers) - headers['Strict-Transport-Security'.freeze] ||= @hsts_header + headers["Strict-Transport-Security".freeze] ||= @hsts_header end def normalize_hsts_options(options) @@ -109,19 +99,19 @@ module ActionDispatch end end - # http://tools.ietf.org/html/rfc6797#section-6.1 + # https://tools.ietf.org/html/rfc6797#section-6.1 def build_hsts_header(hsts) - value = "max-age=#{hsts[:expires].to_i}" + value = "max-age=#{hsts[:expires].to_i}".dup value << "; includeSubDomains" if hsts[:subdomains] value << "; preload" if hsts[:preload] value end def flag_cookies_as_secure!(headers) - if cookies = headers['Set-Cookie'.freeze] + if cookies = headers["Set-Cookie".freeze] cookies = cookies.split("\n".freeze) - headers['Set-Cookie'.freeze] = cookies.map { |cookie| + headers["Set-Cookie".freeze] = cookies.map { |cookie| if cookie !~ /;\s*secure\s*(;|$)/i "#{cookie}; secure" else @@ -132,17 +122,25 @@ module ActionDispatch end def redirect_to_https(request) - [ @redirect.fetch(:status, 301), - { 'Content-Type' => 'text/html', - 'Location' => https_location_for(request) }, + [ @redirect.fetch(:status, redirection_status(request)), + { "Content-Type" => "text/html", + "Location" => https_location_for(request) }, @redirect.fetch(:body, []) ] end + def redirection_status(request) + if request.get? || request.head? + 301 # Issue a permanent redirect via a GET request. + else + 307 # Issue a fresh request redirect to preserve the HTTP method. + end + end + def https_location_for(request) host = @redirect[:host] || request.host port = @redirect[:port] || request.port - location = "https://#{host}" + location = "https://#{host}".dup location << ":#{port}" if port != 80 && port != 443 location << request.fullpath location diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 0b4bee5462..b82f8aa3a3 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/inflector/methods" require "active_support/dependencies" @@ -88,7 +90,6 @@ module ActionDispatch end def delete(target) - target = get_class target middlewares.delete_if { |m| m.klass == target } end @@ -102,32 +103,14 @@ module ActionDispatch private - def assert_index(index, where) - 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 + def assert_index(index, where) + 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 - end - def build_middleware(klass, args, block) - Middleware.new(get_class(klass), args, block) - end + def build_middleware(klass, args, block) + Middleware.new(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 41c220236a..23492e14eb 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -1,20 +1,22 @@ -require 'rack/utils' -require 'active_support/core_ext/uri' +# frozen_string_literal: true + +require "rack/utils" +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 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"]` + # This middleware will render the file specified in <tt>env["PATH_INFO"]</tt> # where the base path is in the +root+ directory. For example, if the +root+ - # is set to `public/`, then a request with `env["PATH_INFO"]` of - # `assets/application.js` will return a response with the contents of a file - # located at `public/assets/application.js` if the file exists. If the file + # is set to +public/+, then a request with <tt>env["PATH_INFO"]</tt> of + # +assets/application.js+ will return a response with the contents of a file + # 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, index: 'index', headers: {}) - @root = root.chomp('/') + def initialize(root, index: "index", headers: {}) + @root = root.chomp("/") @file_server = ::Rack::File.new(@root, headers) @index = index end @@ -23,17 +25,17 @@ module ActionDispatch # correct read permissions, the return value is a URI-escaped string # representing the filename. Otherwise, false is returned. # - # Used by the `Static` class to check the existence of a valid file - # in the server's `public/` directory (see Static#call). + # 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 = ::Rack::Utils.unescape_path path - return false unless valid_path?(path) - path = Rack::Utils.clean_path_info path + return false unless ::Rack::Utils.valid_path? path + 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'.freeze)) + path = File.join(@root, p.dup.force_encoding(Encoding::UTF_8)) begin File.file?(path) && File.readable?(path) rescue SystemCallError @@ -46,7 +48,7 @@ module ActionDispatch end def call(env) - serve ActionDispatch::Request.new env + serve(Rack::Request.new(env)) end def serve(request) @@ -59,13 +61,13 @@ module ActionDispatch if status == 304 return [status, headers, body] end - headers['Content-Encoding'] = 'gzip' - headers['Content-Type'] = content_type(path) + headers["Content-Encoding"] = "gzip" + headers["Content-Type"] = content_type(path) else status, headers, body = @file_server.call(request.env) end - headers['Vary'] = 'Accept-Encoding' if gzip_path + headers["Vary"] = "Accept-Encoding" if gzip_path return [status, headers, body] ensure @@ -78,11 +80,11 @@ module ActionDispatch end def content_type(path) - ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze) + ::Rack::Mime.mime_type(::File.extname(path), "text/plain".freeze) end def gzip_encoding_accepted?(request) - request.accept_encoding =~ /\bgzip\b/i + request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i } end def gzip_file_path(path) @@ -94,39 +96,28 @@ module ActionDispatch false end end - - def valid_path?(path) - path.valid_encoding? && !path.include?("\0") - end end # This middleware will attempt to return the contents of a file's body from # disk in the response. If a file is not found on disk, the request will be # delegated to the application stack. This middleware is commonly initialized - # to serve assets from a server's `public/` directory. + # to serve assets from a server's +public/+ directory. # # This middleware verifies the path to ensure that only files # living in the root directory can be rendered. A request cannot # 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, 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 - + def initialize(app, path, index: "index", headers: {}) @app = app @file_handler = FileHandler.new(path, index: index, headers: headers) end def call(env) - req = ActionDispatch::Request.new env + req = Rack::Request.new env if req.get? || req.head? - path = req.path_info.chomp('/'.freeze) + path = req.path_info.chomp("/".freeze) if match = @file_handler.match?(path) req.path_info = match return @file_handler.serve(req) diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb new file mode 100644 index 0000000000..e1b129ccc5 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb @@ -0,0 +1,21 @@ +<header> + <h1> + <%= @exception.class.to_s %> + <% if @request.parameters['controller'] %> + in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> + <% end %> + </h1> +</header> + +<div id="container"> + <h2> + <%= h @exception.message %> + <% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %> + <br />To resolve this issue run: bin/rails active_storage:install + <% end %> + </h2> + + <%= render template: "rescues/_source" %> + <%= render template: "rescues/_trace" %> + <%= render template: "rescues/_request_and_response" %> +</div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb new file mode 100644 index 0000000000..033518cf8a --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb @@ -0,0 +1,13 @@ +<%= @exception.class.to_s %><% + if @request.parameters['controller'] +%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> +<% end %> + +<%= @exception.message %> +<% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %> +To resolve this issue run: bin/rails active_storage:install +<% end %> + +<%= render template: "rescues/_source" %> +<%= render template: "rescues/_trace" %> +<%= render template: "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb index e0509f56f4..39ea25bdfc 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb @@ -106,6 +106,7 @@ .line { padding-left: 10px; + white-space: pre; } .line:hover { diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index 429ea7057c..1fa0691303 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -17,6 +17,10 @@ line-height: 15px; } + #route_table thead tr.bottom th input#search { + -webkit-appearance: textfield; + } + #route_table tbody tr { border-bottom: 1px solid #ddd; } @@ -60,7 +64,7 @@ <%= link_to "Path", "#", 'data-route-helper' => '_path', title: "Returns a relative path (without the http or domain)" %> / <%= link_to "Url", "#", 'data-route-helper' => '_url', - title: "Returns an absolute url (with the http and domain)" %> + title: "Returns an absolute URL (with the http and domain)" %> </th> <th><%# HTTP Verb %> </th> @@ -93,7 +97,7 @@ } } - // get JSON from url and invoke callback with result + // get JSON from URL and invoke callback with result function getJSON(url, success) { var xhr = new XMLHttpRequest(); xhr.open('GET', url); |