diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware')
18 files changed, 272 insertions, 174 deletions
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 8c0f4052ec..338b116940 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -5,7 +5,7 @@ module ActionDispatch class Callbacks include ActiveSupport::Callbacks - define_callbacks :call, :rescuable => true + define_callbacks :call class << self delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader" @@ -24,9 +24,15 @@ module ActionDispatch end def call(env) - run_callbacks :call do - @app.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 index 39ff58a447..771f075275 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/keys' module ActionDispatch - class Request + class Request < Rack::Request def cookie_jar env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) end @@ -26,9 +26,9 @@ module ActionDispatch # # Sets a cookie that expires in 1 hour. # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } # - # # Sets a signed cookie, which prevents a user from tampering with its value. + # # Sets a signed cookie, which prevents users from tampering with its value. # # The cookie is signed by your app's <tt>config.secret_token</tt> value. - # # Rails generates this value by default when you create a new Rails app. + # # It can be read using the signed method <tt>cookies.signed[:key]</tt> # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -39,9 +39,10 @@ module ActionDispatch # # Examples for reading: # - # cookies[:user_name] # => "david" - # cookies.size # => 2 - # cookies[:lat_lon] # => [47.68, -122.37] + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # cookies[:lat_lon] # => [47.68, -122.37] + # cookies.signed[:login] # => "XJ-122" # # Example for deleting: # @@ -82,7 +83,7 @@ module ActionDispatch TOKEN_KEY = "action_dispatch.secret_token".freeze # Raised when storing more than 4K of session data. - class CookieOverflow < StandardError; end + CookieOverflow = Class.new StandardError class CookieJar #:nodoc: include Enumerable @@ -117,7 +118,6 @@ module ActionDispatch @delete_cookies = {} @host = host @secure = secure - @closed = false @cookies = {} end @@ -154,7 +154,7 @@ module ActionDispatch end elsif options[:domain].is_a? Array # if host matches one of the supplied domains without a dot in front of it - options[:domain] = options[:domain].find {|domain| @host.include? domain[/^\.?(.*)$/, 1] } + options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') } end end @@ -169,12 +169,14 @@ module ActionDispatch options = { :value => value } end - @cookies[key.to_s] = value - handle_options(options) - @set_cookies[key.to_s] = options - @delete_cookies.delete(key.to_s) + if @cookies[key.to_s] != value or options[:expires] + @cookies[key.to_s] = value + @set_cookies[key.to_s] = options + @delete_cookies.delete(key.to_s) + end + value end @@ -182,8 +184,9 @@ module ActionDispatch # and setting its expiration date into 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(key, options = {}) - options.symbolize_keys! + return unless @cookies.has_key? key.to_s + options.symbolize_keys! handle_options(options) value = @cookies.delete(key.to_s) @@ -191,6 +194,15 @@ module ActionDispatch 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?(key, options = {}) + options.symbolize_keys! + handle_options(options) + @delete_cookies[key.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) } @@ -216,7 +228,7 @@ module ActionDispatch # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will # be raised. # - # This jar requires that you set a suitable secret for the verification on your app's config.secret_token. + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_token+. # # Example: # @@ -264,10 +276,6 @@ module ActionDispatch @parent_jar[key] = options end - def signed - @signed ||= SignedCookieJar.new(self, @secret) - end - def method_missing(method, *arguments, &block) @parent_jar.send(method, *arguments, &block) end @@ -334,7 +342,6 @@ module ActionDispatch end def call(env) - cookie_jar = nil status, headers, body = @app.call(env) if cookie_jar = env['action_dispatch.cookies'] diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index c0532c80c4..7349b578d2 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,5 +1,6 @@ require 'action_controller/metal/exceptions' require 'active_support/core_ext/exception' +require 'active_support/core_ext/class/attribute_accessors' module ActionDispatch class ExceptionWrapper @@ -10,7 +11,9 @@ module ActionDispatch 'AbstractController::ActionNotFound' => :not_found, 'ActionController::MethodNotAllowed' => :method_not_allowed, 'ActionController::NotImplemented' => :not_implemented, - 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity + 'ActionController::UnknownFormat' => :not_acceptable, + 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity, + 'ActionController::BadRequest' => :bad_request ) cattr_accessor :rescue_templates @@ -75,4 +78,4 @@ module ActionDispatch @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner'] end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index bc5b163931..9928b7cc3a 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,23 +1,23 @@ module ActionDispatch - class Request + class Request < Rack::Request # Access the contents of the flash. Use <tt>flash["notice"]</tt> to # read a notice you put there or <tt>flash["notice"] = "hello"</tt> # to put a new one. def flash - @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new) + @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new).tap(&:sweep) end end # The flash provides a way to pass temporary objects 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. Example: + # 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 posts_path(@post) + # redirect_to @post # end # # def show @@ -78,8 +78,7 @@ module ActionDispatch include Enumerable def initialize #:nodoc: - @used = Set.new - @closed = false + @discard = Set.new @flashes = {} @now = nil end @@ -93,7 +92,7 @@ module ActionDispatch end def []=(k, v) #:nodoc: - keep(k) + @discard.delete k @flashes[k] = v end @@ -102,7 +101,7 @@ module ActionDispatch end def update(h) #:nodoc: - h.keys.each { |k| keep(k) } + @discard.subtract h.keys @flashes.update h self end @@ -116,6 +115,7 @@ module ActionDispatch end def delete(key) + @discard.delete key @flashes.delete key self end @@ -129,6 +129,7 @@ module ActionDispatch end def clear + @discard.clear @flashes.clear end @@ -139,7 +140,7 @@ module ActionDispatch alias :merge! :update def replace(h) #:nodoc: - @used = Set.new + @discard.clear @flashes.replace h self end @@ -163,7 +164,8 @@ module ActionDispatch # 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) - use(k, false) + @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: @@ -171,24 +173,16 @@ module ActionDispatch # 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) - use(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: - keys.each do |k| - unless @used.include?(k) - @used << k - else - delete(k) - @used.delete(k) - end - end - - # clean up after keys that could have been left over by calling reject! or shift on the flash - (@used - keys).each{ |k| @used.delete(k) } + @discard.each { |k| @flashes.delete k } + @discard.replace @flashes.keys end # Convenience accessor for flash[:alert] @@ -212,22 +206,9 @@ module ActionDispatch end protected - - def now_is_loaded? - !!@now - end - - # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods - # use() # marks the entire flash as used - # use('msg') # marks the "msg" entry as used - # use(nil, false) # marks the entire flash as unused (keeps it around for one more action) - # use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action) - # Returns the single value for the key you asked to be marked (un)used or the FlashHash itself - # if no key is passed. - def use(key = nil, used = true) - Array(key || keys).each { |k| used ? @used << k : @used.delete(k) } - return key ? self[key] : self - end + def now_is_loaded? + @now + end end def initialize(app) @@ -235,13 +216,9 @@ module ActionDispatch end def call(env) - if (session = env['rack.session']) && (flash = session['flash']) - flash.sweep - end - @app.call(env) ensure - session = env['rack.session'] || {} + session = Request::Session.find(env) || {} flash_hash = env[KEY] if flash_hash @@ -255,7 +232,8 @@ module ActionDispatch env[KEY] = new_hash end - if session.key?('flash') && session['flash'].empty? + if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) + session.key?('flash') && session['flash'].empty? session.delete('flash') end end diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 4f48f1c974..2f6968eb2e 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -18,10 +18,10 @@ module ActionDispatch # 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 + # 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 +ActionDispatch::Reloader.prepare!+ - # or +ActionDispatch::Reloader.cleanup!+ are called manually. + # 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 @@ -60,8 +60,10 @@ module ActionDispatch def call(env) @validated = @condition.call prepare! + response = @app.call(env) - response[2].extend(module_hook) + response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! } + response rescue Exception cleanup! @@ -83,18 +85,5 @@ module ActionDispatch def validated? #:nodoc: @validated end - - def module_hook #:nodoc: - middleware = self - Module.new do - define_method :close do - begin - super() if defined?(super) - ensure - middleware.cleanup! - end - end - end - end end end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 66ece60860..ec15a2a715 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -5,11 +5,14 @@ module ActionDispatch # IP addresses that are "trusted proxies" that can be stripped from # the comma-delimited list in the X-Forwarded-For header. See also: # http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces + # http://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses. TRUSTED_PROXIES = %r{ ^127\.0\.0\.1$ | # localhost + ^::1$ | ^(10 | # private IP 10.x.x.x 172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255 - 192\.168 # private IP 192.168.x.x + 192\.168 | # private IP 192.168.x.x + fc00:: # private IP fc00 )\. }x @@ -18,12 +21,14 @@ module ActionDispatch def initialize(app, check_ip_spoofing = true, custom_proxies = nil) @app = app @check_ip = check_ip_spoofing - if custom_proxies - custom_regexp = Regexp.new(custom_proxies) - @proxies = Regexp.union(TRUSTED_PROXIES, custom_regexp) - else - @proxies = TRUSTED_PROXIES - end + @proxies = case custom_proxies + when Regexp + custom_proxies + when nil + TRUSTED_PROXIES + else + Regexp.union(TRUSTED_PROXIES, custom_proxies) + end end def call(env) @@ -32,9 +37,34 @@ module ActionDispatch end class GetIp + + # IP v4 and v6 (with compression) validation regexp + # https://gist.github.com/1289635 + VALID_IP = %r{ + (^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$) | # ip v4 + (^( + (([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}) | # ip v6 not abbreviated + (([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4}) | # ip v6 with double colon in the end + (([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}) | # - ip addresses v6 + (([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}) | # - with + (([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}) | # - double colon + (([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}) | # - in the middle + (([0-9A-Fa-f]{1,4}:){6} ((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3} (\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 + (([0-9A-Fa-f]{1,4}:){1,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 + (([0-9A-Fa-f]{1,4}:){1}:([0-9A-Fa-f]{1,4}:){0,4}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 + (([0-9A-Fa-f]{1,4}:){0,2}:([0-9A-Fa-f]{1,4}:){0,3}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 + (([0-9A-Fa-f]{1,4}:){0,3}:([0-9A-Fa-f]{1,4}:){0,2}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 + (([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 + (::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 + ([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4 + (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the begining + (([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending + )$) + }x + def initialize(env, middleware) - @env = env - @middleware = middleware + @env = env + @middleware = middleware @calculated_ip = false end @@ -42,25 +72,31 @@ module ActionDispatch # but will be wrong if the user is behind a proxy. Proxies will set # HTTP_CLIENT_IP and/or HTTP_X_FORWARDED_FOR, so we prioritize those. # HTTP_X_FORWARDED_FOR may be a comma-delimited list in the case of - # multiple chained proxies. The last address which is not a known proxy - # will be the originating IP. + # multiple chained proxies. The first address which is in this list + # if it's not a known proxy will be the originating IP. + # Format of HTTP_X_FORWARDED_FOR: + # client_ip, proxy_ip1, proxy_ip2... + # http://en.wikipedia.org/wiki/X-Forwarded-For def calculate_ip - client_ip = @env['HTTP_CLIENT_IP'] - forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR') - remote_addrs = ips_from('REMOTE_ADDR') + client_ip = @env['HTTP_CLIENT_IP'] + forwarded_ip = ips_from('HTTP_X_FORWARDED_FOR').first + remote_addrs = ips_from('REMOTE_ADDR') check_ip = client_ip && @middleware.check_ip - if check_ip && !forwarded_ips.include?(client_ip) + if check_ip && forwarded_ip != client_ip # We don't know which came from the proxy, and which from the user raise IpSpoofAttackError, "IP spoofing attack?!" \ "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}" \ "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" end - not_proxy = client_ip || forwarded_ips.last || remote_addrs.first - - # Return first REMOTE_ADDR if there are no other options - not_proxy || ips_from('REMOTE_ADDR', :allow_proxies).first + client_ips = remove_proxies [client_ip, forwarded_ip, remote_addrs].flatten + if client_ips.present? + client_ips.first + else + # If there is no client ip we can return first valid proxy ip from REMOTE_ADDR + remote_addrs.find { |ip| valid_ip? ip } + end end def to_s @@ -69,12 +105,24 @@ module ActionDispatch @ip = calculate_ip end - protected + private - def ips_from(header, allow_proxies = false) - ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : [] - allow_proxies ? ips : ips.reject{|ip| ip =~ @middleware.proxies } + def ips_from(header) + @env[header] ? @env[header].strip.split(/[,\s]+/) : [] end + + def valid_ip?(ip) + ip =~ VALID_IP + end + + def not_a_proxy?(ip) + ip !~ @middleware.proxies + end + + def remove_proxies(ips) + ips.select { |ip| valid_ip?(ip) && not_a_proxy?(ip) } + end + end end diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index d5a0b80fd5..6fff94707c 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -19,10 +19,7 @@ module ActionDispatch def call(env) env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id - status, headers, body = @app.call(env) - - headers["X-Request-Id"] = env["action_dispatch.request_id"] - [ status, headers, body ] + @app.call(env).tap { |status, headers, body| headers["X-Request-Id"] = env["action_dispatch.request_id"] } end private diff --git a/actionpack/lib/action_dispatch/middleware/rescue.rb b/actionpack/lib/action_dispatch/middleware/rescue.rb deleted file mode 100644 index aee672112c..0000000000 --- a/actionpack/lib/action_dispatch/middleware/rescue.rb +++ /dev/null @@ -1,26 +0,0 @@ -module ActionDispatch - class Rescue - def initialize(app, rescuers = {}, &block) - @app, @rescuers = app, {} - rescuers.each { |exception, rescuer| rescue_from(exception, rescuer) } - instance_eval(&block) if block_given? - end - - def call(env) - @app.call(env) - rescue Exception => exception - if rescuer = @rescuers[exception.class.name] - env['action_dispatch.rescue.exception'] = exception - rescuer.call(env) - else - raise exception - end - end - - protected - def rescue_from(exception, rescuer) - exception = exception.class.name if exception.is_a?(Exception) - @rescuers[exception.to_s] = rescuer - 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 index f75b4d4d04..64159fa8e7 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -2,26 +2,23 @@ require 'rack/utils' require 'rack/request' require 'rack/session/abstract/id' require 'action_dispatch/middleware/cookies' +require 'action_dispatch/request/session' require 'active_support/core_ext/object/blank' module ActionDispatch module Session class SessionRestoreError < StandardError #:nodoc: - end + attr_reader :original_exception + + def initialize(const_error) + @original_exception = const_error - module DestroyableSession - def destroy - clear - options = @env[Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY] if @env - options ||= {} - @by.send(:destroy_session, @env, options[:id], options) if @by - options[:id] = nil - @loaded = false + super("Session contains objects whose class definition isn't available.\n" + + "Remember to require the classes for all objects kept in the session.\n" + + "(Original exception: #{const_error.message} [#{const_error.class}])\n") end end - ::Rack::Session::Abstract::SessionHash.send :include, DestroyableSession - module Compatibility def initialize(app, options = {}) options[:key] ||= '_session_id' @@ -30,7 +27,7 @@ module ActionDispatch def generate_sid sid = SecureRandom.hex(16) - sid.encode!('UTF-8') if sid.respond_to?(:encode!) + sid.encode!('UTF-8') sid end @@ -58,11 +55,8 @@ module ActionDispatch begin # Note that the regexp does not allow $1 to end with a ':' $1.constantize - rescue LoadError, NameError => const_error - raise ActionDispatch::Session::SessionRestoreError, - "Session contains objects whose class definition isn't available.\n" + - "Remember to require the classes for all objects kept in the session.\n" + - "(Original exception: #{const_error.message} [#{const_error.class}])\n" + rescue LoadError, NameError => e + raise ActionDispatch::Session::SessionRestoreError, e, e.backtrace end retry else @@ -71,9 +65,27 @@ module ActionDispatch end end + module SessionObject # :nodoc: + def prepare_session(env) + Request::Session.create(self, env, @default_options) + end + + def loaded_session?(session) + !session.is_a?(Request::Session) || session.loaded? + end + end + class AbstractStore < Rack::Session::Abstract::ID include Compatibility include StaleSessionCheck + include SessionObject + + private + + def set_cookie(env, session_id, cookie) + request = ActionDispatch::Request.new(env) + request.cookie_jar[key] = cookie + end end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb index d3b6fd12fa..1db6194271 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -1,5 +1,4 @@ require 'action_dispatch/middleware/session/abstract_store' -require 'rack/session/memcache' module ActionDispatch module Session diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 8ebf870b95..7efc094f98 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -27,7 +27,7 @@ module ActionDispatch # CGI::Session instance as an argument. It's important that the secret # is not vulnerable to a dictionary attack. Therefore, you should choose # a secret consisting of random numbers and letters and more than 30 - # characters. Examples: + # characters. # # :secret => '449fe2e7daee471bffae2fd8dc02313d' # :secret => Proc.new { User.current_user.secret_key } @@ -43,6 +43,7 @@ module ActionDispatch class CookieStore < Rack::Session::Cookie include Compatibility include StaleSessionCheck + include SessionObject private @@ -59,7 +60,8 @@ module ActionDispatch end def set_session(env, sid, session_data, options) - session_data.merge("session_id" => sid) + session_data["session_id"] = sid + session_data end def set_cookie(env, session_id, cookie) diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index 4dd9a946c2..38a737cd2b 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -6,6 +6,7 @@ module ActionDispatch class MemCacheStore < Rack::Session::Memcache include Compatibility include StaleSessionCheck + include SessionObject def initialize(app, options = {}) require 'memcache' diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 3d06214bf1..ab740a0190 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -1,6 +1,5 @@ require 'action_dispatch/http/request' require 'action_dispatch/middleware/exception_wrapper' -require 'active_support/deprecation' module ActionDispatch # This middleware rescues any exception returned by the application @@ -10,7 +9,7 @@ module ActionDispatch # of ShowExceptions. Everytime 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 @@ -39,11 +38,6 @@ module ActionDispatch private - # Define this method because some plugins were monkey patching it. - # Remove this after 3.2 is out with the other deprecations in this class. - def status_code(*) - end - def render_exception(env, exception) wrapper = ExceptionWrapper.new(env, exception) status = wrapper.status_code diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb new file mode 100644 index 0000000000..9098f4e170 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -0,0 +1,70 @@ +module ActionDispatch + class SSL + YEAR = 31536000 + + def self.default_hsts_options + { :expires => YEAR, :subdomains => false } + end + + def initialize(app, options = {}) + @app = app + + @hsts = options.fetch(:hsts, {}) + @hsts = {} if @hsts == true + @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts + + @host = options[:host] + @port = options[:port] + end + + def call(env) + request = Request.new(env) + + if request.ssl? + status, headers, body = @app.call(env) + headers = hsts_headers.merge(headers) + flag_cookies_as_secure!(headers) + [status, headers, body] + else + redirect_to_https(request) + end + end + + private + def redirect_to_https(request) + url = URI(request.url) + url.scheme = "https" + url.host = @host if @host + url.port = @port if @port + headers = hsts_headers.merge('Content-Type' => 'text/html', + 'Location' => url.to_s) + + [301, headers, []] + end + + # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02 + def hsts_headers + if @hsts + value = "max-age=#{@hsts[:expires]}" + value += "; includeSubDomains" if @hsts[:subdomains] + { 'Strict-Transport-Security' => value } + else + {} + end + end + + def flag_cookies_as_secure!(headers) + if cookies = headers['Set-Cookie'] + cookies = cookies.split("\n") + + headers['Set-Cookie'] = cookies.map { |cookie| + if cookie !~ /;\s+secure(;|$)/ + "#{cookie}; secure" + else + cookie + end + }.join("\n") + end + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index a4308f528c..bbf734f103 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -75,6 +75,11 @@ module ActionDispatch middlewares[i] end + def unshift(*args, &block) + middleware = self.class::Middleware.new(*args, &block) + middlewares.unshift(middleware) + end + def initialize_copy(other) self.middlewares = other.middlewares.dup end @@ -93,8 +98,9 @@ module ActionDispatch end def swap(target, *args, &block) - insert_before(target, *args, &block) - delete(target) + index = assert_index(target, :before) + insert(index, *args, &block) + middlewares.delete_at(index + 1) end def delete(target) @@ -109,7 +115,7 @@ module ActionDispatch def build(app = nil, &block) app ||= block raise "MiddlewareStack#build requires an app" unless app - middlewares.reverse.inject(app) { |a, e| e.build(a) } + middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) } end protected diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 404943d720..9073e6582d 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -1,4 +1,5 @@ require 'rack/utils' +require 'active_support/core_ext/uri' module ActionDispatch class FileHandler @@ -11,14 +12,14 @@ module ActionDispatch def match?(path) path = path.dup - full_path = path.empty? ? @root : File.join(@root, ::Rack::Utils.unescape(path)) + full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(unescape_path(path))) paths = "#{full_path}#{ext}" matches = Dir[paths] match = matches.detect { |m| File.file?(m) } if match match.sub!(@compiled_root, '') - match + ::Rack::Utils.escape(match) end end @@ -32,6 +33,15 @@ module ActionDispatch "{,#{ext},/index#{ext}}" end end + + def unescape_path(path) + URI.parser.unescape(path) + end + + def escape_glob_chars(path) + path.force_encoding('binary') if path.respond_to? :force_encoding + path.gsub(/[*?{}\[\]]/, "\\\\\\&") + end end class Static diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb index 0c5bafa666..823f5d25b6 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb @@ -12,8 +12,8 @@ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") - def debug_hash(hash) - hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") + def debug_hash(object) + object.to_hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") end unless self.class.method_defined?(:debug_hash) %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb index f06c07daa5..177d383e94 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb @@ -12,4 +12,6 @@ <% end %> <p> Try running <code>rake routes</code> for more information on available routes. -</p>
\ No newline at end of file +</p> + +<%= render :template => "rescues/_trace" %> |
