diff --git a/README.md b/README.md index e46ec95..d1f3809 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ interface: * `Rack::Cookies` - Adds simple cookie jar hash to env * `Rack::Deflect` - Helps protect against DoS attacks. * `Rack::Evil` - Lets the rack application return a response to the client from any place. +* `Rack::HeaderNameTransformer` - Change the name of a Header. * `Rack::HostMeta` - Configures `/host-meta` using a block * `Rack::JSONBodyParser` - Adds JSON request bodies to the Rack parameters hash. * `Rack::JSONP` - Adds JSON-P support by stripping out the callback param and padding the response with the appropriate callback format. diff --git a/lib/rack/contrib.rb b/lib/rack/contrib.rb index 6d7ad96..2a0490f 100644 --- a/lib/rack/contrib.rb +++ b/lib/rack/contrib.rb @@ -23,6 +23,7 @@ def self.release autoload :Deflect, "rack/contrib/deflect" autoload :EnforceValidEncoding, "rack/contrib/enforce_valid_encoding" autoload :ExpectationCascade, "rack/contrib/expectation_cascade" + autoload :HeaderNameTransformer, "rack/contrib/header_name_transformer" autoload :HostMeta, "rack/contrib/host_meta" autoload :GarbageCollector, "rack/contrib/garbagecollector" autoload :JSONP, "rack/contrib/jsonp" diff --git a/lib/rack/contrib/header_name_transformer.rb b/lib/rack/contrib/header_name_transformer.rb new file mode 100644 index 0000000..9553673 --- /dev/null +++ b/lib/rack/contrib/header_name_transformer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Rack + # Middleware to change the name of a header + # + # So, if a server upstream of Rack sends {'X-Header-Name': "value"} + # you can change header to {'Whatever-You-Want': "value"} + # + # There is a specific use case when ensuring 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::HeaderNameTransformer, 'Vendor-Forwarded-Proto-Header', 'X-Forwarded-Proto'` + # to your application.rb file + + class HeaderNameTransformer + def initialize(app, vendor_header, forwarded_header) + @app = app + # Rack expects to see UPPER_UNDERSCORED_HEADERS, never SnakeCased-Dashed-Headers + @vendor_header = "HTTP_#{vendor_header.upcase.gsub '-', '_'}" + @forwarded_header = "HTTP_#{forwarded_header.upcase.gsub '-', '_'}" + end + + def call(env) + if (value = env[@vendor_header]) + env[@forwarded_header] = value + end + @app.call(env) + end + end +end diff --git a/test/spec_rack_header_name_transformer.rb b/test/spec_rack_header_name_transformer.rb new file mode 100644 index 0000000..eeb4660 --- /dev/null +++ b/test/spec_rack_header_name_transformer.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'rack/contrib/runtime' + +describe Rack::HeaderNameTransformer do + response = ->(_e) { [200, {}, []] } + + it 'leaves the value of headers intact if there is no matching vendor header passed to override it in the request' do + vendor_header = 'not passed in the request' + env = Rack::MockRequest.env_for('/', 'HTTP_X_FORWARDED_PROTO' => 'http') + + Rack::Lint.new(Rack::HeaderNameTransformer.new(response, vendor_header, 'bar')).call env + + _(env['HTTP_X_FORWARDED_PROTO']).must_equal 'http' + end + + it 'copy the value of the vendor header to a newly named header' do + env = Rack::MockRequest.env_for('/', { 'HTTP_VENDOR' => 'value', 'HTTP_FOO' => 'foo, bar' }) + + Rack::Lint.new(Rack::HeaderNameTransformer.new(response, 'Vendor', 'Standard')).call env + Rack::Lint.new(Rack::HeaderNameTransformer.new(response, 'Foo', 'Bar')).call env + + _(env['HTTP_STANDARD']).must_equal 'value' + _(env['HTTP_BAR']).must_equal 'foo, bar' + + # This is a copy operation, so the original headers are still preserved + _(env['HTTP_VENDOR']).must_equal 'value' + _(env['HTTP_FOO']).must_equal 'foo, bar' + end + + # Real world headers and use cases + it 'copy the value of a vendor forward proto header to the standardised forward proto header' do + env = Rack::MockRequest.env_for('/', 'HTTP_VENDOR_FORWARDED_PROTO_HEADER' => 'https') + + Rack::Lint.new( + Rack::HeaderNameTransformer.new( + response, + 'Vendor-Forwarded-Proto-Header', + 'X-Forwarded-Proto' + ) + ).call env + + _(env['HTTP_X_FORWARDED_PROTO']).must_equal 'https' + end + + it 'copy the value of a vendor forward proto header to the standardised header, overwriting existing request value' do + env = Rack::MockRequest.env_for( + '/', + 'HTTP_VENDOR_FORWARDED_PROTO_HEADER' => 'https', + 'HTTP_X_FORWARDED_PROTO' => 'http' + ) + + Rack::Lint.new( + Rack::HeaderNameTransformer.new( + response, + 'Vendor-Forwarded-Proto-Header', + 'X-Forwarded-Proto' + ) + ).call env + + _(env['HTTP_X_FORWARDED_PROTO']).must_equal 'https' + end +end