aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/middleware
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware')
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb37
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb623
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb180
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb149
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb292
-rw-r--r--actionpack/lib/action_dispatch/middleware/load_interlock.rb21
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb44
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb55
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb96
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb183
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb40
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb102
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb52
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb129
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb26
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb60
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb129
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb124
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb135
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb22
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb23
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb27
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb52
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb160
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb11
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb32
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb11
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb20
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb7
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb196
38 files changed, 3108 insertions, 0 deletions
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
new file mode 100644
index 0000000000..f80df78582
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -0,0 +1,37 @@
+
+module ActionDispatch
+ # Provides callbacks to be executed before and after dispatching the request.
+ class Callbacks
+ include ActiveSupport::Callbacks
+
+ define_callbacks :call
+
+ class << self
+ delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
+
+ def before(*args, &block)
+ set_callback(:call, :before, *args, &block)
+ end
+
+ def after(*args, &block)
+ set_callback(:call, :after, *args, &block)
+ end
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ error = nil
+ result = run_callbacks :call do
+ begin
+ @app.call(env)
+ rescue => error
+ end
+ end
+ raise error if error
+ result
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
new file mode 100644
index 0000000000..3477aa8b29
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -0,0 +1,623 @@
+require 'active_support/core_ext/hash/keys'
+require 'active_support/key_generator'
+require 'active_support/message_verifier'
+require 'active_support/json'
+
+module ActionDispatch
+ class Request
+ def cookie_jar
+ 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.
+ #
+ # The cookies being read are the ones received along with the request, the cookies
+ # being written will be sent out with the response. Reading a cookie does not get
+ # the cookie object itself back, just the value it holds.
+ #
+ # Examples of writing:
+ #
+ # # Sets a simple session cookie.
+ # # This cookie will be deleted when the user's browser is closed.
+ # cookies[:user_name] = "david"
+ #
+ # # Cookie values are String based. Other data types need to be serialized.
+ # 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 }
+ #
+ # # 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
+ #
+ # # Sets a "permanent" cookie (which expires in 20 years from now).
+ # cookies.permanent[:login] = "XJ-122"
+ #
+ # # You can also chain these methods:
+ # cookies.permanent.signed[:login] = "XJ-122"
+ #
+ # Examples of reading:
+ #
+ # cookies[:user_name] # => "david"
+ # cookies.size # => 2
+ # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
+ # cookies.signed[:login] # => "XJ-122"
+ # cookies.encrypted[:discount] # => 45
+ #
+ # Example for deleting:
+ #
+ # cookies.delete :user_name
+ #
+ # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
+ #
+ # cookies[:name] = {
+ # value: 'a yummy cookie',
+ # expires: 1.year.from_now,
+ # domain: 'domain.com'
+ # }
+ #
+ # cookies.delete(:name, domain: 'domain.com')
+ #
+ # The option symbols for setting cookies are:
+ #
+ # * <tt>:value</tt> - The cookie's value.
+ # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
+ # of the application.
+ # * <tt>:domain</tt> - The domain for which this cookie applies so you can
+ # restrict to the domain level. If you use a schema like www.example.com
+ # and want to share session with user.example.com set <tt>:domain</tt>
+ # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
+ # <tt>:all</tt> or <tt>Array</tt> again when deleting cookies.
+ #
+ # domain: nil # Does not set cookie domain. (default)
+ # domain: :all # Allow the cookie for the top most level
+ # # domain and subdomains.
+ # domain: %w(.example.com .example.org) # Allow the cookie
+ # # for concrete domain names.
+ #
+ # * <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.
+ # * <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
+ # only HTTP. Defaults to +false+.
+ class Cookies
+ HTTP_HEADER = "Set-Cookie".freeze
+ GENERATOR_KEY = "action_dispatch.key_generator".freeze
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
+ 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 can typically store 4096 bytes.
+ MAX_COOKIE_SIZE = 4096
+
+ # Raised when storing more than 4K of session data.
+ CookieOverflow = Class.new StandardError
+
+ # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed
+ module ChainedCookieJars
+ # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
+ #
+ # cookies.permanent[:prefers_open_id] = true
+ # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
+ #
+ # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
+ #
+ # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
+ #
+ # 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)
+ end
+
+ # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
+ # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
+ # cookie was tampered with by the user (or a 3rd party), nil will be returned.
+ #
+ # If +secrets.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+.
+ #
+ # Example:
+ #
+ # cookies.signed[:discount] = 45
+ # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
+ #
+ # cookies.signed[:discount] # => 45
+ def signed
+ @signed ||=
+ if upgrade_legacy_signed_cookies?
+ UpgradeLegacySignedCookieJar.new(self)
+ else
+ SignedCookieJar.new(self)
+ end
+ 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,
+ # 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+.
+ #
+ # Example:
+ #
+ # cookies.encrypted[:discount] = 45
+ # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
+ #
+ # cookies.encrypted[:discount] # => 45
+ def encrypted
+ @encrypted ||=
+ if upgrade_legacy_signed_cookies?
+ UpgradeLegacyEncryptedCookieJar.new(self)
+ else
+ EncryptedCookieJar.new(self)
+ end
+ end
+
+ # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
+ # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
+ def signed_or_encrypted
+ @signed_or_encrypted ||=
+ 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
+ # 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 }
+ end
+ 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:
+ include Enumerable, ChainedCookieJars
+
+ # This regular expression is used to split the levels of a domain.
+ # The top level domain can be any string without a period or
+ # **.**, ***.** style TLDs like co.uk or com.au
+ #
+ # www.example.co.uk gives:
+ # $& => example.co.uk
+ #
+ # example.com gives:
+ # $& => example.com
+ #
+ # lots.of.subdomains.example.local gives:
+ # $& => example.local
+ DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
+
+ def self.build(req, cookies)
+ new(req).tap do |hash|
+ hash.update(cookies)
+ end
+ end
+
+ attr_reader :request
+
+ def initialize(request)
+ @set_cookies = {}
+ @delete_cookies = {}
+ @request = request
+ @cookies = {}
+ @committed = false
+ end
+
+ def committed?; @committed; end
+
+ def commit!
+ @committed = true
+ @set_cookies.freeze
+ @delete_cookies.freeze
+ end
+
+ def each(&block)
+ @cookies.each(&block)
+ end
+
+ # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
+ def [](name)
+ @cookies[name.to_s]
+ end
+
+ def fetch(name, *args, &block)
+ @cookies.fetch(name.to_s, *args, &block)
+ end
+
+ def key?(name)
+ @cookies.key?(name.to_s)
+ end
+ alias :has_key? :key?
+
+ def update(other_hash)
+ @cookies.update other_hash.stringify_keys
+ 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] ||= "/"
+
+ if options[:domain] == :all || options[:domain] == 'all'
+ # if there is a provided tld length then we use it otherwise default domain regexp
+ domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
+
+ # if host is not ip and matches domain regexp
+ # (ip confirms to domain regexp so we explicitly check for ip)
+ options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
+ ".#{$&}"
+ end
+ elsif options[:domain].is_a? Array
+ # if host matches one of the supplied domains without a dot in front of it
+ options[:domain] = options[:domain].find {|domain| request.host.include? domain.sub(/^\./, '') }
+ end
+ end
+
+ # 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!
+ value = options[:value]
+ else
+ value = options
+ options = { :value => value }
+ end
+
+ handle_options(options)
+
+ if @cookies[name.to_s] != value or options[:expires]
+ @cookies[name.to_s] = value
+ @set_cookies[name.to_s] = options
+ @delete_cookies.delete(name.to_s)
+ end
+
+ value
+ end
+
+ # Removes the cookie on the client machine by setting the value to an empty string
+ # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
+ # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
+ def delete(name, options = {})
+ return unless @cookies.has_key? name.to_s
+
+ options.symbolize_keys!
+ handle_options(options)
+
+ value = @cookies.delete(name.to_s)
+ @delete_cookies[name.to_s] = options
+ value
+ end
+
+ # Whether the given cookie is to be deleted by this CookieJar.
+ # Like <tt>[]=</tt>, you can pass in an options hash to test if a
+ # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
+ def deleted?(name, options = {})
+ options.symbolize_keys!
+ handle_options(options)
+ @delete_cookies[name.to_s] == options
+ end
+
+ # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
+ def clear(options = {})
+ @cookies.each_key{ |k| delete(k, options) }
+ end
+
+ def write(headers)
+ 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 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 AbstractCookieJar # :nodoc:
+ include ChainedCookieJars
+
+ def initialize(parent_jar)
+ @parent_jar = parent_jar
+ end
+
+ def [](name)
+ 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 }
+ end
+
+ 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:
+ def self.load(value)
+ ActiveSupport::JSON.decode(value)
+ end
+
+ def self.dump(value)
+ ActiveSupport::JSON.encode(value)
+ end
+ end
+
+ module SerializedCookieJars # :nodoc:
+ MARSHAL_SIGNATURE = "\x04\x08".freeze
+
+ protected
+ def needs_migration?(value)
+ request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
+ end
+
+ def serialize(value)
+ serializer.dump(value)
+ end
+
+ def deserialize(name, value)
+ if value
+ if needs_migration?(value)
+ Marshal.load(value).tap do |v|
+ self[name] = { value: v }
+ end
+ else
+ serializer.load(value)
+ end
+ end
+ end
+
+ def serializer
+ serializer = request.cookies_serializer || :marshal
+ case serializer
+ when :marshal
+ Marshal
+ when :json, :hybrid
+ JsonSerializer
+ else
+ serializer
+ end
+ end
+
+ def digest
+ request.cookies_digest || 'SHA1'
+ end
+
+ def key_generator
+ request.key_generator
+ end
+ end
+
+ class SignedCookieJar < 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)
+ end
+
+ private
+ def parse(name, signed_message)
+ deserialize name, @verifier.verified(signed_message)
+ end
+
+ def commit(options)
+ options[:value] = @verifier.generate(serialize(options[:value]))
+
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
+ end
+ end
+
+ # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
+ # secrets.secret_token and secrets.secret_key_base are both set. It reads
+ # legacy cookies signed with the old dummy key generator and signs and
+ # re-saves them using the new key generator to provide a smooth upgrade path.
+ class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
+ include VerifyAndUpgradeLegacySignedMessage
+ end
+
+ class EncryptedCookieJar < AbstractCookieJar # :nodoc:
+ 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."
+ end
+
+ secret = key_generator.generate_key(request.encrypted_cookie_salt || '')
+ sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '')
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+ end
+
+ private
+ 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
+ # 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 initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new env
+
+ status, headers, body = @app.call(env)
+
+ 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)
+ headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
+ end
+ end
+ end
+
+ [status, headers, body]
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
new file mode 100644
index 0000000000..b55c937e0c
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -0,0 +1,180 @@
+require 'action_dispatch/http/request'
+require 'action_dispatch/middleware/exception_wrapper'
+require 'action_dispatch/routing/inspector'
+require 'action_view'
+require 'action_view/base'
+
+require 'pp'
+
+module ActionDispatch
+ # This middleware is responsible for logging exceptions and
+ # showing a debugging page in case the request is local.
+ class DebugExceptions
+ RESCUES_TEMPLATE_PATH = File.expand_path('../templates', __FILE__)
+
+ class DebugView < ActionView::Base
+ def debug_params(params)
+ clean_params = params.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ if clean_params.empty?
+ 'None'
+ else
+ PP.pp(clean_params, "", 200)
+ end
+ end
+
+ def debug_headers(headers)
+ if headers.present?
+ headers.inspect.gsub(',', ",\n")
+ else
+ 'None'
+ end
+ end
+
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ end
+ end
+
+ 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'
+ body.close if body.respond_to?(:close)
+ raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
+ end
+
+ response
+ rescue Exception => exception
+ raise exception unless request.show_exceptions?
+ render_exception(request, exception)
+ end
+
+ private
+
+ def render_exception(request, exception)
+ backtrace_cleaner = request.get_header('action_dispatch.backtrace_cleaner')
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
+ log_error(request, wrapper)
+
+ if request.get_header('action_dispatch.show_detailed_exceptions')
+ case @response_format
+ when :api
+ render_for_api_application(request, wrapper)
+ when :default
+ render_for_default_application(request, wrapper)
+ end
+ else
+ raise exception
+ end
+ end
+
+ def render_for_default_application(request, wrapper)
+ template = create_template(request, wrapper)
+ file = "rescues/#{wrapper.rescue_template}"
+
+ if request.xhr?
+ body = template.render(template: file, layout: false, formats: [:text])
+ format = "text/plain"
+ else
+ body = template.render(template: file, layout: 'rescues/layout')
+ format = "text/html"
+ 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(request, wrapper)
+ logger = logger(request)
+ return unless logger
+
+ exception = wrapper.exception
+
+ trace = wrapper.application_trace
+ trace = wrapper.framework_trace if trace.empty?
+
+ ActiveSupport::Deprecation.silence do
+ message = "\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << trace.join("\n ")
+ logger.fatal("#{message}\n\n")
+ end
+ end
+
+ def logger(request)
+ request.logger || stderr_logger
+ end
+
+ def stderr_logger
+ @stderr_logger ||= ActiveSupport::Logger.new($stderr)
+ end
+
+ def routes_inspector(exception)
+ if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
+ ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
new file mode 100644
index 0000000000..3b61824cc9
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -0,0 +1,149 @@
+require 'action_controller/metal/exceptions'
+require 'active_support/core_ext/module/attribute_accessors'
+require 'rack/utils'
+
+module ActionDispatch
+ class ExceptionWrapper
+ cattr_accessor :rescue_responses
+ @@rescue_responses = Hash.new(:internal_server_error)
+ @@rescue_responses.merge!(
+ 'ActionController::RoutingError' => :not_found,
+ 'AbstractController::ActionNotFound' => :not_found,
+ 'ActionController::MethodNotAllowed' => :method_not_allowed,
+ 'ActionController::UnknownHttpMethod' => :method_not_allowed,
+ 'ActionController::NotImplemented' => :not_implemented,
+ 'ActionController::UnknownFormat' => :not_acceptable,
+ 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
+ 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,
+ 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
+ 'ActionController::BadRequest' => :bad_request,
+ 'ActionController::ParameterMissing' => :bad_request,
+ 'Rack::Utils::ParameterTypeError' => :bad_request,
+ 'Rack::Utils::InvalidParameterError' => :bad_request
+ )
+
+ cattr_accessor :rescue_templates
+ @@rescue_templates = Hash.new('diagnostics')
+ @@rescue_templates.merge!(
+ 'ActionView::MissingTemplate' => 'missing_template',
+ 'ActionController::RoutingError' => 'routing_error',
+ 'AbstractController::ActionNotFound' => 'unknown_action',
+ 'ActionView::Template::Error' => 'template_error'
+ )
+
+ attr_reader :backtrace_cleaner, :exception, :line_number, :file
+
+ def initialize(backtrace_cleaner, exception)
+ @backtrace_cleaner = backtrace_cleaner
+ @exception = original_exception(exception)
+
+ expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
+ end
+
+ def rescue_template
+ @@rescue_templates[@exception.class.name]
+ end
+
+ def status_code
+ self.class.status_code_for_exception(@exception.class.name)
+ end
+
+ def application_trace
+ clean_backtrace(:silent)
+ end
+
+ def framework_trace
+ clean_backtrace(:noise)
+ end
+
+ def full_trace
+ clean_backtrace(:all)
+ end
+
+ def traces
+ application_trace_with_ids = []
+ framework_trace_with_ids = []
+ full_trace_with_ids = []
+
+ full_trace.each_with_index do |trace, idx|
+ trace_with_id = { id: idx, trace: trace }
+
+ if application_trace.include?(trace)
+ application_trace_with_ids << trace_with_id
+ else
+ framework_trace_with_ids << trace_with_id
+ end
+
+ full_trace_with_ids << trace_with_id
+ end
+
+ {
+ "Application Trace" => application_trace_with_ids,
+ "Framework Trace" => framework_trace_with_ids,
+ "Full Trace" => full_trace_with_ids
+ }
+ end
+
+ def self.status_code_for_exception(class_name)
+ Rack::Utils.status_code(@@rescue_responses[class_name])
+ end
+
+ def source_extracts
+ backtrace.map do |trace|
+ file, line_number = extract_file_and_line_number(trace)
+
+ {
+ code: source_fragment(file, line_number),
+ line_number: line_number
+ }
+ end
+ end
+
+ private
+
+ def backtrace
+ Array(@exception.backtrace)
+ end
+
+ def original_exception(exception)
+ if @@rescue_responses.has_key?(exception.cause.class.name)
+ exception.cause
+ else
+ exception
+ end
+ end
+
+ def clean_backtrace(*args)
+ if backtrace_cleaner
+ backtrace_cleaner.clean(backtrace, *args)
+ else
+ backtrace
+ end
+ end
+
+ def source_fragment(path, line)
+ return unless Rails.respond_to?(:root) && Rails.root
+ full_path = Rails.root.join(path)
+ if File.exist?(full_path)
+ File.open(full_path, "r") do |file|
+ start = [line - 3, 0].max
+ lines = file.each_line.drop(start).take(6)
+ Hash[*(start+1..(lines.count+start)).zip(lines).flatten]
+ end
+ end
+ end
+
+ def extract_file_and_line_number(trace)
+ # Split by the first colon followed by some digits, which works for both
+ # Windows and Unix path styles.
+ file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
+ [file, line.to_i]
+ end
+
+ def expand_backtrace
+ @exception.backtrace.unshift(
+ @exception.to_s.split("\n")
+ ).flatten!
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
new file mode 100644
index 0000000000..c51dcd542a
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -0,0 +1,292 @@
+require 'active_support/core_ext/hash/keys'
+
+module ActionDispatch
+ # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
+ # 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
+ # then expose the flash to its template. Actually, that exposure is automatically done.
+ #
+ # class PostsController < ActionController::Base
+ # def create
+ # # save post
+ # flash[:notice] = "Post successfully created"
+ # redirect_to @post
+ # end
+ #
+ # def show
+ # # doesn't need to assign the flash notice to the template, that's done automatically
+ # end
+ # end
+ #
+ # show.html.erb
+ # <% if flash[:notice] %>
+ # <div class="notice"><%= flash[:notice] %></div>
+ # <% end %>
+ #
+ # Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
+ #
+ # flash.alert = "You must be logged in"
+ # flash.notice = "Post successfully created"
+ #
+ # This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
+ # non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
+ # use sanitize helper.
+ #
+ # Just remember: They'll be gone by the time the next action has been performed.
+ #
+ # See docs on the FlashHash class for more details about the flash.
+ 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
+
+ def initialize(flash)
+ @flash = flash
+ end
+
+ def []=(k, v)
+ k = k.to_s
+ @flash[k] = v
+ @flash.discard(k)
+ v
+ end
+
+ def [](k)
+ @flash[k.to_s]
+ end
+
+ # Convenience accessor for <tt>flash.now[:alert]=</tt>.
+ def alert=(message)
+ self[:alert] = message
+ end
+
+ # Convenience accessor for <tt>flash.now[:notice]=</tt>.
+ def notice=(message)
+ self[:notice] = message
+ end
+ end
+
+ class FlashHash
+ include Enumerable
+
+ def self.from_session_value(value) #:nodoc:
+ case value
+ when FlashHash # Rails 3.1, 3.2
+ flashes = value.instance_variable_get(:@flashes)
+ if discard = value.instance_variable_get(:@used)
+ flashes.except!(*discard)
+ end
+ new(flashes, flashes.keys)
+ when Hash # Rails 4.0
+ flashes = value['flashes']
+ if discard = value['discard']
+ flashes.except!(*discard)
+ end
+ new(flashes, flashes.keys)
+ else
+ new
+ end
+ end
+
+ # Builds a hash containing the flashes to keep for the next request.
+ # If there are none to keep, returns nil.
+ def to_session_value #:nodoc:
+ flashes_to_keep = @flashes.except(*@discard)
+ return nil if flashes_to_keep.empty?
+ {'flashes' => flashes_to_keep}
+ end
+
+ def initialize(flashes = {}, discard = []) #:nodoc:
+ @discard = Set.new(stringify_array(discard))
+ @flashes = flashes.stringify_keys
+ @now = nil
+ end
+
+ def initialize_copy(other)
+ if other.now_is_loaded?
+ @now = other.now.dup
+ @now.flash = self
+ end
+ super
+ end
+
+ def []=(k, v)
+ k = k.to_s
+ @discard.delete k
+ @flashes[k] = v
+ end
+
+ def [](k)
+ @flashes[k.to_s]
+ end
+
+ def update(h) #:nodoc:
+ @discard.subtract stringify_array(h.keys)
+ @flashes.update h.stringify_keys
+ self
+ end
+
+ def keys
+ @flashes.keys
+ end
+
+ def key?(name)
+ @flashes.key? name.to_s
+ end
+
+ def delete(key)
+ key = key.to_s
+ @discard.delete key
+ @flashes.delete key
+ self
+ end
+
+ def to_hash
+ @flashes.dup
+ end
+
+ def empty?
+ @flashes.empty?
+ end
+
+ def clear
+ @discard.clear
+ @flashes.clear
+ end
+
+ def each(&block)
+ @flashes.each(&block)
+ end
+
+ alias :merge! :update
+
+ def replace(h) #:nodoc:
+ @discard.clear
+ @flashes.replace h.stringify_keys
+ self
+ end
+
+ # Sets a flash that will not be available to the next action, only to the current.
+ #
+ # flash.now[:message] = "Hello current action"
+ #
+ # This method enables you to use the flash as a central messaging system in your app.
+ # When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
+ # When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
+ # vanish when the current action is done.
+ #
+ # Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
+ #
+ # Also, brings two convenience accessors:
+ #
+ # flash.now.alert = "Beware now!"
+ # # Equivalent to flash.now[:alert] = "Beware now!"
+ #
+ # flash.now.notice = "Good luck now!"
+ # # Equivalent to flash.now[:notice] = "Good luck now!"
+ def now
+ @now ||= FlashNow.new(self)
+ end
+
+ # Keeps either the entire current flash or a specific flash entry available for the next action:
+ #
+ # flash.keep # keeps the entire flash
+ # flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
+ def keep(k = nil)
+ k = k.to_s if k
+ @discard.subtract Array(k || keys)
+ k ? self[k] : self
+ end
+
+ # Marks the entire flash or a single flash entry to be discarded by the end of the current action:
+ #
+ # flash.discard # discard the entire flash at the end of the current action
+ # flash.discard(:warning) # discard only the "warning" entry at the end of the current action
+ def discard(k = nil)
+ k = k.to_s if k
+ @discard.merge Array(k || keys)
+ k ? self[k] : self
+ end
+
+ # Mark for removal entries that were kept, and delete unkept ones.
+ #
+ # This method is called automatically by filters, so you generally don't need to care about it.
+ def sweep #:nodoc:
+ @discard.each { |k| @flashes.delete k }
+ @discard.replace @flashes.keys
+ end
+
+ # Convenience accessor for <tt>flash[:alert]</tt>.
+ def alert
+ self[:alert]
+ end
+
+ # Convenience accessor for <tt>flash[:alert]=</tt>.
+ def alert=(message)
+ self[:alert] = message
+ end
+
+ # Convenience accessor for <tt>flash[:notice]</tt>.
+ def notice
+ self[:notice]
+ end
+
+ # Convenience accessor for <tt>flash[:notice]=</tt>.
+ def notice=(message)
+ self[:notice] = message
+ end
+
+ protected
+ def now_is_loaded?
+ @now
+ end
+
+ def stringify_array(array)
+ array.map do |item|
+ item.kind_of?(Symbol) ? item.to_s : item
+ end
+ end
+ end
+
+ def self.new(app) app; 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
new file mode 100644
index 0000000000..c2a4f46e67
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb
@@ -0,0 +1,44 @@
+require 'action_dispatch/http/request'
+
+module ActionDispatch
+ # ActionDispatch::ParamsParser works for all the requests having any Content-Length
+ # (like POST). It takes raw data from the request and puts it through the parser
+ # that is picked based on Content-Type header.
+ #
+ # In case of any error while parsing data ParamsParser::ParseError is raised.
+ class ParamsParser
+ # Raised when raw data from the request cannot be parsed by the parser
+ # defined for request's content mime type.
+ class ParseError < StandardError
+
+ def initialize(message = nil, original_exception = nil)
+ if message
+ ActiveSupport::Deprecation.warn("Passing #message is deprecated and has no effect. " \
+ "#{self.class} will automatically capture the message " \
+ "of the original exception.", caller)
+ end
+
+ if original_exception
+ ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
+ "Exceptions will automatically capture the original exception.", caller)
+ end
+
+ super($!.message)
+ end
+
+ def original_exception
+ ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
+ cause
+ end
+ end
+
+ # Create a new +ParamsParser+ middleware instance.
+ #
+ # The +parsers+ argument can take Hash of parsers where key is identifying
+ # content mime type, and value is a lambda that is going to process data.
+ def self.new(app, parsers = {})
+ 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
new file mode 100644
index 0000000000..0f27984550
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -0,0 +1,55 @@
+module ActionDispatch
+ # When called, this middleware renders an error page. By default if an HTML
+ # response is expected it will render static error pages from the `/public`
+ # directory. For example when this middleware receives a 500 response it will
+ # render the template found in `/public/500.html`.
+ # If an internationalized locale is set, this middleware will attempt to render
+ # the template in `/public/500.<locale>.html`. If an internationalized template
+ # is not found it will fall back on `/public/500.html`.
+ #
+ # When a request with a content type other than HTML is made, this middleware
+ # will attempt to convert error information into the appropriate response type.
+ class PublicExceptions
+ attr_accessor :public_path
+
+ def initialize(public_path)
+ @public_path = public_path
+ end
+
+ def call(env)
+ 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]) }
+
+ render(status, content_type, body)
+ end
+
+ private
+
+ def render(status, content_type, body)
+ format = "to_#{content_type.to_sym}" if content_type
+ if format && body.respond_to?(format)
+ render_format(status, content_type, body.public_send(format))
+ else
+ render_html(status)
+ end
+ end
+
+ def render_format(status, content_type, body)
+ [status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
+ 'Content-Length' => body.bytesize.to_s}, [body]]
+ end
+
+ def render_html(status)
+ path = "#{public_path}/#{status}.#{I18n.locale}.html"
+ path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
+
+ if found || File.exist?(path)
+ render_format(status, 'text/html', File.read(path))
+ else
+ [404, { "X-Cascade" => "pass" }, []]
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
new file mode 100644
index 0000000000..af9a29eb07
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -0,0 +1,96 @@
+module ActionDispatch
+ # ActionDispatch::Reloader provides prepare and cleanup callbacks,
+ # intended to assist with code reloading during development.
+ #
+ # Prepare callbacks are run before each request, and cleanup callbacks
+ # after each request. In this respect they are analogs of ActionDispatch::Callback's
+ # before and after callbacks. However, cleanup callbacks are not called until the
+ # request is fully complete -- that is, after #close has been called on
+ # the response body. This is important for streaming responses such as the
+ # following:
+ #
+ # self.response_body = -> (response, output) do
+ # # code here which refers to application models
+ # end
+ #
+ # Cleanup callbacks will not be called until after the response_body lambda
+ # is evaluated, ensuring that it can refer to application models and other
+ # classes before they are unloaded.
+ #
+ # By default, ActionDispatch::Reloader is included in the middleware stack
+ # only in the development environment; specifically, when +config.cache_classes+
+ # is false. Callbacks may be registered even when it is not included in the
+ # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt>
+ # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually.
+ #
+ class Reloader
+ include ActiveSupport::Callbacks
+ include ActiveSupport::Deprecation::Reporting
+
+ define_callbacks :prepare
+ define_callbacks :cleanup
+
+ # Add a prepare callback. Prepare callbacks are run before each request, prior
+ # to ActionDispatch::Callback's before callbacks.
+ def self.to_prepare(*args, &block)
+ unless block_given?
+ warn "to_prepare without a block is deprecated. Please use a block"
+ end
+ set_callback(:prepare, *args, &block)
+ end
+
+ # Add a cleanup callback. Cleanup callbacks are run after each request is
+ # complete (after #close is called on the response body).
+ def self.to_cleanup(*args, &block)
+ unless block_given?
+ warn "to_cleanup without a block is deprecated. Please use a block"
+ end
+ set_callback(:cleanup, *args, &block)
+ end
+
+ # Execute all prepare callbacks.
+ def self.prepare!
+ new(nil).prepare!
+ end
+
+ # Execute all cleanup callbacks.
+ def self.cleanup!
+ new(nil).cleanup!
+ end
+
+ def initialize(app, condition=nil)
+ @app = app
+ @condition = condition || lambda { true }
+ @validated = true
+ end
+
+ def call(env)
+ @validated = @condition.call
+ prepare!
+
+ response = @app.call(env)
+ response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! }
+
+ response
+ rescue Exception
+ cleanup!
+ raise
+ end
+
+ def prepare! #:nodoc:
+ run_callbacks :prepare if validated?
+ end
+
+ def cleanup! #:nodoc:
+ run_callbacks :cleanup if validated?
+ ensure
+ @validated = true
+ end
+
+ private
+
+ def validated? #:nodoc:
+ @validated
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
new file mode 100644
index 0000000000..31b75498b6
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -0,0 +1,183 @@
+require 'ipaddr'
+
+module ActionDispatch
+ # This middleware calculates the IP address of the remote client that is
+ # making the request. It does this by checking various headers that could
+ # contain the address, and then picking the last-set address that is not
+ # on the list of trusted IPs. This follows the precedent set by e.g.
+ # {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
+ # with {reasoning explained at length}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
+ # by @gingerlime. A more detailed explanation of the algorithm is given
+ # at GetIp#calculate_ip.
+ #
+ # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
+ # requires. Some Rack servers simply drop preceding headers, and only report
+ # the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
+ # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
+ # then you should test your Rack server to make sure your data is good.
+ #
+ # IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING.
+ # This middleware assumes that there is at least one proxy sitting around
+ # and setting headers with the client's remote IP address. If you don't use
+ # a proxy, because you are hosted on e.g. Heroku without SSL, any client can
+ # claim to have any IP address by setting the X-Forwarded-For header. If you
+ # care about that, then you need to explicitly drop or ignore those headers
+ # sometime before this middleware runs.
+ class RemoteIp
+ class IpSpoofAttackError < StandardError; end
+
+ # The default trusted IPs list simply includes IP addresses that are
+ # guaranteed by the IP specification to be private addresses. Those will
+ # not be the ultimate client IP in production, and so are discarded. See
+ # http://en.wikipedia.org/wiki/Private_network for details.
+ TRUSTED_PROXIES = [
+ "127.0.0.1", # localhost IPv4
+ "::1", # localhost IPv6
+ "fc00::/7", # private IPv6 range fc00::/7
+ "10.0.0.0/8", # private IPv4 range 10.x.x.x
+ "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
+ "192.168.0.0/16", # private IPv4 range 192.168.x.x
+ ].map { |proxy| IPAddr.new(proxy) }
+
+ attr_reader :check_ip, :proxies
+
+ # Create a new +RemoteIp+ middleware instance.
+ #
+ # 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
+ # incorrect or confusing way (like AWS ELB).
+ #
+ # The +custom_proxies+ argument can take an Array of string, IPAddr, or
+ # Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
+ # single string, IPAddr, or Regexp object is provided, it will be used in
+ # addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
+ # want in the middle (or at the beginning) of the X-Forwarded-For list,
+ # 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, ip_spoofing_check = true, custom_proxies = nil)
+ @app = app
+ @check_ip = ip_spoofing_check
+ @proxies = if custom_proxies.blank?
+ TRUSTED_PROXIES
+ elsif custom_proxies.respond_to?(:any?)
+ custom_proxies
+ else
+ Array(custom_proxies) + TRUSTED_PROXIES
+ end
+ end
+
+ # Since the IP address may not be needed, we store the object here
+ # without calculating the IP to keep from slowing down the majority of
+ # 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)
+ 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(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
+ # likely to be the address of the actual remote client making this
+ # request.
+ #
+ # REMOTE_ADDR will be correct if the request is made directly against the
+ # Ruby process, on e.g. Heroku. When the request is proxied by another
+ # server like HAProxy or NGINX, the IP address that made the original
+ # request will be put in an X-Forwarded-For header. If there are multiple
+ # proxies, that header may contain a list of IPs. Other proxy services
+ # set the Client-Ip header instead, so we check that too.
+ #
+ # As discussed in {this post about Rails IP Spoofing}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
+ # while the first IP in the list is likely to be the "originating" IP,
+ # it could also have been set by the client maliciously.
+ #
+ # In order to find the first address that is (probably) accurate, we
+ # take the list of IPs, remove known and trusted proxies, and then take
+ # 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(@req.remote_addr).last
+
+ # Could be a CSV list and/or repeated headers that were concatenated.
+ 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 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=#{@req.client_ip.inspect} " +
+ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
+ end
+
+ # We assume these things about the IP headers:
+ #
+ # - X-Forwarded-For will be a list of IPs, one per proxy, or blank
+ # - Client-Ip is propagated from the outermost proxy, or is blank
+ # - REMOTE_ADDR will be the IP that made the request to Rack
+ ips = [forwarded_ips, client_ips, remote_addr].flatten.compact
+
+ # If every single IP option is in the trusted list, just return REMOTE_ADDR
+ filter_proxies(ips).first || remote_addr
+ end
+
+ # Memoizes the value returned by #calculate_ip and returns it for
+ # ActionDispatch::Request to use.
+ def to_s
+ @ip ||= calculate_ip
+ end
+
+ protected
+
+ def ips_from(header)
+ return [] unless header
+ # Split the comma-separated list into an array of strings
+ ips = header.strip.split(/[,\s]+/)
+ ips.select do |ip|
+ begin
+ # Only return IPs that are valid according to the IPAddr#new method
+ range = IPAddr.new(ip).to_range
+ # we want to make sure nobody is sneaking a netmask in
+ range.begin == range.end
+ rescue ArgumentError
+ nil
+ end
+ end
+ end
+
+ def filter_proxies(ips)
+ ips.reject do |ip|
+ @proxies.any? { |proxy| proxy === ip }
+ end
+ end
+
+ end
+
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
new file mode 100644
index 0000000000..1555ff72af
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -0,0 +1,40 @@
+require 'securerandom'
+require 'active_support/core_ext/string/access'
+
+module ActionDispatch
+ # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
+ # ActionDispatch::Request#uuid or the alias ActionDispatch::Request#request_id) and sends the same id to the client via the X-Request-Id header.
+ #
+ # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
+ # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
+ # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
+ #
+ # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
+ # from multiple pieces of the stack.
+ class RequestId
+ X_REQUEST_ID = "X-Request-Id".freeze # :nodoc:
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ req = ActionDispatch::Request.new env
+ req.request_id = make_request_id(req.x_request_id)
+ @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
+ end
+
+ private
+ def make_request_id(request_id)
+ if request_id.presence
+ request_id.gsub(/[^\w\-]/, "".freeze).first(255)
+ else
+ internal_request_id
+ end
+ end
+
+ def internal_request_id
+ SecureRandom.uuid
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
new file mode 100644
index 0000000000..5fb5953811
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -0,0 +1,102 @@
+require 'rack/utils'
+require 'rack/request'
+require 'rack/session/abstract/id'
+require 'action_dispatch/middleware/cookies'
+require 'action_dispatch/request/session'
+
+module ActionDispatch
+ module Session
+ class SessionRestoreError < StandardError #:nodoc:
+
+ def initialize(const_error = nil)
+ if const_error
+ ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
+ "Exceptions will automatically capture the original exception.", caller)
+ end
+
+ super("Session contains objects whose class definition isn't available.\n" +
+ "Remember to require the classes for all objects kept in the session.\n" +
+ "(Original exception: #{$!.message} [#{$!.class}])\n")
+ set_backtrace $!.backtrace
+ end
+
+ def original_exception
+ ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
+ cause
+ end
+ end
+
+ module Compatibility
+ def initialize(app, options = {})
+ options[:key] ||= '_session_id'
+ super
+ end
+
+ def generate_sid
+ sid = SecureRandom.hex(16)
+ sid.encode!(Encoding::UTF_8)
+ sid
+ end
+
+ protected
+
+ def initialize_sid
+ @default_options.delete(:sidbits)
+ @default_options.delete(:secure_random)
+ end
+
+ private
+ def make_request(env)
+ ActionDispatch::Request.new env
+ end
+ end
+
+ module StaleSessionCheck
+ def load_session(env)
+ stale_session_check! { super }
+ end
+
+ def extract_session_id(env)
+ stale_session_check! { super }
+ end
+
+ def stale_session_check!
+ yield
+ rescue ArgumentError => argument_error
+ if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
+ begin
+ # Note that the regexp does not allow $1 to end with a ':'
+ $1.constantize
+ rescue LoadError, NameError
+ raise ActionDispatch::Session::SessionRestoreError
+ end
+ retry
+ else
+ raise
+ end
+ end
+ end
+
+ module SessionObject # :nodoc:
+ def prepare_session(req)
+ Request::Session.create(self, req, @default_options)
+ end
+
+ def loaded_session?(session)
+ !session.is_a?(Request::Session) || session.loaded?
+ end
+ end
+
+ class AbstractStore < Rack::Session::Abstract::Persisted
+ include Compatibility
+ include StaleSessionCheck
+ include SessionObject
+
+ private
+
+ def set_cookie(request, session_id, cookie)
+ request.cookie_jar[key] = cookie
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
new file mode 100644
index 0000000000..589ae46e38
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -0,0 +1,52 @@
+require 'action_dispatch/middleware/session/abstract_store'
+
+module ActionDispatch
+ module Session
+ # A session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful
+ # if you don't store critical data in your sessions and you don't need them to live for extended periods
+ # of time.
+ #
+ # ==== Options
+ # * <tt>cache</tt> - The cache to use. If it is not specified, <tt>Rails.cache</tt> will be used.
+ # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
+ # By default, the <tt>:expires_in</tt> option of the cache is used.
+ class CacheStore < AbstractStore
+ def initialize(app, options = {})
+ @cache = options[:cache] || Rails.cache
+ options[:expire_after] ||= @cache.options[:expires_in]
+ super
+ end
+
+ # Get a session from the cache.
+ def find_session(env, sid)
+ unless sid and session = @cache.read(cache_key(sid))
+ sid, session = generate_sid, {}
+ end
+ [sid, session]
+ end
+
+ # Set a session in the cache.
+ def write_session(env, sid, session, options)
+ key = cache_key(sid)
+ if session
+ @cache.write(key, session, :expires_in => options[:expire_after])
+ else
+ @cache.delete(key)
+ end
+ sid
+ end
+
+ # Remove a session from the cache.
+ def delete_session(env, sid, options)
+ @cache.delete(cache_key(sid))
+ generate_sid
+ end
+
+ private
+ # Turn the session id into a cache key.
+ def cache_key(sid)
+ "_session_id:#{sid}"
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
new file mode 100644
index 0000000000..0e636b8257
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -0,0 +1,129 @@
+require 'active_support/core_ext/hash/keys'
+require 'action_dispatch/middleware/session/abstract_store'
+require 'rack/session/cookie'
+
+module ActionDispatch
+ module Session
+ # This cookie-based session store is the Rails default. It is
+ # dramatically faster than the alternatives.
+ #
+ # Sessions typically contain at most a user_id and flash message; both fit
+ # within the 4K cookie size limit. A CookieOverflow exception is raised if
+ # you attempt to store more than 4K of data.
+ #
+ # The cookie jar used for storage is automatically configured to be the
+ # best possible option given your application's configuration.
+ #
+ # If you only have secret_token set, your cookies will be signed, but
+ # not encrypted. This means a user cannot alter their +user_id+ without
+ # 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
+ # goes a step further than signed cookies in that encrypted cookies cannot
+ # be altered or read by users. This is the default starting in Rails 4.
+ #
+ # If you have both secret_token and secret_key base set, your cookies will
+ # be encrypted, and signed cookies generated by Rails 3 will be
+ # transparently read and encrypted to provide a smooth upgrade path.
+ #
+ # Configure your session store in config/initializers/session_store.rb:
+ #
+ # Rails.application.config.session_store :cookie_store, key: '_your_app_session'
+ #
+ # Configure your secret key in config/secrets.yml:
+ #
+ # development:
+ # secret_key_base: 'secret key'
+ #
+ # To generate a secret key for an existing application, run `rake 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.
+ # Note that you should wait to set secret_key_base until you have 100% of
+ # your userbase on Rails 4 and are reasonably sure you will not need to
+ # rollback to Rails 3. This is because cookies signed based on the new
+ # secret_key_base in Rails 4 are not backwards compatible with Rails 3.
+ # You are free to leave your existing secret_token in place, not set the
+ # new secret_key_base, and ignore the deprecation warnings until you are
+ # reasonably sure that your upgrade is otherwise complete. Additionally,
+ # you should take care to make sure you are not relying on the ability to
+ # decode signed cookies generated by your app in external applications or
+ # JavaScript before upgrading.
+ #
+ # Note that changing the secret key will invalidate all existing sessions!
+ #
+ # 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:
+ #
+ # Rails.application.config.session_store :cookie_store, expire_after: 14.days
+ #
+ # 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 < AbstractStore
+ def initialize(app, options={})
+ super(app, options.merge!(:cookie_only => true))
+ end
+
+ def delete_session(req, session_id, options)
+ new_sid = generate_sid unless options[:drop]
+ # Reset hash and Assign the new session id
+ req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})
+ new_sid
+ end
+
+ def load_session(req)
+ stale_session_check! do
+ data = unpacked_cookie_data(req)
+ data = persistent_session_id!(data)
+ [data["session_id"], data]
+ end
+ end
+
+ private
+
+ def extract_session_id(req)
+ stale_session_check! do
+ unpacked_cookie_data(req)["session_id"]
+ end
+ end
+
+ def unpacked_cookie_data(req)
+ req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
+ v = stale_session_check! do
+ if data = get_cookie(req)
+ data.stringify_keys!
+ end
+ data || {}
+ end
+ req.set_header k, v
+ end
+ end
+
+ def persistent_session_id!(data, sid=nil)
+ data ||= {}
+ data["session_id"] ||= sid || generate_sid
+ data
+ end
+
+ def write_session(req, sid, session_data, options)
+ session_data["session_id"] = sid
+ session_data
+ end
+
+ def set_cookie(request, session_id, cookie)
+ cookie_jar(request)[@key] = cookie
+ end
+
+ def get_cookie(req)
+ cookie_jar(req)[@key]
+ end
+
+ def cookie_jar(request)
+ request.cookie_jar.signed_or_encrypted
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
new file mode 100644
index 0000000000..cb19786f0b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
@@ -0,0 +1,26 @@
+require 'action_dispatch/middleware/session/abstract_store'
+begin
+ require 'rack/session/dalli'
+rescue LoadError => e
+ $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
+ raise e
+end
+
+module ActionDispatch
+ module Session
+ # A session store that uses MemCache to implement storage.
+ #
+ # ==== Options
+ # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
+ class MemCacheStore < Rack::Session::Dalli
+ include Compatibility
+ include StaleSessionCheck
+ include SessionObject
+
+ def initialize(app, options = {})
+ options[:expire_after] ||= options[:expires]
+ super
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
new file mode 100644
index 0000000000..64695f9738
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -0,0 +1,60 @@
+require 'action_dispatch/http/request'
+require 'action_dispatch/middleware/exception_wrapper'
+
+module ActionDispatch
+ # This middleware rescues any exception returned by the application
+ # and calls an exceptions app that will wrap it in a format for the end user.
+ #
+ # The exceptions app should be passed as parameter on initialization
+ # of ShowExceptions. Every time there is an exception, ShowExceptions will
+ # store the exception in env["action_dispatch.exception"], rewrite the
+ # PATH_INFO to the exception status code and call the rack app.
+ #
+ # If the application returns a "X-Cascade" pass response, this middleware
+ # will send an empty response as result with the correct status code.
+ # If any exception happens inside the exceptions app, this middleware
+ # catches the exceptions and returns a FAILSAFE_RESPONSE.
+ class ShowExceptions
+ FAILSAFE_RESPONSE = [500, { 'Content-Type' => 'text/plain' },
+ ["500 Internal Server Error\n" \
+ "If you are the administrator of this website, then please read this web " \
+ "application's log file and/or the web server's log file to find out what " \
+ "went wrong."]]
+
+ def initialize(app, exceptions_app)
+ @app = app
+ @exceptions_app = exceptions_app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new env
+ @app.call(env)
+ rescue Exception => exception
+ if request.show_exceptions?
+ render_exception(request, exception)
+ else
+ raise exception
+ end
+ end
+
+ private
+
+ def render_exception(request, exception)
+ backtrace_cleaner = request.get_header 'action_dispatch.backtrace_cleaner'
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
+ status = wrapper.status_code
+ request.set_header "action_dispatch.exception", wrapper.exception
+ request.set_header "action_dispatch.original_path", request.path_info
+ request.path_info = "/#{status}"
+ response = @exceptions_app.call(request.env)
+ response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
+ rescue Exception => failsafe_error
+ $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
+ FAILSAFE_RESPONSE
+ end
+
+ def pass_response(status)
+ [status, {"Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0"}, []]
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
new file mode 100644
index 0000000000..47f475559a
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -0,0 +1,129 @@
+module ActionDispatch
+ # This middleware is added to the stack when `config.force_ssl = true`.
+ # It does three jobs to enforce secure HTTP requests:
+ #
+ # 1. TLS redirect. http:// requests are permanently redirected to https://
+ # with the same URL host, path, etc. Pass `:host` and/or `:port` to
+ # modify the destination URL. This is always enabled.
+ #
+ # 2. Secure cookies. Sets the `secure` flag on cookies to tell browsers they
+ # mustn't be sent along with http:// requests. This is always enabled.
+ #
+ # 3. HTTP Strict Transport Security (HSTS). Tells the browser to remember
+ # this site as TLS-only and automatically redirect non-TLS requests.
+ # Enabled by default. Pass `hsts: false` to disable.
+ #
+ # Configure HSTS with `hsts: { … }`:
+ # * `expires`: How long, in seconds, these settings will stick. Defaults to
+ # `180.days` (recommended). The minimum required to qualify for browser
+ # preload lists is `18.weeks`.
+ # * `subdomains`: Set to `true` to tell the browser to apply these settings
+ # to all subdomains. This protects your cookies from interception by a
+ # vulnerable site on a subdomain. Defaults to `false`.
+ # * `preload`: Advertise that this site may be included in browsers'
+ # preloaded HSTS lists. HSTS protects your site on every visit *except the
+ # first visit* since it hasn't seen your HSTS header yet. To close this
+ # gap, browser vendors include a baked-in list of HSTS-enabled sites.
+ # Go to https://hstspreload.appspot.com to submit your site for inclusion.
+ #
+ # Disabling HSTS: To turn off HSTS, omitting the header is not enough.
+ # Browsers will remember the original HSTS directive until it expires.
+ # Instead, use the header to tell browsers to expire HSTS immediately.
+ # Setting `hsts: false` is a shortcut for `hsts: { expires: 0 }`.
+ class SSL
+ # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/
+ # and greater than the 18-week requirement for browser preload lists.
+ HSTS_EXPIRES_IN = 15552000
+
+ def self.default_hsts_options
+ { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false }
+ end
+
+ def initialize(app, redirect: {}, hsts: {}, **options)
+ @app = app
+
+ if options[:host] || options[:port]
+ ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc
+ The `:host` and `:port` options are moving within `:redirect`:
+ `config.ssl_options = { redirect: { host: …, port: … }}`.
+ end_warning
+ @redirect = options.slice(:host, :port)
+ else
+ @redirect = redirect
+ end
+
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
+ end
+
+ def call(env)
+ request = Request.new env
+
+ if request.ssl?
+ @app.call(env).tap do |status, headers, body|
+ set_hsts_header! headers
+ flag_cookies_as_secure! headers
+ end
+ else
+ redirect_to_https request
+ end
+ end
+
+ private
+ def set_hsts_header!(headers)
+ headers['Strict-Transport-Security'.freeze] ||= @hsts_header
+ end
+
+ 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'.freeze]
+ cookies = cookies.split("\n".freeze)
+
+ headers['Set-Cookie'.freeze] = cookies.map { |cookie|
+ if cookie !~ /;\s*secure\s*(;|$)/i
+ "#{cookie}; secure"
+ else
+ cookie
+ end
+ }.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
new file mode 100644
index 0000000000..44fc1ee736
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/stack.rb
@@ -0,0 +1,124 @@
+require "active_support/inflector/methods"
+require "active_support/dependencies"
+
+module ActionDispatch
+ class MiddlewareStack
+ class Middleware
+ attr_reader :args, :block, :klass
+
+ def initialize(klass, args, block)
+ @klass = klass
+ @args = args
+ @block = block
+ end
+
+ def name; klass.name; end
+
+ def inspect
+ if klass.is_a?(Class)
+ klass.to_s
+ else
+ klass.class.to_s
+ end
+ end
+
+ def build(app)
+ klass.new(app, *args, &block)
+ end
+ end
+
+ include Enumerable
+
+ attr_accessor :middlewares
+
+ def initialize(*args)
+ @middlewares = []
+ yield(self) if block_given?
+ end
+
+ def each
+ @middlewares.each { |x| yield x }
+ end
+
+ def size
+ middlewares.size
+ end
+
+ def last
+ middlewares.last
+ end
+
+ def [](i)
+ middlewares[i]
+ end
+
+ 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, klass, *args, &block)
+ index = assert_index(index, :before)
+ middlewares.insert(index, build_middleware(klass, args, block))
+ end
+
+ alias_method :insert_before, :insert
+
+ def insert_after(index, *args, &block)
+ index = assert_index(index, :after)
+ insert(index + 1, *args, &block)
+ end
+
+ def swap(target, *args, &block)
+ index = assert_index(target, :before)
+ insert(index, *args, &block)
+ middlewares.delete_at(index + 1)
+ end
+
+ def delete(target)
+ target = get_class target
+ middlewares.delete_if { |m| m.klass == target }
+ end
+
+ def use(klass, *args, &block)
+ middlewares.push(build_middleware(klass, args, block))
+ end
+
+ def build(app = Proc.new)
+ middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
+ end
+
+ private
+
+ def assert_index(index, where)
+ index = get_class index
+ i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
+ raise "No such middleware to insert #{where}: #{index.inspect}" unless i
+ i
+ end
+
+ def get_class(klass)
+ if klass.is_a?(String) || klass.is_a?(Symbol)
+ classcache = ActiveSupport::Dependencies::Reference
+ converted_klass = classcache[klass.to_s]
+ ActiveSupport::Deprecation.warn <<-eowarn
+Passing strings or symbols to the middleware builder is deprecated, please change
+them to actual class references. For example:
+
+ "#{klass}" => #{converted_klass}
+
+ eowarn
+ converted_klass
+ else
+ klass
+ 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
new file mode 100644
index 0000000000..ea9ab3821d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -0,0 +1,135 @@
+require 'rack/utils'
+require 'active_support/core_ext/uri'
+
+module ActionDispatch
+ # This middleware returns a file's contents from disk in the body response.
+ # When initialized, it can accept optional HTTP headers, which will be set
+ # when a response containing a file's contents is delivered.
+ #
+ # This middleware will render the file specified in `env["PATH_INFO"]`
+ # where the base path is in the +root+ directory. For example, if the +root+
+ # is set to `public/`, then a request with `env["PATH_INFO"]` of
+ # `assets/application.js` will return a response with the contents of a file
+ # located at `public/assets/application.js` if the file exists. If the file
+ # does not exist, a 404 "File not Found" response will be returned.
+ class FileHandler
+ def initialize(root, index: 'index', headers: {})
+ @root = root.chomp('/')
+ @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.
+ #
+ # Used by the `Static` class to check the existence of a valid file
+ # in the server's `public/` directory (see Static#call).
+ def match?(path)
+ path = ::Rack::Utils.unescape_path path
+ return false unless path.valid_encoding?
+ path = Rack::Utils.clean_path_info path
+
+ paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
+
+ if match = paths.detect { |p|
+ path = File.join(@root, p.force_encoding('UTF-8'.freeze))
+ begin
+ File.file?(path) && File.readable?(path)
+ rescue SystemCallError
+ false
+ end
+
+ }
+ return ::Rack::Utils.escape_path(match)
+ end
+ end
+
+ def call(env)
+ 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?(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(request.env)
+ end
+
+ headers['Vary'] = 'Accept-Encoding' if gzip_path
+
+ return [status, headers, body]
+ ensure
+ request.path_info = path
+ end
+
+ private
+ def ext
+ ::ActionController::Base.default_static_extension
+ end
+
+ def content_type(path)
+ ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze)
+ end
+
+ 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_path(gzip_path)))
+ gzip_path
+ else
+ false
+ end
+ end
+ end
+
+ # This middleware will attempt to return the contents of a file's body from
+ # disk in the response. If a file is not found on disk, the request will be
+ # delegated to the application stack. This middleware is commonly initialized
+ # to serve assets from a server's `public/` directory.
+ #
+ # This middleware verifies the path to ensure that only files
+ # living in the root directory can be rendered. A request cannot
+ # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
+ # requests will result in a file being returned.
+ class Static
+ def initialize(app, path, deprecated_cache_control = :not_set, index: 'index', headers: {})
+ if deprecated_cache_control != :not_set
+ ActiveSupport::Deprecation.warn("The `cache_control` argument is deprecated," \
+ "replaced by `headers: { 'Cache-Control' => #{deprecated_cache_control} }`, " \
+ " and will be removed in Rails 5.1.")
+ headers['Cache-Control'.freeze] = deprecated_cache_control
+ end
+
+ @app = app
+ @file_handler = FileHandler.new(path, index: index, headers: headers)
+ end
+
+ def call(env)
+ req = ActionDispatch::Request.new env
+
+ if req.get? || req.head?
+ path = req.path_info.chomp('/'.freeze)
+ if match = @file_handler.match?(path)
+ req.path_info = match
+ return @file_handler.serve(req)
+ end
+ end
+
+ @app.call(req.env)
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
new file mode 100644
index 0000000000..49b1e83551
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
@@ -0,0 +1,22 @@
+<% unless @exception.blamed_files.blank? %>
+ <% if (hide = @exception.blamed_files.length > 8) %>
+ <a href="#" onclick="return toggleTrace()">Toggle blamed files</a>
+ <% end %>
+ <pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre>
+<% end %>
+
+<h2 style="margin-top: 30px">Request</h2>
+<p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
+
+<div class="details">
+ <div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
+ <div id="session_dump" style="display:none"><pre><%= debug_hash @request.session %></pre></div>
+</div>
+
+<div class="details">
+ <div class="summary"><a href="#" onclick="return toggleEnvDump()">Toggle env dump</a></div>
+ <div id="env_dump" style="display:none"><pre><%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %></pre></div>
+</div>
+
+<h2 style="margin-top: 30px">Response</h2>
+<p><b>Headers</b>:</p> <pre><%= debug_headers(defined?(@response) ? @response.headers : {}) %></pre>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb
new file mode 100644
index 0000000000..396768ecee
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb
@@ -0,0 +1,23 @@
+<%
+ clean_params = @request.filtered_parameters.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
+
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ end unless self.class.method_defined?(:debug_hash)
+%>
+
+Request parameters
+<%= request_dump %>
+
+Session dump
+<%= debug_hash @request.session %>
+
+Env dump
+<%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %>
+
+Response headers
+<%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
new file mode 100644
index 0000000000..e7b913bbe4
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
@@ -0,0 +1,27 @@
+<% @source_extracts.each_with_index do |source_extract, index| %>
+ <% if source_extract[:code] %>
+ <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>">
+ <div class="info">
+ Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
+ </div>
+ <div class="data">
+ <table cellpadding="0" cellspacing="0" class="lines">
+ <tr>
+ <td>
+ <pre class="line_numbers">
+ <% source_extract[:code].each_key do |line_number| %>
+<span><%= line_number -%></span>
+ <% end %>
+ </pre>
+ </td>
+<td width="100%">
+<pre>
+<% source_extract[:code].each do |line, source| -%><div class="line<%= " active" if line == source_extract[:line_number] -%>"><%= source -%></div><% end -%>
+</pre>
+</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <% end %>
+<% end %>
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/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
new file mode 100644
index 0000000000..ab57b11c7d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
@@ -0,0 +1,52 @@
+<% names = @traces.keys %>
+
+<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
+
+<div id="traces">
+ <% names.each do |name| %>
+ <%
+ show = "show('#{name.gsub(/\s/, '-')}');"
+ hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}');"}
+ %>
+ <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
+ <% end %>
+
+ <% @traces.each do |name, trace| %>
+ <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == @trace_to_show) ? 'block' : 'none' %>;">
+ <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre>
+ </div>
+ <% end %>
+
+ <script type="text/javascript">
+ var traceFrames = document.getElementsByClassName('trace-frames');
+ var selectedFrame, currentSource = document.getElementById('frame-source-0');
+
+ // Add click listeners for all stack frames
+ for (var i = 0; i < traceFrames.length; i++) {
+ traceFrames[i].addEventListener('click', function(e) {
+ e.preventDefault();
+ var target = e.target;
+ var frame_id = target.dataset.frameId;
+
+ if (selectedFrame) {
+ selectedFrame.className = selectedFrame.className.replace("selected", "");
+ }
+
+ target.className += " selected";
+ selectedFrame = target;
+
+ // Change the extracted source code
+ changeSourceExtract(frame_id);
+ });
+
+ function changeSourceExtract(frame_id) {
+ var el = document.getElementById('frame-source-' + frame_id);
+ if (currentSource && el) {
+ currentSource.className += " hidden";
+ el.className = el.className.replace(" hidden", "");
+ currentSource = el;
+ }
+ }
+ }
+ </script>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
new file mode 100644
index 0000000000..c0b53068f7
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
@@ -0,0 +1,9 @@
+Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %>
+
+<% @traces.each do |name, trace| %>
+<% if trace.any? %>
+<%= name %>
+<%= trace.map { |t| t[:trace] }.join("\n") %>
+
+<% end %>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
new file mode 100644
index 0000000000..f154021ae6
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
@@ -0,0 +1,16 @@
+<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 %></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/diagnostics.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb
new file mode 100644
index 0000000000..603de54b8b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb
@@ -0,0 +1,9 @@
+<%= @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 %>
+<%= 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
new file mode 100644
index 0000000000..e0509f56f4
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>Action Controller: Exception caught</title>
+ <style>
+ body {
+ background-color: #FAFAFA;
+ color: #333;
+ margin: 0px;
+ }
+
+ body, p, ol, ul, td {
+ font-family: helvetica, verdana, arial, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ }
+
+ pre {
+ font-size: 11px;
+ white-space: pre-wrap;
+ }
+
+ pre.box {
+ border: 1px solid #EEE;
+ padding: 10px;
+ margin: 0px;
+ width: 958px;
+ }
+
+ header {
+ color: #F0F0F0;
+ background: #C52F24;
+ padding: 0.5em 1.5em;
+ }
+
+ h1 {
+ margin: 0.2em 0;
+ line-height: 1.1em;
+ font-size: 2em;
+ }
+
+ h2 {
+ color: #C52F24;
+ line-height: 25px;
+ }
+
+ .details {
+ border: 1px solid #D0D0D0;
+ border-radius: 4px;
+ margin: 1em 0px;
+ display: block;
+ width: 978px;
+ }
+
+ .summary {
+ padding: 8px 15px;
+ border-bottom: 1px solid #D0D0D0;
+ display: block;
+ }
+
+ .details pre {
+ margin: 5px;
+ border: none;
+ }
+
+ #container {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 0 1.5em;
+ }
+
+ .source * {
+ margin: 0px;
+ padding: 0px;
+ }
+
+ .source {
+ border: 1px solid #D9D9D9;
+ background: #ECECEC;
+ width: 978px;
+ }
+
+ .source pre {
+ padding: 10px 0px;
+ border: none;
+ }
+
+ .source .data {
+ font-size: 80%;
+ overflow: auto;
+ background-color: #FFF;
+ }
+
+ .info {
+ padding: 0.5em;
+ }
+
+ .source .data .line_numbers {
+ background-color: #ECECEC;
+ color: #AAA;
+ padding: 1em .5em;
+ border-right: 1px solid #DDD;
+ text-align: right;
+ }
+
+ .line {
+ padding-left: 10px;
+ }
+
+ .line:hover {
+ background-color: #F6F6F6;
+ }
+
+ .line.active {
+ background-color: #FFCCCC;
+ }
+
+ .hidden {
+ display: none;
+ }
+
+ a { color: #980905; }
+ a:visited { color: #666; }
+ a.trace-frames { color: #666; }
+ a:hover { color: #C52F24; }
+ a.trace-frames.selected { color: #C52F24 }
+
+ <%= yield :style %>
+ </style>
+
+ <script>
+ var toggle = function(id) {
+ var s = document.getElementById(id).style;
+ s.display = s.display == 'none' ? 'block' : 'none';
+ return false;
+ }
+ var show = function(id) {
+ document.getElementById(id).style.display = 'block';
+ }
+ var hide = function(id) {
+ document.getElementById(id).style.display = 'none';
+ }
+ var toggleTrace = function() {
+ return toggle('blame_trace');
+ }
+ var toggleSessionDump = function() {
+ return toggle('session_dump');
+ }
+ var toggleEnvDump = function() {
+ return toggle('env_dump');
+ }
+ </script>
+</head>
+<body>
+
+<%= yield %>
+
+</body>
+</html>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
new file mode 100644
index 0000000000..2a65fd06ad
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
@@ -0,0 +1,11 @@
+<header>
+ <h1>Template is missing</h1>
+</header>
+
+<div id="container">
+ <h2><%= h @exception.message %></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/missing_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb
new file mode 100644
index 0000000000..ae62d9eb02
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb
@@ -0,0 +1,3 @@
+Template is missing
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
new file mode 100644
index 0000000000..55dd5ddc7b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
@@ -0,0 +1,32 @@
+<header>
+ <h1>Routing Error</h1>
+</header>
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+ <% unless @exception.failures.empty? %>
+ <p>
+ <h2>Failure reasons:</h2>
+ <ol>
+ <% @exception.failures.each do |route, reason| %>
+ <li><code><%= route.inspect.delete('\\') %></code> failed because <%= reason.downcase %></li>
+ <% end %>
+ </ol>
+ </p>
+ <% end %>
+
+ <%= render template: "rescues/_trace" %>
+
+ <% if @routes_inspector %>
+ <h2>
+ Routes
+ </h2>
+
+ <p>
+ Routes match in priority from top to bottom
+ </p>
+
+ <%= @routes_inspector.format(ActionDispatch::Routing::HtmlTableFormatter.new(self)) %>
+ <% end %>
+
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb
new file mode 100644
index 0000000000..f6e4dac1f3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb
@@ -0,0 +1,11 @@
+Routing Error
+
+<%= @exception.message %>
+<% unless @exception.failures.empty? %>
+Failure reasons:
+<% @exception.failures.each do |route, reason| %>
+ - <%= route.inspect.delete('\\') %></code> failed because <%= reason.downcase %>
+<% end %>
+<% end %>
+
+<%= render template: "rescues/_trace", format: :text %>
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
new file mode 100644
index 0000000000..5060da9369
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
@@ -0,0 +1,20 @@
+<header>
+ <h1>
+ <%= @exception.cause.class.to_s %> in
+ <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
+ </h1>
+</header>
+
+<div id="container">
+ <p>
+ Showing <i><%= @exception.file_name %></i> where line <b>#<%= @exception.line_number %></b> raised:
+ </p>
+ <pre><code><%= h @exception.message %></code></pre>
+
+ <%= render template: "rescues/_source" %>
+
+ <p><%= @exception.sub_template_message %></p>
+
+ <%= render template: "rescues/_trace" %>
+ <%= render template: "rescues/_request_and_response" %>
+</div>
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
new file mode 100644
index 0000000000..78d52acd96
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
@@ -0,0 +1,7 @@
+<%= @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 %>
+<%= @exception.sub_template_message %>
+<%= render template: "rescues/_trace", format: :text %>
+<%= render template: "rescues/_request_and_response", format: :text %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb
new file mode 100644
index 0000000000..259fb2bb3b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb
@@ -0,0 +1,6 @@
+<header>
+ <h1>Unknown action</h1>
+</header>
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb
new file mode 100644
index 0000000000..83973addcb
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb
@@ -0,0 +1,3 @@
+Unknown action
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb
new file mode 100644
index 0000000000..6e995c85c1
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb
@@ -0,0 +1,16 @@
+<tr class='route_row' data-helper='path'>
+ <td data-route-name='<%= route[:name] %>'>
+ <% if route[:name].present? %>
+ <%= route[:name] %><span class='helper'>_path</span>
+ <% end %>
+ </td>
+ <td>
+ <%= route[:verb] %>
+ </td>
+ <td data-route-path='<%= route[:path] %>'>
+ <%= route[:path] %>
+ </td>
+ <td>
+ <%=simple_format route[:reqs] %>
+ </td>
+</tr>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
new file mode 100644
index 0000000000..429ea7057c
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -0,0 +1,196 @@
+<% content_for :style do %>
+ #route_table {
+ margin: 0;
+ border-collapse: collapse;
+ }
+
+ #route_table thead tr {
+ border-bottom: 2px solid #ddd;
+ }
+
+ #route_table thead tr.bottom {
+ border-bottom: none;
+ }
+
+ #route_table thead tr.bottom th {
+ padding: 10px 0;
+ line-height: 15px;
+ }
+
+ #route_table tbody tr {
+ border-bottom: 1px solid #ddd;
+ }
+
+ #route_table tbody tr:nth-child(odd) {
+ background: #f2f2f2;
+ }
+
+ #route_table tbody.exact_matches,
+ #route_table tbody.fuzzy_matches {
+ background-color: LightGoldenRodYellow;
+ border-bottom: solid 2px SlateGrey;
+ }
+
+ #route_table tbody.exact_matches tr,
+ #route_table tbody.fuzzy_matches tr {
+ background: none;
+ border-bottom: none;
+ }
+
+ #route_table td {
+ padding: 4px 30px;
+ }
+
+ #path_search {
+ width: 80%;
+ font-size: inherit;
+ }
+<% end %>
+
+<table id='route_table' class='route_table'>
+ <thead>
+ <tr>
+ <th>Helper</th>
+ <th>HTTP Verb</th>
+ <th>Path</th>
+ <th>Controller#Action</th>
+ </tr>
+ <tr class='bottom'>
+ <th><%# Helper %>
+ <%= link_to "Path", "#", 'data-route-helper' => '_path',
+ title: "Returns a relative path (without the http or domain)" %> /
+ <%= link_to "Url", "#", 'data-route-helper' => '_url',
+ title: "Returns an absolute url (with the http and domain)" %>
+ </th>
+ <th><%# HTTP Verb %>
+ </th>
+ <th><%# Path %>
+ <%= search_field(:path, nil, id: 'search', placeholder: "Path Match") %>
+ </th>
+ <th><%# Controller#action %>
+ </th>
+ </tr>
+ </thead>
+ <tbody class='exact_matches' id='exact_matches'>
+ </tbody>
+ <tbody class='fuzzy_matches' id='fuzzy_matches'>
+ </tbody>
+ <tbody>
+ <%= yield %>
+ </tbody>
+</table>
+
+<script type='text/javascript'>
+ // support forEarch iterator on NodeList
+ NodeList.prototype.forEach = Array.prototype.forEach;
+
+ // Enables path search functionality
+ function setupMatchPaths() {
+ // Check if there are any matched results in a section
+ function checkNoMatch(section, noMatchText) {
+ if (section.children.length <= 1) {
+ section.innerHTML += noMatchText;
+ }
+ }
+
+ // get JSON from url and invoke callback with result
+ function getJSON(url, success) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url);
+ xhr.onload = function() {
+ if (this.status == 200)
+ success(JSON.parse(this.response));
+ };
+ xhr.send();
+ }
+
+ function delayedKeyup(input, callback) {
+ var timeout;
+ input.onkeyup = function(){
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(callback, 300);
+ }
+ }
+
+ // remove params or fragments
+ function sanitizePath(path) {
+ return path.replace(/[#?].*/, '');
+ }
+
+ var pathElements = document.querySelectorAll('#route_table [data-route-path]'),
+ searchElem = document.querySelector('#search'),
+ exactSection = document.querySelector('#exact_matches'),
+ fuzzySection = document.querySelector('#fuzzy_matches');
+
+ // Remove matches when no search value is present
+ searchElem.onblur = function(e) {
+ if (searchElem.value === "") {
+ exactSection.innerHTML = "";
+ fuzzySection.innerHTML = "";
+ }
+ }
+
+ // On key press perform a search for matching paths
+ delayedKeyup(searchElem, function() {
+ var path = sanitizePath(searchElem.value),
+ defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + path +'):</th></tr>',
+ defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + path +'):</th></tr>',
+ noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>',
+ noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>';
+
+ if (!path)
+ return searchElem.onblur();
+
+ getJSON('/rails/info/routes?path=' + path, function(matches){
+ // Clear out results section
+ exactSection.innerHTML = defaultExactMatch;
+ fuzzySection.innerHTML = defaultFuzzyMatch;
+
+ // Display exact matches and fuzzy matches
+ pathElements.forEach(function(elem) {
+ var elemPath = elem.getAttribute('data-route-path');
+
+ if (matches['exact'].indexOf(elemPath) != -1)
+ exactSection.appendChild(elem.parentNode.cloneNode(true));
+
+ if (matches['fuzzy'].indexOf(elemPath) != -1)
+ fuzzySection.appendChild(elem.parentNode.cloneNode(true));
+ })
+
+ // Display 'No Matches' message when no matches are found
+ checkNoMatch(exactSection, noExactMatch);
+ checkNoMatch(fuzzySection, noFuzzyMatch);
+ })
+ })
+ }
+
+ // Enables functionality to toggle between `_path` and `_url` helper suffixes
+ function setupRouteToggleHelperLinks() {
+
+ // Sets content for each element
+ function setValOn(elems, val) {
+ elems.forEach(function(elem) {
+ elem.innerHTML = val;
+ });
+ }
+
+ // Sets onClick event for each element
+ function onClick(elems, func) {
+ elems.forEach(function(elem) {
+ elem.onclick = func;
+ });
+ }
+
+ var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]');
+
+ onClick(toggleLinks, function(){
+ var helperTxt = this.getAttribute("data-route-helper"),
+ helperElems = document.querySelectorAll('[data-route-name] span.helper');
+
+ setValOn(helperElems, helperTxt);
+ });
+ }
+
+ setupMatchPaths();
+ setupRouteToggleHelperLinks();
+</script>