diff --git a/lib/rack/contrib.rb b/lib/rack/contrib.rb index 6d7ad96..d0e3b8c 100644 --- a/lib/rack/contrib.rb +++ b/lib/rack/contrib.rb @@ -35,6 +35,7 @@ def self.release autoload :ProcTitle, "rack/contrib/proctitle" autoload :Profiler, "rack/contrib/profiler" autoload :ResponseHeaders, "rack/contrib/response_headers" + autoload :SetXForwardedProtoHeader, "rack/contrib/set_x_forwarded_proto_header" autoload :Signals, "rack/contrib/signals" autoload :SimpleEndpoint, "rack/contrib/simple_endpoint" autoload :TimeZone, "rack/contrib/time_zone" diff --git a/lib/rack/contrib/set_x_forwarded_proto_header.rb b/lib/rack/contrib/set_x_forwarded_proto_header.rb new file mode 100644 index 0000000..4199fc5 --- /dev/null +++ b/lib/rack/contrib/set_x_forwarded_proto_header.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Rack + # Middleware to set the X-Forwarded-Proto header to the value + # of another header. + # + # This header can be used to ensure the scheme matches when comparing + # request.origin and request.base_url for CSRF checking, but Rack + # expects that value to be in the X_FORWARDED_PROTO header. + # + # Example Rails usage: + # If you use a vendor managed proxy or CDN which sends the proto in a header add + #`config.middleware.use Rack::SetXForwardedProtoHeader, 'Vendor-Forwarded-Proto-Header'` + # to your application.rb file + + class SetXForwardedProtoHeader + def initialize(app, vendor_forwarded_header) + @app = app + # Rack expects to see UPPER_UNDERSCORED_HEADERS, never SnakeCased-Dashed-Headers + @vendor_forwarded_header = "HTTP_#{vendor_forwarded_header.upcase.gsub "-", "_"}" + end + + def call(env) + if value = env[@vendor_forwarded_header] + env["HTTP_X_FORWARDED_PROTO"] = value + end + @app.call(env) + end + + end +end \ No newline at end of file diff --git a/test/spec_rack_set_x_forwarded_proto_header.rb b/test/spec_rack_set_x_forwarded_proto_header.rb new file mode 100644 index 0000000..26634fa --- /dev/null +++ b/test/spec_rack_set_x_forwarded_proto_header.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'rack/contrib/runtime' + +describe Rack::SetXForwardedProtoHeader do + response = lambda {|e| [200, {}, []] } + + it "leaves the value of X_FORWARDED_PROTO intact if there is no vendor header passed in the request" do + vendor_forwarded_header = "not passed in the request" + env = Rack::MockRequest.env_for("/", "HTTP_X_FORWARDED_PROTO" => "http") + + Rack::Lint.new(Rack::SetXForwardedProtoHeader.new(response, vendor_forwarded_header)).call env + + env["HTTP_X_FORWARDED_PROTO"].must_equal "http" + end + + it "does not set X-Forwarded-Proto when there is no vendor header passed in the request" do + vendor_forwarded_header = "not passed in the request" + env = Rack::MockRequest.env_for("/", "FOO" => "bar") + + Rack::Lint.new(Rack::SetXForwardedProtoHeader.new(response, vendor_forwarded_header)).call env + + env["FOO"].must_equal "bar" + assert_nil(env["HTTP_X_FORWARDED_PROTO"]) + end + + + it "copies the value of the header to X-Forwarded-Proto" do + env = Rack::MockRequest.env_for("/", "HTTP_VENDOR_FORWARDED_PROTO_HEADER" => "https") + + Rack::Lint.new(Rack::SetXForwardedProtoHeader.new(response, "Vendor-Forwarded-Proto-Header")).call env + + env["HTTP_X_FORWARDED_PROTO"].must_equal "https" + end + + it "copies the value of the header to X-Forwarded-Proto overwriting an existing X-Forwarded-Proto" do + env = Rack::MockRequest.env_for("/", "HTTP_VENDOR_FORWARDED_PROTO_HEADER" => "https", "HTTP_X_FORWARDED_PROTO" => "http") + + Rack::Lint.new(Rack::SetXForwardedProtoHeader.new(response, "Vendor-Forwarded-Proto-Header")).call env + + env["HTTP_X_FORWARDED_PROTO"].must_equal "https" + end +end \ No newline at end of file