diff --git a/lib/fastly-rails/rack/set_x_forwarded_headers.rb b/lib/fastly-rails/rack/set_x_forwarded_headers.rb new file mode 100644 index 0000000..a45f31b --- /dev/null +++ b/lib/fastly-rails/rack/set_x_forwarded_headers.rb @@ -0,0 +1,44 @@ +module FastlyRails + module Rack + # Set the X-Forwarded-Proto header to reflect + # the browser/Fastly connection. + # + # Useful for apps hosted on Heroku, where the X-Forwarded + # headers are set by the Heroku router to represent only the + # direct connection from Fastly's proxy server to Heroku. + # + # Example usage: + # ``` + # config.middleware.insert_after(Rack::MethodOverride, FastlyRails::Rack::SetXForwardedHeaders) + # ``` + class SetXForwardedHeaders + def initialize(app) + @app = app + end + + def call(env) + if is_behind_fastly?(env) + if is_ssl?(env) + env["HTTPS"] = "on" + env["HTTP_X_FORWARDED_PROTO"] = "https" + else + env["HTTP_X_FORWARDED_PROTO"] = "http" + end + # Disambiguate + env.delete("HTTP_X_FORWARDED_PORT") + env.delete("HTTP_X_FORWARDED_SSL") + env.delete("HTTP_X_FORWARDED_SCHEME") + end + @app.call(env) + end + + def is_behind_fastly?(env) + env["HTTP_FASTLY_CLIENT_IP"].present? + end + + def is_ssl?(env) + env["HTTP_FASTLY_SSL"].present? + end + end + end +end diff --git a/test/fastly-rails/set_x_forwarded_headers_test.rb b/test/fastly-rails/set_x_forwarded_headers_test.rb new file mode 100644 index 0000000..34aafd3 --- /dev/null +++ b/test/fastly-rails/set_x_forwarded_headers_test.rb @@ -0,0 +1,50 @@ +require 'test_helper' +require 'fastly-rails/rack/set_x_forwarded_headers' + +describe FastlyRails::Rack::SetXForwardedHeaders do + let(:ip) { Faker::Internet.ip_v4_address } + let(:middleware) { FastlyRails::Rack::SetXForwardedHeaders.new(app) } + + describe 'without fastly' do + let(:headers) { {"HTTP_X_FORWARDED_PROTO" => "ftp"} } + let(:app) { + Proc.new { |env| + assert_nil env['HTTPS'], "Env var should not exist" + assert_equal "ftp", env['HTTP_X_FORWARDED_PROTO'], "Header should not have been modifed" + [200, {'Content-Type' => 'text/html'}, ["hello"]] + } + } + it 'should not change the env' do + middleware.call(headers) + end + end + + describe 'with fastly' do + describe 'without ssl header' do + let(:headers) { {"HTTP_FASTLY_CLIENT_IP" => ip} } + let(:app) { + Proc.new { |env| + assert_nil env['HTTPS'], "Env var should not exist" + assert_equal "http", env['HTTP_X_FORWARDED_PROTO'] + [200, {'Content-Type' => 'text/html'}, ["hello"]] + } + } + it 'should modify the env properly' do + middleware.call(headers) + end + end + describe 'with ssl header' do + let(:headers) { {"HTTP_FASTLY_CLIENT_IP" => ip, "HTTP_FASTLY_SSL" => "1"} } + let(:app) { + Proc.new { |env| + assert_equal "on", env['HTTPS'] + assert_equal "https", env['HTTP_X_FORWARDED_PROTO'] + [200, {'Content-Type' => 'text/html'}, ["hello"]] + } + } + it 'should modify the env properly' do + middleware.call(headers) + end + end + end +end