Reading CloudFlare headers in a Django middleware
For my new Django project, DB Buddy, I’m using CloudFlare as my CDN. It has a bunch of useful features that would otherwise take extra work, such as DDoS protection, HTML minification, and analytics.
Since CDNs proxy your site, all requests are seen to be coming from their servers’ IPs, rather than actual users’ IPs. But sometimes you need to inspect real user IPs, for example to implement rate limiting. To help with this, CloudFlare adds several useful headers, including CF-Connecting-IP
with the client’s real IP.
In order to provide an interface to get the client’s IP, whether my project is running in development or production, I added a middleware to attach the client IP as request.ip
:
class CloudflareMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
ip = request.headers["CF-Connecting-IP"]
except KeyError:
ip = request.META["REMOTE_ADDR"]
request.ip = ip
return self.get_response(request)
Although the middleware is short, I still made sure to write some tests. I based these on my subclass of Django’s SimpleTestCase
, since no DB queries should be made. To generate example requests, I used Django’s RequestFactory
. The end result was this test case with one test for each branch in the middleware:
from django.http import HttpResponse
from django.test import RequestFactory
from db_buddy.core.middleware import CloudflareMiddleware
from db_buddy.test import SimpleTestCase
class CloudflareMiddlewareTests(SimpleTestCase):
request_factory = RequestFactory()
def setUp(self):
def dummy_view(request):
return HttpResponse()
self.middleware = CloudflareMiddleware(dummy_view)
def test_ip_from_remote_addr(self):
request = self.request_factory.get("/", REMOTE_ADDR="1.2.3.4")
self.middleware(request)
assert request.ip == "1.2.3.4"
def test_ip_from_cloudflare(self):
request = self.request_factory.get(
"/", REMOTE_ADDR="1.2.3.4", HTTP_CF_CONNECTING_IP="2.3.4.5"
)
self.middleware(request)
assert request.ip == "2.3.4.5"
With the middleware now built, I installed it in the MIDDLEWARE
setting and then implemented login rate limiting with django-ratelimit using request.ip
.
Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.
One summary email a week, no spam, I pinky promise.
Related posts:
- Simple In-Memory Caching of Django Model Data With cachetools
- Efficient Reloading in Django’s Runserver With Watchman
- Introducing django-version-checks
Tags: django