diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 157a038b7c2bd37e6135b3c08b009ffcba6803f0..3eb40a586ebe64eef27d100517fe416787187c53 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,20 @@ ## Rails 4.0.0 (unreleased) ## +* `ActionDispatch::IntegrationTest` allows headers and rack env + variables to be passed when performing requests. + Fixes #6513. + + Example: + + get "/success", {}, "HTTP_REFERER" => "http://test.com/", + "Host" => "http://test.com" + + *Yves Senn* + +* Http::Headers respects headers that are not prefixed with HTTP_ + + *Yves Senn* + * Fix incorrectly appended square brackets to a multiple select box if an explicit name has been given and it already ends with "[]" diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index dc04d4577b6599730cf31c63386ce8c15f3108ca..1574518a16929b430a2406cff1a962a21f6235fa 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,38 +1,63 @@ module ActionDispatch module Http class Headers + CGI_VARIABLES = %w( + CONTENT_TYPE CONTENT_LENGTH + HTTPS AUTH_TYPE GATEWAY_INTERFACE + PATH_INFO PATH_TRANSLATED QUERY_STRING + REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER + REQUEST_METHOD SCRIPT_NAME + SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE + ) + HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ + include Enumerable + attr_reader :env def initialize(env = {}) - @headers = env + @env = {} + merge!(env) + end + + def [](key) + @env[env_name(key)] end - def [](header_name) - @headers[env_name(header_name)] + def []=(key, value) + @env[env_name(key)] = value end - def []=(k,v); @headers[k] = v; end - def key?(k); @headers.key? k; end + def key?(key); @env.key? key; end alias :include? :key? - def fetch(header_name, *args, &block) - @headers.fetch env_name(header_name), *args, &block + def fetch(key, *args, &block) + @env.fetch env_name(key), *args, &block end def each(&block) - @headers.each(&block) + @env.each(&block) end - private + def merge(headers_or_env) + headers = Http::Headers.new(env.dup) + headers.merge!(headers_or_env) + headers + end - # Converts a HTTP header name to an environment variable name if it is - # not contained within the headers hash. - def env_name(header_name) - @headers.include?(header_name) ? header_name : cgi_name(header_name) + def merge!(headers_or_env) + headers_or_env.each do |key, value| + self[env_name(key)] = value + end end - def cgi_name(k) - "HTTP_#{k.upcase.gsub(/-/, '_')}" + private + def env_name(key) + key = key.to_s + if key =~ HTTP_HEADER + key = key.upcase.tr('-', '_') + key = "HTTP_" + key unless CGI_VARIABLES.include?(key) + end + key end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index ed4e88aab68c8fb7cb540e373cb3a143c36a570e..ae1b0b5deaef809fe9b230f61f15314c9040fc79 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -17,7 +17,7 @@ module RequestHelpers # a Hash, or a String that is appropriately encoded # (application/x-www-form-urlencoded or # multipart/form-data). - # - +headers+: Additional headers to pass, as a Hash. The headers will be + # - +headers_or_env+: Additional headers to pass, as a Hash. The headers will be # merged into the Rack env hash. # # This method returns a Response object, which one can use to @@ -28,44 +28,44 @@ module RequestHelpers # # You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with # +#post+, +#patch+, +#put+, +#delete+, and +#head+. - def get(path, parameters = nil, headers = nil) - process :get, path, parameters, headers + def get(path, parameters = nil, headers_or_env = nil) + process :get, path, parameters, headers_or_env end # Performs a POST request with the given parameters. See +#get+ for more # details. - def post(path, parameters = nil, headers = nil) - process :post, path, parameters, headers + def post(path, parameters = nil, headers_or_env = nil) + process :post, path, parameters, headers_or_env end # Performs a PATCH request with the given parameters. See +#get+ for more # details. - def patch(path, parameters = nil, headers = nil) - process :patch, path, parameters, headers + def patch(path, parameters = nil, headers_or_env = nil) + process :patch, path, parameters, headers_or_env end # Performs a PUT request with the given parameters. See +#get+ for more # details. - def put(path, parameters = nil, headers = nil) - process :put, path, parameters, headers + def put(path, parameters = nil, headers_or_env = nil) + process :put, path, parameters, headers_or_env end # Performs a DELETE request with the given parameters. See +#get+ for # more details. - def delete(path, parameters = nil, headers = nil) - process :delete, path, parameters, headers + def delete(path, parameters = nil, headers_or_env = nil) + process :delete, path, parameters, headers_or_env end # Performs a HEAD request with the given parameters. See +#get+ for more # details. - def head(path, parameters = nil, headers = nil) - process :head, path, parameters, headers + def head(path, parameters = nil, headers_or_env = nil) + process :head, path, parameters, headers_or_env end # Performs a OPTIONS request with the given parameters. See +#get+ for # more details. - def options(path, parameters = nil, headers = nil) - process :options, path, parameters, headers + def options(path, parameters = nil, headers_or_env = nil) + process :options, path, parameters, headers_or_env end # Performs an XMLHttpRequest request with the given parameters, mirroring @@ -74,11 +74,11 @@ def options(path, parameters = nil, headers = nil) # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart # string; the headers are a hash. - def xml_http_request(request_method, path, parameters = nil, headers = nil) - headers ||= {} - headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') - process(request_method, path, parameters, headers) + def xml_http_request(request_method, path, parameters = nil, headers_or_env = nil) + headers_or_env ||= {} + headers_or_env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + headers_or_env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + process(request_method, path, parameters, headers_or_env) end alias xhr :xml_http_request @@ -95,40 +95,40 @@ def follow_redirect! # redirect. Note that the redirects are followed until the response is # not a redirect--this means you may run into an infinite loop if your # redirect loops back to itself. - def request_via_redirect(http_method, path, parameters = nil, headers = nil) - process(http_method, path, parameters, headers) + def request_via_redirect(http_method, path, parameters = nil, headers_or_env = nil) + process(http_method, path, parameters, headers_or_env) follow_redirect! while redirect? status end # Performs a GET request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def get_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:get, path, parameters, headers) + def get_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:get, path, parameters, headers_or_env) end # Performs a POST request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def post_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:post, path, parameters, headers) + def post_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:post, path, parameters, headers_or_env) end # Performs a PATCH request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def patch_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:patch, path, parameters, headers) + def patch_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:patch, path, parameters, headers_or_env) end # Performs a PUT request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def put_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:put, path, parameters, headers) + def put_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:put, path, parameters, headers_or_env) end # Performs a DELETE request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def delete_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:delete, path, parameters, headers) + def delete_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:delete, path, parameters, headers_or_env) end end @@ -268,8 +268,8 @@ def _mock_session end # Performs the actual request. - def process(method, path, parameters = nil, rack_env = nil) - rack_env ||= {} + def process(method, path, parameters = nil, headers_or_env = nil) + rack_env = Http::Headers.new(headers_or_env || {}).env if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 72b882539c4fd6a0bc5f146d8884f9512eae5913..c3bdf74d93f8eba3ac1f44a1f8f62f5113042490 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -573,6 +573,21 @@ def test_failed_get def test_generate_url_without_controller assert_equal 'http://www.example.com/foo', url_for(:controller => "foo") end + + def test_pass_headers + get "/success", {}, "Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com" + + assert_equal "http://nohost.com", @request.env["HTTP_HOST"] + assert_equal "http://www.example.com/foo", @request.env["HTTP_REFERER"] + end + + def test_pass_env + get "/success", {}, "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" + + assert_equal "http://test.com", @request.env["HTTP_HOST"] + assert_equal "http://test.com/", @request.env["HTTP_REFERER"] + end + end class ApplicationIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb index 42432510c385d657c0acb2468e1f13f6bdb6e969..3bb3b3db239d5cd13a6fb94bcdce1d721b6ee88b 100644 --- a/actionpack/test/dispatch/header_test.rb +++ b/actionpack/test/dispatch/header_test.rb @@ -1,41 +1,127 @@ -require 'abstract_unit' +require "abstract_unit" class HeaderTest < ActiveSupport::TestCase - def setup + setup do @headers = ActionDispatch::Http::Headers.new( - "HTTP_CONTENT_TYPE" => "text/plain" + "CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page" ) end - def test_each + test "#new with mixed headers and env" do + headers = ActionDispatch::Http::Headers.new( + "Content-Type" => "application/json", + "HTTP_REFERER" => "/some/page", + "Host" => "http://test.com") + + assert_equal({"CONTENT_TYPE" => "application/json", + "HTTP_REFERER" => "/some/page", + "HTTP_HOST" => "http://test.com"}, headers.env) + end + + test "#env returns the headers as env variables" do + assert_equal({"CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "#each iterates through the env variables" do headers = [] @headers.each { |pair| headers << pair } - assert_equal [["HTTP_CONTENT_TYPE", "text/plain"]], headers + assert_equal [["CONTENT_TYPE", "text/plain"], + ["HTTP_REFERER", "/some/page"]], headers + end + + test "set new headers" do + @headers["Host"] = "127.0.0.1" + + assert_equal "127.0.0.1", @headers["Host"] + assert_equal "127.0.0.1", @headers["HTTP_HOST"] + end + + test "headers can contain numbers" do + @headers["Content-MD5"] = "Q2hlY2sgSW50ZWdyaXR5IQ==" + + assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["Content-MD5"] + assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["HTTP_CONTENT_MD5"] + end + + test "set new env variables" do + @headers["HTTP_HOST"] = "127.0.0.1" + + assert_equal "127.0.0.1", @headers["Host"] + assert_equal "127.0.0.1", @headers["HTTP_HOST"] end - def test_setter - @headers['foo'] = "bar" - assert_equal "bar", @headers['foo'] + test "key?" do + assert @headers.key?("CONTENT_TYPE") + assert @headers.include?("CONTENT_TYPE") end - def test_key? - assert @headers.key?('HTTP_CONTENT_TYPE') - assert @headers.include?('HTTP_CONTENT_TYPE') + test "fetch with block" do + assert_equal "omg", @headers.fetch("notthere") { "omg" } end - def test_fetch_with_block - assert_equal 'omg', @headers.fetch('notthere') { 'omg' } + test "accessing http header" do + assert_equal "/some/page", @headers["Referer"] + assert_equal "/some/page", @headers["referer"] + assert_equal "/some/page", @headers["HTTP_REFERER"] end - test "content type" do + test "accessing special header" do assert_equal "text/plain", @headers["Content-Type"] assert_equal "text/plain", @headers["content-type"] assert_equal "text/plain", @headers["CONTENT_TYPE"] - assert_equal "text/plain", @headers["HTTP_CONTENT_TYPE"] end test "fetch" do assert_equal "text/plain", @headers.fetch("content-type", nil) - assert_equal "not found", @headers.fetch('not-found', 'not found') + assert_equal "not found", @headers.fetch("not-found", "not found") + end + + test "#merge! headers with mutation" do + @headers.merge!("Host" => "http://example.test", + "Content-Type" => "text/html") + assert_equal({"HTTP_HOST" => "http://example.test", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "#merge! env with mutation" do + @headers.merge!("HTTP_HOST" => "http://first.com", + "CONTENT_TYPE" => "text/html") + assert_equal({"HTTP_HOST" => "http://first.com", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "merge without mutation" do + combined = @headers.merge("HTTP_HOST" => "http://example.com", + "CONTENT_TYPE" => "text/html") + assert_equal({"HTTP_HOST" => "http://example.com", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, combined.env) + + assert_equal({"CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "env variables with . are not modified" do + headers = ActionDispatch::Http::Headers.new + headers.merge! "rack.input" => "", + "rack.request.cookie_hash" => "", + "action_dispatch.logger" => "" + + assert_equal(["action_dispatch.logger", + "rack.input", + "rack.request.cookie_hash"], headers.env.keys.sort) + end + + test "symbols are treated as strings" do + headers = ActionDispatch::Http::Headers.new(:SERVER_NAME => "example.com", + "HTTP_REFERER" => "/", + :Host => "test.com") + assert_equal "example.com", headers["SERVER_NAME"] + assert_equal "/", headers[:HTTP_REFERER] + assert_equal "test.com", headers["HTTP_HOST"] end end