From 19c3495a671c364e0dc76c276efbcd9dc6914c74 Mon Sep 17 00:00:00 2001 From: Yehuda Katz + Carl Lerche Date: Mon, 15 Jun 2009 16:29:45 -0700 Subject: rm -r controller/base! --- actionpack/lib/action_controller/base/cookies.rb | 94 +++++++ .../base/filter_parameter_logging.rb | 95 +++++++ actionpack/lib/action_controller/base/flash.rb | 196 +++++++++++++ .../action_controller/base/http_authentication.rb | 309 +++++++++++++++++++++ .../lib/action_controller/base/mime_responds.rb | 188 +++++++++++++ .../base/request_forgery_protection.rb | 123 ++++++++ .../action_controller/base/session_management.rb | 54 ++++ actionpack/lib/action_controller/base/streaming.rb | 188 +++++++++++++ .../lib/action_controller/base/verification.rb | 133 +++++++++ 9 files changed, 1380 insertions(+) create mode 100644 actionpack/lib/action_controller/base/cookies.rb create mode 100644 actionpack/lib/action_controller/base/filter_parameter_logging.rb create mode 100644 actionpack/lib/action_controller/base/flash.rb create mode 100644 actionpack/lib/action_controller/base/http_authentication.rb create mode 100644 actionpack/lib/action_controller/base/mime_responds.rb create mode 100644 actionpack/lib/action_controller/base/request_forgery_protection.rb create mode 100644 actionpack/lib/action_controller/base/session_management.rb create mode 100644 actionpack/lib/action_controller/base/streaming.rb create mode 100644 actionpack/lib/action_controller/base/verification.rb (limited to 'actionpack/lib/action_controller/base') diff --git a/actionpack/lib/action_controller/base/cookies.rb b/actionpack/lib/action_controller/base/cookies.rb new file mode 100644 index 0000000000..d4806623c3 --- /dev/null +++ b/actionpack/lib/action_controller/base/cookies.rb @@ -0,0 +1,94 @@ +module ActionController #:nodoc: + # 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 for writing: + # + # # Sets a simple session cookie. + # cookies[:user_name] = "david" + # + # # Sets a cookie that expires in 1 hour. + # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } + # + # Examples for reading: + # + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # + # 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[:key] = { + # :value => 'a yummy cookie', + # :expires => 1.year.from_now, + # :domain => 'domain.com' + # } + # + # cookies.delete(:key, :domain => 'domain.com') + # + # The option symbols for setting cookies are: + # + # * :value - The cookie's value or list of values (as an array). + # * :path - The path for which this cookie applies. Defaults to the root + # of the application. + # * :domain - The domain for which this cookie applies. + # * :expires - The time at which this cookie expires, as a Time object. + # * :secure - Whether this cookie is a only transmitted to HTTPS servers. + # Default is +false+. + # * :httponly - Whether this cookie is accessible via scripting or + # only HTTP. Defaults to +false+. + module Cookies + def self.included(base) + base.helper_method :cookies + end + + protected + # Returns the cookie container, which operates as described above. + def cookies + @cookies ||= CookieJar.new(self) + end + end + + class CookieJar < Hash #:nodoc: + def initialize(controller) + @controller, @cookies = controller, controller.request.cookies + super() + update(@cookies) + end + + # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists. + def [](name) + super(name.to_s) + end + + # Sets the cookie named +name+. The second argument may be the very cookie + # value, or a hash of options as documented above. + def []=(key, options) + if options.is_a?(Hash) + options.symbolize_keys! + else + options = { :value => options } + end + + options[:path] = "/" unless options.has_key?(:path) + super(key.to_s, options[:value]) + @controller.response.set_cookie(key, options) + end + + # Removes the cookie on the client machine by setting the value to an empty string + # and setting its expiration date into the past. Like []=, you can pass in + # an options hash to delete cookies with extra data such as a :path. + def delete(key, options = {}) + options.symbolize_keys! + options[:path] = "/" unless options.has_key?(:path) + super(key.to_s) + @controller.response.delete_cookie(key, options) + end + end +end diff --git a/actionpack/lib/action_controller/base/filter_parameter_logging.rb b/actionpack/lib/action_controller/base/filter_parameter_logging.rb new file mode 100644 index 0000000000..8370ba6fc0 --- /dev/null +++ b/actionpack/lib/action_controller/base/filter_parameter_logging.rb @@ -0,0 +1,95 @@ +module ActionController + module FilterParameterLogging + extend ActiveSupport::Concern + + # TODO : Remove the defined? check when new base is the main base + if defined?(ActionController::Http) + include AbstractController::Logger + end + + included do + include InstanceMethodsForNewBase + end + + module ClassMethods + # Replace sensitive parameter data from the request log. + # Filters parameters that have any of the arguments as a substring. + # Looks in all subhashes of the param hash for keys to filter. + # If a block is given, each key and value of the parameter hash and all + # subhashes is passed to it, the value or key + # can be replaced using String#replace or similar method. + # + # Examples: + # filter_parameter_logging + # => Does nothing, just slows the logging process down + # + # filter_parameter_logging :password + # => replaces the value to all keys matching /password/i with "[FILTERED]" + # + # filter_parameter_logging :foo, "bar" + # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" + # + # filter_parameter_logging { |k,v| v.reverse! if k =~ /secret/i } + # => reverses the value to all keys matching /secret/i + # + # filter_parameter_logging(:foo, "bar") { |k,v| v.reverse! if k =~ /secret/i } + # => reverses the value to all keys matching /secret/i, and + # replaces the value to all keys matching /foo|bar/i with "[FILTERED]" + def filter_parameter_logging(*filter_words, &block) + parameter_filter = Regexp.new(filter_words.collect{ |s| s.to_s }.join('|'), true) if filter_words.length > 0 + + define_method(:filter_parameters) do |unfiltered_parameters| + filtered_parameters = {} + + unfiltered_parameters.each do |key, value| + if key =~ parameter_filter + filtered_parameters[key] = '[FILTERED]' + elsif value.is_a?(Hash) + filtered_parameters[key] = filter_parameters(value) + elsif block_given? + key = key.dup + value = value.dup if value + yield key, value + filtered_parameters[key] = value + else + filtered_parameters[key] = value + end + end + + filtered_parameters + end + protected :filter_parameters + end + end + + module InstanceMethodsForNewBase + # TODO : Fix the order of information inside such that it's exactly same as the old base + def process(*) + ret = super + + if logger + parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup + parameters = parameters.except!(:controller, :action, :format, :_method, :only_path) + + unless parameters.empty? + # TODO : Move DelayedLog to AS + log = AbstractController::Logger::DelayedLog.new { " Parameters: #{parameters.inspect}" } + logger.info(log) + end + end + + ret + end + end + + private + + # TODO : This method is not needed for the new base + def log_processing_for_parameters + parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup + parameters = parameters.except!(:controller, :action, :format, :_method) + + logger.info " Parameters: #{parameters.inspect}" unless parameters.empty? + end + end +end diff --git a/actionpack/lib/action_controller/base/flash.rb b/actionpack/lib/action_controller/base/flash.rb new file mode 100644 index 0000000000..42c6e430ca --- /dev/null +++ b/actionpack/lib/action_controller/base/flash.rb @@ -0,0 +1,196 @@ +module ActionController #:nodoc: + # 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 flash[:notice] = "Successfully created" before redirecting to a display action that can + # then expose the flash to its template. Actually, that exposure is automatically done. Example: + # + # class PostsController < ActionController::Base + # def create + # # save post + # flash[:notice] = "Successfully created post" + # redirect_to posts_path(@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] %> + #
<%= flash[:notice] %>
+ # <% end %> + # + # This example just places a string in the flash, but you can put any object in there. And of course, you can put as + # many as you like at a time too. 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. + module Flash + extend ActiveSupport::Concern + + # TODO : Remove the defined? check when new base is the main base + include Session if defined?(ActionController::Http) + + included do + # TODO : Remove the defined? check when new base is the main base + if defined?(ActionController::Http) + include InstanceMethodsForNewBase + else + include InstanceMethodsForBase + + alias_method_chain :perform_action, :flash + alias_method_chain :reset_session, :flash + end + end + + class FlashNow #:nodoc: + def initialize(flash) + @flash = flash + end + + def []=(k, v) + @flash[k] = v + @flash.discard(k) + v + end + + def [](k) + @flash[k] + end + end + + class FlashHash < Hash + def initialize #:nodoc: + super + @used = {} + end + + def []=(k, v) #:nodoc: + keep(k) + super + end + + def update(h) #:nodoc: + h.keys.each { |k| keep(k) } + super + end + + alias :merge! :update + + def replace(h) #:nodoc: + @used = {} + super + 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 ([]=). + # When you need to pass an object to the current action, you use now, and your object will + # vanish when the current action is done. + # + # Entries set via now are accessed the same way as standard entries: flash['my-key']. + def 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) + use(k, false) + 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) + use(k) + 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[k] + use(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 - keys).each{ |k| @used.delete(k) } + end + + def store(session, key = "flash") + return if self.empty? + session[key] = self + end + + private + # Used internally by the keep and discard 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[k] = used } + return key ? self[key] : self + end + end + + module InstanceMethodsForBase #:nodoc: + protected + def perform_action_with_flash + perform_action_without_flash + if defined? @_flash + @_flash.store(session) + remove_instance_variable(:@_flash) + end + end + + def reset_session_with_flash + reset_session_without_flash + remove_instance_variable(:@_flash) if defined?(@_flash) + end + end + + module InstanceMethodsForNewBase #:nodoc: + protected + def process_action(method_name) + super + if defined? @_flash + @_flash.store(session) + remove_instance_variable(:@_flash) + end + end + + def reset_session + super + remove_instance_variable(:@_flash) if defined?(@_flash) + end + end + + protected + # Access the contents of the flash. Use flash["notice"] to + # read a notice you put there or flash["notice"] = "hello" + # to put a new one. + def flash #:doc: + if !defined?(@_flash) + @_flash = session["flash"] || FlashHash.new + @_flash.sweep + end + + @_flash + end + end +end diff --git a/actionpack/lib/action_controller/base/http_authentication.rb b/actionpack/lib/action_controller/base/http_authentication.rb new file mode 100644 index 0000000000..2519f55269 --- /dev/null +++ b/actionpack/lib/action_controller/base/http_authentication.rb @@ -0,0 +1,309 @@ +require 'active_support/base64' + +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| + # 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, + # 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 + # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) } + # @current_user = user + # else + # request_http_basic_authentication + # end + # else + # if session_authenticated? + # @current_user = @account.users.find(session[:authenticated][:user_id]) + # else + # redirect_to(login_url) and return false + # end + # end + # end + # end + # + # In your integration tests, you can do something like this: + # + # def test_access_granted_from_xml + # get( + # "/notes/1.xml", nil, + # :authorization => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) + # ) + # + # assert_equal 200, status + # end + # + # Simple Digest example: + # + # require 'digest/md5' + # class PostsController < ApplicationController + # REALM = "SuperSecret" + # USERS = {"dhh" => "secret", #plain text password + # "dap" => Digest:MD5::hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password + # + # 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 or the ha1 digest hash so the framework can appropriately + # hash to check the user's credentials. Returning +nil+ will cause authentication to fail. + # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If + # the password file or database is compromised, the attacker would be able to use the ha1 hash to + # authenticate as the user at this +realm+, but would not have the user's password to try using at + # other sites. + # + # 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 + + module ControllerMethods + def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) + authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) + end + + def authenticate_with_http_basic(&login_procedure) + HttpAuthentication::Basic.authenticate(self, &login_procedure) + end + + def request_http_basic_authentication(realm = "Application") + HttpAuthentication::Basic.authentication_request(self, realm) + end + end + + def authenticate(controller, &login_procedure) + unless authorization(controller.request).blank? + login_procedure.call(*user_name_and_password(controller.request)) + end + end + + 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 + + def encode_credentials(user_name, password) + "Basic #{ActiveSupport::Base64.encode64("#{user_name}:#{password}")}" + end + + def authentication_request(controller, realm) + controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}") + 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 + + # Returns false unless the request credentials response value matches the expected value. + # First try the password as a ha1 digest password. If this fails, then try it as a plain + # text password. + 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 == credentials[:opaque] + password = password_procedure.call(credentials[:username]) + return false unless password + + method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD'] + + [true, false].any? do |password_is_ha1| + expected = expected_response(method, request.env['REQUEST_URI'], credentials, password, password_is_ha1) + expected == credentials[:response] + end + end + end + + # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+ + # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead + # of a plain-text password. + def expected_response(http_method, uri, credentials, password, password_is_ha1=true) + ha1 = password_is_ha1 ? password : ha1(credentials, password) + 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 ha1(credentials, password) + ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':')) + end + + def encode_credentials(http_method, credentials, password, password_is_ha1) + credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1) + "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) + controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}") + 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. Composed of Time, and hash of Time with secret + # key from the Rails session secret generated upon creation of project. Ensures + # the time cannot be modifed by client. + def nonce(time = Time.now) + t = time.to_i + hashed = [t, secret_key] + digest = ::Digest::MD5.hexdigest(hashed.join(":")) + ActiveSupport::Base64.encode64("#{t}:#{digest}").gsub("\n", '') + end + + # Might want a shorter timeout depending on whether the request + # is a PUT or POST, and if client is browser or web service. + # Can be much shorter if the Stale directive is implemented. This would + # allow a user to use new nonce without prompting user again for their + # username and password. + def validate_nonce(request, value, seconds_to_timeout=5*60) + t = ActiveSupport::Base64.decode64(value).split(":").first.to_i + nonce(t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout + end + + # Opaque based on random generation - but changing each request? + def opaque() + ::Digest::MD5.hexdigest(secret_key) + end + + # Set in /initializers/session_store.rb, and loaded even if sessions are not in use. + def secret_key + ActionController::Base.session_options[:secret] + end + + end + end +end diff --git a/actionpack/lib/action_controller/base/mime_responds.rb b/actionpack/lib/action_controller/base/mime_responds.rb new file mode 100644 index 0000000000..5c7218691e --- /dev/null +++ b/actionpack/lib/action_controller/base/mime_responds.rb @@ -0,0 +1,188 @@ +module ActionController #:nodoc: + module MimeResponds #:nodoc: + # Without web-service support, an action which collects the data for displaying a list of people + # might look something like this: + # + # def index + # @people = Person.find(:all) + # end + # + # Here's the same action, with web-service support baked in: + # + # def index + # @people = Person.find(:all) + # + # respond_to do |format| + # format.html + # format.xml { render :xml => @people.to_xml } + # end + # end + # + # What that says is, "if the client wants HTML in response to this action, just respond as we + # would have before, but if the client wants XML, return them the list of people in XML format." + # (Rails determines the desired response format from the HTTP Accept header submitted by the client.) + # + # Supposing you have an action that adds a new person, optionally creating their company + # (by name) if it does not already exist, without web-services, it might look like this: + # + # def create + # @company = Company.find_or_create_by_name(params[:company][:name]) + # @person = @company.people.create(params[:person]) + # + # redirect_to(person_list_url) + # end + # + # Here's the same action, with web-service support baked in: + # + # def create + # company = params[:person].delete(:company) + # @company = Company.find_or_create_by_name(company[:name]) + # @person = @company.people.create(params[:person]) + # + # respond_to do |format| + # format.html { redirect_to(person_list_url) } + # format.js + # format.xml { render :xml => @person.to_xml(:include => @company) } + # end + # end + # + # If the client wants HTML, we just redirect them back to the person list. If they want Javascript + # (format.js), then it is an RJS request and we render the RJS template associated with this action. + # Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also + # include the person's company in the rendered XML, so you get something like this: + # + # + # ... + # ... + # + # ... + # ... + # ... + # + # + # + # Note, however, the extra bit at the top of that action: + # + # company = params[:person].delete(:company) + # @company = Company.find_or_create_by_name(company[:name]) + # + # This is because the incoming XML document (if a web-service request is in process) can only contain a + # single root-node. So, we have to rearrange things so that the request looks like this (url-encoded): + # + # person[name]=...&person[company][name]=...&... + # + # And, like this (xml-encoded): + # + # + # ... + # + # ... + # + # + # + # In other words, we make the request so that it operates on a single entity's person. Then, in the action, + # we extract the company data from the request, find or create the company, and then create the new person + # with the remaining data. + # + # Note that you can define your own XML parameter parser which would allow you to describe multiple entities + # in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow + # and accept Rails' defaults, life will be much easier. + # + # If you need to use a MIME type which isn't supported by default, you can register your own handlers in + # environment.rb as follows. + # + # Mime::Type.register "image/jpg", :jpg + def respond_to(*types, &block) + raise ArgumentError, "respond_to takes either types or a block, never both" unless types.any? ^ block + block ||= lambda { |responder| types.each { |type| responder.send(type) } } + responder = Responder.new(self) + block.call(responder) + responder.respond + end + + class Responder #:nodoc: + + def initialize(controller) + @controller = controller + @request = controller.request + @response = controller.response + + @mime_type_priority = @request.formats + + @order = [] + @responses = {} + end + + def custom(mime_type, &block) + mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s) + + @order << mime_type + + @responses[mime_type] ||= Proc.new do + # TODO: Remove this when new base is merged in + if defined?(Http) + @controller.formats = [mime_type.to_sym] + end + + @controller.template.formats = [mime_type.to_sym] + @response.content_type = mime_type + + block_given? ? block.call : @controller.send(:render, :action => @controller.action_name) + end + end + + def any(*args, &block) + if args.any? + args.each { |type| send(type, &block) } + else + custom(@mime_type_priority.first, &block) + end + end + + def self.generate_method_for_mime(mime) + sym = mime.is_a?(Symbol) ? mime : mime.to_sym + const = sym.to_s.upcase + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{sym}(&block) # def html(&block) + custom(Mime::#{const}, &block) # custom(Mime::HTML, &block) + end # end + RUBY + end + + Mime::SET.each do |mime| + generate_method_for_mime(mime) + end + + def method_missing(symbol, &block) + mime_constant = Mime.const_get(symbol.to_s.upcase) + + if Mime::SET.include?(mime_constant) + self.class.generate_method_for_mime(mime_constant) + send(symbol, &block) + else + super + end + end + + def respond + for priority in @mime_type_priority + if priority == Mime::ALL + @responses[@order.first].call + return + else + if @responses[priority] + @responses[priority].call + return # mime type match found, be happy and return + end + end + end + + if @order.include?(Mime::ALL) + @responses[Mime::ALL].call + else + @controller.send :head, :not_acceptable + end + end + end + end +end diff --git a/actionpack/lib/action_controller/base/request_forgery_protection.rb b/actionpack/lib/action_controller/base/request_forgery_protection.rb new file mode 100644 index 0000000000..a470c8eec1 --- /dev/null +++ b/actionpack/lib/action_controller/base/request_forgery_protection.rb @@ -0,0 +1,123 @@ +module ActionController #:nodoc: + class InvalidAuthenticityToken < ActionControllerError #:nodoc: + end + + module RequestForgeryProtection + extend ActiveSupport::Concern + + # TODO : Remove the defined? check when new base is the main base + if defined?(ActionController::Http) + include AbstractController::Helpers, Session + end + + included do + if defined?(ActionController::Http) + # Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+ + # sets it to :authenticity_token by default. + cattr_accessor :request_forgery_protection_token + + # Controls whether request forgergy protection is turned on or not. Turned off by default only in test mode. + class_inheritable_accessor :allow_forgery_protection + self.allow_forgery_protection = true + end + + helper_method :form_authenticity_token + helper_method :protect_against_forgery? + end + + # Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current web application, not a + # forged link from another site, is done by embedding a token based on a random string stored in the session (which an attacker wouldn't know) in all + # forms and Ajax requests generated by Rails and then verifying the authenticity of that token in the controller. Only + # HTML/JavaScript requests are checked, so this will not protect your XML API (presumably you'll have a different authentication + # scheme there anyway). Also, GET requests are not protected as these should be idempotent anyway. + # + # This is turned on with the protect_from_forgery method, which will check the token and raise an + # ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the error message in + # production by editing public/422.html. A call to this method in ApplicationController is generated by default in post-Rails 2.0 + # applications. + # + # The token parameter is named authenticity_token by default. If you are generating an HTML form manually (without the + # use of Rails' form_for, form_tag or other helpers), you have to include a hidden field named like that and + # set its value to what is returned by form_authenticity_token. Same applies to manually constructed Ajax requests. To + # make the token available through a global variable to scripts on a certain page, you could add something like this to a view: + # + # <%= javascript_tag "window._token = '#{form_authenticity_token}'" %> + # + # Request forgery protection is disabled by default in test environment. If you are upgrading from Rails 1.x, add this to + # config/environments/test.rb: + # + # # Disable request forgery protection in test environment + # config.action_controller.allow_forgery_protection = false + # + # == Learn more about CSRF (Cross-Site Request Forgery) attacks + # + # Here are some resources: + # * http://isc.sans.org/diary.html?storyid=1750 + # * http://en.wikipedia.org/wiki/Cross-site_request_forgery + # + # Keep in mind, this is NOT a silver-bullet, plug 'n' play, warm security blanket for your rails application. + # There are a few guidelines you should follow: + # + # * Keep your GET requests safe and idempotent. More reading material: + # * http://www.xml.com/pub/a/2002/04/24/deviant.html + # * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 + # * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look for "Expires: at end of session" + # + module ClassMethods + # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. + # + # Example: + # + # class FooController < ApplicationController + # protect_from_forgery :except => :index + # + # # you can disable csrf protection on controller-by-controller basis: + # skip_before_filter :verify_authenticity_token + # end + # + # Valid Options: + # + # * :only/:except - Passed to the before_filter call. Set which actions are verified. + def protect_from_forgery(options = {}) + self.request_forgery_protection_token ||= :authenticity_token + before_filter :verify_authenticity_token, :only => options.delete(:only), :except => options.delete(:except) + if options[:secret] || options[:digest] + ActiveSupport::Deprecation.warn("protect_from_forgery only takes :only and :except options now. :digest and :secret have no effect", caller) + end + end + end + + protected + # The actual before_filter that is used. Modify this to change how you handle unverified requests. + def verify_authenticity_token + verified_request? || raise(ActionController::InvalidAuthenticityToken) + end + + # Returns true or false if a request is verified. Checks: + # + # * is the format restricted? By default, only HTML requests are checked. + # * is it a GET request? Gets should be safe and idempotent + # * Does the form_authenticity_token match the given token value from the params? + def verified_request? + !protect_against_forgery? || + request.method == :get || + request.xhr? || + !verifiable_request_format? || + form_authenticity_token == params[request_forgery_protection_token] + end + + def verifiable_request_format? + !request.content_type.nil? && request.content_type.verify_request? + end + + # Sets the token value for the current session. Pass a :secret option + # in +protect_from_forgery+ to add a custom salt to the hash. + def form_authenticity_token + session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32) + end + + def protect_against_forgery? + allow_forgery_protection && request_forgery_protection_token + end + end +end diff --git a/actionpack/lib/action_controller/base/session_management.rb b/actionpack/lib/action_controller/base/session_management.rb new file mode 100644 index 0000000000..ffce8e1bd1 --- /dev/null +++ b/actionpack/lib/action_controller/base/session_management.rb @@ -0,0 +1,54 @@ +module ActionController #:nodoc: + module SessionManagement #:nodoc: + def self.included(base) + base.class_eval do + extend ClassMethods + end + end + + module ClassMethods + # Set the session store to be used for keeping the session data between requests. + # By default, sessions are stored in browser cookies (:cookie_store), + # but you can also specify one of the other included stores (:active_record_store, + # :mem_cache_store, or your own custom class. + def session_store=(store) + if store == :active_record_store + self.session_store = ActiveRecord::SessionStore + else + @@session_store = store.is_a?(Symbol) ? + Session.const_get(store.to_s.camelize) : + store + end + end + + # Returns the session store class currently used. + def session_store + if defined? @@session_store + @@session_store + else + ActionDispatch::Session::CookieStore + end + end + + def session=(options = {}) + self.session_store = nil if options.delete(:disabled) + session_options.merge!(options) + end + + # Returns the hash used to configure the session. Example use: + # + # ActionController::Base.session_options[:secure] = true # session only available over HTTPS + def session_options + @session_options ||= {} + end + + def session(*args) + ActiveSupport::Deprecation.warn( + "Disabling sessions for a single controller has been deprecated. " + + "Sessions are now lazy loaded. So if you don't access them, " + + "consider them off. You can still modify the session cookie " + + "options with request.session_options.", caller) + end + end + end +end diff --git a/actionpack/lib/action_controller/base/streaming.rb b/actionpack/lib/action_controller/base/streaming.rb new file mode 100644 index 0000000000..5c72fc9ad9 --- /dev/null +++ b/actionpack/lib/action_controller/base/streaming.rb @@ -0,0 +1,188 @@ +module ActionController #:nodoc: + # Methods for sending arbitrary data and for streaming files to the browser, + # instead of rendering. + module Streaming + extend ActiveSupport::Concern + + # TODO : Remove the defined? check when new base is the main base + if defined?(ActionController::Http) + include ActionController::Renderer + end + + DEFAULT_SEND_FILE_OPTIONS = { + :type => 'application/octet-stream'.freeze, + :disposition => 'attachment'.freeze, + :stream => true, + :buffer_size => 4096, + :x_sendfile => false + }.freeze + + X_SENDFILE_HEADER = 'X-Sendfile'.freeze + + protected + # Sends the file, by default streaming it 4096 bytes at a time. This way the + # whole file doesn't need to be read into memory at once. This makes it + # feasible to send even large files. You can optionally turn off streaming + # and send the whole file at once. + # + # Be careful to sanitize the path parameter if it is coming from a web + # page. send_file(params[:path]) allows a malicious user to + # download any file on your server. + # + # Options: + # * :filename - suggests a filename for the browser to use. + # Defaults to File.basename(path). + # * :type - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify + # either a string or a symbol for a registered type register with Mime::Type.register, for example :json + # * :length - used to manually override the length (in bytes) of the content that + # is going to be sent to the client. Defaults to File.size(path). + # * :disposition - specifies whether the file will be shown inline or downloaded. + # Valid values are 'inline' and 'attachment' (default). + # * :stream - whether to send the file to the user agent as it is read (+true+) + # or to read the entire file before sending (+false+). Defaults to +true+. + # * :buffer_size - specifies size (in bytes) of the buffer used to stream the file. + # Defaults to 4096. + # * :status - specifies the status code to send with the response. Defaults to '200 OK'. + # * :url_based_filename - set to +true+ if you want the browser guess the filename from + # the URL, which is necessary for i18n filenames on certain browsers + # (setting :filename overrides this option). + # * :x_sendfile - uses X-Sendfile to send the file when set to +true+. This is currently + # only available with Lighttpd/Apache2 and specific modules installed and activated. Since this + # uses the web server to send the file, this may lower memory consumption on your server and + # it will not block your application for further requests. + # See http://blog.lighttpd.net/articles/2006/07/02/x-sendfile and + # http://tn123.ath.cx/mod_xsendfile/ for details. Defaults to +false+. + # + # The default Content-Type and Content-Disposition headers are + # set to download arbitrary binary files in as many browsers as + # possible. IE versions 4, 5, 5.5, and 6 are all known to have + # a variety of quirks (especially when downloading over SSL). + # + # Simple download: + # + # send_file '/path/to.zip' + # + # Show a JPEG in the browser: + # + # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' + # + # Show a 404 page in the browser: + # + # send_file '/path/to/404.html', :type => 'text/html; charset=utf-8', :status => 404 + # + # Read about the other Content-* HTTP headers if you'd like to + # provide the user with more information (such as Content-Description) in + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11. + # + # Also be aware that the document may be cached by proxies and browsers. + # The Pragma and Cache-Control headers declare how the file may be cached + # by intermediaries. They default to require clients to validate with + # the server before releasing cached responses. See + # http://www.mnot.net/cache_docs/ for an overview of web caching and + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + # for the Cache-Control header spec. + def send_file(path, options = {}) #:doc: + raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path) + + options[:length] ||= File.size(path) + options[:filename] ||= File.basename(path) unless options[:url_based_filename] + send_file_headers! options + + @performed_render = false + + if options[:x_sendfile] + logger.info "Sending #{X_SENDFILE_HEADER} header #{path}" if logger + head options[:status], X_SENDFILE_HEADER => path + else + if options[:stream] + # TODO : Make render :text => proc {} work with the new base + render :status => options[:status], :text => Proc.new { |response, output| + logger.info "Streaming file #{path}" unless logger.nil? + len = options[:buffer_size] || 4096 + File.open(path, 'rb') do |file| + while buf = file.read(len) + output.write(buf) + end + end + } + else + logger.info "Sending file #{path}" unless logger.nil? + File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read } + end + end + end + + # Sends the given binary data to the browser. This method is similar to + # render :text => data, but also allows you to specify whether + # the browser should display the response as a file attachment (i.e. in a + # download dialog) or as inline data. You may also set the content type, + # the apparent file name, and other things. + # + # Options: + # * :filename - suggests a filename for the browser to use. + # * :type - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify + # either a string or a symbol for a registered type register with Mime::Type.register, for example :json + # * :disposition - specifies whether the file will be shown inline or downloaded. + # Valid values are 'inline' and 'attachment' (default). + # * :status - specifies the status code to send with the response. Defaults to '200 OK'. + # + # Generic data download: + # + # send_data buffer + # + # Download a dynamically-generated tarball: + # + # send_data generate_tgz('dir'), :filename => 'dir.tgz' + # + # Display an image Active Record in the browser: + # + # send_data image.data, :type => image.content_type, :disposition => 'inline' + # + # See +send_file+ for more information on HTTP Content-* headers and caching. + # + # Tip: if you want to stream large amounts of on-the-fly generated + # data to the browser, then use render :text => proc { ... } + # instead. See ActionController::Base#render for more information. + def send_data(data, options = {}) #:doc: + logger.info "Sending data #{options[:filename]}" if logger + send_file_headers! options.merge(:length => data.size) + @performed_render = false + render :status => options[:status], :text => data + end + + private + def send_file_headers!(options) + options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) + [:length, :type, :disposition].each do |arg| + raise ArgumentError, ":#{arg} option required" if options[arg].nil? + end + + disposition = options[:disposition].dup || 'attachment' + + disposition <<= %(; filename="#{options[:filename]}") if options[:filename] + + content_type = options[:type] + + if content_type.is_a?(Symbol) + raise ArgumentError, "Unknown MIME type #{options[:type]}" unless Mime::EXTENSION_LOOKUP.key?(content_type.to_s) + self.content_type = Mime::Type.lookup_by_extension(content_type.to_s) + else + self.content_type = content_type + end + + headers.merge!( + 'Content-Length' => options[:length], + 'Content-Disposition' => disposition, + 'Content-Transfer-Encoding' => 'binary' + ) + + # Fix a problem with IE 6.0 on opening downloaded files: + # If Cache-Control: no-cache is set (which Rails does by default), + # IE removes the file it just downloaded from its cache immediately + # after it displays the "open/save" dialog, which means that if you + # hit "open" the file isn't there anymore when the application that + # is called for handling the download is run, so let's workaround that + headers['Cache-Control'] = 'private' if headers['Cache-Control'] == 'no-cache' + end + end +end diff --git a/actionpack/lib/action_controller/base/verification.rb b/actionpack/lib/action_controller/base/verification.rb new file mode 100644 index 0000000000..d87b348ed4 --- /dev/null +++ b/actionpack/lib/action_controller/base/verification.rb @@ -0,0 +1,133 @@ +module ActionController #:nodoc: + module Verification #:nodoc: + extend ActiveSupport::Concern + + # TODO : Remove the defined? check when new base is the main base + if defined?(ActionController::Http) + include AbstractController::Callbacks, Session, Flash, Renderer + end + + # This module provides a class-level method for specifying that certain + # actions are guarded against being called without certain prerequisites + # being met. This is essentially a special kind of before_filter. + # + # An action may be guarded against being invoked without certain request + # parameters being set, or without certain session values existing. + # + # When a verification is violated, values may be inserted into the flash, and + # a specified redirection is triggered. If no specific action is configured, + # verification failures will by default result in a 400 Bad Request response. + # + # Usage: + # + # class GlobalController < ActionController::Base + # # Prevent the #update_settings action from being invoked unless + # # the 'admin_privileges' request parameter exists. The + # # settings action will be redirected to in current controller + # # if verification fails. + # verify :params => "admin_privileges", :only => :update_post, + # :redirect_to => { :action => "settings" } + # + # # Disallow a post from being updated if there was no information + # # submitted with the post, and if there is no active post in the + # # session, and if there is no "note" key in the flash. The route + # # named category_url will be redirected to if verification fails. + # + # verify :params => "post", :session => "post", "flash" => "note", + # :only => :update_post, + # :add_flash => { "alert" => "Failed to create your message" }, + # :redirect_to => :category_url + # + # Note that these prerequisites are not business rules. They do not examine + # the content of the session or the parameters. That level of validation should + # be encapsulated by your domain model or helper methods in the controller. + module ClassMethods + # Verify the given actions so that if certain prerequisites are not met, + # the user is redirected to a different action. The +options+ parameter + # is a hash consisting of the following key/value pairs: + # + # :params:: + # a single key or an array of keys that must be in the params + # hash in order for the action(s) to be safely called. + # :session:: + # a single key or an array of keys that must be in the session + # in order for the action(s) to be safely called. + # :flash:: + # a single key or an array of keys that must be in the flash in order + # for the action(s) to be safely called. + # :method:: + # a single key or an array of keys--any one of which must match the + # current request method in order for the action(s) to be safely called. + # (The key should be a symbol: :get or :post, for + # example.) + # :xhr:: + # true/false option to ensure that the request is coming from an Ajax + # call or not. + # :add_flash:: + # a hash of name/value pairs that should be merged into the session's + # flash if the prerequisites cannot be satisfied. + # :add_headers:: + # a hash of name/value pairs that should be merged into the response's + # headers hash if the prerequisites cannot be satisfied. + # :redirect_to:: + # the redirection parameters to be used when redirecting if the + # prerequisites cannot be satisfied. You can redirect either to named + # route or to the action in some controller. + # :render:: + # the render parameters to be used when the prerequisites cannot be satisfied. + # :only:: + # only apply this verification to the actions specified in the associated + # array (may also be a single value). + # :except:: + # do not apply this verification to the actions specified in the associated + # array (may also be a single value). + def verify(options={}) + before_filter :only => options[:only], :except => options[:except] do |c| + c.__send__ :verify_action, options + end + end + end + + private + + def verify_action(options) #:nodoc: + if prereqs_invalid?(options) + flash.update(options[:add_flash]) if options[:add_flash] + response.headers.merge!(options[:add_headers]) if options[:add_headers] + apply_remaining_actions(options) unless performed? + end + end + + def prereqs_invalid?(options) # :nodoc: + verify_presence_of_keys_in_hash_flash_or_params(options) || + verify_method(options) || + verify_request_xhr_status(options) + end + + def verify_presence_of_keys_in_hash_flash_or_params(options) # :nodoc: + [*options[:params] ].find { |v| v && params[v.to_sym].nil? } || + [*options[:session]].find { |v| session[v].nil? } || + [*options[:flash] ].find { |v| flash[v].nil? } + end + + def verify_method(options) # :nodoc: + [*options[:method]].all? { |v| request.method != v.to_sym } if options[:method] + end + + def verify_request_xhr_status(options) # :nodoc: + request.xhr? != options[:xhr] unless options[:xhr].nil? + end + + def apply_redirect_to(redirect_to_option) # :nodoc: + (redirect_to_option.is_a?(Symbol) && redirect_to_option != :back) ? self.__send__(redirect_to_option) : redirect_to_option + end + + def apply_remaining_actions(options) # :nodoc: + case + when options[:render] ; render(options[:render]) + when options[:redirect_to] ; redirect_to(apply_redirect_to(options[:redirect_to])) + else head(:bad_request) + end + end + end +end \ No newline at end of file -- cgit v1.2.3