From 8538c21d4ec667a1bfc835dd1d816bec8aa6a561 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 3 Nov 2025 13:39:55 -0500 Subject: [PATCH] Copy Cloudflare's True-Client-IP header to X-Forwarded-For ref: https://3.basecamp.com/2914079/buckets/37331921/todos/9243121525 --- config/initializers/true_client_ip.rb | 24 +++++++++++++++++++ test/lib/true_client_ip_test.rb | 34 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 config/initializers/true_client_ip.rb create mode 100644 test/lib/true_client_ip_test.rb diff --git a/config/initializers/true_client_ip.rb b/config/initializers/true_client_ip.rb new file mode 100644 index 000000000..cac696107 --- /dev/null +++ b/config/initializers/true_client_ip.rb @@ -0,0 +1,24 @@ +# +# Cloudflare sets a True-Client-IP header, which for most 37signals apps gets copied to +# X-Forwarded-For by an iRule on the F5 load balancers: +# +# https://github.com/basecamp/f5-tf/blob/1543f7bfa3961a79e397f80cf041d75567f1b2f8/ams-base/iRules/manage_x_forwarded.tcl +# +# However, for Fizzy the F5s are configured to do passthrough, so the header value isn't being +# copied for us. Let's do that bit of work here, before Rails' RemoteIp middleware. +# +class TrackTrueClientIp + def initialize(app) + @app = app + end + + def call(env) + if env["HTTP_TRUE_CLIENT_IP"].present? + env["HTTP_X_FORWARDED_FOR"] = env["HTTP_TRUE_CLIENT_IP"] + end + + @app.call(env) + end +end + +Rails.application.config.middleware.insert_before ActionDispatch::RemoteIp, TrackTrueClientIp diff --git a/test/lib/true_client_ip_test.rb b/test/lib/true_client_ip_test.rb new file mode 100644 index 000000000..5c8c2e4d9 --- /dev/null +++ b/test/lib/true_client_ip_test.rb @@ -0,0 +1,34 @@ +require "test_helper" + +class TrackTrueClientIpTest < ActiveSupport::TestCase + setup do + @app = ->(env) { [ 200, {}, [ "OK" ] ] } + @middleware = TrackTrueClientIp.new(@app) + end + + test "sets X-Forwarded-For header when True-Client-IP header is present" do + env = { "HTTP_TRUE_CLIENT_IP" => "123.123.123.123" } + @middleware.call(env) + assert_equal "123.123.123.123", env["HTTP_X_FORWARDED_FOR"] + end + + test "does not modify environment when True-Client-IP header is absent" do + env = {} + @middleware.call(env) + assert_nil env["HTTP_X_FORWARDED_FOR"] + + env = { "HTTP_X_FORWARDED_FOR" => "234.234.234.234" } + @middleware.call(env) + assert_equal "234.234.234.234", env["HTTP_X_FORWARDED_FOR"] + end + + test "calls the next middleware in the stack" do + called = false + app = ->(env) { called = true; [ 200, {}, [ "OK" ] ] } + middleware = TrackTrueClientIp.new(app) + + middleware.call({}) + + assert called + end +end