diff options
Diffstat (limited to 'actionpack/lib/action_controller')
10 files changed, 252 insertions, 93 deletions
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 50b965ce4c..36b80d5780 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1315,10 +1315,6 @@ module ActionController #:nodoc: "#{request.protocol}#{request.host}#{request.request_uri}" end - def close_session - @_session.close if @_session && @_session.respond_to?(:close) - end - def default_template(action_name = self.action_name) self.view_paths.find_template(default_template_name(action_name), default_template_format) end @@ -1342,15 +1338,14 @@ module ActionController #:nodoc: end def process_cleanup - close_session end end Base.class_eval do [ Filters, Layout, Benchmarking, Rescue, Flash, MimeResponds, Helpers, Cookies, Caching, Verification, Streaming, SessionManagement, - HttpAuthentication::Basic::ControllerMethods, RecordIdentifier, - RequestForgeryProtection, Translation + HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods, + RecordIdentifier, RequestForgeryProtection, Translation ].each do |mod| include mod end diff --git a/actionpack/lib/action_controller/caching/sweeping.rb b/actionpack/lib/action_controller/caching/sweeping.rb index c7992d7769..c1be264ffb 100644 --- a/actionpack/lib/action_controller/caching/sweeping.rb +++ b/actionpack/lib/action_controller/caching/sweeping.rb @@ -87,9 +87,9 @@ module ActionController #:nodoc: __send__(action_callback_method_name) if respond_to?(action_callback_method_name, true) end - def method_missing(method, *arguments) + def method_missing(method, *arguments, &block) return if @controller.nil? - @controller.__send__(method, *arguments) + @controller.__send__(method, *arguments, &block) end end end diff --git a/actionpack/lib/action_controller/http_authentication.rb b/actionpack/lib/action_controller/http_authentication.rb index 2ed810db7d..5d915fda08 100644 --- a/actionpack/lib/action_controller/http_authentication.rb +++ b/actionpack/lib/action_controller/http_authentication.rb @@ -1,42 +1,42 @@ module ActionController module HttpAuthentication # Makes it dead easy to do HTTP Basic authentication. - # + # # Simple Basic example: - # + # # class PostsController < ApplicationController # USER_NAME, PASSWORD = "dhh", "secret" - # + # # before_filter :authenticate, :except => [ :index ] - # + # # def index # render :text => "Everyone can see me!" # end - # + # # def edit # render :text => "I'm only accessible if you know the password" # end - # + # # private # def authenticate - # authenticate_or_request_with_http_basic do |user_name, password| + # authenticate_or_request_with_http_basic do |user_name, password| # user_name == USER_NAME && password == PASSWORD # end # end # end - # - # - # Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication, + # + # + # Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication, # the regular HTML interface is protected by a session approach: - # + # # class ApplicationController < ActionController::Base # before_filter :set_account, :authenticate - # + # # protected # def set_account # @account = Account.find_by_url_name(request.subdomains.first) # end - # + # # def authenticate # case request.format # when Mime::XML, Mime::ATOM @@ -54,24 +54,48 @@ module ActionController # end # end # end - # - # + # # In your integration tests, you can do something like this: - # + # # def test_access_granted_from_xml # get( - # "/notes/1.xml", nil, + # "/notes/1.xml", nil, # :authorization => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) # ) - # + # # assert_equal 200, status # end - # - # + # + # Simple Digest example: + # + # class PostsController < ApplicationController + # USERS = {"dhh" => "secret"} + # + # before_filter :authenticate, :except => [:index] + # + # def index + # render :text => "Everyone can see me!" + # end + # + # def edit + # render :text => "I'm only accessible if you know the password" + # end + # + # private + # def authenticate + # authenticate_or_request_with_http_digest(realm) do |username| + # USERS[username] + # end + # end + # end + # + # NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password so the framework can appropriately + # hash it to check the user's credentials. Returning +nil+ will cause authentication to fail. + # # On shared hosts, Apache sometimes doesn't pass authentication headers to # FCGI instances. If your environment matches this description and you cannot # authenticate, try this rule in your Apache setup: - # + # # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Basic extend self @@ -99,14 +123,14 @@ module ActionController def user_name_and_password(request) decode_credentials(request).split(/:/, 2) end - + def authorization(request) request.env['HTTP_AUTHORIZATION'] || request.env['X-HTTP_AUTHORIZATION'] || request.env['X_HTTP_AUTHORIZATION'] || request.env['REDIRECT_X_HTTP_AUTHORIZATION'] end - + def decode_credentials(request) ActiveSupport::Base64.decode64(authorization(request).split.last || '') end @@ -120,5 +144,131 @@ module ActionController controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized end end + + module Digest + extend self + + module ControllerMethods + def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure) + authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm) + end + + # Authenticate with HTTP Digest, returns true or false + def authenticate_with_http_digest(realm = "Application", &password_procedure) + HttpAuthentication::Digest.authenticate(self, realm, &password_procedure) + end + + # Render output including the HTTP Digest authentication header + def request_http_digest_authentication(realm = "Application", message = nil) + HttpAuthentication::Digest.authentication_request(self, realm, message) + end + end + + # Returns false on a valid response, true otherwise + def authenticate(controller, realm, &password_procedure) + authorization(controller.request) && validate_digest_response(controller.request, realm, &password_procedure) + end + + def authorization(request) + request.env['HTTP_AUTHORIZATION'] || + request.env['X-HTTP_AUTHORIZATION'] || + request.env['X_HTTP_AUTHORIZATION'] || + request.env['REDIRECT_X_HTTP_AUTHORIZATION'] + end + + # Raises error unless the request credentials response value matches the expected value. + def validate_digest_response(request, realm, &password_procedure) + credentials = decode_credentials_header(request) + valid_nonce = validate_nonce(request, credentials[:nonce]) + + if valid_nonce && realm == credentials[:realm] && opaque(request.session.session_id) == credentials[:opaque] + password = password_procedure.call(credentials[:username]) + expected = expected_response(request.env['REQUEST_METHOD'], request.url, credentials, password) + expected == credentials[:response] + end + end + + # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+ + def expected_response(http_method, uri, credentials, password) + ha1 = ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':')) + ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(':')) + ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(':')) + end + + def encode_credentials(http_method, credentials, password) + credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password) + "Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ') + end + + def decode_credentials_header(request) + decode_credentials(authorization(request)) + end + + def decode_credentials(header) + header.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair| + key, value = pair.split('=', 2) + hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '') + hash + end + end + + def authentication_header(controller, realm) + session_id = controller.request.session.session_id + controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce(session_id)}", opaque="#{opaque(session_id)}") + end + + def authentication_request(controller, realm, message = nil) + message ||= "HTTP Digest: Access denied.\n" + authentication_header(controller, realm) + controller.__send__ :render, :text => message, :status => :unauthorized + end + + # Uses an MD5 digest based on time to generate a value to be used only once. + # + # A server-specified data string which should be uniquely generated each time a 401 response is made. + # It is recommended that this string be base64 or hexadecimal data. + # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed. + # + # The contents of the nonce are implementation dependent. + # The quality of the implementation depends on a good choice. + # A nonce might, for example, be constructed as the base 64 encoding of + # + # => time-stamp H(time-stamp ":" ETag ":" private-key) + # + # where time-stamp is a server-generated time or other non-repeating value, + # ETag is the value of the HTTP ETag header associated with the requested entity, + # and private-key is data known only to the server. + # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and + # reject the request if it did not match the nonce from that header or + # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity. + # The inclusion of the ETag prevents a replay request for an updated version of the resource. + # (Note: including the IP address of the client in the nonce would appear to offer the server the ability + # to limit the reuse of the nonce to the same client that originally got it. + # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm. + # Also, IP address spoofing is not that hard.) + # + # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to + # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for + # POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4 + # of this document. + # + # The nonce is opaque to the client. + def nonce(session_id, time = Time.now) + t = time.to_i + hashed = [t, session_id] + digest = ::Digest::MD5.hexdigest(hashed.join(":")) + Base64.encode64("#{t}:#{digest}").gsub("\n", '') + end + + def validate_nonce(request, value) + t = Base64.decode64(value).split(":").first.to_i + nonce(request.session.session_id, t) == value && (t - Time.now.to_i).abs <= 10 * 60 + end + + # Opaque based on digest of session_id + def opaque(session_id) + Base64.encode64(::Digest::MD5::hexdigest(session_id)).gsub("\n", '') + end + end end end diff --git a/actionpack/lib/action_controller/layout.rb b/actionpack/lib/action_controller/layout.rb index 159c5c7326..183d56c2e8 100644 --- a/actionpack/lib/action_controller/layout.rb +++ b/actionpack/lib/action_controller/layout.rb @@ -179,7 +179,7 @@ module ActionController #:nodoc: end def layout_list #:nodoc: - Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] } + Array(view_paths).sum([]) { |path| Dir["#{path.to_str}/layouts/**/*"] } end def find_layout(layout, *formats) #:nodoc: diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index f4485f6941..a8729bb6f5 100755 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -7,7 +7,6 @@ require 'action_controller/cgi_ext' module ActionController class Request < Rack::Request - extend ActiveSupport::Memoizable %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_TRANSLATED REMOTE_HOST @@ -33,9 +32,8 @@ module ActionController # <tt>:get</tt>. If the request \method is not listed in the HTTP_METHODS # constant above, an UnknownHttpMethod exception is raised. def request_method - HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}") + @request_method ||= HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}") end - memoize :request_method # Returns the HTTP request \method used for action processing as a # lowercase symbol, such as <tt>:post</tt>. (Unlike #request_method, this @@ -75,9 +73,8 @@ module ActionController # # request.headers["Content-Type"] # => "text/plain" def headers - ActionController::Http::Headers.new(@env) + @headers ||= ActionController::Http::Headers.new(@env) end - memoize :headers # Returns the content length of the request as an integer. def content_length @@ -89,32 +86,33 @@ module ActionController # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. def content_type - if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ - Mime::Type.lookup($1.strip.downcase) - else - nil + @content_type ||= begin + if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ + Mime::Type.lookup($1.strip.downcase) + else + nil + end end end - memoize :content_type # Returns the accepted MIME type for the request. def accepts - header = @env['HTTP_ACCEPT'].to_s.strip + @accepts ||= begin + header = @env['HTTP_ACCEPT'].to_s.strip - if header.empty? - [content_type, Mime::ALL].compact - else - Mime::Type.parse(header) + if header.empty? + [content_type, Mime::ALL].compact + else + Mime::Type.parse(header) + end end end - memoize :accepts def if_modified_since if since = env['HTTP_IF_MODIFIED_SINCE'] Time.rfc2822(since) rescue nil end end - memoize :if_modified_since def if_none_match env['HTTP_IF_NONE_MATCH'] @@ -257,25 +255,21 @@ EOM @env['REMOTE_ADDR'] end - memoize :remote_ip # Returns the lowercase name of the HTTP server software. def server_software (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil end - memoize :server_software # Returns the complete URL used for this request. def url protocol + host_with_port + request_uri end - memoize :url # Returns 'https://' if this is an SSL request and 'http://' otherwise. def protocol ssl? ? 'https://' : 'http://' end - memoize :protocol # Is this an SSL request? def ssl? @@ -295,14 +289,12 @@ EOM def host raw_host_with_port.sub(/:\d+$/, '') end - memoize :host # Returns a \host:\port string for this request, such as "example.com" or # "example.com:8080". def host_with_port "#{host}#{port_string}" end - memoize :host_with_port # Returns the port number of this request as an integer. def port @@ -312,7 +304,6 @@ EOM standard_port end end - memoize :port # Returns the standard \port number for this request's protocol. def standard_port @@ -350,7 +341,6 @@ EOM def query_string @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '') end - memoize :query_string # Returns the request URI, accounting for server idiosyncrasies. # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. @@ -376,7 +366,6 @@ EOM end end end - memoize :request_uri # Returns the interpreted \path to requested resource after all the installation # directory of this application was taken into account. @@ -385,7 +374,6 @@ EOM path.sub!(/\A#{ActionController::Base.relative_url_root}/, '') path end - memoize :path # Read the request \body. This is useful for web services that need to # work with raw requests directly. diff --git a/actionpack/lib/action_controller/rewindable_input.rb b/actionpack/lib/action_controller/rewindable_input.rb index 36f655c51e..cedfb7fd75 100644 --- a/actionpack/lib/action_controller/rewindable_input.rb +++ b/actionpack/lib/action_controller/rewindable_input.rb @@ -3,12 +3,12 @@ module ActionController class RewindableIO < ActiveSupport::BasicObject def initialize(io) @io = io - @rewindable = io.is_a?(StringIO) + @rewindable = io.is_a?(::StringIO) end def method_missing(method, *args, &block) unless @rewindable - @io = StringIO.new(@io.read) + @io = ::StringIO.new(@io.read) @rewindable = true end diff --git a/actionpack/lib/action_controller/session/abstract_store.rb b/actionpack/lib/action_controller/session/abstract_store.rb index bf09fd33c5..9434c2e05e 100644 --- a/actionpack/lib/action_controller/session/abstract_store.rb +++ b/actionpack/lib/action_controller/session/abstract_store.rb @@ -102,8 +102,10 @@ module ActionController response = @app.call(env) session_data = env[ENV_SESSION_KEY] - if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) - options = env[ENV_SESSION_OPTIONS_KEY] + options = env[ENV_SESSION_OPTIONS_KEY] + + if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after] + session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?) if session_data.is_a?(AbstractStore::SessionHash) sid = session_data.id diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb index 6ad6369950..5a728d1877 100644 --- a/actionpack/lib/action_controller/session/cookie_store.rb +++ b/actionpack/lib/action_controller/session/cookie_store.rb @@ -93,12 +93,14 @@ module ActionController status, headers, body = @app.call(env) session_data = env[ENV_SESSION_KEY] - if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) + options = env[ENV_SESSION_OPTIONS_KEY] + + if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after] + session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?) session_data = marshal(session_data.to_hash) raise CookieOverflow if session_data.size > MAX - options = env[ENV_SESSION_OPTIONS_KEY] cookie = Hash.new cookie[:value] = session_data unless options[:expire_after].nil? diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb index 22b97fc157..4b5fc3a3c1 100644 --- a/actionpack/lib/action_controller/test_process.rb +++ b/actionpack/lib/action_controller/test_process.rb @@ -15,7 +15,7 @@ module ActionController #:nodoc: end def reset_session - @session = TestSession.new + @session.reset end # Wraps raw_post in a StringIO. @@ -35,7 +35,6 @@ module ActionController #:nodoc: def port=(number) @env["SERVER_PORT"] = number.to_i - port(true) end def action=(action_name) @@ -49,8 +48,6 @@ module ActionController #:nodoc: @env["REQUEST_URI"] = value @request_uri = nil @path = nil - request_uri(true) - path(true) end def request_uri=(uri) @@ -58,9 +55,13 @@ module ActionController #:nodoc: @path = uri.split("?").first end + def request_method=(method) + @request_method = method + end + def accept=(mime_types) @env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",") - accepts(true) + @accepts = nil end def if_modified_since=(last_modified) @@ -76,11 +77,11 @@ module ActionController #:nodoc: end def request_uri(*args) - @request_uri || super + @request_uri || super() end def path(*args) - @path || super + @path || super() end def assign_parameters(controller_path, action, parameters) @@ -107,7 +108,7 @@ module ActionController #:nodoc: def recycle! self.query_parameters = {} self.path_parameters = {} - unmemoize_all + @headers, @request_method, @accepts, @content_type = nil, nil, nil, nil end def user_agent=(user_agent) @@ -279,38 +280,62 @@ module ActionController #:nodoc: end end - class TestSession #:nodoc: + class TestSession < Hash #:nodoc: attr_accessor :session_id def initialize(attributes = nil) - @session_id = '' - @attributes = attributes.nil? ? nil : attributes.stringify_keys - @saved_attributes = nil + reset_session_id + replace_attributes(attributes) + end + + def reset + reset_session_id + replace_attributes({ }) end def data - @attributes ||= @saved_attributes || {} + to_hash end def [](key) - data[key.to_s] + super(key.to_s) end def []=(key, value) - data[key.to_s] = value + super(key.to_s, value) end - def update - @saved_attributes = @attributes + def update(hash = nil) + if hash.nil? + ActiveSupport::Deprecation.warn('use replace instead', caller) + replace({}) + else + super(hash) + end end - def delete - @attributes = nil + def delete(key = nil) + if key.nil? + ActiveSupport::Deprecation.warn('use clear instead', caller) + clear + else + super(key.to_s) + end end def close - update - delete + ActiveSupport::Deprecation.warn('sessions should no longer be closed', caller) + end + + private + + def reset_session_id + @session_id = '' + end + + def replace_attributes(attributes = nil) + attributes ||= {} + replace(attributes.stringify_keys) end end diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb index d86e2db67d..bb6cb437b7 100644 --- a/actionpack/lib/action_controller/url_rewriter.rb +++ b/actionpack/lib/action_controller/url_rewriter.rb @@ -92,15 +92,12 @@ module ActionController # end # end module UrlWriter - # The default options for urls written by this writer. Typically a <tt>:host</tt> - # pair is provided. - mattr_accessor :default_url_options - self.default_url_options = {} - def self.included(base) #:nodoc: ActionController::Routing::Routes.install_helpers(base) base.mattr_accessor :default_url_options - base.default_url_options ||= default_url_options + + # The default options for urls written by this writer. Typically a <tt>:host</tt> pair is provided. + base.default_url_options ||= {} end # Generate a url based on the options provided, default_url_options and the |