From fb34553fe9141a42d48833f75ca86a5901f29ee3 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 24 Jul 2025 20:29:48 -0500 Subject: [PATCH] Add Cloudflare header support for real client IP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated IP geolocation to check CF-Connecting-IP header first - Added support for X-Real-IP header as additional fallback - Updated rate limiter to use same header priority for consistency - Headers checked in order: CF-Connecting-IP > X-Forwarded-For > X-Real-IP > remote_ip This ensures proper client IP detection when the site is behind Cloudflare. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lib/aprsme_web/plugs/ip_geolocation.ex | 24 ++++++++++++++++++++---- lib/aprsme_web/plugs/rate_limiter.ex | 19 ++++++++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/aprsme_web/plugs/ip_geolocation.ex b/lib/aprsme_web/plugs/ip_geolocation.ex index f88f790..ae5cfc1 100644 --- a/lib/aprsme_web/plugs/ip_geolocation.ex +++ b/lib/aprsme_web/plugs/ip_geolocation.ex @@ -68,12 +68,22 @@ defmodule AprsmeWeb.Plugs.IPGeolocation do end defp get_client_ip(conn) do - # Check for forwarded IP first (when behind proxy/load balancer) + # Check for Cloudflare header first (CF-Connecting-IP) + cf_ip = get_req_header(conn, "cf-connecting-ip") + + # Then check for standard forwarded headers forwarded_for = get_req_header(conn, "x-forwarded-for") + real_ip = get_req_header(conn, "x-real-ip") ip = - case forwarded_for do - [forwarded | _] -> + case {cf_ip, forwarded_for, real_ip} do + {[cf | _], _, _} -> + # Cloudflare header takes precedence + ip_from_cf = String.trim(cf) + Logger.info("IP geolocation: Using Cloudflare CF-Connecting-IP: #{ip_from_cf}") + ip_from_cf + + {[], [forwarded | _], _} -> # Take the first IP from the X-Forwarded-For header ip_from_header = forwarded @@ -84,7 +94,13 @@ defmodule AprsmeWeb.Plugs.IPGeolocation do Logger.info("IP geolocation: Using forwarded IP from X-Forwarded-For header: #{ip_from_header}") ip_from_header - [] -> + {[], [], [real | _]} -> + # Use X-Real-IP header + ip_from_real = String.trim(real) + Logger.info("IP geolocation: Using X-Real-IP header: #{ip_from_real}") + ip_from_real + + {[], [], []} -> # Fall back to remote_ip ip_from_remote = case conn.remote_ip do diff --git a/lib/aprsme_web/plugs/rate_limiter.ex b/lib/aprsme_web/plugs/rate_limiter.ex index 6c59035..d9aae0a 100644 --- a/lib/aprsme_web/plugs/rate_limiter.ex +++ b/lib/aprsme_web/plugs/rate_limiter.ex @@ -40,9 +40,22 @@ defmodule AprsmeWeb.Plugs.RateLimiter do end defp get_key(conn, :ip) do - case get_req_header(conn, "x-forwarded-for") do - [ip | _] -> ip - [] -> conn.remote_ip |> :inet.ntoa() |> to_string() + # Check headers in order of preference + cond do + # Cloudflare header takes precedence + cf_ip = get_req_header(conn, "cf-connecting-ip") != [] -> + hd(cf_ip) + + # Then standard forwarded headers + forwarded = get_req_header(conn, "x-forwarded-for") != [] -> + forwarded |> hd() |> String.split(",") |> List.first() |> String.trim() + + real_ip = get_req_header(conn, "x-real-ip") != [] -> + hd(real_ip) + + # Fall back to remote_ip + true -> + conn.remote_ip |> :inet.ntoa() |> to_string() end end