diff options
Diffstat (limited to 'actionpack')
111 files changed, 2001 insertions, 719 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index b56a1c5d4b..a952eade08 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,10 +1,182 @@ +* Add `Referrer-Policy` header to default headers set. + + *Guillermo Iguaran* + +* Changed the system tests to set Puma as default server only when the + user haven't specified manually another server. + + *Guillermo Iguaran* + +* Add secure `X-Download-Options` and `X-Permitted-Cross-Domain-Policies` to + default headers set. + + *Guillermo Iguaran* + +* Add headless firefox support to System Tests. + + *bogdanvlviv* + +* Changed the default system test screenshot output from `inline` to `simple`. + + `inline` works well for iTerm2 but not everyone uses iTerm2. Some terminals like + Terminal.app ignore the `inline` and output the path to the file since it can't + render the image. Other terminals, like those on Ubuntu, cannot handle the image + inline, but also don't handle it gracefully and instead of outputting the file + path, it dumps binary into the terminal. + + Commit 9d6e28 fixes this by changing the default for screenshot to be `simple`. + + *Eileen M. Uchitelle* + +* Register most popular audio/video/font mime types supported by modern browsers. + + *Guillermo Iguaran* + +* Fix optimized url helpers when using relative url root + + Fixes #31220. + + *Andrew White* + + +## Rails 5.2.0.beta2 (November 28, 2017) ## + +* No changes. + + +## Rails 5.2.0.beta1 (November 27, 2017) ## + +* Add DSL for configuring Content-Security-Policy header + + The DSL allows you to configure a global Content-Security-Policy + header and then override within a controller. For more information + about the Content-Security-Policy header see MDN: + + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + + Example global policy: + + # config/initializers/content_security_policy.rb + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + p.font_src :self, :https, :data + p.img_src :self, :https, :data + p.object_src :none + p.script_src :self, :https + p.style_src :self, :https, :unsafe_inline + end + + Example controller overrides: + + # Override policy inline + class PostsController < ApplicationController + content_security_policy do |p| + p.upgrade_insecure_requests true + end + end + + # Using literal values + class PostsController < ApplicationController + content_security_policy do |p| + p.base_uri "https://www.example.com" + end + end + + # Using mixed static and dynamic values + class PostsController < ApplicationController + content_security_policy do |p| + p.base_uri :self, -> { "https://#{current_user.domain}.example.com" } + end + end + + Allows you to also only report content violations for migrating + legacy content using the `content_security_policy_report_only` + configuration attribute, e.g; + + # config/initializers/content_security_policy.rb + Rails.application.config.content_security_policy_report_only = true + + # controller override + class PostsController < ApplicationController + self.content_security_policy_report_only = true + end + + Note that this feature does not validate the header for performance + reasons since the header is calculated at runtime. + + *Andrew White* + +* Make `assert_recognizes` to traverse mounted engines + + *Yuichiro Kaneko* + +* Remove deprecated `ActionController::ParamsParser::ParseError`. + + *Rafael Mendonça França* + +* Add `:allow_other_host` option to `redirect_back` method. + + When `allow_other_host` is set to `false`, the `redirect_back` will not allow redirecting from a + different host. `allow_other_host` is `true` by default. + + *Tim Masliuchenko* + +* Add headless chrome support to System Tests. + + *Yuji Yaginuma* + +* Add ability to enable Early Hints for HTTP/2 + + If supported by the server, and enabled in Puma this allows H2 Early Hints to be used. + + The `javascript_include_tag` and the `stylesheet_link_tag` automatically add Early Hints if requested. + + *Eileen M. Uchitelle*, *Aaron Patterson* + +* Simplify cookies middleware with key rotation support + + Use the `rotate` method for both `MessageEncryptor` and + `MessageVerifier` to add key rotation support for encrypted and + signed cookies. This also helps simplify support for legacy cookie + security. + + *Michael J Coyne* + +* Use Capybara registered `:puma` server config. + + The Capybara registered `:puma` server ensures the puma server is run in process so + connection sharing and open request detection work correctly by default. + + *Thomas Walpole* + +* Cookies `:expires` option supports `ActiveSupport::Duration` object. + + cookies[:user_name] = { value: "assain", expires: 1.hour } + cookies[:key] = { value: "a yummy cookie", expires: 6.months } + + Pull Request: #30121 + + *Assain Jaleel* + +* Enforce signed/encrypted cookie expiry server side. + + Rails can thwart attacks by malicious clients that don't honor a cookie's expiry. + + It does so by stashing the expiry within the written cookie and relying on the + signing/encrypting to vouch that it hasn't been tampered with. Then on a + server-side read, the expiry is verified and any expired cookie is discarded. + + Pull Request: #30121 + + *Assain Jaleel* + * Make `take_failed_screenshot` work within engine. Fixes #30405. *Yuji Yaginuma* -* Deprecate `ActionDispatch::TestResponse` response aliases +* Deprecate `ActionDispatch::TestResponse` response aliases. `#success?`, `#missing?` & `#error?` are not supported by the actual `ActionDispatch::Response` object and can produce false-positives. Instead, @@ -26,7 +198,7 @@ *Kir Shatrov* -* `driven_by` now registers poltergeist and capybara-webkit +* `driven_by` now registers poltergeist and capybara-webkit. If poltergeist or capybara-webkit are set as drivers is set for System Tests, `driven_by` will register the driver and set additional options passed via @@ -36,7 +208,7 @@ *Mario Chavez* -* AEAD encrypted cookies and sessions with GCM +* AEAD encrypted cookies and sessions with GCM. Encrypted cookies now use AES-GCM which couples authentication and encryption in one faster step and produces shorter ciphertexts. Cookies diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index ac810e86d0..1cb3add0fc 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 93b2a0932a..f56230ffa0 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -30,7 +30,7 @@ The latest version of Action Pack can be installed with RubyGems: $ gem install actionpack -Source code can be downloaded as part of the Rails project on GitHub +Source code can be downloaded as part of the Rails project on GitHub: * https://github.com/rails/rails/tree/master/actionpack @@ -44,11 +44,11 @@ Action Pack is released under the MIT license: == Support -API documentation is at +API documentation is at: * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 3761054bb7..a312af6715 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "error" +require "abstract_controller/error" require "active_support/configurable" require "active_support/descendants_tracker" require "active_support/core_ext/module/anonymous" @@ -180,8 +180,6 @@ module AbstractController # # ==== Parameters # * <tt>name</tt> - The name of an action to be tested - # - # :api: private def action_method?(name) self.class.action_methods.include?(name) end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 715d043b4e..146d17cf40 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -35,8 +35,8 @@ module AbstractController skip_after_callbacks_if_terminated: true end - # Override AbstractController::Base's process_action to run the - # process_action callbacks around the normal behavior. + # Override <tt>AbstractController::Base#process_action</tt> to run the + # <tt>process_action</tt> callbacks around the normal behavior. def process_action(*args) run_callbacks(:process_action) do super diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 41898c4c2e..8ba2b25552 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "error" +require "abstract_controller/error" require "action_view" require "action_view/view_paths" require "set" @@ -20,7 +20,6 @@ module AbstractController # Normalizes arguments, options and then delegates render_to_body and # sticks the result in <tt>self.response_body</tt>. - # :api: public def render(*args, &block) options = _normalize_render(*args, &block) rendered_body = render_to_body(options) @@ -42,19 +41,16 @@ module AbstractController # (as ActionController extends it to be anything that # responds to the method each), this method needs to be # overridden in order to still return a string. - # :api: plugin def render_to_string(*args, &block) options = _normalize_render(*args, &block) render_to_body(options) end # Performs the actual template rendering. - # :api: public def render_to_body(options = {}) end - # Returns Content-Type of rendered content - # :api: public + # Returns Content-Type of rendered content. def rendered_format Mime[:text] end @@ -65,7 +61,6 @@ module AbstractController # This method should return a hash with assigns. # You can overwrite this configuration per controller. - # :api: public def view_assigns protected_vars = _protected_ivars variables = instance_variables @@ -76,11 +71,11 @@ module AbstractController } end + private # Normalize args by converting <tt>render "foo"</tt> to # <tt>render :action => "foo"</tt> and <tt>render "foo/bar"</tt> to # <tt>render :file => "foo/bar"</tt>. - # :api: plugin - def _normalize_args(action = nil, options = {}) + def _normalize_args(action = nil, options = {}) # :doc: if action.respond_to?(:permitted?) if action.permitted? action @@ -95,20 +90,17 @@ module AbstractController end # Normalize options. - # :api: plugin - def _normalize_options(options) + def _normalize_options(options) # :doc: options end # Process extra options. - # :api: plugin - def _process_options(options) + def _process_options(options) # :doc: options end # Process the rendered format. - # :api: private - def _process_format(format) + def _process_format(format) # :nodoc: end def _process_variant(options) @@ -121,8 +113,7 @@ module AbstractController end # Normalize args and options. - # :api: private - def _normalize_render(*args, &block) + def _normalize_render(*args, &block) # :nodoc: options = _normalize_args(*args, &block) _process_variant(options) _normalize_options(options) diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index e893507baa..f43784f9f2 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -3,8 +3,8 @@ require "active_support/rails" require "abstract_controller" require "action_dispatch" -require_relative "action_controller/metal/live" -require_relative "action_controller/metal/strong_parameters" +require "action_controller/metal/live" +require "action_controller/metal/strong_parameters" module ActionController extend ActiveSupport::Autoload @@ -22,6 +22,7 @@ module ActionController autoload_under "metal" do autoload :ConditionalGet + autoload :ContentSecurityPolicy autoload :Cookies autoload :DataStreaming autoload :EtagWithTemplateDigest diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index ba9af4767e..b192e496de 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -2,7 +2,7 @@ require "action_view" require "action_controller" -require_relative "log_subscriber" +require "action_controller/log_subscriber" module ActionController # API Controller is a lightweight version of <tt>ActionController::Base</tt>, diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index bbc48e6eb7..204a3d400c 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true require "action_view" -require_relative "log_subscriber" -require_relative "metal/params_wrapper" +require "action_controller/log_subscriber" +require "action_controller/metal/params_wrapper" module ActionController # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed @@ -225,6 +225,7 @@ module ActionController Flash, FormBuilder, RequestForgeryProtection, + ContentSecurityPolicy, ForceSSL, Streaming, DataStreaming, diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb new file mode 100644 index 0000000000..48a7109bea --- /dev/null +++ b/actionpack/lib/action_controller/metal/content_security_policy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ActionController #:nodoc: + module ContentSecurityPolicy + # TODO: Documentation + extend ActiveSupport::Concern + + module ClassMethods + def content_security_policy(**options, &block) + before_action(options) do + if block_given? + policy = request.content_security_policy.clone + yield policy + request.content_security_policy = policy + end + end + end + + def content_security_policy_report_only(report_only = true, **options) + before_action(options) do + request.content_security_policy_report_only = report_only + end + end + end + end +end diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 882f6f3d0a..5a82ccf668 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "exceptions" +require "action_controller/metal/exceptions" module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index f808295720..a65857d6ef 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -34,9 +34,6 @@ module ActionController class NotImplemented < MethodNotAllowed #:nodoc: end - class UnknownController < ActionControllerError #:nodoc: - end - class MissingFile < ActionControllerError #:nodoc: end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index 0ba1f9f783..7de500d119 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -39,7 +39,7 @@ module ActionController # end # # ==== URL Options - # You can pass any of the following options to affect the redirect url + # You can pass any of the following options to affect the redirect URL # * <tt>host</tt> - Redirect to a different host name # * <tt>subdomain</tt> - Redirect to a different subdomain # * <tt>domain</tt> - Redirect to a different domain @@ -73,7 +73,7 @@ module ActionController # Redirect the existing request to use the HTTPS protocol. # # ==== Parameters - # * <tt>host_or_options</tt> - Either a host name or any of the url and + # * <tt>host_or_options</tt> - Either a host name or any of the URL and # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 08d9b094f3..01676f3237 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -72,10 +72,10 @@ module ActionController before_action(options.except(:name, :password, :realm)) do authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password| # This comparison uses & so that it doesn't short circuit and - # uses `variable_size_secure_compare` so that length information + # uses `secure_compare` so that length information # isn't leaked. - ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) & - ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password]) + ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) & + ActiveSupport::SecurityUtils.secure_compare(password, options[:password]) end end end @@ -248,7 +248,7 @@ module ActionController def decode_credentials(header) ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair| key, value = pair.split("=", 2) - [key.strip, value.to_s.gsub(/^"|"$/, "").delete('\'')] + [key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")] end] end @@ -350,10 +350,7 @@ module ActionController # authenticate_or_request_with_http_token do |token, options| # # Compare the tokens in a time-constant manner, to mitigate # # timing attacks. - # ActiveSupport::SecurityUtils.secure_compare( - # ::Digest::SHA256.hexdigest(token), - # ::Digest::SHA256.hexdigest(TOKEN) - # ) + # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN) # end # end # end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 476f0843b2..be9449629f 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -83,16 +83,13 @@ module ActionController # def cleanup_view_runtime # super - time_taken_in_something_expensive # end - # - # :api: plugin - def cleanup_view_runtime + def cleanup_view_runtime # :doc: yield end # Every time after an action is processed, this method is invoked # with the payload, so you can add more information. - # :api: plugin - def append_info_to_payload(payload) + def append_info_to_payload(payload) # :doc: payload[:view_runtime] = view_runtime end @@ -100,7 +97,6 @@ module ActionController # A hook which allows other frameworks to log what happened during # controller process action. This method should return an array # with the messages to be added. - # :api: plugin def log_process_action(payload) #:nodoc: messages, view_runtime = [], payload[:view_runtime] messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index f4f2381286..a678377d4f 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -112,6 +112,14 @@ module ActionController else self.include = m.attribute_names end + + if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any? + self.include += m.nested_attributes_options.keys.map do |key| + key.to_s.concat("_attributes") + end + end + + self.include end end end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 5cd8568d8d..4c2b5120eb 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -68,7 +68,7 @@ module ActionController # if possible, otherwise redirects to the provided default fallback # location. # - # The referrer information is pulled from the HTTP `Referer` (sic) header on + # The referrer information is pulled from the HTTP +Referer+ (sic) header on # the request. This is an optional header and its presence on the request is # subject to browser security settings and user preferences. If the request # is missing this header, the <tt>fallback_location</tt> will be used. @@ -79,15 +79,18 @@ module ActionController # redirect_back fallback_location: "/images/screenshot.jpg" # redirect_back fallback_location: posts_url # redirect_back fallback_location: proc { edit_post_url(@post) } + # redirect_back fallback_location: '/', allow_other_host: false # - # All options that can be passed to <tt>redirect_to</tt> are accepted as + # ==== Options + # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header. + # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true. + # + # All other options that can be passed to <tt>redirect_to</tt> are accepted as # options and the behavior is identical. - def redirect_back(fallback_location:, **args) - if referer = request.headers["Referer"] - redirect_to referer, **args - else - redirect_to fallback_location, **args - end + def redirect_back(fallback_location:, allow_other_host: true, **args) + referer = request.headers["Referer"] + redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer)) + redirect_to redirect_to_referer ? referer : fallback_location, **args end def _compute_redirect_to_location(request, options) #:nodoc: @@ -120,5 +123,11 @@ module ActionController 302 end end + + def _url_host_allowed?(url) + URI(url.to_s).host == request.host + rescue ArgumentError, URI::Error + false + end end end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 26752571f8..b81d3ef539 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -85,7 +85,7 @@ module ActionController def self.remove(key) RENDERERS.delete(key.to_sym) method_name = _render_with_renderer_method_name(key) - remove_method(method_name) if method_defined?(method_name) + remove_possible_method(method_name) end def self._render_with_renderer_method_name(key) diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index d32eabf9ba..6d181e6456 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/string/filters" - module ActionController module Rendering extend ActiveSupport::Concern @@ -42,7 +40,7 @@ module ActionController def render_to_string(*) result = super if result.respond_to?(:each) - string = "" + string = "".dup result.each { |r| string << r } string else diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 813a7e00d4..0ab313e398 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true require "rack/session/abstract/id" -require_relative "exceptions" +require "action_controller/metal/exceptions" require "active_support/security_utils" +require "active_support/core_ext/string/strip" module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: @@ -216,7 +217,7 @@ module ActionController #:nodoc: # The actual before_action that is used to verify the CSRF token. # Don't override this directly. Provide your own forgery protection # strategy instead. If you override, you'll disable same-origin - # `<script>` verification. + # <tt><script></tt> verification. # # Lean on the protect_from_forgery declaration to mark which actions are # due for same-origin request verification. If protect_from_forgery is @@ -248,8 +249,9 @@ module ActionController #:nodoc: "If you know what you're doing, go ahead and disable forgery " \ "protection on this action to permit cross-origin JavaScript embedding." private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING + # :startdoc: - # If `verify_authenticity_token` was run (indicating that we have + # If +verify_authenticity_token+ was run (indicating that we have # forgery protection enabled for this request) then also verify that # we aren't serving an unauthorized cross-origin response. def verify_same_origin_request # :doc: @@ -266,7 +268,7 @@ module ActionController #:nodoc: @marked_for_same_origin_verification = request.get? end - # If the `verify_authenticity_token` before_action ran, verify that + # If the +verify_authenticity_token+ before_action ran, verify that # JavaScript responses are only served to same-origin GET requests. def marked_for_same_origin_verification? # :doc: @marked_for_same_origin_verification ||= false @@ -368,7 +370,7 @@ module ActionController #:nodoc: end def compare_with_real_token(token, session) # :doc: - ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) + ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session)) end def valid_per_form_csrf_token?(token, session) # :doc: @@ -379,7 +381,7 @@ module ActionController #:nodoc: request.request_method ) - ActiveSupport::SecurityUtils.secure_compare(token, correct_token) + ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token) else false end @@ -414,11 +416,21 @@ module ActionController #:nodoc: allow_forgery_protection end + NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc + The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually + means you have the 'no-referrer' Referrer-Policy header enabled, or that you the request came from a site that + refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the + best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin. + If you cannot change the referrer policy, you can disable origin checking with the + Rails.application.config.action_controller.forgery_protection_origin_check setting. + MSG + # Checks if the request originated from the same origin by looking at the # Origin header. def valid_request_origin? # :doc: if forgery_protection_origin_check # We accept blank origin headers because some user agents don't send it. + raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null" request.origin.nil? || request.origin == request.base_url else true diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 0b1598bf1b..8dc01a5eb9 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -183,7 +183,7 @@ module ActionController #:nodoc: # unicorn_rails --config-file unicorn.config.rb # # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>. - # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen + # Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen # # If you are using Unicorn with NGINX, you may need to tweak NGINX. # Streaming should work out of the box on Rainbows. diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index ef7c4c4c16..a56ac749f8 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -335,7 +335,7 @@ module ActionController # the same way as <tt>Hash#each_pair</tt>. def each_pair(&block) @parameters.each_pair do |key, value| - yield key, convert_hashes_to_parameters(key, value) + yield [key, convert_hashes_to_parameters(key, value)] end end alias_method :each, :each_pair diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index b07f1f3d8c..6e8a95040f 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -12,11 +12,5 @@ module ActionController self.params = nil end end - - module ClassMethods - def before_filters - _process_action_callbacks.find_all { |x| x.kind == :before }.map(&:name) - end - end end end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 769be39004..7d42f5d931 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -4,7 +4,7 @@ require "rails" require "action_controller" require "action_dispatch/railtie" require "abstract_controller/railties/routes_helpers" -require_relative "railties/helpers" +require "action_controller/railties/helpers" require "action_view/railtie" module ActionController diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 50a96bce98..4b408750a4 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -4,9 +4,10 @@ require "rack/session/abstract/id" require "active_support/core_ext/hash/conversions" require "active_support/core_ext/object/to_query" require "active_support/core_ext/module/anonymous" +require "active_support/core_ext/module/redefine_method" require "active_support/core_ext/hash/keys" require "active_support/testing/constant_lookup" -require_relative "template_assertions" +require "action_controller/template_assertions" require "rails-dom-testing" module ActionController @@ -19,7 +20,7 @@ module ActionController # the database on the main thread, so they could open a txn, then the # controller thread will open a new connection and try to access data # that's only visible to the main thread's txn. This is the problem in #23483. - remove_method :new_controller_thread + silence_redefinition_of_method :new_controller_thread def new_controller_thread # :nodoc: yield end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 34937f3229..0822cdc0a6 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -42,6 +42,7 @@ module ActionDispatch eager_autoload do autoload_under "http" do + autoload :ContentSecurityPolicy autoload :Request autoload :Response end diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 8073685b78..a8febc32b3 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -133,7 +133,7 @@ module ActionDispatch end def generate_strong_etag(validators) - %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}") + %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}") end def cache_control_segments @@ -166,19 +166,23 @@ module ActionDispatch @cache_control = cache_control_headers end - def handle_conditional_get! - if etag? || last_modified? || !@cache_control.empty? - set_conditional_cache_control!(@cache_control) - end - end - DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze NO_CACHE = "no-cache".freeze PUBLIC = "public".freeze PRIVATE = "private".freeze MUST_REVALIDATE = "must-revalidate".freeze - def set_conditional_cache_control!(cache_control) + def handle_conditional_get! + # Normally default cache control setting is handled by ETag + # middleware. But, if an etag is already set, the middleware + # defaults to `no-cache` unless a default `Cache-Control` value is + # previously set. So, set a default one here. + if (etag? || last_modified?) && !self._cache_control + self._cache_control = DEFAULT_CACHE_CONTROL + end + end + + def merge_and_normalize_cache_control!(cache_control) control = {} cc_headers = cache_control_headers if extras = cc_headers.delete(:extras) @@ -191,7 +195,7 @@ module ActionDispatch control.merge! cache_control if control.empty? - self._cache_control = DEFAULT_CACHE_CONTROL + # Let middleware handle default behavior elsif control[:no_cache] self._cache_control = NO_CACHE if control[:extras] diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb new file mode 100644 index 0000000000..4883e23d24 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +require "active_support/core_ext/object/deep_dup" + +module ActionDispatch #:nodoc: + class ContentSecurityPolicy + class Middleware + CONTENT_TYPE = "Content-Type".freeze + POLICY = "Content-Security-Policy".freeze + POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze + + def initialize(app) + @app = app + end + + def call(env) + request = ActionDispatch::Request.new env + _, headers, _ = response = @app.call(env) + + return response unless html_response?(headers) + return response if policy_present?(headers) + + if policy = request.content_security_policy + headers[header_name(request)] = policy.build(request.controller_instance) + end + + response + end + + private + + def html_response?(headers) + if content_type = headers[CONTENT_TYPE] + content_type =~ /html/ + end + end + + def header_name(request) + if request.content_security_policy_report_only + POLICY_REPORT_ONLY + else + POLICY + end + end + + def policy_present?(headers) + headers[POLICY] || headers[POLICY_REPORT_ONLY] + end + end + + module Request + POLICY = "action_dispatch.content_security_policy".freeze + POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze + + def content_security_policy + get_header(POLICY) + end + + def content_security_policy=(policy) + set_header(POLICY, policy) + end + + def content_security_policy_report_only + get_header(POLICY_REPORT_ONLY) + end + + def content_security_policy_report_only=(value) + set_header(POLICY_REPORT_ONLY, value) + end + end + + MAPPINGS = { + self: "'self'", + unsafe_eval: "'unsafe-eval'", + unsafe_inline: "'unsafe-inline'", + none: "'none'", + http: "http:", + https: "https:", + data: "data:", + mediastream: "mediastream:", + blob: "blob:", + filesystem: "filesystem:", + report_sample: "'report-sample'", + strict_dynamic: "'strict-dynamic'" + }.freeze + + DIRECTIVES = { + base_uri: "base-uri", + child_src: "child-src", + connect_src: "connect-src", + default_src: "default-src", + font_src: "font-src", + form_action: "form-action", + frame_ancestors: "frame-ancestors", + frame_src: "frame-src", + img_src: "img-src", + manifest_src: "manifest-src", + media_src: "media-src", + object_src: "object-src", + script_src: "script-src", + style_src: "style-src", + worker_src: "worker-src" + }.freeze + + private_constant :MAPPINGS, :DIRECTIVES + + attr_reader :directives + + def initialize + @directives = {} + yield self if block_given? + end + + def initialize_copy(other) + @directives = other.directives.deep_dup + end + + DIRECTIVES.each do |name, directive| + define_method(name) do |*sources| + if sources.first + @directives[directive] = apply_mappings(sources) + else + @directives.delete(directive) + end + end + end + + def block_all_mixed_content(enabled = true) + if enabled + @directives["block-all-mixed-content"] = true + else + @directives.delete("block-all-mixed-content") + end + end + + def plugin_types(*types) + if types.first + @directives["plugin-types"] = types + else + @directives.delete("plugin-types") + end + end + + def report_uri(uri) + @directives["report-uri"] = [uri] + end + + def require_sri_for(*types) + if types.first + @directives["require-sri-for"] = types + else + @directives.delete("require-sri-for") + end + end + + def sandbox(*values) + if values.empty? + @directives["sandbox"] = true + elsif values.first + @directives["sandbox"] = values + else + @directives.delete("sandbox") + end + end + + def upgrade_insecure_requests(enabled = true) + if enabled + @directives["upgrade-insecure-requests"] = true + else + @directives.delete("upgrade-insecure-requests") + end + end + + def build(context = nil) + build_directives(context).compact.join("; ") + ";" + end + + private + def apply_mappings(sources) + sources.map do |source| + case source + when Symbol + apply_mapping(source) + when String, Proc + source + else + raise ArgumentError, "Invalid content security policy source: #{source.inspect}" + end + end + end + + def apply_mapping(source) + MAPPINGS.fetch(source) do + raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}" + end + end + + def build_directives(context) + @directives.map do |directive, sources| + if sources.is_a?(Array) + "#{directive} #{build_directive(sources, context).join(' ')}" + elsif sources + directive + else + nil + end + end + end + + def build_directive(sources, context) + sources.map { |source| resolve_source(source, context) } + end + + def resolve_source(source, context) + case source + when String + source + when Symbol + source.to_s + when Proc + if context.nil? + raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}" + else + context.instance_exec(&source) + end + else + raise RuntimeError, "Unexpected content security policy source: #{source.inspect}" + end + end + end +end diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index b7141cc1b9..ec86b8bc47 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "parameter_filter" +require "action_dispatch/http/parameter_filter" module ActionDispatch module Http @@ -9,7 +9,7 @@ module ActionDispatch # sub-hashes of the params hash to filter. Filtering only certain sub-keys # from a hash is possible by using the dot notation: 'credit_card.number'. # If a block is given, each key and value of the params hash and all - # sub-hashes is passed to it, the value or key can be replaced using + # sub-hashes is passed to it, where the value or the key can be replaced using # String#replace or similar method. # # env["action_dispatch.parameter_filter"] = [:password] @@ -48,7 +48,7 @@ module ActionDispatch @filtered_env ||= env_filter.filter(@env) end - # Reconstructed a path with all sensitive GET parameters replaced. + # Reconstructs a path with all sensitive GET parameters replaced. def filtered_path @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}" end diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 0ca18d98a1..d7435fa8df 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -12,9 +12,6 @@ module ActionDispatch end # The MIME type of the HTTP request, such as Mime[:xml]. - # - # For backward compatibility, the post \format is extracted from the - # X-Post-Data-Format HTTP header if present. def content_mime_type fetch_header("action_dispatch.request.content_type") do |k| v = if get_header("CONTENT_TYPE") =~ /^([^,\;]*)/ diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index d797e90e52..d2b2106845 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -339,4 +339,4 @@ module Mime end end -require_relative "mime_types" +require "action_dispatch/http/mime_types" diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index f8e6fca36d..342e6de312 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -10,6 +10,7 @@ Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv Mime::Type.register "text/vcard", :vcf +Mime::Type.register "text/vtt", :vtt, %w(vtt) Mime::Type.register "image/png", :png, [], %w(png) Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) @@ -20,6 +21,18 @@ Mime::Type.register "image/svg+xml", :svg Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe) +Mime::Type.register "audio/mpeg", :mp3, [], %w(mp1 mp2 mp3) +Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus) +Mime::Type.register "audio/aac", :m4a, %w( audio/mp4 ), %w(m4a mpg4 aac) + +Mime::Type.register "video/webm", :webm, [], %w(webm) +Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v) + +Mime::Type.register "font/otf", :otf, [], %w(otf) +Mime::Type.register "font/ttf", :ttf, [], %w(ttf) +Mime::Type.register "font/woff", :woff, [], %w(woff) +Mime::Type.register "font/woff2", :woff2, [], %w(woff2) + Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml ) Mime::Type.register "application/rss+xml", :rss Mime::Type.register "application/atom+xml", :atom diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index ae875eb830..8d7431fd6b 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -123,9 +123,4 @@ module ActionDispatch end end end - - module ParamsParser - include ActiveSupport::Deprecation::DeprecatedConstantAccessor - deprecate_constant "ParseError", "ActionDispatch::Http::Parameters::ParseError" - end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index dee7be184a..3838b84a7a 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -3,15 +3,15 @@ require "stringio" require "active_support/inflector" -require_relative "headers" +require "action_dispatch/http/headers" require "action_controller/metal/exceptions" require "rack/request" -require_relative "cache" -require_relative "mime_negotiation" -require_relative "parameters" -require_relative "filter_parameters" -require_relative "upload" -require_relative "url" +require "action_dispatch/http/cache" +require "action_dispatch/http/mime_negotiation" +require "action_dispatch/http/parameters" +require "action_dispatch/http/filter_parameters" +require "action_dispatch/http/upload" +require "action_dispatch/http/url" require "active_support/core_ext/array/conversions" module ActionDispatch @@ -22,6 +22,7 @@ module ActionDispatch include ActionDispatch::Http::Parameters include ActionDispatch::Http::FilterParameters include ActionDispatch::Http::URL + include ActionDispatch::ContentSecurityPolicy::Request include Rack::Request::Env autoload :Session, "action_dispatch/request/session" @@ -199,6 +200,23 @@ module ActionDispatch @headers ||= Http::Headers.new(self) end + # Early Hints is an HTTP/2 status code that indicates hints to help a client start + # making preparations for processing the final response. + # + # If the env contains +rack.early_hints+ then the server accepts HTTP2 push for Link headers. + # + # The +send_early_hints+ method accepts a hash of links as follows: + # + # send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload") + # + # If you are using +javascript_include_tag+ or +stylesheet_link_tag+ the + # Early Hints headers are included by default if supported. + def send_early_hints(links) + return unless env["rack.early_hints"] + + env["rack.early_hints"].call(links) + end + # Returns a +String+ with the last requested path including their params. # # # get '/foo' diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index b314dbecfe..7e50cb6d23 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true require "active_support/core_ext/module/attribute_accessors" -require_relative "filter_redirect" -require_relative "cache" +require "action_dispatch/http/filter_redirect" +require "action_dispatch/http/cache" require "monitor" module ActionDispatch # :nodoc: @@ -433,6 +433,7 @@ module ActionDispatch # :nodoc: def before_committed return if committed? assign_default_content_type_and_charset! + merge_and_normalize_cache_control!(@cache_control) handle_conditional_get! handle_no_content! end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index f0344fd927..35ba44005a 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -274,7 +274,7 @@ module ActionDispatch def standard_port case protocol when "https://" then 443 - else 80 + else 80 end end diff --git a/actionpack/lib/action_dispatch/journey.rb b/actionpack/lib/action_dispatch/journey.rb index 903063d00f..2852efa6ae 100644 --- a/actionpack/lib/action_dispatch/journey.rb +++ b/actionpack/lib/action_dispatch/journey.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative "journey/router" -require_relative "journey/gtg/builder" -require_relative "journey/gtg/simulator" -require_relative "journey/nfa/builder" -require_relative "journey/nfa/simulator" +require "action_dispatch/journey/router" +require "action_dispatch/journey/gtg/builder" +require "action_dispatch/journey/gtg/simulator" +require "action_dispatch/journey/nfa/builder" +require "action_dispatch/journey/nfa/simulator" diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb index 7e3d957baa..44c31053cb 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/builder.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "transition_table" +require "action_dispatch/journey/gtg/transition_table" module ActionDispatch module Journey # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb index 6ed478f816..ea647e051a 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "../nfa/dot" +require "action_dispatch/journey/nfa/dot" module ActionDispatch module Journey # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/nfa/builder.rb b/actionpack/lib/action_dispatch/journey/nfa/builder.rb index 3135c05ffa..d22302e101 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/builder.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/builder.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative "transition_table" -require_relative "../gtg/transition_table" +require "action_dispatch/journey/nfa/transition_table" +require "action_dispatch/journey/gtg/transition_table" module ActionDispatch module Journey # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb index bdb78d8d48..56e9e3c83d 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb @@ -9,16 +9,16 @@ module ActionDispatch " #{from} -> #{to} [label=\"#{sym || 'ε'}\"];" } - #memo_nodes = memos.values.flatten.map { |n| - # label = n - # if Journey::Route === n - # label = "#{n.verb.source} #{n.path.spec}" - # end - # " #{n.object_id} [label=\"#{label}\", shape=box];" - #} - #memo_edges = memos.flat_map { |k, memos| - # (memos || []).map { |v| " #{k} -> #{v.object_id};" } - #}.uniq + # memo_nodes = memos.values.flatten.map { |n| + # label = n + # if Journey::Route === n + # label = "#{n.verb.source} #{n.path.spec}" + # end + # " #{n.object_id} [label=\"#{label}\", shape=box];" + # } + # memo_edges = memos.flat_map { |k, memos| + # (memos || []).map { |v| " #{k} -> #{v.object_id};" } + # }.uniq <<-eodot digraph nfa { diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb index bfd929357b..fe55861507 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "dot" +require "action_dispatch/journey/nfa/dot" module ActionDispatch module Journey # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index 0a84f28c1a..08b931a3cd 100644 --- a/actionpack/lib/action_dispatch/journey/nodes/node.rb +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "../visitors" +require "action_dispatch/journey/visitors" module ActionDispatch module Journey # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index 6ddfe96098..e002755bcf 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -8,7 +8,7 @@ require 'racc/parser.rb' # :stopdoc: -require_relative "parser_extras" +require "action_dispatch/journey/parser_extras" module ActionDispatch module Journey class Parser < Racc::Parser diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y index 850c84ea1a..f9b1a7a958 100644 --- a/actionpack/lib/action_dispatch/journey/parser.y +++ b/actionpack/lib/action_dispatch/journey/parser.y @@ -47,4 +47,4 @@ end ---- header # :stopdoc: -require_relative "parser_extras" +require "action_dispatch/journey/parser_extras" diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb index dfbc6c4529..18ec6c9b9b 100644 --- a/actionpack/lib/action_dispatch/journey/parser_extras.rb +++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative "scanner" -require_relative "nodes/node" +require "action_dispatch/journey/scanner" +require "action_dispatch/journey/nodes/node" module ActionDispatch # :stopdoc: diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 9987a9bfa1..30af3ff930 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require_relative "router/utils" -require_relative "routes" -require_relative "formatter" +require "action_dispatch/journey/router/utils" +require "action_dispatch/journey/routes" +require "action_dispatch/journey/formatter" before = $-w $-w = false -require_relative "parser" +require "action_dispatch/journey/parser" $-w = before -require_relative "route" -require_relative "path/pattern" +require "action_dispatch/journey/route" +require "action_dispatch/journey/path/pattern" module ActionDispatch module Journey # :nodoc: @@ -61,7 +61,7 @@ module ActionDispatch return [status, headers, body] end - return [404, { "X-Cascade" => "pass" }, ["Not Found"]] + [404, { "X-Cascade" => "pass" }, ["Not Found"]] end def recognize(rails_req) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index c0913715ac..ea4156c972 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -49,6 +49,18 @@ module ActionDispatch 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 @@ -64,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 @@ -83,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 # @@ -100,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: # @@ -118,7 +136,7 @@ module ActionDispatch # # cookies[:name] = { # value: 'a yummy cookie', - # expires: 1.year.from_now, + # expires: 1.year, # domain: 'domain.com' # } # @@ -143,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 @@ -156,10 +174,14 @@ module ActionDispatch 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 @@ -188,10 +210,10 @@ module ActionDispatch # 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. # - # 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: # @@ -200,40 +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 +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. # # 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 +secrets.secret_key_base+. + # 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) - elsif upgrade_legacy_hmac_aes_cbc_cookies? - UpgradeLegacyHmacAesCbcCookieJar.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. @@ -254,34 +264,18 @@ module ActionDispatch end def upgrade_legacy_hmac_aes_cbc_cookies? - request.secret_key_base.present? && - request.authenticated_encrypted_cookie_salt.present? && - request.encrypted_signed_cookie_salt.present? && - request.encrypted_cookie_salt.present? + request.secret_key_base.present? && + request.encrypted_signed_cookie_salt.present? && + request.encrypted_cookie_salt.present? && + request.use_authenticated_cookie_encryption end - 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 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 @@ -523,6 +517,7 @@ module ActionDispatch module SerializedCookieJars # :nodoc: MARSHAL_SIGNATURE = "\x04\x08".freeze + SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer protected def needs_migration?(value) @@ -533,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 @@ -560,24 +559,31 @@ module ActionDispatch def digest request.cookies_digest || "SHA1" end - - def key_generator - request.key_generator - 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) @@ -587,37 +593,47 @@ module ActionDispatch 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 - cipher = "aes-256-gcm" - key_len = ActiveSupport::MessageEncryptor.key_len(cipher) - secret = key_generator.generate_key(request.authenticated_encrypted_cookie_salt || "")[0, key_len] + 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 - @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, 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) @@ -625,39 +641,15 @@ module ActionDispatch 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 - end + def parse_legacy_signed_message(name, legacy_signed_message) + if defined?(@legacy_verifier) + deserialize(name) do |rotate| + rotate.call - # UpgradeLegacyHmacAesCbcCookieJar is used by ActionDispatch::Session::CookieStore - # to upgrade cookies encrypted with AES-256-CBC with HMAC to AES-256-GCM - class UpgradeLegacyHmacAesCbcCookieJar < EncryptedCookieJar - def initialize(parent_jar) - super - - secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len] - sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "") - - @legacy_encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) - end - - def decrypt_and_verify_legacy_encrypted_message(name, signed_message) - deserialize(name, @legacy_encryptor.decrypt_and_verify(signed_message)).tap do |value| - self[name] = { value: value } - end - rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage - nil - end - - private - def parse(name, signed_message) - super || decrypt_and_verify_legacy_encrypted_message(name, signed_message) + @legacy_verifier.verified(legacy_signed_message) + end + end end end diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 3006cd97ce..511306eb0e 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require_relative "../http/request" -require_relative "exception_wrapper" -require_relative "../routing/inspector" +require "action_dispatch/http/request" +require "action_dispatch/middleware/exception_wrapper" +require "action_dispatch/routing/inspector" require "action_view" require "action_view/base" diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 4f69abfa6f..d1b4508378 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -25,6 +25,7 @@ module ActionDispatch "ActionView::MissingTemplate" => "missing_template", "ActionController::RoutingError" => "routing_error", "AbstractController::ActionNotFound" => "unknown_action", + "ActiveRecord::StatementInvalid" => "invalid_statement", "ActionView::Template::Error" => "template_error" ) diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index e054fefc9b..5b0be96223 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -3,8 +3,8 @@ require "rack/utils" require "rack/request" require "rack/session/abstract/id" -require_relative "../cookies" -require_relative "../../request/session" +require "action_dispatch/middleware/cookies" +require "action_dispatch/request/session" module ActionDispatch module Session diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb index c84bc8bfad..a6d965a644 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "abstract_store" +require "action_dispatch/middleware/session/abstract_store" module ActionDispatch module Session diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index a12cb00d36..4ea96196d3 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "active_support/core_ext/hash/keys" -require_relative "abstract_store" +require "action_dispatch/middleware/session/abstract_store" require "rack/session/cookie" module ActionDispatch @@ -21,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 <tt>config/initializers/session_store.rb</tt>: # # Rails.application.config.session_store :cookie_store, key: '_your_app_session' # - # Configure your secret key in <tt>config/secrets.yml</tt>: - # - # development: - # secret_key_base: 'secret key' - # - # To generate a secret key for an existing application, run <tt>rails secret</tt>. + # 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. # - # 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. + # 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. # - # 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 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 f0aec39c9c..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,6 @@ # frozen_string_literal: true -require_relative "abstract_store" +require "action_dispatch/middleware/session/abstract_store" begin require "rack/session/dalli" rescue LoadError => e diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index d2e739d27f..3c88afd4d3 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative "../http/request" -require_relative "exception_wrapper" +require "action_dispatch/http/request" +require "action_dispatch/middleware/exception_wrapper" module ActionDispatch # This middleware rescues any exception returned by the application diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 45290b6ac3..ef633aadc6 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -1,47 +1,52 @@ # 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. # - # 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. + # Requests can opt-out of redirection with +exclude+: # - # 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. + # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } } # - # Set `config.ssl_options` with `hsts: { … }` to configure HSTS: - # * `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). - # * `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. - # Defaults to `false`. + # 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. # - # 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 }`. + # 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. # - # Requests can opt-out of redirection with `exclude`: + # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS: # - # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } } + # * +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). + # + # * +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 <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+. + # + # 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 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/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 4743a7ce61..eb6fbca6ba 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "action_dispatch" +require "active_support/messages/rotation_configuration" module ActionDispatch class Railtie < Rails::Railtie # :nodoc: @@ -18,15 +19,21 @@ module ActionDispatch config.action_dispatch.signed_cookie_salt = "signed cookie" config.action_dispatch.encrypted_cookie_salt = "encrypted cookie" config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie" + config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" config.action_dispatch.use_authenticated_cookie_encryption = false config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { "X-Frame-Options" => "SAMEORIGIN", "X-XSS-Protection" => "1; mode=block", - "X-Content-Type-Options" => "nosniff" + "X-Content-Type-Options" => "nosniff", + "X-Download-Options" => "noopen", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "strict-origin-when-cross-origin" } + config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new + config.eager_load_namespaces << ActionDispatch initializer "action_dispatch.configure" do |app| @@ -39,8 +46,6 @@ module ActionDispatch ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses) ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates) - config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" if config.action_dispatch.use_authenticated_cookie_encryption - config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil? ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb index e911b6537b..24dced1efd 100644 --- a/actionpack/lib/action_dispatch/routing/endpoint.rb +++ b/actionpack/lib/action_dispatch/routing/endpoint.rb @@ -3,10 +3,12 @@ module ActionDispatch module Routing class Endpoint # :nodoc: - def dispatcher?; false; end - def redirect?; false; end - def matches?(req); true; end - def app; self; end + def dispatcher?; false; end + def redirect?; false; end + def engine?; rack_app.respond_to?(:routes); end + def matches?(req); true; end + def app; self; end + def rack_app; app; end end end end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index b2868b7427..a2205569b4 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -15,7 +15,7 @@ module ActionDispatch end def rack_app - app.app + app.rack_app end def path @@ -47,7 +47,7 @@ module ActionDispatch end def engine? - rack_app.respond_to?(:routes) + app.engine? end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 2b43ade081..31eb6104fe 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -4,8 +4,8 @@ require "active_support/core_ext/hash/slice" require "active_support/core_ext/enumerable" require "active_support/core_ext/array/extract_options" require "active_support/core_ext/regexp" -require_relative "redirection" -require_relative "endpoint" +require "action_dispatch/routing/redirection" +require "action_dispatch/routing/endpoint" module ActionDispatch module Routing @@ -475,6 +475,16 @@ module ActionDispatch # # resources :users, param: :name # + # The +users+ resource here will have the following routes generated for it: + # + # GET /users(.:format) + # POST /users(.:format) + # GET /users/new(.:format) + # GET /users/:name/edit(.:format) + # GET /users/:name(.:format) + # PATCH/PUT /users/:name(.:format) + # DELETE /users/:name(.:format) + # # You can override <tt>ActiveRecord::Base#to_param</tt> of a related # model to construct a URL: # @@ -484,8 +494,8 @@ module ActionDispatch # end # end # - # user = User.find_by(name: 'Phusion') - # user_path(user) # => "/users/Phusion" + # user = User.find_by(name: 'Phusion') + # user_path(user) # => "/users/Phusion" # # [:path] # The path prefix for the routes. @@ -1265,7 +1275,7 @@ module ActionDispatch # POST /profile # # === Options - # Takes same options as +resources+. + # Takes same options as resources[rdoc-ref:#resources] def resource(*resources, &block) options = resources.extract_options!.dup @@ -1330,7 +1340,7 @@ module ActionDispatch # DELETE /photos/:photo_id/comments/:id # # === Options - # Takes same options as <tt>Base#match</tt> as well as: + # Takes same options as match[rdoc-ref:Base#match] as well as: # # [:path_names] # Allows you to change the segment component of the +edit+ and +new+ actions. @@ -1563,7 +1573,7 @@ module ActionDispatch # Matches a URL pattern to one or more routes. # For more information, see match[rdoc-ref:Base#match]. # - # match 'path' => 'controller#action', via: patch + # match 'path' => 'controller#action', via: :patch # match 'path', to: 'controller#action', via: :post # match 'path', 'otherpath', on: :member, via: :get def match(path, *rest, &block) @@ -2036,7 +2046,7 @@ module ActionDispatch end module CustomUrls - # Define custom url helpers that will be added to the application's + # Define custom URL helpers that will be added to the application's # routes. This allows you to override and/or replace the default behavior # of routing helpers, e.g: # @@ -2056,11 +2066,11 @@ module ActionDispatch # arguments for +url_for+ which will actually build the URL string. This can # be one of the following: # - # * A string, which is treated as a generated URL - # * A hash, e.g. { controller: "pages", action: "index" } - # * An array, which is passed to `polymorphic_url` - # * An Active Model instance - # * An Active Model class + # * A string, which is treated as a generated URL + # * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt> + # * An array, which is passed to +polymorphic_url+ + # * An Active Model instance + # * An Active Model class # # NOTE: Other URL helpers can be called in the block but be careful not to invoke # your custom URL helper again otherwise it will result in a stack overflow error. @@ -2072,9 +2082,9 @@ module ActionDispatch # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ] # end # - # In this instance the +params+ object comes from the context in which the the + # In this instance the +params+ object comes from the context in which the # block is executed, e.g. generating a URL inside a controller action or a view. - # If the block is executed where there isn't a params object such as this: + # If the block is executed where there isn't a +params+ object such as this: # # Rails.application.routes.url_helpers.browse_path # diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 2e2bc87b57..143a4b3d62 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require_relative "../http/request" +require "action_dispatch/http/request" require "active_support/core_ext/uri" require "active_support/core_ext/array/extract_options" require "rack/utils" require "action_controller/metal/exceptions" -require_relative "endpoint" +require "action_dispatch/routing/endpoint" module ActionDispatch module Routing diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 357eaec572..9eff30fa53 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true -require_relative "../journey" +require "action_dispatch/journey" require "active_support/core_ext/object/to_query" require "active_support/core_ext/hash/slice" +require "active_support/core_ext/module/redefine_method" require "active_support/core_ext/module/remove_method" require "active_support/core_ext/array/extract_options" require "action_controller/metal/exceptions" -require_relative "../http/request" -require_relative "endpoint" +require "action_dispatch/http/request" +require "action_dispatch/routing/endpoint" module ActionDispatch module Routing @@ -198,6 +199,16 @@ module ActionDispatch if args.size == arg_size && !inner_options && optimize_routes_generation?(t) options = t.url_options.merge @options options[:path] = optimized_helper(args) + + original_script_name = options.delete(:original_script_name) + script_name = t._routes.find_script_name(options) + + if original_script_name + script_name = original_script_name + script_name + end + + options[:script_name] = script_name + url_strategy.call options else super @@ -546,7 +557,7 @@ module ActionDispatch # plus a singleton class method called _routes ... included do - singleton_class.send(:redefine_method, :_routes) { routes } + redefine_singleton_method(:_routes) { routes } end # And an instance method _routes. Note that @@ -583,14 +594,14 @@ module ActionDispatch if route.segment_keys.include?(:controller) ActiveSupport::Deprecation.warn(<<-MSG.squish) Using a dynamic :controller segment in a route is deprecated and - will be removed in Rails 5.2. + will be removed in Rails 6.0. MSG end if route.segment_keys.include?(:action) ActiveSupport::Deprecation.warn(<<-MSG.squish) Using a dynamic :action segment in a route is deprecated and - will be removed in Rails 5.2. + will be removed in Rails 6.0. MSG end @@ -841,6 +852,10 @@ module ActionDispatch end req = make_request(env) + recognize_path_with_request(req, path, extras) + end + + def recognize_path_with_request(req, path, extras) @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| @@ -859,6 +874,9 @@ module ActionDispatch end return req.path_parameters + elsif app.matches?(req) && app.engine? + path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras) + return path_parameters end end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 3ae533dd37..fa345dccdf 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -155,7 +155,7 @@ module ActionDispatch # Missing routes keys may be filled in from the current request's parameters # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are # placed in the path). Given that the current action has been reached - # through `GET /users/1`: + # through <tt>GET /users/1</tt>: # # url_for(only_path: true) # => '/users/1' # url_for(only_path: true, action: 'edit') # => '/users/1/edit' diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index ae4aeac59d..f85f816bb9 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -1,13 +1,16 @@ # frozen_string_literal: true +gem "capybara", "~> 2.15" + require "capybara/dsl" require "capybara/minitest" require "action_controller" -require_relative "system_testing/driver" -require_relative "system_testing/server" -require_relative "system_testing/test_helpers/screenshot_helper" -require_relative "system_testing/test_helpers/setup_and_teardown" -require_relative "system_testing/test_helpers/undef_methods" +require "action_dispatch/system_testing/driver" +require "action_dispatch/system_testing/browser" +require "action_dispatch/system_testing/server" +require "action_dispatch/system_testing/test_helpers/screenshot_helper" +require "action_dispatch/system_testing/test_helpers/setup_and_teardown" +require "action_dispatch/system_testing/test_helpers/undef_methods" module ActionDispatch # = System Testing @@ -67,6 +70,9 @@ module ActionDispatch # size of the browser screen. These two options are not applicable for # headless drivers and will be silently ignored if passed. # + # Headless browsers such as headless Chrome and headless Firefox are also supported. + # You can use these browsers by setting the +:using+ argument to +:headless_chrome+ or +:headless_firefox+. + # # To use a headless driver, like Poltergeist, update your Gemfile to use # Poltergeist instead of Selenium and then declare the driver name in the # +application_system_test_case.rb+ file. In this case, you would leave out @@ -119,14 +125,22 @@ module ActionDispatch # # driven_by :poltergeist # + # driven_by :selenium, screen_size: [800, 800] + # + # driven_by :selenium, using: :chrome + # + # driven_by :selenium, using: :headless_chrome + # # driven_by :selenium, using: :firefox # - # driven_by :selenium, screen_size: [800, 800] + # driven_by :selenium, using: :headless_firefox def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}) self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options) end driven_by :selenium + + ActiveSupport.run_load_hooks(:action_dispatch_system_test_case, self) end SystemTestCase.start_application diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb new file mode 100644 index 0000000000..10e6888ab3 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module ActionDispatch + module SystemTesting + class Browser # :nodoc: + attr_reader :name + + def initialize(name) + @name = name + end + + def type + case name + when :headless_chrome + :chrome + when :headless_firefox + :firefox + else + name + end + end + + def options + case name + when :headless_chrome + headless_chrome_browser_options + when :headless_firefox + headless_firefox_browser_options + end + end + + private + def headless_chrome_browser_options + options = Selenium::WebDriver::Chrome::Options.new + options.args << "--headless" + options.args << "--disable-gpu" + + options + end + + def headless_firefox_browser_options + options = Selenium::WebDriver::Firefox::Options.new + options.args << "-headless" + + options + end + end + end +end diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb index 4279336f2f..5252ff6746 100644 --- a/actionpack/lib/action_dispatch/system_testing/driver.rb +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -5,7 +5,7 @@ module ActionDispatch class Driver # :nodoc: def initialize(name, **options) @name = name - @browser = options[:using] + @browser = Browser.new(options[:using]) @screen_size = options[:screen_size] @options = options[:options] end @@ -31,8 +31,12 @@ module ActionDispatch end end + def browser_options + @options.merge(options: @browser.options).compact + end + def register_selenium(app) - Capybara::Selenium::Driver.new(app, { browser: @browser }.merge(@options)).tap do |driver| + Capybara::Selenium::Driver.new(app, { browser: @browser.type }.merge(browser_options)).tap do |driver| driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size) end end @@ -43,7 +47,7 @@ module ActionDispatch def register_webkit(app) Capybara::Webkit::Driver.new(app, Capybara::Webkit::Configuration.to_hash.merge(@options)).tap do |driver| - driver.resize_window(*@screen_size) + driver.resize_window_to(driver.current_window_handle, *@screen_size) end end diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb index 76bada8df1..4fc1f33767 100644 --- a/actionpack/lib/action_dispatch/system_testing/server.rb +++ b/actionpack/lib/action_dispatch/system_testing/server.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "rack/handler/puma" - module ActionDispatch module SystemTesting class Server # :nodoc: @@ -12,29 +10,17 @@ module ActionDispatch self.silence_puma = false def run - register setup end private - def register - Capybara.register_server :rails_puma do |app, port, host| - Rack::Handler::Puma.run( - app, - Port: port, - Threads: "0:1", - Silent: self.class.silence_puma - ) - end - end - def setup set_server set_port end def set_server - Capybara.server = :rails_puma + Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default] end def set_port diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb index 6c337cdc31..df0c5d3f0e 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb @@ -15,12 +15,11 @@ module ActionDispatch # # You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT+ environment variable to # control the output. Possible values are: - # * [+inline+ (default)] display the screenshot in the terminal using the + # * [+simple+ (default)] Only displays the screenshot path. + # This is the default value. + # * [+inline+] Display the screenshot in the terminal using the # iTerm image protocol (https://iterm2.com/documentation-images.html). - # * [+simple+] only display the screenshot path. - # This is the default value if the +CI+ environment variables - # is defined. - # * [+artifact+] display the screenshot in the terminal, using the terminal + # * [+artifact+] Display the screenshot in the terminal, using the terminal # artifact format (https://buildkite.github.io/terminal/inline-images/). def take_screenshot save_image @@ -59,11 +58,8 @@ module ActionDispatch # Environment variables have priority output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] || ENV["CAPYBARA_INLINE_SCREENSHOT"] - # If running in a CI environment, default to simple - output_type ||= "simple" if ENV["CI"] - - # Default - output_type ||= "inline" + # Default to outputting a path to the screenshot + output_type ||= "simple" output_type end diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb index ef680cafed..d64be3b3d9 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb @@ -14,7 +14,7 @@ module ActionDispatch def method_missing(method, *args, &block) if METHODS.include?(method) - raise NoMethodError + raise NoMethodError, "System tests cannot make direct requests via ##{method}; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information." else super end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index ae1f368e8b..7171b6942c 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -7,43 +7,43 @@ require "active_support/core_ext/object/try" require "rack/test" require "minitest" -require_relative "request_encoder" +require "action_dispatch/testing/request_encoder" module ActionDispatch module Integration #:nodoc: module RequestHelpers - # Performs a GET request with the given parameters. See +#process+ for more - # details. + # Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process + # for more details. def get(path, **args) process(:get, path, **args) end - # Performs a POST request with the given parameters. See +#process+ for more - # details. + # Performs a POST request with the given parameters. See ActionDispatch::Integration::Session#process + # for more details. def post(path, **args) process(:post, path, **args) end - # Performs a PATCH request with the given parameters. See +#process+ for more - # details. + # Performs a PATCH request with the given parameters. See ActionDispatch::Integration::Session#process + # for more details. def patch(path, **args) process(:patch, path, **args) end - # Performs a PUT request with the given parameters. See +#process+ for more - # details. + # Performs a PUT request with the given parameters. See ActionDispatch::Integration::Session#process + # for more details. def put(path, **args) process(:put, path, **args) end - # Performs a DELETE request with the given parameters. See +#process+ for - # more details. + # Performs a DELETE request with the given parameters. See ActionDispatch::Integration::Session#process + # for more details. def delete(path, **args) process(:delete, path, **args) end - # Performs a HEAD request with the given parameters. See +#process+ for more - # details. + # Performs a HEAD request with the given parameters. See ActionDispatch::Integration::Session#process + # for more details. def head(path, *args) process(:head, path, *args) end diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 3b63706aaa..8ac50c730d 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative "../middleware/cookies" -require_relative "../middleware/flash" +require "action_dispatch/middleware/cookies" +require "action_dispatch/middleware/flash" module ActionDispatch module TestProcess diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index b23ea7479c..1e6b21f235 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "request_encoder" +require "action_dispatch/testing/request_encoder" module ActionDispatch # Integration test methods such as ActionDispatch::Integration::Session#get diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index fe2fc7c474..3f69109633 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -23,4 +23,4 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -require_relative "action_pack/version" +require "action_pack/version" diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb index 28bc153f4d..97f4934b58 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -10,7 +10,7 @@ module ActionPack MAJOR = 5 MINOR = 2 TINY = 0 - PRE = "alpha" + PRE = "beta2" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index caa56018f8..f4787ed27a 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -44,7 +44,7 @@ module Rails @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test") end - def root; end; + def root; end end end @@ -380,10 +380,8 @@ class ForkingExecutor def initialize(size) @size = size @queue = Server.new - file = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname("rails-tests", "fd") - @url = "drbunix://#{file}" @pool = nil - DRb.start_service @url, @queue + @url = DRb.start_service("drbunix:", @queue).uri end def <<(work); @queue << work; end @@ -449,3 +447,11 @@ end class DrivenBySeleniumWithChrome < ActionDispatch::SystemTestCase driven_by :selenium, using: :chrome end + +class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_chrome +end + +class DrivenBySeleniumWithHeadlessFirefox < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_firefox +end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index e0300539c9..3557f9f888 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -317,7 +317,7 @@ end class CacheHelperOutputBufferTest < ActionController::TestCase class MockController def read_fragment(name, options) - return false + false end def write_fragment(name, fragment, options) @@ -333,9 +333,9 @@ class CacheHelperOutputBufferTest < ActionController::TestCase output_buffer = ActionView::OutputBuffer.new controller = MockController.new cache_helper = Class.new do - def self.controller; end; - def self.output_buffer; end; - def self.output_buffer=; end; + def self.controller; end + def self.output_buffer; end + def self.output_buffer=; end end cache_helper.extend(ActionView::Helpers::CacheHelper) @@ -354,9 +354,9 @@ class CacheHelperOutputBufferTest < ActionController::TestCase output_buffer = ActiveSupport::SafeBuffer.new controller = MockController.new cache_helper = Class.new do - def self.controller; end; - def self.output_buffer; end; - def self.output_buffer=; end; + def self.controller; end + def self.output_buffer; end + def self.output_buffer=; end end cache_helper.extend(ActionView::Helpers::CacheHelper) diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index d92ae0b817..34bc2c0caa 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "abstract_unit" -require "active_support/key_generator" +require "active_support/messages/rotation_configuration" class FlashTest < ActionController::TestCase class TestController < ActionController::Base @@ -243,6 +243,7 @@ end class FlashIntegrationTest < ActionDispatch::IntegrationTest SessionKey = "_myapp_session" Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33") + Rotations = ActiveSupport::Messages::RotationConfiguration.new class TestController < ActionController::Base add_flash_types :bar @@ -348,6 +349,7 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest args[0] ||= {} args[0][:env] ||= {} args[0][:env]["action_dispatch.key_generator"] ||= Generator + args[0][:env]["action_dispatch.cookies_rotations"] = Rotations super(path, *args) end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 8cfb43a6bc..431fe90b23 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -464,7 +464,7 @@ module ActionController end def test_stale_with_etag - @request.if_none_match = %(W/"#{Digest::MD5.hexdigest('123')}") + @request.if_none_match = %(W/"#{ActiveSupport::Digest.hexdigest('123')}") get :with_stale assert_equal 304, response.status.to_i end diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index f0f106c8ba..be455642de 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -98,7 +98,7 @@ class ACLogSubscriberTest < ActionController::TestCase @old_logger = ActionController::Base.logger - @cache_path = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname("tmp", "cache") + @cache_path = Dir.mktmpdir(%w[tmp cache]) @controller.cache_store = :file_store, @cache_path ActionController::LogSubscriber.attach_to :action_controller end diff --git a/actionpack/test/controller/metal_test.rb b/actionpack/test/controller/metal_test.rb index c235c9df86..c3ebcb22b8 100644 --- a/actionpack/test/controller/metal_test.rb +++ b/actionpack/test/controller/metal_test.rb @@ -9,7 +9,7 @@ class MetalControllerInstanceTests < ActiveSupport::TestCase end end - def test_response_has_default_headers + def test_response_does_not_have_default_headers original_default_headers = ActionDispatch::Response.default_headers ActionDispatch::Response.default_headers = { diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb index d9f200b2a7..280134f8d2 100644 --- a/actionpack/test/controller/new_base/base_test.rb +++ b/actionpack/test/controller/new_base/base_test.rb @@ -47,7 +47,6 @@ module Dispatching end class BaseTest < Rack::TestCase - # :api: plugin test "simple dispatching" do get "/dispatching/simple/index" @@ -56,14 +55,12 @@ module Dispatching assert_content_type "text/plain; charset=utf-8" end - # :api: plugin test "directly modifying response body" do get "/dispatching/simple/modify_response_body" assert_body "success" end - # :api: plugin test "directly modifying response body twice" do get "/dispatching/simple/modify_response_body_twice" diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 43cabae7d2..154430d4b0 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -51,6 +51,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase @params.each { |key, value| assert_not(value.permitted?) if key == "person" } end + test "each returns key,value array for block with arity 1" do + @params.each do |arg| + assert_kind_of Array, arg + assert_equal "person", arg[0] + assert_kind_of ActionController::Parameters, arg[1] + end + end + test "each_pair carries permitted status" do @params.permit! @params.each_pair { |key, value| assert(value.permitted?) if key == "person" } @@ -60,6 +68,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase @params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" } end + test "each_pair returns key,value array for block with arity 1" do + @params.each_pair do |arg| + assert_kind_of Array, arg + assert_equal "person", arg[0] + assert_kind_of ActionController::Parameters, arg[1] + end + end + test "empty? returns true when params contains no key/value pairs" do params = ActionController::Parameters.new assert params.empty? diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index ebdaca0162..e9b94b056b 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -59,7 +59,7 @@ class ParametersPermitTest < ActiveSupport::TestCase test "key: permitted scalar values" do values = ["a", :a, nil] - values += [0, 1.0, 2**128, BigDecimal.new(1)] + values += [0, 1.0, 2**128, BigDecimal(1)] values += [true, false] values += [Date.today, Time.now, DateTime.now] values += [STDOUT, StringIO.new, ActionDispatch::Http::UploadedFile.new(tempfile: __FILE__), diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index df68ef25a3..c4c74e8f2b 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -255,6 +255,20 @@ class ParamsWrapperTest < ActionController::TestCase assert_equal "", @response.body end end + + def test_derived_wrapped_keys_from_nested_attributes + def User.nested_attributes_options + { person: {} } + end + + assert_called(User, :attribute_names, times: 2, returns: ["username"]) do + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "person_attributes" => { "title" => "Developer" } } + assert_parameters("username" => "sikachu", "person_attributes" => { "title" => "Developer" }, "user" => { "username" => "sikachu", "person_attributes" => { "title" => "Developer" } }) + end + end + end end class NamespacedParamsWrapperTest < ActionController::TestCase @@ -262,7 +276,7 @@ class NamespacedParamsWrapperTest < ActionController::TestCase module Admin module Users - class UsersController < ActionController::Base; + class UsersController < ActionController::Base class << self attr_accessor :last_parameters end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index e447b66486..2959dc3e4d 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -62,6 +62,10 @@ class RedirectController < ActionController::Base redirect_back(fallback_location: "/things/stuff", status: 307) end + def safe_redirect_back_with_status + redirect_back(fallback_location: "/things/stuff", status: 307, allow_other_host: false) + end + def host_redirect redirect_to action: "other_host", only_path: false, host: "other.test.host" end @@ -259,6 +263,23 @@ class RedirectTest < ActionController::TestCase assert_equal "http://test.host/things/stuff", redirect_to_url end + def test_safe_redirect_back_from_other_host + @request.env["HTTP_REFERER"] = "http://another.host/coming/from" + get :safe_redirect_back_with_status + + assert_response 307 + assert_equal "http://test.host/things/stuff", redirect_to_url + end + + def test_safe_redirect_back_from_the_same_host + referer = "http://test.host/coming/from" + @request.env["HTTP_REFERER"] = referer + get :safe_redirect_back_with_status + + assert_response 307 + assert_equal referer, redirect_to_url + end + def test_redirect_to_record with_routing do |set| set.draw do diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 3619afc513..7c5101f993 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -162,6 +162,17 @@ class TestController < ActionController::Base render action: "hello_world" end + def conditional_hello_with_expires_and_confliciting_cache_control_headers + response.headers["Cache-Control"] = "no-cache, must-revalidate" + expires_now + render action: "hello_world" + end + + def conditional_hello_without_expires_and_confliciting_cache_control_headers + response.headers["Cache-Control"] = "no-cache, must-revalidate" + render action: "hello_world" + end + def conditional_hello_with_bangs render action: "hello_world" end @@ -368,6 +379,16 @@ class ExpiresInRenderTest < ActionController::TestCase assert_match(/no-transform/, @response.headers["Cache-Control"]) end + def test_expires_now_with_conflicting_cache_control_headers + get :conditional_hello_with_expires_and_confliciting_cache_control_headers + assert_equal "no-cache", @response.headers["Cache-Control"] + end + + def test_no_expires_now_with_conflicting_cache_control_headers + get :conditional_hello_without_expires_and_confliciting_cache_control_headers + assert_equal "no-cache", @response.headers["Cache-Control"] + end + def test_date_header_when_expires_in time = Time.mktime(2011, 10, 30) Time.stub :now, time do @@ -571,7 +592,7 @@ class EtagRenderTest < ActionController::TestCase end def strong_etag(record) - %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}") + %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}") end end @@ -606,6 +627,18 @@ class MetalRenderTest < ActionController::TestCase end end +class ActionControllerRenderTest < ActionController::TestCase + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + end + + def test_direct_render_to_string_with_body + mc = MinimalController.new + assert_equal "Hello world!", mc.render_to_string(body: ["Hello world!"]) + end +end + class ActionControllerBaseRenderTest < ActionController::TestCase def test_direct_render_to_string ac = ActionController::Base.new() diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 12ae95d602..4822d85bcb 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -2,6 +2,7 @@ require "abstract_unit" require "active_support/log_subscriber/test_helper" +require "active_support/messages/rotation_configuration" # common controller actions module RequestForgeryProtectionActions @@ -445,6 +446,19 @@ module RequestForgeryProtectionTests end end + def test_should_raise_for_post_with_null_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + exception = assert_raises(ActionController::InvalidAuthenticityToken) do + @request.set_header "HTTP_ORIGIN", "null" + post :index, params: { custom_authenticity_token: @token } + end + assert_match "The browser returned a 'null' origin for a request", exception.message + end + end + end + def test_should_block_post_with_origin_checking_and_wrong_origin old_logger = ActionController::Base.logger logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new @@ -630,13 +644,14 @@ end class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase class NullSessionDummyKeyGenerator - def generate_key(secret) + def generate_key(secret, length = nil) "03312270731a2ed0d11ed091c2338a06" end end def setup @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new + @request.env[ActionDispatch::Cookies::COOKIES_ROTATIONS] = ActiveSupport::Messages::RotationConfiguration.new end test "should allow to set signed cookies" do diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index 07f8c9dd8a..4ed79073e5 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -33,7 +33,7 @@ class RescueController < ActionController::Base class ResourceUnavailableToRescueAsString < StandardError end - # We use a fully-qualified name in some strings, and a relative constant + # We use a fully qualified name in some strings, and a relative constant # name in some other to test correct handling of both cases. rescue_from NotAuthorized, with: :deny_access diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 3d98237003..30bea64c55 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -307,7 +307,7 @@ class ResourcesTest < ActionController::TestCase set.draw do resources :messages do member do - match :mark , via: method + match :mark, via: method match :unmark, via: method end end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index f09051b306..ec939e946a 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -213,7 +213,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal expected, ActiveSupport::JSON.decode(get(u)) end - def test_regexp_precidence + def test_regexp_precedence rs.draw do get "/whois/:domain", constraints: { domain: /\w+\.[\w\.]+/ }, @@ -1687,7 +1687,7 @@ class RouteSetTest < ActiveSupport::TestCase def test_routes_with_symbols set.draw do get "unnamed", controller: :pages, action: :show, name: :as_symbol - get "named" , controller: :pages, action: :show, name: :as_symbol, as: :named + get "named", controller: :pages, action: :show, name: :as_symbol, as: :named end assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/unnamed")) assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/named")) @@ -1893,7 +1893,7 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal({ controller: "blog", action: "show_date", year: "2006", month: "07", day: "28" }, controller.request.path_parameters) assert_equal("/blog/2006/07/25", controller.url_for(day: 25, only_path: true)) assert_equal("/blog/2005", controller.url_for(year: 2005, only_path: true)) - assert_equal("/blog/show/123", controller.url_for(action: "show" , id: 123, only_path: true)) + assert_equal("/blog/show/123", controller.url_for(action: "show", id: 123, only_path: true)) assert_equal("/blog/2006", controller.url_for(year: 2006, only_path: true)) assert_equal("/blog/2006", controller.url_for(year: 2006, month: nil, only_path: true)) end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index fd2399e433..7b1a52b277 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -178,7 +178,7 @@ class SendFileTest < ActionController::TestCase "image.jpg" => "image/jpeg", "image.tif" => "image/tiff", "image.gif" => "image/gif", - "movie.mpg" => "video/mpeg", + "movie.mp4" => "video/mp4", "file.zip" => "application/zip", "file.unk" => "application/octet-stream", "zip" => "application/octet-stream" diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 46369ebbb0..536c5ed97a 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -6,7 +6,7 @@ require "active_support/json/decoding" require "rails/engine" class TestCaseTest < ActionController::TestCase - def self.fixture_path; end; + def self.fixture_path; end class TestController < ActionController::Base def no_op diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb index a7c7356921..a1521da702 100644 --- a/actionpack/test/controller/url_for_integration_test.rb +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -35,7 +35,6 @@ module ActionPack as: "blog" resources :people - #match 'legacy/people' => "people#index", :legacy => "true" get "symbols", controller: :symbols, action: :show, name: :as_symbol get "id_default(/:id)" => "foo#id_default", :id => 1 diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index 0f79c83b6d..ca83b850d5 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -19,7 +19,7 @@ class UrlRewriterTests < ActionController::TestCase def setup @params = {} - @rewriter = Rewriter.new(@request) #.new(@request, @params) + @rewriter = Rewriter.new(@request) @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| r.draw do ActiveSupport::Deprecation.silence do diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb new file mode 100644 index 0000000000..7c4a65a633 --- /dev/null +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -0,0 +1,368 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ContentSecurityPolicyTest < ActiveSupport::TestCase + def setup + @policy = ActionDispatch::ContentSecurityPolicy.new + end + + def test_build + assert_equal ";", @policy.build + + @policy.script_src :self + assert_equal "script-src 'self';", @policy.build + end + + def test_dup + @policy.img_src :self + @policy.block_all_mixed_content + @policy.upgrade_insecure_requests + @policy.sandbox + copied = @policy.dup + assert_equal copied.build, @policy.build + end + + def test_mappings + @policy.script_src :data + assert_equal "script-src data:;", @policy.build + + @policy.script_src :mediastream + assert_equal "script-src mediastream:;", @policy.build + + @policy.script_src :blob + assert_equal "script-src blob:;", @policy.build + + @policy.script_src :filesystem + assert_equal "script-src filesystem:;", @policy.build + + @policy.script_src :self + assert_equal "script-src 'self';", @policy.build + + @policy.script_src :unsafe_inline + assert_equal "script-src 'unsafe-inline';", @policy.build + + @policy.script_src :unsafe_eval + assert_equal "script-src 'unsafe-eval';", @policy.build + + @policy.script_src :none + assert_equal "script-src 'none';", @policy.build + + @policy.script_src :strict_dynamic + assert_equal "script-src 'strict-dynamic';", @policy.build + + @policy.script_src :none, :report_sample + assert_equal "script-src 'none' 'report-sample';", @policy.build + end + + def test_fetch_directives + @policy.child_src :self + assert_match %r{child-src 'self'}, @policy.build + + @policy.child_src false + assert_no_match %r{child-src}, @policy.build + + @policy.connect_src :self + assert_match %r{connect-src 'self'}, @policy.build + + @policy.connect_src false + assert_no_match %r{connect-src}, @policy.build + + @policy.default_src :self + assert_match %r{default-src 'self'}, @policy.build + + @policy.default_src false + assert_no_match %r{default-src}, @policy.build + + @policy.font_src :self + assert_match %r{font-src 'self'}, @policy.build + + @policy.font_src false + assert_no_match %r{font-src}, @policy.build + + @policy.frame_src :self + assert_match %r{frame-src 'self'}, @policy.build + + @policy.frame_src false + assert_no_match %r{frame-src}, @policy.build + + @policy.img_src :self + assert_match %r{img-src 'self'}, @policy.build + + @policy.img_src false + assert_no_match %r{img-src}, @policy.build + + @policy.manifest_src :self + assert_match %r{manifest-src 'self'}, @policy.build + + @policy.manifest_src false + assert_no_match %r{manifest-src}, @policy.build + + @policy.media_src :self + assert_match %r{media-src 'self'}, @policy.build + + @policy.media_src false + assert_no_match %r{media-src}, @policy.build + + @policy.object_src :self + assert_match %r{object-src 'self'}, @policy.build + + @policy.object_src false + assert_no_match %r{object-src}, @policy.build + + @policy.script_src :self + assert_match %r{script-src 'self'}, @policy.build + + @policy.script_src false + assert_no_match %r{script-src}, @policy.build + + @policy.style_src :self + assert_match %r{style-src 'self'}, @policy.build + + @policy.style_src false + assert_no_match %r{style-src}, @policy.build + + @policy.worker_src :self + assert_match %r{worker-src 'self'}, @policy.build + + @policy.worker_src false + assert_no_match %r{worker-src}, @policy.build + end + + def test_document_directives + @policy.base_uri "https://example.com" + assert_match %r{base-uri https://example\.com;}, @policy.build + + @policy.plugin_types "application/x-shockwave-flash" + assert_match %r{plugin-types application/x-shockwave-flash;}, @policy.build + + @policy.sandbox + assert_match %r{sandbox;}, @policy.build + + @policy.sandbox "allow-scripts", "allow-modals" + assert_match %r{sandbox allow-scripts allow-modals;}, @policy.build + + @policy.sandbox false + assert_no_match %r{sandbox}, @policy.build + end + + def test_navigation_directives + @policy.form_action :self + assert_match %r{form-action 'self';}, @policy.build + + @policy.frame_ancestors :self + assert_match %r{frame-ancestors 'self';}, @policy.build + end + + def test_reporting_directives + @policy.report_uri "/violations" + assert_match %r{report-uri /violations;}, @policy.build + end + + def test_other_directives + @policy.block_all_mixed_content + assert_match %r{block-all-mixed-content;}, @policy.build + + @policy.block_all_mixed_content false + assert_no_match %r{block-all-mixed-content}, @policy.build + + @policy.require_sri_for :script, :style + assert_match %r{require-sri-for script style;}, @policy.build + + @policy.require_sri_for "script", "style" + assert_match %r{require-sri-for script style;}, @policy.build + + @policy.require_sri_for + assert_no_match %r{require-sri-for}, @policy.build + + @policy.upgrade_insecure_requests + assert_match %r{upgrade-insecure-requests;}, @policy.build + + @policy.upgrade_insecure_requests false + assert_no_match %r{upgrade-insecure-requests}, @policy.build + end + + def test_multiple_sources + @policy.script_src :self, :https + assert_equal "script-src 'self' https:;", @policy.build + end + + def test_multiple_directives + @policy.script_src :self, :https + @policy.style_src :self, :https + assert_equal "script-src 'self' https:; style-src 'self' https:;", @policy.build + end + + def test_dynamic_directives + request = Struct.new(:host).new("www.example.com") + controller = Struct.new(:request).new(request) + + @policy.script_src -> { request.host } + assert_equal "script-src www.example.com;", @policy.build(controller) + end + + def test_mixed_static_and_dynamic_directives + @policy.script_src :self, -> { "foo.com" }, "bar.com" + assert_equal "script-src 'self' foo.com bar.com;", @policy.build(Object.new) + end + + def test_invalid_directive_source + exception = assert_raises(ArgumentError) do + @policy.script_src [:self] + end + + assert_equal "Invalid content security policy source: [:self]", exception.message + end + + def test_missing_context_for_dynamic_source + @policy.script_src -> { request.host } + + exception = assert_raises(RuntimeError) do + @policy.build + end + + assert_match %r{\AMissing context for the dynamic content security policy source:}, exception.message + end + + def test_raises_runtime_error_when_unexpected_source + @policy.plugin_types [:flash] + + exception = assert_raises(RuntimeError) do + @policy.build + end + + assert_match %r{\AUnexpected content security policy source:}, exception.message + end +end + +class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest + class PolicyController < ActionController::Base + content_security_policy only: :inline do |p| + p.default_src "https://example.com" + end + + content_security_policy only: :conditional, if: :condition? do |p| + p.default_src "https://true.example.com" + end + + content_security_policy only: :conditional, unless: :condition? do |p| + p.default_src "https://false.example.com" + end + + content_security_policy only: :report_only do |p| + p.report_uri "/violations" + end + + content_security_policy_report_only only: :report_only + + def index + head :ok + end + + def inline + head :ok + end + + def conditional + head :ok + end + + def report_only + head :ok + end + + private + def condition? + params[:condition] == "true" + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "content_security_policy_integration_test" do + get "/", to: "policy#index" + get "/inline", to: "policy#inline" + get "/conditional", to: "policy#conditional" + get "/report-only", to: "policy#report_only" + end + end + + POLICY = ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src :self + end + + class PolicyConfigMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.content_security_policy"] = POLICY + env["action_dispatch.content_security_policy_report_only"] = false + env["action_dispatch.show_exceptions"] = false + + @app.call(env) + end + end + + APP = build_app(ROUTES) do |middleware| + middleware.use PolicyConfigMiddleware + middleware.use ActionDispatch::ContentSecurityPolicy::Middleware + end + + def app + APP + end + + def test_generates_content_security_policy_header + get "/" + assert_policy "default-src 'self';" + end + + def test_generates_inline_content_security_policy + get "/inline" + assert_policy "default-src https://example.com;" + end + + def test_generates_conditional_content_security_policy + get "/conditional", params: { condition: "true" } + assert_policy "default-src https://true.example.com;" + + get "/conditional", params: { condition: "false" } + assert_policy "default-src https://false.example.com;" + end + + def test_generates_report_only_content_security_policy + get "/report-only" + assert_policy "default-src 'self'; report-uri /violations;", report_only: true + end + + private + + def env_config + Rails.application.env_config + end + + def content_security_policy + env_config["action_dispatch.content_security_policy"] + end + + def content_security_policy=(policy) + env_config["action_dispatch.content_security_policy"] = policy + end + + def assert_policy(expected, report_only: false) + assert_response :success + + if report_only + expected_header = "Content-Security-Policy-Report-Only" + unexpected_header = "Content-Security-Policy" + else + expected_header = "Content-Security-Policy" + unexpected_header = "Content-Security-Policy-Report-Only" + end + + assert_nil response.headers[unexpected_header] + assert_equal expected, response.headers[expected_header] + end +end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index cb225c0f62..40cbad3b0d 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -3,7 +3,7 @@ require "abstract_unit" require "openssl" require "active_support/key_generator" -require "active_support/message_verifier" +require "active_support/messages/rotation_configuration" class CookieJarTest < ActiveSupport::TestCase attr_reader :request @@ -287,15 +287,25 @@ class CookiesTest < ActionController::TestCase tests TestController - SALT = "b3c631c314c0bbca50c1b2843150fe33" + SECRET_KEY_BASE = "b3c631c314c0bbca50c1b2843150fe33" + SIGNED_COOKIE_SALT = "signed cookie" + ENCRYPTED_COOKIE_SALT = "encrypted cookie" + ENCRYPTED_SIGNED_COOKIE_SALT = "sigend encrypted cookie" + AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "authenticated encrypted cookie" def setup super - @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2) + @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE, iterations: 2) + @request.env["action_dispatch.cookies_rotations"] = ActiveSupport::Messages::RotationConfiguration.new - @request.env["action_dispatch.signed_cookie_salt"] = - @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = SALT + @request.env["action_dispatch.secret_key_base"] = SECRET_KEY_BASE + @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true + + @request.env["action_dispatch.signed_cookie_salt"] = SIGNED_COOKIE_SALT + @request.env["action_dispatch.encrypted_cookie_salt"] = ENCRYPTED_COOKIE_SALT + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = ENCRYPTED_SIGNED_COOKIE_SALT + @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = AUTHENTICATED_ENCRYPTED_COOKIE_SALT @request.host = "www.nextangle.com" end @@ -430,28 +440,72 @@ class CookiesTest < ActionController::TestCase assert_equal 45, cookies.signed[:user_id] key_generator = @request.env["action_dispatch.key_generator"] - signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] - secret = key_generator.generate_key(signed_cookie_salt) + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA1") assert_equal verifier.generate(45), cookies[:user_id] end def test_signed_cookie_using_custom_digest - @request.env["action_dispatch.cookies_digest"] = "SHA256" + @request.env["action_dispatch.signed_cookie_digest"] = "SHA256" + get :set_signed_cookie cookies = @controller.send :cookies assert_not_equal 45, cookies[:user_id] assert_equal 45, cookies.signed[:user_id] key_generator = @request.env["action_dispatch.key_generator"] - signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] - secret = key_generator.generate_key(signed_cookie_salt) + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA256") assert_equal verifier.generate(45), cookies[:user_id] end + def test_signed_cookie_rotating_secret_and_digest + secret = "b3c631c314c0bbca50c1b2843150fe33" + + @request.env["action_dispatch.signed_cookie_digest"] = "SHA256" + @request.env["action_dispatch.cookies_rotations"].rotate :signed, secret, digest: "SHA1" + + old_message = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal).generate(45) + @request.headers["Cookie"] = "user_id=#{old_message}" + + get :get_signed_cookie + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256", serializer: Marshal) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_signed_cookie_with_legacy_secret_scheme + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + old_message = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", digest: "SHA1", serializer: Marshal).generate(45) + + @request.headers["Cookie"] = "user_id=#{old_message}" + get :get_signed_cookie + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key("signed cookie") + verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_tampered_with_signed_cookie + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA1") + message = verifier.generate(45) + + @request.headers["Cookie"] = "user_id=#{Marshal.dump 45}--#{message.split("--").last}" + get :get_signed_cookie + assert_nil @controller.send(:cookies).signed[:user_id] + end + def test_signed_cookie_using_default_serializer get :set_signed_cookie cookies = @controller.send :cookies @@ -494,8 +548,7 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :hybrid key_generator = @request.env["action_dispatch.key_generator"] - signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] - secret = key_generator.generate_key(signed_cookie_salt) + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) marshal_value = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal).generate(45) @request.headers["Cookie"] = "user_id=#{marshal_value}" @@ -514,8 +567,8 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :hybrid key_generator = @request.env["action_dispatch.key_generator"] - signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] - secret = key_generator.generate_key(signed_cookie_salt) + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + json_value = ActiveSupport::MessageVerifier.new(secret, serializer: JSON).generate(45) @request.headers["Cookie"] = "user_id=#{json_value}" @@ -578,11 +631,10 @@ class CookiesTest < ActionController::TestCase def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json @request.env["action_dispatch.cookies_serializer"] = :hybrid - cipher = "aes-256-gcm" - salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32) + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal) marshal_value = encryptor.encrypt_and_sign("bar") @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape marshal_value}" @@ -592,7 +644,7 @@ class CookiesTest < ActionController::TestCase assert_not_equal "bar", cookies[:foo] assert_equal "bar", cookies.encrypted[:foo] - json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) + json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON) assert_not_nil @response.cookies["foo"] assert_equal "bar", json_encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -600,11 +652,10 @@ class CookiesTest < ActionController::TestCase def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value @request.env["action_dispatch.cookies_serializer"] = :hybrid - cipher = "aes-256-gcm" - salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32) + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON) json_value = encryptor.encrypt_and_sign("bar") @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape json_value}" @@ -691,65 +742,8 @@ class CookiesTest < ActionController::TestCase } end - def test_signed_uses_signed_cookie_jar_if_only_secret_token_is_set - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = nil - get :set_signed_cookie - assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed - end - - def test_signed_uses_signed_cookie_jar_if_only_secret_key_base_is_set - @request.env["action_dispatch.secret_token"] = nil - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - get :set_signed_cookie - assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed - end - - def test_signed_uses_upgrade_legacy_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - get :set_signed_cookie - assert_kind_of ActionDispatch::Cookies::UpgradeLegacySignedCookieJar, cookies.signed - end - - def test_signed_or_encrypted_uses_signed_cookie_jar_if_only_secret_token_is_set - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = nil - get :get_encrypted_cookie - assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed_or_encrypted - end - - def test_signed_or_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set - @request.env["action_dispatch.secret_token"] = nil - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - get :get_encrypted_cookie - assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.signed_or_encrypted - end - - def test_signed_or_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - get :get_encrypted_cookie - assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.signed_or_encrypted - end - - def test_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set - @request.env["action_dispatch.secret_token"] = nil - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - get :get_encrypted_cookie - assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.encrypted - end - - def test_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - get :get_encrypted_cookie - assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.encrypted - end - def test_legacy_signed_cookie_is_read_and_transparently_upgraded_by_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45) @@ -766,9 +760,6 @@ class CookiesTest < ActionController::TestCase def test_legacy_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar") @@ -777,17 +768,14 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - cipher = "aes-256-gcm" - salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) + secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32) + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set @request.env["action_dispatch.cookies_serializer"] = :json @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45) @@ -805,7 +793,6 @@ class CookiesTest < ActionController::TestCase def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set @request.env["action_dispatch.cookies_serializer"] = :json @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") @@ -824,7 +811,6 @@ class CookiesTest < ActionController::TestCase def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set @request.env["action_dispatch.cookies_serializer"] = :hybrid @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45) @@ -842,7 +828,6 @@ class CookiesTest < ActionController::TestCase def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set @request.env["action_dispatch.cookies_serializer"] = :hybrid @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") @@ -851,17 +836,15 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - cipher = "aes-256-gcm" salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end def test_legacy_marshal_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set @request.env["action_dispatch.cookies_serializer"] = :hybrid @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45) @@ -878,6 +861,8 @@ class CookiesTest < ActionController::TestCase def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set @request.env["action_dispatch.cookies_serializer"] = :hybrid + + @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" @@ -888,16 +873,14 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - cipher = "aes-256-gcm" salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" @request.headers["Cookie"] = "user_id=45" get :get_signed_cookie @@ -908,7 +891,6 @@ class CookiesTest < ActionController::TestCase def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" @request.headers["Cookie"] = "foo=baz" get :get_encrypted_cookie @@ -917,18 +899,50 @@ class CookiesTest < ActionController::TestCase assert_nil @response.cookies["foo"] end - def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + def test_use_authenticated_cookie_encryption_uses_legacy_hmac_aes_cbc_encryption_when_not_enabled + @request.env["action_dispatch.use_authenticated_cookie_encryption"] = nil - @request.env["action_dispatch.encrypted_cookie_salt"] = - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", digest: "SHA1", serializer: Marshal) + + get :set_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_rotating_signed_cookies_digest + @request.env["action_dispatch.signed_cookie_digest"] = "SHA256" + @request.env["action_dispatch.cookies_rotations"].rotate :signed, digest: "SHA1" key_generator = @request.env["action_dispatch.key_generator"] + + old_secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + old_value = ActiveSupport::MessageVerifier.new(old_secret).generate(45) + + @request.headers["Cookie"] = "user_id=#{old_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256") + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie + key_generator = @request.env["action_dispatch.key_generator"] encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] - secret = key_generator.generate_key(encrypted_cookie_salt) + secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")) sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) - marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar") + marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: Marshal).encrypt_and_sign("bar") @request.headers["Cookie"] = "foo=#{marshal_value}" @@ -938,27 +952,22 @@ class CookiesTest < ActionController::TestCase assert_not_equal "bar", cookies[:foo] assert_equal "bar", cookies.encrypted[:foo] - aead_cipher = "aes-256-gcm" aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)] - aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: Marshal) + aead_secret = key_generator.generate_key(aead_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")) + aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: "aes-256-gcm", serializer: Marshal) assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"]) end def test_legacy_hmac_aes_cbc_encrypted_json_cookie_is_upgraded_to_authenticated_encrypted_cookie @request.env["action_dispatch.cookies_serializer"] = :json - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - - @request.env["action_dispatch.encrypted_cookie_salt"] = - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT key_generator = @request.env["action_dispatch.key_generator"] encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] - secret = key_generator.generate_key(encrypted_cookie_salt) + secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")) sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) - marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar") + marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: JSON).encrypt_and_sign("bar") @request.headers["Cookie"] = "foo=#{marshal_value}" @@ -968,19 +977,17 @@ class CookiesTest < ActionController::TestCase assert_not_equal "bar", cookies[:foo] assert_equal "bar", cookies.encrypted[:foo] - aead_cipher = "aes-256-gcm" aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)] - aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: JSON) + aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")] + aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: "aes-256-gcm", serializer: JSON) assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"]) end def test_legacy_hmac_aes_cbc_encrypted_cookie_using_64_byte_key_is_upgraded_to_authenticated_encrypted_cookie @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - - @request.env["action_dispatch.encrypted_cookie_salt"] = - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33" # Cookie generated with 64 bytes secret message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*") @@ -991,15 +998,35 @@ class CookiesTest < ActionController::TestCase cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] assert_equal "bar", cookies.encrypted[:foo] - cipher = "aes-256-gcm" salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) + secret = @request.env["action_dispatch.key_generator"].generate_key(salt, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")) + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end + def test_encrypted_cookie_rotating_secret + secret = "b3c631c314c0bbca50c1b2843150fe33" + + @request.env["action_dispatch.encrypted_cookie_cipher"] = "aes-256-gcm" + @request.env["action_dispatch.cookies_rotations"].rotate :encrypted, secret + + key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-gcm") + + old_message = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign(45) + + @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape old_message}" + + get :get_encrypted_cookie + assert_equal 45, @controller.send(:cookies).encrypted[:foo] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], key_len) + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal) + assert_equal 45, encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + def test_cookie_with_all_domain_option get :set_cookie_with_domain assert_response :success diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 90e95e972d..6854783386 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -30,21 +30,21 @@ class MimeTypeTest < ActiveSupport::TestCase test "parse text with trailing star at the beginning" do accept = "text/*, text/html, application/json, multipart/form-data" - expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]] + expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]] parsed = Mime::Type.parse(accept) - assert_equal expect, parsed + assert_equal expect.map(&:to_s), parsed.map(&:to_s) end test "parse text with trailing star in the end" do accept = "text/html, application/json, multipart/form-data, text/*" - expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml]] + expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml]] parsed = Mime::Type.parse(accept) - assert_equal expect, parsed + assert_equal expect.map(&:to_s), parsed.map(&:to_s) end test "parse text with trailing star" do accept = "text/*" - expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json]] + expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json]] parsed = Mime::Type.parse(accept) assert_equal expect.map(&:to_s).sort!, parsed.map(&:to_s).sort! end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 68c6d26364..66736e7722 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -763,7 +763,7 @@ class RequestMethod < BaseRequestTest test "post uneffected by local inflections" do existing_acronyms = ActiveSupport::Inflector.inflections.acronyms.dup - existing_acronym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup + assert_deprecated { ActiveSupport::Inflector.inflections.acronym_regex.dup } begin ActiveSupport::Inflector.inflections do |inflect| inflect.acronym "POS" @@ -777,7 +777,7 @@ class RequestMethod < BaseRequestTest # Reset original acronym set ActiveSupport::Inflector.inflections do |inflect| inflect.send(:instance_variable_set, "@acronyms", existing_acronyms) - inflect.send(:instance_variable_set, "@acronym_regex", existing_acronym_regex) + inflect.send(:define_acronym_regex_patterns) end end end @@ -785,50 +785,44 @@ end class RequestFormat < BaseRequestTest test "xml format" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :xml }) do - assert_equal Mime[:xml], request.format - end + request = stub_request "QUERY_STRING" => "format=xml" + + assert_equal Mime[:xml], request.format end test "xhtml format" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :xhtml }) do - assert_equal Mime[:html], request.format - end + request = stub_request "QUERY_STRING" => "format=xhtml" + + assert_equal Mime[:html], request.format end test "txt format" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :txt }) do - assert_equal Mime[:text], request.format - end + request = stub_request "QUERY_STRING" => "format=txt" + + assert_equal Mime[:text], request.format end test "XMLHttpRequest" do request = stub_request( "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(",") + "HTTP_ACCEPT" => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(","), + "QUERY_STRING" => "" ) - assert_called(request, :parameters, times: 1, returns: {}) do - assert request.xhr? - assert_equal Mime[:js], request.format - end + assert request.xhr? + assert_equal Mime[:js], request.format end test "can override format with parameter negative" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :txt }) do - assert !request.format.xml? - end + request = stub_request("QUERY_STRING" => "format=txt") + + assert !request.format.xml? end test "can override format with parameter positive" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :xml }) do - assert request.format.xml? - end + request = stub_request("QUERY_STRING" => "format=xml") + + assert request.format.xml? end test "formats text/html with accept header" do @@ -853,27 +847,24 @@ class RequestFormat < BaseRequestTest end test "formats format:text with accept header" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :txt }) do - assert_equal [Mime[:text]], request.formats - end + request = stub_request("QUERY_STRING" => "format=txt") + + assert_equal [Mime[:text]], request.formats end test "formats format:unknown with accept header" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :unknown }) do - assert_instance_of Mime::NullType, request.format - end + request = stub_request("QUERY_STRING" => "format=unknown") + + assert_instance_of Mime::NullType, request.format end test "format is not nil with unknown format" do - request = stub_request - assert_called(request, :parameters, times: 2, returns: { format: :hello }) do - assert request.format.nil? - assert_not request.format.html? - assert_not request.format.xml? - assert_not request.format.json? - end + request = stub_request("QUERY_STRING" => "format=hello") + + assert_nil request.format + assert_not request.format.html? + assert_not request.format.xml? + assert_not request.format.json? end test "format does not throw exceptions when malformed parameters" do @@ -883,10 +874,10 @@ class RequestFormat < BaseRequestTest end test "formats with xhr request" do - request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [Mime[:js]], request.formats - end + request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "QUERY_STRING" => "" + + assert_equal [Mime[:js]], request.formats end test "ignore_accept_header" do @@ -894,62 +885,58 @@ class RequestFormat < BaseRequestTest ActionDispatch::Request.ignore_accept_header = true begin - request = stub_request "HTTP_ACCEPT" => "application/xml" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime[:html] ], request.formats - end + request = stub_request "HTTP_ACCEPT" => "application/xml", + "QUERY_STRING" => "" - request = stub_request "HTTP_ACCEPT" => "koz-asked/something-crazy" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime[:html] ], request.formats - end + assert_equal [ Mime[:html] ], request.formats - request = stub_request "HTTP_ACCEPT" => "*/*;q=0.1" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime[:html] ], request.formats - end + request = stub_request "HTTP_ACCEPT" => "koz-asked/something-crazy", + "QUERY_STRING" => "" - request = stub_request "HTTP_ACCEPT" => "application/jxw" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime[:html] ], request.formats - end + assert_equal [ Mime[:html] ], request.formats + + request = stub_request "HTTP_ACCEPT" => "*/*;q=0.1", + "QUERY_STRING" => "" + + assert_equal [ Mime[:html] ], request.formats + + request = stub_request "HTTP_ACCEPT" => "application/jxw", + "QUERY_STRING" => "" + + assert_equal [ Mime[:html] ], request.formats request = stub_request "HTTP_ACCEPT" => "application/xml", - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "QUERY_STRING" => "" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime[:js] ], request.formats - end + assert_equal [ Mime[:js] ], request.formats request = stub_request "HTTP_ACCEPT" => "application/xml", - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" - assert_called(request, :parameters, times: 2, returns: { format: :json }) do - assert_equal [ Mime[:json] ], request.formats - end + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "QUERY_STRING" => "format=json" + + assert_equal [ Mime[:json] ], request.formats ensure ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header end end test "format taken from the path extension" do - request = stub_request "PATH_INFO" => "/foo.xml" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [Mime[:xml]], request.formats - end + request = stub_request "PATH_INFO" => "/foo.xml", "QUERY_STRING" => "" - request = stub_request "PATH_INFO" => "/foo.123" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [Mime[:html]], request.formats - end + assert_equal [Mime[:xml]], request.formats + + request = stub_request "PATH_INFO" => "/foo.123", "QUERY_STRING" => "" + + assert_equal [Mime[:html]], request.formats end test "formats from accept headers have higher precedence than path extension" do request = stub_request "HTTP_ACCEPT" => "application/json", - "PATH_INFO" => "/foo.xml" + "PATH_INFO" => "/foo.xml", + "QUERY_STRING" => "" - assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [Mime[:json]], request.formats - end + assert_equal [Mime[:json]], request.formats end end @@ -997,15 +984,14 @@ end class RequestParameters < BaseRequestTest test "parameters" do - request = stub_request + request = stub_request "CONTENT_TYPE" => "application/json", + "CONTENT_LENGTH" => 9, + "RAW_POST_DATA" => '{"foo":1}', + "QUERY_STRING" => "bar=2" - assert_called(request, :request_parameters, times: 2, returns: { "foo" => 1 }) do - assert_called(request, :query_parameters, times: 2, returns: { "bar" => 2 }) do - assert_equal({ "foo" => 1, "bar" => 2 }, request.parameters) - assert_equal({ "foo" => 1 }, request.request_parameters) - assert_equal({ "bar" => 2 }, request.query_parameters) - end - end + assert_equal({ "foo" => 1, "bar" => "2" }, request.parameters) + assert_equal({ "foo" => 1 }, request.request_parameters) + assert_equal({ "bar" => "2" }, request.query_parameters) end test "parameters not accessible after rack parse error" do @@ -1304,3 +1290,18 @@ class RequestFormData < BaseRequestTest assert !request.form_data? end end + +class EarlyHintsRequestTest < BaseRequestTest + def setup + super + @env["rack.early_hints"] = lambda { |links| links } + @request = stub_request + end + + test "when early hints is set in the env link headers are sent" do + early_hints = @request.send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload") + expected_hints = { "Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload" } + + assert_equal expected_hints, early_hints + end +end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index a14083c6a2..0b727dad3d 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -311,13 +311,16 @@ class ResponseTest < ActiveSupport::TestCase end end - test "read x_frame_options, x_content_type_options and x_xss_protection" do + test "read x_frame_options, x_content_type_options, x_xss_protection, x_download_options and x_permitted_cross_domain_policies, referrer_policy" do original_default_headers = ActionDispatch::Response.default_headers begin ActionDispatch::Response.default_headers = { "X-Frame-Options" => "DENY", "X-Content-Type-Options" => "nosniff", - "X-XSS-Protection" => "1;" + "X-XSS-Protection" => "1;", + "X-Download-Options" => "noopen", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "strict-origin-when-cross-origin" } resp = ActionDispatch::Response.create.tap { |response| response.body = "Hello" @@ -327,6 +330,9 @@ class ResponseTest < ActiveSupport::TestCase assert_equal("DENY", resp.headers["X-Frame-Options"]) assert_equal("nosniff", resp.headers["X-Content-Type-Options"]) assert_equal("1;", resp.headers["X-XSS-Protection"]) + assert_equal("noopen", resp.headers["X-Download-Options"]) + assert_equal("none", resp.headers["X-Permitted-Cross-Domain-Policies"]) + assert_equal("strict-origin-when-cross-origin", resp.headers["Referrer-Policy"]) ensure ActionDispatch::Response.default_headers = original_default_headers end @@ -378,10 +384,10 @@ class ResponseTest < ActiveSupport::TestCase app = lambda { |env| @response.to_a } env = Rack::MockRequest.env_for("/") - status, headers, body = app.call(env) + _status, headers, _body = app.call(env) assert_nil headers["Content-Length"] - status, headers, body = Rack::ContentLength.new(app).call(env) + _status, headers, _body = Rack::ContentLength.new(app).call(env) assert_equal "5", headers["Content-Length"] end end diff --git a/actionpack/test/dispatch/routing_assertions_test.rb b/actionpack/test/dispatch/routing_assertions_test.rb index e492a56653..a5198f2f13 100644 --- a/actionpack/test/dispatch/routing_assertions_test.rb +++ b/actionpack/test/dispatch/routing_assertions_test.rb @@ -1,14 +1,40 @@ # frozen_string_literal: true require "abstract_unit" +require "rails/engine" require "controller/fake_controllers" class SecureArticlesController < ArticlesController; end class BlockArticlesController < ArticlesController; end class QueryArticlesController < ArticlesController; end +class SecureBooksController < BooksController; end +class BlockBooksController < BooksController; end +class QueryBooksController < BooksController; end + class RoutingAssertionsTest < ActionController::TestCase def setup + engine = Class.new(Rails::Engine) do + def self.name + "blog_engine" + end + end + engine.routes.draw do + resources :books + + scope "secure", constraints: { protocol: "https://" } do + resources :books, controller: "secure_books" + end + + scope "block", constraints: lambda { |r| r.ssl? } do + resources :books, controller: "block_books" + end + + scope "query", constraints: lambda { |r| r.params[:use_query] == "true" } do + resources :books, controller: "query_books" + end + end + @routes = ActionDispatch::Routing::RouteSet.new @routes.draw do resources :articles @@ -24,6 +50,8 @@ class RoutingAssertionsTest < ActionController::TestCase scope "query", constraints: lambda { |r| r.params[:use_query] == "true" } do resources :articles, controller: "query_articles" end + + mount engine => "/shelf" end end @@ -83,6 +111,49 @@ class RoutingAssertionsTest < ActionController::TestCase assert_match err.message, "This is a really bad msg" end + def test_assert_recognizes_with_engine + assert_recognizes({ controller: "books", action: "index" }, "/shelf/books") + assert_recognizes({ controller: "books", action: "show", id: "1" }, "/shelf/books/1") + end + + def test_assert_recognizes_with_engine_and_extras + assert_recognizes({ controller: "books", action: "index", page: "1" }, "/shelf/books", page: "1") + end + + def test_assert_recognizes_with_engine_and_method + assert_recognizes({ controller: "books", action: "create" }, { path: "/shelf/books", method: :post }) + assert_recognizes({ controller: "books", action: "update", id: "1" }, { path: "/shelf/books/1", method: :put }) + end + + def test_assert_recognizes_with_engine_and_hash_constraint + assert_raise(Assertion) do + assert_recognizes({ controller: "secure_books", action: "index" }, "http://test.host/shelf/secure/books") + end + assert_recognizes({ controller: "secure_books", action: "index", protocol: "https://" }, "https://test.host/shelf/secure/books") + end + + def test_assert_recognizes_with_engine_and_block_constraint + assert_raise(Assertion) do + assert_recognizes({ controller: "block_books", action: "index" }, "http://test.host/shelf/block/books") + end + assert_recognizes({ controller: "block_books", action: "index" }, "https://test.host/shelf/block/books") + end + + def test_assert_recognizes_with_engine_and_query_constraint + assert_raise(Assertion) do + assert_recognizes({ controller: "query_books", action: "index", use_query: "false" }, "/shelf/query/books", use_query: "false") + end + assert_recognizes({ controller: "query_books", action: "index", use_query: "true" }, "/shelf/query/books", use_query: "true") + end + + def test_assert_recognizes_raises_message_with_engine + err = assert_raise(Assertion) do + assert_recognizes({ controller: "secure_books", action: "index" }, "http://test.host/shelf/secure/books", {}, "This is a really bad msg") + end + + assert_match err.message, "This is a really bad msg" + end + def test_assert_routing assert_routing("/articles", controller: "articles", action: "index") end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 446b65a9b9..8f4e7c96a9 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3,6 +3,7 @@ require "erb" require "abstract_unit" require "controller/fake_controllers" +require "active_support/messages/rotation_configuration" class TestRoutingMapper < ActionDispatch::IntegrationTest SprocketsApp = lambda { |env| @@ -4224,7 +4225,7 @@ class TestGlobRoutingMapper < ActionDispatch::IntegrationTest end end - #include Routes.url_helpers + # include Routes.url_helpers APP = build_app Routes def app; APP end @@ -4947,6 +4948,7 @@ end class FlashRedirectTest < ActionDispatch::IntegrationTest SessionKey = "_myapp_session" Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33") + Rotations = ActiveSupport::Messages::RotationConfiguration.new class KeyGeneratorMiddleware def initialize(app) @@ -4955,6 +4957,8 @@ class FlashRedirectTest < ActionDispatch::IntegrationTest def call(env) env["action_dispatch.key_generator"] ||= Generator + env["action_dispatch.cookies_rotations"] ||= Rotations + @app.call(env) end end @@ -5053,3 +5057,40 @@ class TestRecognizePath < ActionDispatch::IntegrationTest Routes.recognize_path(*args) end end + +class TestRelativeUrlRootGeneration < ActionDispatch::IntegrationTest + config = ActionDispatch::Routing::RouteSet::Config.new("/blog", false) + + stub_controllers(config) do |routes| + Routes = routes + + routes.draw do + get "/", to: "posts#index", as: :posts + get "/:id", to: "posts#show", as: :post + end + end + + include Routes.url_helpers + + APP = build_app Routes + + def app + APP + end + + def test_url_helpers + assert_equal "/blog/", posts_path({}) + assert_equal "/blog/", Routes.url_helpers.posts_path({}) + + assert_equal "/blog/1", post_path(id: "1") + assert_equal "/blog/1", Routes.url_helpers.post_path(id: "1") + end + + def test_optimized_url_helpers + assert_equal "/blog/", posts_path + assert_equal "/blog/", Routes.url_helpers.posts_path + + assert_equal "/blog/1", post_path("1") + assert_equal "/blog/1", Routes.url_helpers.post_path("1") + end +end diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 6517cf4c99..e34426a471 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -3,14 +3,19 @@ require "abstract_unit" require "stringio" require "active_support/key_generator" +require "active_support/messages/rotation_configuration" class CookieStoreTest < ActionDispatch::IntegrationTest SessionKey = "_myapp_session" SessionSecret = "b3c631c314c0bbca50c1b2843150fe33" - Generator = ActiveSupport::LegacyKeyGenerator.new(SessionSecret) + SessionSalt = "authenticated encrypted cookie" - Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, digest: "SHA1") - SignedBar = Verifier.generate(foo: "bar", session_id: SecureRandom.hex(16)) + Generator = ActiveSupport::KeyGenerator.new(SessionSecret, iterations: 1000) + Rotations = ActiveSupport::Messages::RotationConfiguration.new + + Encryptor = ActiveSupport::MessageEncryptor.new( + Generator.generate_key(SessionSalt, 32), cipher: "aes-256-gcm", serializer: Marshal + ) class TestController < ActionController::Base def no_session_access @@ -23,12 +28,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def set_session_value session[:foo] = "bar" - render plain: Rack::Utils.escape(Verifier.generate(session.to_hash)) - end - - def set_session_value_expires_in_five_hours - session[:foo] = "bar" - render plain: Rack::Utils.escape(Verifier.generate(session.to_hash, expires_in: 5.hours)) + render body: nil end def get_session_value @@ -70,19 +70,35 @@ class CookieStoreTest < ActionDispatch::IntegrationTest end end + def parse_cookie_from_header + cookie_matches = headers["Set-Cookie"].match(/#{SessionKey}=([^;]+)/) + cookie_matches && cookie_matches[1] + end + + def assert_session_cookie(cookie_string, contents) + assert_includes headers["Set-Cookie"], cookie_string + + session_value = parse_cookie_from_header + session_data = Encryptor.decrypt_and_verify(Rack::Utils.unescape(session_value)) rescue nil + + assert_not_nil session_data, "session failed to decrypt" + assert_equal session_data.slice(*contents.keys), contents + end + def test_setting_session_value with_test_route_set do get "/set_session_value" + assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" end end def test_getting_session_value with_test_route_set do - cookies[SessionKey] = SignedBar + get "/set_session_value" get "/get_session_value" + assert_response :success assert_equal 'foo: "bar"', response.body end @@ -90,8 +106,9 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_getting_session_id with_test_route_set do - cookies[SessionKey] = SignedBar + get "/set_session_value" get "/persistent_session_id" + assert_response :success assert_equal 32, response.body.size session_id = response.body @@ -104,8 +121,12 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_disregards_tampered_sessions with_test_route_set do - cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--123456780" + encryptor = ActiveSupport::MessageEncryptor.new("A" * 32, cipher: "aes-256-gcm", serializer: Marshal) + + cookies[SessionKey] = encryptor.encrypt_and_sign("foo" => "bar", "session_id" => "abc") + get "/get_session_value" + assert_response :success assert_equal "foo: nil", response.body end @@ -133,19 +154,19 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_does_set_secure_cookies_over_https with_test_route_set(secure: true) do get "/set_session_value", headers: { "HTTPS" => "on" } + assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; secure; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; secure; HttpOnly", "foo" => "bar" end end # {:foo=>#<SessionAutoloadTest::Foo bar:"baz">, :session_id=>"ce8b0752a6ab7c7af3cdb8a80e6b9e46"} - SignedSerializedCookie = "BAh7BzoIZm9vbzodU2Vzc2lvbkF1dG9sb2FkVGVzdDo6Rm9vBjoJQGJhciIIYmF6Og9zZXNzaW9uX2lkIiVjZThiMDc1MmE2YWI3YzdhZjNjZGI4YTgwZTZiOWU0Ng==--2bf3af1ae8bd4e52b9ac2099258ace0c380e601c" + EncryptedSerializedCookie = "9RZ2Fij0qLveUwM4s+CCjGqhpjyUC8jiBIf/AiBr9M3TB8xh2vQZtvSOMfN3uf6oYbbpIDHAcOFIEl69FcW1ozQYeSrCLonYCazoh34ZdYskIQfGwCiSYleVXG1OD9Z4jFqeVArw4Ewm0paOOPLbN1rc6A==--I359v/KWdZ1ok0ey--JFFhuPOY7WUo6tB/eP05Aw==" def test_deserializes_unloaded_classes_on_get_id with_test_route_set do with_autoload_path "session_autoload_test" do - cookies[SessionKey] = SignedSerializedCookie + cookies[SessionKey] = EncryptedSerializedCookie get "/get_session_id" assert_response :success assert_equal "id: ce8b0752a6ab7c7af3cdb8a80e6b9e46", response.body, "should auto-load unloaded class" @@ -156,7 +177,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_deserializes_unloaded_classes_on_get_value with_test_route_set do with_autoload_path "session_autoload_test" do - cookies[SessionKey] = SignedSerializedCookie + cookies[SessionKey] = EncryptedSerializedCookie get "/get_session_value" assert_response :success assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class" @@ -195,8 +216,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest get "/set_session_value" assert_response :success session_payload = response.body - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" get "/call_reset_session" assert_response :success @@ -214,8 +234,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set do get "/set_session_value" assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" get "/get_class_after_reset_session" assert_response :success @@ -237,8 +256,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set do get "/set_session_value" assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" get "/call_session_clear" assert_response :success @@ -251,7 +269,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_persistent_session_id with_test_route_set do - cookies[SessionKey] = SignedBar + get "/set_session_value" get "/persistent_session_id" assert_response :success assert_equal 32, response.body.size @@ -266,8 +284,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_setting_session_id_to_nil_is_respected with_test_route_set do - cookies[SessionKey] = SignedBar - + get "/set_session_value" get "/get_session_id" sid = response.body assert_equal 36, sid.size @@ -281,31 +298,53 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set(expire_after: 5.hours) do # First request accesses the session time = Time.local(2008, 4, 24) - cookie_body = nil Time.stub :now, time do expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") - cookies[SessionKey] = SignedBar + get "/set_session_value" - get "/set_session_value_expires_in_five_hours" assert_response :success - - cookie_body = response.body - assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" end # Second request does not access the session - time = Time.local(2008, 4, 25) + time = time + 3.hours Time.stub :now, time do expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") get "/no_session_access" + assert_response :success + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" + end + end + end + + def test_session_store_with_expire_after_does_not_accept_expired_session + with_test_route_set(expire_after: 5.hours) do + # First request accesses the session + time = Time.local(2017, 11, 12) + + Time.stub :now, time do + expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") + + get "/set_session_value" + get "/get_session_value" - assert_equal "_myapp_session=#{cookies[SessionKey]}; path=/; expires=#{expected_expiry}; HttpOnly", - headers["Set-Cookie"] + assert_response :success + assert_equal 'foo: "bar"', response.body + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" + end + + # Second request is beyond the expiry time and the session is invalidated + time += 5.hours + 1.minute + + Time.stub :now, time do + get "/get_session_value" + + assert_response :success + assert_equal "foo: nil", response.body end end end @@ -345,7 +384,15 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def get(path, *args) args[0] ||= {} args[0][:headers] ||= {} - args[0][:headers]["action_dispatch.key_generator"] ||= Generator + args[0][:headers].tap do |config| + config["action_dispatch.secret_key_base"] = SessionSecret + config["action_dispatch.authenticated_encrypted_cookie_salt"] = SessionSalt + config["action_dispatch.use_authenticated_cookie_encryption"] = true + + config["action_dispatch.key_generator"] ||= Generator + config["action_dispatch.cookies_rotations"] ||= Rotations + end + super(path, *args) end diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb index e6f9353b22..a824ee0c84 100644 --- a/actionpack/test/dispatch/system_testing/driver_test.rb +++ b/actionpack/test/dispatch/system_testing/driver_test.rb @@ -12,7 +12,24 @@ class DriverTest < ActiveSupport::TestCase test "initializing the driver with a browser" do driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) assert_equal :selenium, driver.instance_variable_get(:@name) - assert_equal :chrome, driver.instance_variable_get(:@browser) + assert_equal :chrome, driver.instance_variable_get(:@browser).name + assert_nil driver.instance_variable_get(:@browser).options + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) + end + + test "initializing the driver with a headless chrome" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) + assert_equal :selenium, driver.instance_variable_get(:@name) + assert_equal :headless_chrome, driver.instance_variable_get(:@browser).name + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) + end + + test "initializing the driver with a headless firefox" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_firefox, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) + assert_equal :selenium, driver.instance_variable_get(:@name) + assert_equal :headless_firefox, driver.instance_variable_get(:@browser).name assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) end diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb index 2afda31cf5..264844fc7d 100644 --- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb +++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb @@ -35,6 +35,11 @@ class ScreenshotHelperTest < ActiveSupport::TestCase end end + test "defaults to simple output for the screenshot" do + new_test = DrivenBySeleniumWithChrome.new("x") + assert_equal "simple", new_test.send(:output_type) + end + test "display_image return artifact format when specify RAILS_SYSTEM_TESTING_SCREENSHOT environment" do begin original_output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] @@ -42,6 +47,8 @@ class ScreenshotHelperTest < ActiveSupport::TestCase new_test = DrivenBySeleniumWithChrome.new("x") + assert_equal "artifact", new_test.send(:output_type) + Rails.stub :root, Pathname.getwd do new_test.stub :passed?, false do assert_match %r|url=artifact://.+?tmp/screenshots/failures_x\.png|, new_test.send(:display_image) diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb index ed65d93e49..95e411faf4 100644 --- a/actionpack/test/dispatch/system_testing/server_test.rb +++ b/actionpack/test/dispatch/system_testing/server_test.rb @@ -6,14 +6,27 @@ require "action_dispatch/system_testing/server" class ServerTest < ActiveSupport::TestCase setup do + @old_capybara_server = Capybara.server + end + + test "port is always included" do + ActionDispatch::SystemTesting::Server.new.run + assert Capybara.always_include_port, "expected Capybara.always_include_port to be true" + end + + test "server is changed from `default` to `puma`" do + Capybara.server = :default ActionDispatch::SystemTesting::Server.new.run + refute_equal Capybara.server, Capybara.servers[:default] end - test "initializing the server port" do - assert_includes Capybara.servers, :rails_puma + test "server is not changed to `puma` when is different than default" do + Capybara.server = :webrick + ActionDispatch::SystemTesting::Server.new.run + assert_equal Capybara.server, Capybara.servers[:webrick] end - test "port is always included" do - assert Capybara.always_include_port, "expected Capybara.always_include_port to be true" + teardown do + Capybara.server = @old_capybara_server end end diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb index b60ec559ae..b078a5abc5 100644 --- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb +++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb @@ -22,6 +22,18 @@ class SetDriverToSeleniumTest < DrivenBySeleniumWithChrome end end +class SetDriverToSeleniumHeadlessChromeTest < DrivenBySeleniumWithHeadlessChrome + test "uses selenium headless chrome" do + assert_equal :selenium, Capybara.current_driver + end +end + +class SetDriverToSeleniumHeadlessFirefoxTest < DrivenBySeleniumWithHeadlessFirefox + test "uses selenium headless firefox" do + assert_equal :selenium, Capybara.current_driver + end +end + class SetHostTest < DrivenByRackTest test "sets default host" do assert_equal "http://127.0.0.1", Capybara.app_host @@ -36,32 +48,37 @@ end class UndefMethodsTest < DrivenBySeleniumWithChrome test "get" do - assert_raise NoMethodError do + exception = assert_raise NoMethodError do get "http://example.com" end + assert_equal "System tests cannot make direct requests via #get; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message end test "post" do - assert_raise NoMethodError do + exception = assert_raise NoMethodError do post "http://example.com" end + assert_equal "System tests cannot make direct requests via #post; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message end test "put" do - assert_raise NoMethodError do + exception = assert_raise NoMethodError do put "http://example.com" end + assert_equal "System tests cannot make direct requests via #put; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message end test "patch" do - assert_raise NoMethodError do + exception = assert_raise NoMethodError do patch "http://example.com" end + assert_equal "System tests cannot make direct requests via #patch; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message end test "delete" do - assert_raise NoMethodError do + exception = assert_raise NoMethodError do delete "http://example.com" end + assert_equal "System tests cannot make direct requests via #delete; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message end end diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index 4673d7cc11..24c7135c7e 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -18,7 +18,7 @@ module ActionDispatch def test_filename_is_different_object file_str = "foo" uf = Http::UploadedFile.new(filename: file_str, tempfile: Object.new) - assert_not_equal file_str.object_id , uf.original_filename.object_id + assert_not_equal file_str.object_id, uf.original_filename.object_id end def test_filename_should_be_in_utf_8 diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 29cc74471d..183f421bcf 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -30,7 +30,7 @@ module ActionDispatch def test_unicode get "/ほげ", to: "foo#bar" - #match the escaped version of /ほげ + # match the escaped version of /ほげ env = rails_env "PATH_INFO" => "/%E3%81%BB%E3%81%92" called = false router.recognize(env) do |r, params| |