diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware')
20 files changed, 644 insertions, 450 deletions
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index dd1f140051..3477aa8b29 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,15 +1,63 @@ require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/object/blank' require 'active_support/key_generator' require 'active_support/message_verifier' require 'active_support/json' module ActionDispatch - class Request < Rack::Request + class Request def cookie_jar - env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) + fetch_header('action_dispatch.cookies'.freeze) do + self.cookie_jar = Cookies::CookieJar.build(self, cookies) + end + end + + # :stopdoc: + prepend Module.new { + def commit_cookie_jar! + cookie_jar.commit! + end + } + + def have_cookie_jar? + has_header? 'action_dispatch.cookies'.freeze + end + + def cookie_jar=(jar) + set_header 'action_dispatch.cookies'.freeze, jar + end + + def key_generator + get_header Cookies::GENERATOR_KEY + end + + def signed_cookie_salt + get_header Cookies::SIGNED_COOKIE_SALT + end + + def encrypted_cookie_salt + get_header Cookies::ENCRYPTED_COOKIE_SALT + end + + def encrypted_signed_cookie_salt + get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT end + + def secret_token + get_header Cookies::SECRET_TOKEN + end + + def secret_key_base + get_header Cookies::SECRET_KEY_BASE + end + + def cookies_serializer + get_header Cookies::COOKIES_SERIALIZER + end + + def cookies_digest + get_header Cookies::COOKIES_DIGEST + end + # :startdoc: end # \Cookies are read and written through ActionController#cookies. @@ -35,6 +83,12 @@ module ActionDispatch # # 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 + # # # Sets a "permanent" cookie (which expires in 20 years from now). # cookies.permanent[:login] = "XJ-122" # @@ -47,6 +101,7 @@ module ActionDispatch # cookies.size # => 2 # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] # cookies.signed[:login] # => "XJ-122" + # cookies.encrypted[:discount] # => 45 # # Example for deleting: # @@ -118,7 +173,7 @@ module ActionDispatch # cookies.permanent.signed[:remember_me] = current_user.id # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) + @permanent ||= PermanentCookieJar.new(self) end # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from @@ -138,10 +193,10 @@ module ActionDispatch # cookies.signed[:discount] # => 45 def signed @signed ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacySignedCookieJar.new(self, @key_generator, @options) + if upgrade_legacy_signed_cookies? + UpgradeLegacySignedCookieJar.new(self) else - SignedCookieJar.new(self, @key_generator, @options) + SignedCookieJar.new(self) end end @@ -161,10 +216,10 @@ module ActionDispatch # cookies.encrypted[:discount] # => 45 def encrypted @encrypted ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options) + if upgrade_legacy_signed_cookies? + UpgradeLegacyEncryptedCookieJar.new(self) else - EncryptedCookieJar.new(self, @key_generator, @options) + EncryptedCookieJar.new(self) end end @@ -172,12 +227,18 @@ module ActionDispatch # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores. def signed_or_encrypted @signed_or_encrypted ||= - if @options[:secret_key_base].present? + if request.secret_key_base.present? encrypted else signed end end + + private + + def upgrade_legacy_signed_cookies? + request.secret_token.present? && request.secret_key_base.present? + end end # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream @@ -187,7 +248,7 @@ module ActionDispatch module VerifyAndUpgradeLegacySignedMessage # :nodoc: def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) + @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) @@ -197,6 +258,11 @@ module ActionDispatch rescue ActiveSupport::MessageVerifier::InvalidSignature nil end + + private + def parse(name, signed_message) + super || verify_and_upgrade_legacy_signed_message(name, signed_message) + end end class CookieJar #:nodoc: @@ -216,38 +282,18 @@ module ActionDispatch # $& => example.local DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ - def self.options_for_env(env) #:nodoc: - { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', - encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', - encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', - secret_token: env[SECRET_TOKEN], - secret_key_base: env[SECRET_KEY_BASE], - upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, - serializer: env[COOKIES_SERIALIZER], - digest: env[COOKIES_DIGEST] - } - end - - def self.build(request) - env = request.env - key_generator = env[GENERATOR_KEY] - options = options_for_env env - - host = request.host - secure = request.ssl? - - new(key_generator, host, secure, options).tap do |hash| - hash.update(request.cookies) + def self.build(req, cookies) + new(req).tap do |hash| + hash.update(cookies) end end - def initialize(key_generator, host = nil, secure = false, options = {}) - @key_generator = key_generator + attr_reader :request + + def initialize(request) @set_cookies = {} @delete_cookies = {} - @host = host - @secure = secure - @options = options + @request = request @cookies = {} @committed = false end @@ -283,6 +329,17 @@ module ActionDispatch self end + def update_cookies_from_jar + request_jar = @request.cookie_jar.instance_variable_get(:@cookies) + set_cookies = request_jar.reject { |k,_| @delete_cookies.key?(k) } + + @cookies.update set_cookies if set_cookies + end + + def to_header + @cookies.map { |k,v| "#{k}=#{v}" }.join ';' + end + def handle_options(options) #:nodoc: options[:path] ||= "/" @@ -292,12 +349,12 @@ module ActionDispatch # if host is not ip and matches domain regexp # (ip confirms to domain regexp so we explicitly check for ip) - options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp) + options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp) ".#{$&}" end elsif options[:domain].is_a? Array # if host matches one of the supplied domains without a dot in front of it - options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') } + options[:domain] = options[:domain].find {|domain| request.host.include? domain.sub(/^\./, '') } end end @@ -352,47 +409,71 @@ module ActionDispatch end def write(headers) - @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } - @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } - end - - def recycle! #:nodoc: - @set_cookies = {} - @delete_cookies = {} + if header = make_set_cookie_header(headers[HTTP_HEADER]) + headers[HTTP_HEADER] = header + end end mattr_accessor :always_write_cookie self.always_write_cookie = false private - def write_cookie?(cookie) - @secure || !cookie[:secure] || always_write_cookie - end + + def make_set_cookie_header(header) + header = @set_cookies.inject(header) { |m, (k, v)| + if write_cookie?(v) + ::Rack::Utils.add_cookie_to_header(m, k, v) + else + m + end + } + @delete_cookies.inject(header) { |m, (k, v)| + ::Rack::Utils.add_remove_cookie_to_header(m, k, v) + } + end + + def write_cookie?(cookie) + request.ssl? || !cookie[:secure] || always_write_cookie + end end - class PermanentCookieJar #:nodoc: + class AbstractCookieJar # :nodoc: include ChainedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) @parent_jar = parent_jar - @key_generator = key_generator - @options = options end def [](name) - @parent_jar[name.to_s] + if data = @parent_jar[name.to_s] + parse name, data + end end def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! else - options = { :value => options } + options = { value: options } end - options[:expires] = 20.years.from_now + commit(options) @parent_jar[name] = options end + + protected + def request; @parent_jar.request; end + + private + def parse(name, data); data; end + def commit(options); end + end + + class PermanentCookieJar < AbstractCookieJar # :nodoc: + private + def commit(options) + options[:expires] = 20.years.from_now + end end class JsonSerializer # :nodoc: @@ -410,7 +491,7 @@ module ActionDispatch protected def needs_migration?(value) - @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) + request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE) end def serialize(value) @@ -430,7 +511,7 @@ module ActionDispatch end def serializer - serializer = @options[:serializer] || :marshal + serializer = request.cookies_serializer || :marshal case serializer when :marshal Marshal @@ -442,48 +523,32 @@ module ActionDispatch end def digest - @options[:digest] || 'SHA1' + request.cookies_digest || 'SHA1' + end + + def key_generator + request.key_generator end end - class SignedCookieJar #:nodoc: - include ChainedCookieJars + class SignedCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) - @parent_jar = parent_jar - @options = options - secret = key_generator.generate_key(@options[:signed_cookie_salt]) + def initialize(parent_jar) + super + secret = key_generator.generate_key(request.signed_cookie_salt) @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end - # Returns the value of the cookie by +name+ if it is untampered, - # returns +nil+ otherwise or if no such cookie exists. - def [](name) - if signed_message = @parent_jar[name] - deserialize name, verify(signed_message) + private + def parse(name, signed_message) + deserialize name, @verifier.verified(signed_message) end - end - # Signs and sets the cookie named +name+. The second argument may be the cookie's - # value or a hash of options as documented above. - def []=(name, options) - if options.is_a?(Hash) - options.symbolize_keys! + def commit(options) options[:value] = @verifier.generate(serialize(options[:value])) - else - options = { :value => @verifier.generate(serialize(options)) } - end - raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE - @parent_jar[name] = options - end - - private - def verify(signed_message) - @verifier.verify(signed_message) - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end end @@ -493,60 +558,36 @@ module ActionDispatch # re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: include VerifyAndUpgradeLegacySignedMessage - - def [](name) - if signed_message = @parent_jar[name] - deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message) - end - end end - class EncryptedCookieJar #:nodoc: - include ChainedCookieJars + class EncryptedCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) + super + if ActiveSupport::LegacyKeyGenerator === key_generator raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end - @parent_jar = parent_jar - @options = options - secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) - sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) + secret = key_generator.generate_key(request.encrypted_cookie_salt || '') + sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '') @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end - # Returns the value of the cookie by +name+ if it is untampered, - # returns +nil+ otherwise or if no such cookie exists. - def [](name) - if encrypted_message = @parent_jar[name] - deserialize name, decrypt_and_verify(encrypted_message) - end - end - - # Encrypts and sets the cookie named +name+. The second argument may be the cookie's - # value or a hash of options as documented above. - def []=(name, options) - if options.is_a?(Hash) - options.symbolize_keys! - else - options = { :value => options } - end - - options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) - - raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE - @parent_jar[name] = options - end - private - def decrypt_and_verify(encrypted_message) - @encryptor.decrypt_and_verify(encrypted_message) + def parse(name, encrypted_message) + deserialize name, @encryptor.decrypt_and_verify(encrypted_message) rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage nil end + + def commit(options) + options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) + + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE + end end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore @@ -555,12 +596,6 @@ module ActionDispatch # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: include VerifyAndUpgradeLegacySignedMessage - - def [](name) - if encrypted_or_signed_message = @parent_jar[name] - deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) - end - end end def initialize(app) @@ -568,9 +603,12 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env + status, headers, body = @app.call(env) - if cookie_jar = env['action_dispatch.cookies'] + if request.have_cookie_jar? + cookie_jar = request.cookie_jar unless cookie_jar.committed? cookie_jar.write(headers) if headers[HTTP_HEADER].respond_to?(:join) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 9082aac271..b55c937e0c 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -38,12 +38,14 @@ module ActionDispatch end end - def initialize(app, routes_app = nil) - @app = app - @routes_app = routes_app + def initialize(app, routes_app = nil, response_format = :default) + @app = app + @routes_app = routes_app + @response_format = response_format end def call(env) + request = ActionDispatch::Request.new env _, headers, body = response = @app.call(env) if headers['X-Cascade'] == 'pass' @@ -53,61 +55,99 @@ module ActionDispatch response rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) + raise exception unless request.show_exceptions? + render_exception(request, exception) end private - def render_exception(env, exception) - wrapper = ExceptionWrapper.new(env, exception) - log_error(env, wrapper) - - if env['action_dispatch.show_detailed_exceptions'] - request = Request.new(env) - traces = wrapper.traces - - trace_to_show = 'Application Trace' - if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error' - trace_to_show = 'Full Trace' + def render_exception(request, exception) + backtrace_cleaner = request.get_header('action_dispatch.backtrace_cleaner') + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) + log_error(request, wrapper) + + if request.get_header('action_dispatch.show_detailed_exceptions') + case @response_format + when :api + render_for_api_application(request, wrapper) + when :default + render_for_default_application(request, wrapper) end + else + raise exception + end + end - if source_to_show = traces[trace_to_show].first - source_to_show_id = source_to_show[:id] - end + def render_for_default_application(request, wrapper) + template = create_template(request, wrapper) + file = "rescues/#{wrapper.rescue_template}" - template = DebugView.new([RESCUES_TEMPLATE_PATH], - request: request, - exception: wrapper.exception, - traces: traces, - show_source_idx: source_to_show_id, - trace_to_show: trace_to_show, - routes_inspector: routes_inspector(exception), - source_extracts: wrapper.source_extracts, - line_number: wrapper.line_number, - file: wrapper.file - ) - file = "rescues/#{wrapper.rescue_template}" - - if request.xhr? - body = template.render(template: file, layout: false, formats: [:text]) - format = "text/plain" - else - body = template.render(template: file, layout: 'rescues/layout') - format = "text/html" - end - render(wrapper.status_code, body, format) + if request.xhr? + body = template.render(template: file, layout: false, formats: [:text]) + format = "text/plain" else - raise exception + body = template.render(template: file, layout: 'rescues/layout') + format = "text/html" + end + render(wrapper.status_code, body, format) + end + + def render_for_api_application(request, wrapper) + body = { + status: wrapper.status_code, + error: Rack::Utils::HTTP_STATUS_CODES.fetch( + wrapper.status_code, + Rack::Utils::HTTP_STATUS_CODES[500] + ), + exception: wrapper.exception.inspect, + traces: wrapper.traces + } + + content_type = request.formats.first + to_format = "to_#{content_type.to_sym}" + + if content_type && body.respond_to?(to_format) + formatted_body = body.public_send(to_format) + format = content_type + else + formatted_body = body.to_json + format = Mime[:json] end + + render(wrapper.status_code, formatted_body, format) + end + + def create_template(request, wrapper) + traces = wrapper.traces + + trace_to_show = 'Application Trace' + if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error' + trace_to_show = 'Full Trace' + end + + if source_to_show = traces[trace_to_show].first + source_to_show_id = source_to_show[:id] + end + + DebugView.new([RESCUES_TEMPLATE_PATH], + request: request, + exception: wrapper.exception, + traces: traces, + show_source_idx: source_to_show_id, + trace_to_show: trace_to_show, + routes_inspector: routes_inspector(wrapper.exception), + source_extracts: wrapper.source_extracts, + line_number: wrapper.line_number, + file: wrapper.file + ) end def render(status, body, format) [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] end - def log_error(env, wrapper) - logger = logger(env) + def log_error(request, wrapper) + logger = logger(request) return unless logger exception = wrapper.exception @@ -123,8 +163,8 @@ module ActionDispatch end end - def logger(env) - env['action_dispatch.logger'] || stderr_logger + def logger(request) + request.logger || stderr_logger end def stderr_logger diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index d176a73633..3b61824cc9 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -17,7 +17,9 @@ module ActionDispatch 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity, 'ActionDispatch::ParamsParser::ParseError' => :bad_request, 'ActionController::BadRequest' => :bad_request, - 'ActionController::ParameterMissing' => :bad_request + 'ActionController::ParameterMissing' => :bad_request, + 'Rack::Utils::ParameterTypeError' => :bad_request, + 'Rack::Utils::InvalidParameterError' => :bad_request ) cattr_accessor :rescue_templates @@ -29,13 +31,13 @@ module ActionDispatch 'ActionView::Template::Error' => 'template_error' ) - attr_reader :env, :exception, :line_number, :file + attr_reader :backtrace_cleaner, :exception, :line_number, :file - def initialize(env, exception) - @env = env + def initialize(backtrace_cleaner, exception) + @backtrace_cleaner = backtrace_cleaner @exception = original_exception(exception) - expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError) + expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError) end def rescue_template @@ -59,7 +61,7 @@ module ActionDispatch end def traces - appplication_trace_with_ids = [] + application_trace_with_ids = [] framework_trace_with_ids = [] full_trace_with_ids = [] @@ -67,7 +69,7 @@ module ActionDispatch trace_with_id = { id: idx, trace: trace } if application_trace.include?(trace) - appplication_trace_with_ids << trace_with_id + application_trace_with_ids << trace_with_id else framework_trace_with_ids << trace_with_id end @@ -76,7 +78,7 @@ module ActionDispatch end { - "Application Trace" => appplication_trace_with_ids, + "Application Trace" => application_trace_with_ids, "Framework Trace" => framework_trace_with_ids, "Full Trace" => full_trace_with_ids } @@ -104,17 +106,13 @@ module ActionDispatch end def original_exception(exception) - if registered_original_exception?(exception) - exception.original_exception + if @@rescue_responses.has_key?(exception.cause.class.name) + exception.cause else exception end end - def registered_original_exception?(exception) - exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name) - end - def clean_backtrace(*args) if backtrace_cleaner backtrace_cleaner.clean(backtrace, *args) @@ -123,10 +121,6 @@ module ActionDispatch end end - def backtrace_cleaner - @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner'] - end - def source_fragment(path, line) return unless Rails.respond_to?(:root) && Rails.root full_path = Rails.root.join(path) diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 59639a010e..c51dcd542a 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,15 +1,6 @@ require 'active_support/core_ext/hash/keys' module ActionDispatch - class Request < Rack::Request - # Access the contents of the flash. Use <tt>flash["notice"]</tt> to - # read a notice you put there or <tt>flash["notice"] = "hello"</tt> - # to put a new one. - def flash - @env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"]) - end - end - # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can @@ -47,6 +38,40 @@ module ActionDispatch class Flash KEY = 'action_dispatch.request.flash_hash'.freeze + module RequestMethods + # Access the contents of the flash. Use <tt>flash["notice"]</tt> to + # read a notice you put there or <tt>flash["notice"] = "hello"</tt> + # to put a new one. + def flash + flash = flash_hash + return flash if flash + self.flash = Flash::FlashHash.from_session_value(session["flash"]) + end + + def flash=(flash) + set_header Flash::KEY, flash + end + + def flash_hash # :nodoc: + get_header Flash::KEY + end + + def commit_flash # :nodoc: + session = self.session || {} + flash_hash = self.flash_hash + + if flash_hash && (flash_hash.present? || session.key?('flash')) + session["flash"] = flash_hash.to_session_value + self.flash = flash_hash.dup + end + + if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) + session.key?('flash') && session['flash'].nil? + session.delete('flash') + end + end + end + class FlashNow #:nodoc: attr_accessor :flash @@ -258,25 +283,10 @@ module ActionDispatch end end - def initialize(app) - @app = app - end - - def call(env) - @app.call(env) - ensure - session = Request::Session.find(env) || {} - flash_hash = env[KEY] - - if flash_hash && (flash_hash.present? || session.key?('flash')) - session["flash"] = flash_hash.to_session_value - env[KEY] = flash_hash.dup - end + def self.new(app) app; end + end - if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) - session.key?('flash') && session['flash'].nil? - session.delete('flash') - end - end + class Request + prepend Flash::RequestMethods end end diff --git a/actionpack/lib/action_dispatch/middleware/load_interlock.rb b/actionpack/lib/action_dispatch/middleware/load_interlock.rb new file mode 100644 index 0000000000..07f498319c --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/load_interlock.rb @@ -0,0 +1,21 @@ +require 'active_support/dependencies' +require 'rack/body_proxy' + +module ActionDispatch + class LoadInterlock + def initialize(app) + @app = app + end + + def call(env) + interlock = ActiveSupport::Dependencies.interlock + interlock.start_running + response = @app.call(env) + body = Rack::BodyProxy.new(response[2]) { interlock.done_running } + response[2] = body + response + ensure + interlock.done_running unless body + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 29d43faeed..c2a4f46e67 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -1,60 +1,44 @@ -require 'active_support/core_ext/hash/conversions' require 'action_dispatch/http/request' -require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch + # ActionDispatch::ParamsParser works for all the requests having any Content-Length + # (like POST). It takes raw data from the request and puts it through the parser + # that is picked based on Content-Type header. + # + # In case of any error while parsing data ParamsParser::ParseError is raised. class ParamsParser + # Raised when raw data from the request cannot be parsed by the parser + # defined for request's content mime type. class ParseError < StandardError - attr_reader :original_exception - def initialize(message, original_exception) - super(message) - @original_exception = original_exception - end - end - - DEFAULT_PARSERS = { Mime::JSON => :json } - - def initialize(app, parsers = {}) - @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) - end - - def call(env) - if params = parse_formatted_parameters(env) - env["action_dispatch.request.request_parameters"] = params - end - - @app.call(env) - end - - private - def parse_formatted_parameters(env) - request = Request.new(env) - - return false if request.content_length.zero? - - strategy = @parsers[request.content_mime_type] - - return false unless strategy + def initialize(message = nil, original_exception = nil) + if message + ActiveSupport::Deprecation.warn("Passing #message is deprecated and has no effect. " \ + "#{self.class} will automatically capture the message " \ + "of the original exception.", caller) + end - case strategy - when Proc - strategy.call(request.raw_post) - when :json - data = ActiveSupport::JSON.decode(request.raw_post) - data = {:_json => data} unless data.is_a?(Hash) - Request::Utils.deep_munge(data).with_indifferent_access - else - false + if original_exception + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) end - rescue => e # JSON or Ruby code block errors - logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" - raise ParseError.new(e.message, e) + super($!.message) end - def logger(env) - env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr) + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end + end + + # Create a new +ParamsParser+ middleware instance. + # + # The +parsers+ argument can take Hash of parsers where key is identifying + # content mime type, and value is a lambda that is going to process data. + def self.new(app, parsers = {}) + ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers) + app + end end end diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index 7cde76b30e..0f27984550 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -17,8 +17,8 @@ module ActionDispatch end def call(env) - status = env["PATH_INFO"][1..-1].to_i request = ActionDispatch::Request.new(env) + status = request.path_info[1..-1].to_i content_type = request.formats.first body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) } diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 6c7fba00cb..af9a29eb07 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation/reporting' - module ActionDispatch # ActionDispatch::Reloader provides prepare and cleanup callbacks, # intended to assist with code reloading during development. diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 7c4236518d..31b75498b6 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -43,7 +43,7 @@ module ActionDispatch # Create a new +RemoteIp+ middleware instance. # - # The +check_ip_spoofing+ option is on by default. When on, an exception + # The +ip_spoofing_check+ option is on by default. When on, an exception # is raised if it looks like the client is trying to lie about its own IP # address. It makes sense to turn off this check on sites aimed at non-IP # clients (like WAP devices), or behind proxies that set headers in an @@ -57,9 +57,9 @@ module ActionDispatch # with your proxy servers after it. If your proxies aren't removed, pass # them in via the +custom_proxies+ parameter. That way, the middleware will # ignore those IP addresses, and return the one that you want. - def initialize(app, check_ip_spoofing = true, custom_proxies = nil) + def initialize(app, ip_spoofing_check = true, custom_proxies = nil) @app = app - @check_ip = check_ip_spoofing + @check_ip = ip_spoofing_check @proxies = if custom_proxies.blank? TRUSTED_PROXIES elsif custom_proxies.respond_to?(:any?) @@ -74,18 +74,19 @@ module ActionDispatch # requests. For those requests that do need to know the IP, the # GetIp#calculate_ip method will calculate the memoized client IP address. def call(env) - env["action_dispatch.remote_ip"] = GetIp.new(env, self) - @app.call(env) + req = ActionDispatch::Request.new env + req.remote_ip = GetIp.new(req, check_ip, proxies) + @app.call(req.env) end # The GetIp class exists as a way to defer processing of the request data # into an actual IP address. If the ActionDispatch::Request#remote_ip method # is called, this class will calculate the value and then memoize it. class GetIp - def initialize(env, middleware) - @env = env - @check_ip = middleware.check_ip - @proxies = middleware.proxies + def initialize(req, check_ip, proxies) + @req = req + @check_ip = check_ip + @proxies = proxies end # Sort through the various IP address headers, looking for the IP most @@ -108,23 +109,31 @@ module ActionDispatch # the last address left, which was presumably set by one of those proxies. def calculate_ip # Set by the Rack web server, this is a single value. - remote_addr = ips_from('REMOTE_ADDR').last + remote_addr = ips_from(@req.remote_addr).last # Could be a CSV list and/or repeated headers that were concatenated. - client_ips = ips_from('HTTP_CLIENT_IP').reverse - forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse + client_ips = ips_from(@req.client_ip).reverse + forwarded_ips = ips_from(@req.x_forwarded_for).reverse # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set. - # If they are both set, it means that this request passed through two - # proxies with incompatible IP header conventions, and there is no way - # for us to determine which header is the right one after the fact. - # Since we have no idea, we give up and explode. + # If they are both set, it means that either: + # + # 1) This request passed through two proxies with incompatible IP header + # conventions. + # 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+ + # (whichever the proxy servers weren't using) themselves. + # + # Either way, there is no way for us to determine which header is the + # right one after the fact. Since we have no idea, if we are concerned + # about IP spoofing we need to give up and explode. (If you're not + # concerned about IP spoofing you can turn the +ip_spoofing_check+ + # option off.) should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user raise IpSpoofAttackError, "IP spoofing attack?! " + - "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} " + - "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" + "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " + + "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" end # We assume these things about the IP headers: @@ -147,8 +156,9 @@ module ActionDispatch protected def ips_from(header) + return [] unless header # Split the comma-separated list into an array of strings - ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : [] + ips = header.strip.split(/[,\s]+/) ips.select do |ip| begin # Only return IPs that are valid according to the IPAddr#new method diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 84df55fd5a..5fb5953811 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -7,14 +7,22 @@ require 'action_dispatch/request/session' module ActionDispatch module Session class SessionRestoreError < StandardError #:nodoc: - attr_reader :original_exception - def initialize(const_error) - @original_exception = const_error + def initialize(const_error = nil) + if const_error + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end super("Session contains objects whose class definition isn't available.\n" + "Remember to require the classes for all objects kept in the session.\n" + - "(Original exception: #{const_error.message} [#{const_error.class}])\n") + "(Original exception: #{$!.message} [#{$!.class}])\n") + set_backtrace $!.backtrace + end + + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end end @@ -36,6 +44,11 @@ module ActionDispatch @default_options.delete(:sidbits) @default_options.delete(:secure_random) end + + private + def make_request(env) + ActionDispatch::Request.new env + end end module StaleSessionCheck @@ -54,8 +67,8 @@ module ActionDispatch begin # Note that the regexp does not allow $1 to end with a ':' $1.constantize - rescue LoadError, NameError => e - raise ActionDispatch::Session::SessionRestoreError, e, e.backtrace + rescue LoadError, NameError + raise ActionDispatch::Session::SessionRestoreError end retry else @@ -65,8 +78,8 @@ module ActionDispatch end module SessionObject # :nodoc: - def prepare_session(env) - Request::Session.create(self, env, @default_options) + def prepare_session(req) + Request::Session.create(self, req, @default_options) end def loaded_session?(session) @@ -74,15 +87,14 @@ module ActionDispatch end end - class AbstractStore < Rack::Session::Abstract::ID + class AbstractStore < Rack::Session::Abstract::Persisted include Compatibility include StaleSessionCheck include SessionObject private - def set_cookie(env, session_id, cookie) - request = ActionDispatch::Request.new(env) + def set_cookie(request, session_id, cookie) request.cookie_jar[key] = cookie end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb index 857e49a682..589ae46e38 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -18,7 +18,7 @@ module ActionDispatch end # Get a session from the cache. - def get_session(env, sid) + def find_session(env, sid) unless sid and session = @cache.read(cache_key(sid)) sid, session = generate_sid, {} end @@ -26,7 +26,7 @@ module ActionDispatch end # Set a session in the cache. - def set_session(env, sid, session, options) + def write_session(env, sid, session, options) key = cache_key(sid) if session @cache.write(key, session, :expires_in => options[:expire_after]) @@ -37,7 +37,7 @@ module ActionDispatch end # Remove a session from the cache. - def destroy_session(env, sid, options) + def delete_session(env, sid, options) @cache.delete(cache_key(sid)) generate_sid end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index d8f9614904..429a98f236 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -36,7 +36,7 @@ module ActionDispatch # development: # secret_key_base: 'secret key' # - # To generate a secret key for an existing application, run `rake secret`. + # To generate a secret key for an existing application, run `rails secret`. # # 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. @@ -53,7 +53,7 @@ module ActionDispatch # # Note that changing the secret key will invalidate all existing sessions! # - # Because CookieStore extends Rack::Session::Abstract::ID, many of the + # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the # options described there can be used to customize the session cookie that # is generated. For example: # @@ -62,25 +62,21 @@ module ActionDispatch # would set the session cookie to expire automatically 14 days after creation. # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and # <tt>:httponly</tt>. - class CookieStore < Rack::Session::Abstract::ID - include Compatibility - include StaleSessionCheck - include SessionObject - + class CookieStore < AbstractStore def initialize(app, options={}) super(app, options.merge!(:cookie_only => true)) end - def destroy_session(env, session_id, options) + def delete_session(req, session_id, options) new_sid = generate_sid unless options[:drop] # Reset hash and Assign the new session id - env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {} + req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {}) new_sid end - def load_session(env) + def load_session(req) stale_session_check! do - data = unpacked_cookie_data(env) + data = unpacked_cookie_data(req) data = persistent_session_id!(data) [data["session_id"], data] end @@ -88,20 +84,21 @@ module ActionDispatch private - def extract_session_id(env) + def extract_session_id(req) stale_session_check! do - unpacked_cookie_data(env)["session_id"] + unpacked_cookie_data(req)["session_id"] end end - def unpacked_cookie_data(env) - env["action_dispatch.request.unsigned_session_cookie"] ||= begin - stale_session_check! do - if data = get_cookie(env) + def unpacked_cookie_data(req) + req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k| + v = stale_session_check! do + if data = get_cookie(req) data.stringify_keys! end data || {} end + req.set_header k, v end end @@ -111,21 +108,20 @@ module ActionDispatch data end - def set_session(env, sid, session_data, options) + def write_session(req, sid, session_data, options) session_data["session_id"] = sid session_data end - def set_cookie(env, session_id, cookie) - cookie_jar(env)[@key] = cookie + def set_cookie(request, session_id, cookie) + cookie_jar(request)[@key] = cookie end - def get_cookie(env) - cookie_jar(env)[@key] + def get_cookie(req) + cookie_jar(req)[@key] end - def cookie_jar(env) - request = ActionDispatch::Request.new(env) + def cookie_jar(request) request.cookie_jar.signed_or_encrypted end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index f0779279c1..64695f9738 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -27,24 +27,26 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env @app.call(env) rescue Exception => exception - if env['action_dispatch.show_exceptions'] == false - raise exception + if request.show_exceptions? + render_exception(request, exception) else - render_exception(env, exception) + raise exception end end private - def render_exception(env, exception) - wrapper = ExceptionWrapper.new(env, exception) + def render_exception(request, exception) + backtrace_cleaner = request.get_header 'action_dispatch.backtrace_cleaner' + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) status = wrapper.status_code - env["action_dispatch.exception"] = wrapper.exception - env["action_dispatch.original_path"] = env["PATH_INFO"] - env["PATH_INFO"] = "/#{status}" - response = @exceptions_app.call(env) + request.set_header "action_dispatch.exception", wrapper.exception + request.set_header "action_dispatch.original_path", request.path_info + request.path_info = "/#{status}" + response = @exceptions_app.call(request.env) response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response rescue Exception => failsafe_error $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}" diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 0c7caef25d..735b5939dd 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -1,72 +1,135 @@ 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 + # 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. + # + # 2. Secure cookies: Sets the `secure` flag on cookies to tell browsers they + # mustn't be sent along with http:// requests. Enabled by default. Set + # `config.ssl_options` with `secure_cookies: false` to disable this feature. + # + # 3. HTTP Strict Transport Security (HSTS): Tells the browser to remember + # this site as TLS-only and automatically redirect non-TLS requests. + # Enabled by default. Configure `config.ssl_options` with `hsts: false` to disable. + # + # Set `config.ssl_options` with `hsts: { … }` to configure HSTS: + # * `expires`: How long, in seconds, these settings will stick. Defaults to + # `180.days` (recommended). The minimum required to qualify for browser + # preload lists is `18.weeks`. + # * `subdomains`: Set to `true` to tell the browser to apply these settings + # to all subdomains. This protects your cookies from interception by a + # vulnerable site on a subdomain. Defaults to `false`. + # * `preload`: Advertise that this site may be included in browsers' + # preloaded HSTS lists. HSTS protects your site on every visit *except the + # first visit* since it hasn't seen your HSTS header yet. To close this + # gap, browser vendors include a baked-in list of HSTS-enabled sites. + # Go to https://hstspreload.appspot.com to submit your site for inclusion. + # + # To turn off HSTS, omitting the header is not enough. Browsers will remember the + # original HSTS directive until it expires. Instead, use the header to tell browsers to + # expire HSTS immediately. Setting `hsts: false` is a shortcut for + # `hsts: { expires: 0 }`. class SSL - YEAR = 31536000 + # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/ + # and greater than the 18-week requirement for browser preload lists. + HSTS_EXPIRES_IN = 15552000 def self.default_hsts_options - { :expires => YEAR, :subdomains => false } + { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false } end - def initialize(app, options = {}) + def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, **options) @app = app - @hsts = options.fetch(:hsts, {}) - @hsts = {} if @hsts == true - @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts + if options[:host] || options[:port] + ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc + The `:host` and `:port` options are moving within `:redirect`: + `config.ssl_options = { redirect: { host: …, port: … }}`. + end_warning + @redirect = options.slice(:host, :port) + else + @redirect = redirect + end - @host = options[:host] - @port = options[:port] + @secure_cookies = secure_cookies + @hsts_header = build_hsts_header(normalize_hsts_options(hsts)) end def call(env) - request = Request.new(env) + request = Request.new env if request.ssl? - status, headers, body = @app.call(env) - headers = hsts_headers.merge(headers) - flag_cookies_as_secure!(headers) - [status, headers, body] + @app.call(env).tap do |status, headers, body| + set_hsts_header! headers + flag_cookies_as_secure! headers if @secure_cookies + end else - redirect_to_https(request) + return redirect_to_https request if @redirect + @app.call(env) end end private - def redirect_to_https(request) - host = @host || request.host - port = @port || request.port - - location = "https://#{host}" - location << ":#{port}" if port != 80 - location << request.fullpath - - headers = { 'Content-Type' => 'text/html', 'Location' => location } - - [301, headers, []] + def set_hsts_header!(headers) + headers['Strict-Transport-Security'.freeze] ||= @hsts_header end - # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02 - def hsts_headers - if @hsts - value = "max-age=#{@hsts[:expires].to_i}" - value += "; includeSubDomains" if @hsts[:subdomains] - { 'Strict-Transport-Security' => value } + def normalize_hsts_options(options) + case options + # Explicitly disabling HSTS clears the existing setting from browsers + # by setting expiry to 0. + when false + self.class.default_hsts_options.merge(expires: 0) + # Default to enabled, with default options. + when nil, true + self.class.default_hsts_options else - {} + self.class.default_hsts_options.merge(options) end end + # http://tools.ietf.org/html/rfc6797#section-6.1 + def build_hsts_header(hsts) + value = "max-age=#{hsts[:expires].to_i}" + value << "; includeSubDomains" if hsts[:subdomains] + value << "; preload" if hsts[:preload] + value + end + def flag_cookies_as_secure!(headers) - if cookies = headers['Set-Cookie'] - cookies = cookies.split("\n") + if cookies = headers['Set-Cookie'.freeze] + cookies = cookies.split("\n".freeze) - headers['Set-Cookie'] = cookies.map { |cookie| + headers['Set-Cookie'.freeze] = cookies.map { |cookie| if cookie !~ /;\s*secure\s*(;|$)/i "#{cookie}; secure" else cookie end - }.join("\n") + }.join("\n".freeze) end end + + def redirect_to_https(request) + [ @redirect.fetch(:status, 301), + { 'Content-Type' => 'text/html', + 'Location' => https_location_for(request) }, + @redirect.fetch(:body, []) ] + end + + def https_location_for(request) + host = @redirect[:host] || request.host + port = @redirect[:port] || request.port + + location = "https://#{host}" + location << ":#{port}" if port != 80 && port != 443 + location << request.fullpath + location + end end end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index bbf734f103..0b4bee5462 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -4,25 +4,15 @@ require "active_support/dependencies" module ActionDispatch class MiddlewareStack class Middleware - attr_reader :args, :block, :name, :classcache + attr_reader :args, :block, :klass - def initialize(klass_or_name, *args, &block) - @klass = nil - - if klass_or_name.respond_to?(:name) - @klass = klass_or_name - @name = @klass.name - else - @name = klass_or_name.to_s - end - - @classcache = ActiveSupport::Dependencies::Reference - @args, @block = args, block + def initialize(klass, args, block) + @klass = klass + @args = args + @block = block end - def klass - @klass || classcache[@name] - end + def name; klass.name; end def ==(middleware) case middleware @@ -30,24 +20,20 @@ module ActionDispatch klass == middleware.klass when Class klass == middleware - else - normalize(@name) == normalize(middleware) end end def inspect - klass.to_s + if klass.is_a?(Class) + klass.to_s + else + klass.class.to_s + end end def build(app) klass.new(app, *args, &block) end - - private - - def normalize(object) - object.to_s.strip.sub(/^::/, '') - end end include Enumerable @@ -75,19 +61,17 @@ module ActionDispatch middlewares[i] end - def unshift(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - middlewares.unshift(middleware) + def unshift(klass, *args, &block) + middlewares.unshift(build_middleware(klass, args, block)) end def initialize_copy(other) self.middlewares = other.middlewares.dup end - def insert(index, *args, &block) + def insert(index, klass, *args, &block) index = assert_index(index, :before) - middleware = self.class::Middleware.new(*args, &block) - middlewares.insert(index, middleware) + middlewares.insert(index, build_middleware(klass, args, block)) end alias_method :insert_before, :insert @@ -104,26 +88,46 @@ module ActionDispatch end def delete(target) - middlewares.delete target + target = get_class target + middlewares.delete_if { |m| m.klass == target } end - def use(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - middlewares.push(middleware) + def use(klass, *args, &block) + middlewares.push(build_middleware(klass, args, block)) end - def build(app = nil, &block) - app ||= block - raise "MiddlewareStack#build requires an app" unless app + def build(app = Proc.new) middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) } end - protected + private def assert_index(index, where) - i = index.is_a?(Integer) ? index : middlewares.index(index) + index = get_class index + i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index } raise "No such middleware to insert #{where}: #{index.inspect}" unless i i end + + def get_class(klass) + if klass.is_a?(String) || klass.is_a?(Symbol) + classcache = ActiveSupport::Dependencies::Reference + converted_klass = classcache[klass.to_s] + ActiveSupport::Deprecation.warn <<-eowarn +Passing strings or symbols to the middleware builder is deprecated, please change +them to actual class references. For example: + + "#{klass}" => #{converted_klass} + + eowarn + converted_klass + else + klass + end + end + + def build_middleware(klass, args, block) + Middleware.new(get_class(klass), args, block) + end end end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index bc5ef1abc9..41c220236a 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -3,8 +3,8 @@ require 'active_support/core_ext/uri' module ActionDispatch # This middleware returns a file's contents from disk in the body response. - # When initialized, it can accept an optional 'Cache-Control' header, which - # will be set when a response containing a file's contents is delivered. + # When initialized, it can accept optional HTTP headers, which will be set + # when a response containing a file's contents is delivered. # # This middleware will render the file specified in `env["PATH_INFO"]` # where the base path is in the +root+ directory. For example, if the +root+ @@ -13,14 +13,12 @@ module ActionDispatch # located at `public/assets/application.js` if the file exists. If the file # does not exist, a 404 "File not Found" response will be returned. class FileHandler - def initialize(root, cache_control) + def initialize(root, index: 'index', headers: {}) @root = root.chomp('/') - @compiled_root = /^#{Regexp.escape(root)}/ - headers = cache_control && { 'Cache-Control' => cache_control } - @file_server = ::Rack::File.new(@root, headers) + @file_server = ::Rack::File.new(@root, headers) + @index = index end - # Takes a path to a file. If the file is found, has valid encoding, and has # correct read permissions, the return value is a URI-escaped string # representing the filename. Otherwise, false is returned. @@ -28,14 +26,14 @@ module ActionDispatch # Used by the `Static` class to check the existence of a valid file # in the server's `public/` directory (see Static#call). def match?(path) - path = URI.parser.unescape(path) - return false unless path.valid_encoding? + path = ::Rack::Utils.unescape_path path + return false unless valid_path?(path) path = Rack::Utils.clean_path_info path - paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"] + paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p.force_encoding('UTF-8')) + path = File.join(@root, p.force_encoding('UTF-8'.freeze)) begin File.file?(path) && File.readable?(path) rescue SystemCallError @@ -43,31 +41,35 @@ module ActionDispatch end } - return ::Rack::Utils.escape(match) + return ::Rack::Utils.escape_path(match) end end def call(env) - path = env['PATH_INFO'] + serve ActionDispatch::Request.new env + end + + def serve(request) + path = request.path_info gzip_path = gzip_file_path(path) - if gzip_path && gzip_encoding_accepted?(env) - env['PATH_INFO'] = gzip_path - status, headers, body = @file_server.call(env) + if gzip_path && gzip_encoding_accepted?(request) + request.path_info = gzip_path + status, headers, body = @file_server.call(request.env) if status == 304 return [status, headers, body] end headers['Content-Encoding'] = 'gzip' headers['Content-Type'] = content_type(path) else - status, headers, body = @file_server.call(env) + status, headers, body = @file_server.call(request.env) end headers['Vary'] = 'Accept-Encoding' if gzip_path return [status, headers, body] ensure - env['PATH_INFO'] = path + request.path_info = path end private @@ -76,22 +78,26 @@ module ActionDispatch end def content_type(path) - ::Rack::Mime.mime_type(::File.extname(path), 'text/plain') + ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze) end - def gzip_encoding_accepted?(env) - env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i + def gzip_encoding_accepted?(request) + request.accept_encoding =~ /\bgzip\b/i end def gzip_file_path(path) can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/ gzip_path = "#{path}.gz" - if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path))) + if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path))) gzip_path else false end end + + def valid_path?(path) + path.valid_encoding? && !path.include?("\0") + end end # This middleware will attempt to return the contents of a file's body from @@ -104,22 +110,30 @@ module ActionDispatch # produce a directory traversal using this middleware. Only 'GET' and 'HEAD' # requests will result in a file being returned. class Static - def initialize(app, path, cache_control=nil) + def initialize(app, path, deprecated_cache_control = :not_set, index: 'index', headers: {}) + if deprecated_cache_control != :not_set + ActiveSupport::Deprecation.warn("The `cache_control` argument is deprecated," \ + "replaced by `headers: { 'Cache-Control' => #{deprecated_cache_control} }`, " \ + " and will be removed in Rails 5.1.") + headers['Cache-Control'.freeze] = deprecated_cache_control + end + @app = app - @file_handler = FileHandler.new(path, cache_control) + @file_handler = FileHandler.new(path, index: index, headers: headers) end def call(env) - case env['REQUEST_METHOD'] - when 'GET', 'HEAD' - path = env['PATH_INFO'].chomp('/') + req = ActionDispatch::Request.new env + + if req.get? || req.head? + path = req.path_info.chomp('/'.freeze) if match = @file_handler.match?(path) - env["PATH_INFO"] = match - return @file_handler.call(env) + req.path_info = match + return @file_handler.serve(req) end end - @app.call(env) + @app.call(req.env) end end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb index e7b913bbe4..e7b913bbe4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb new file mode 100644 index 0000000000..23a9c7ba3f --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb @@ -0,0 +1,8 @@ +<% @source_extracts.first(3).each do |source_extract| %> +<% if source_extract[:code] %> +Extracted source (around line #<%= source_extract[:line_number] %>): + +<% source_extract[:code].each do |line, source| -%> +<%= line == source_extract[:line_number] ? "*#{line}" : "##{line}" -%> <%= source -%><% end -%> +<% end %> +<% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb index c1e8b6cae3..5060da9369 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb @@ -1,6 +1,6 @@ <header> <h1> - <%= @exception.original_exception.class.to_s %> in + <%= @exception.cause.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> </h1> </header> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb index 77bcd26726..78d52acd96 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb @@ -1,4 +1,4 @@ -<%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> +<%= @exception.cause.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised: <%= @exception.message %> |