From 38d789c75890981ae1deab6ad8efcb1b7d63b8b5 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 23 Jan 2026 10:23:26 +0100 Subject: [PATCH 1/2] Bump ipinfo to 5.4.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f017532..8210d80 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,6 @@ author_email="support@ipinfo.io", license="Apache License 2.0", packages=["ipinfo_django", "ipinfo_django.ip_selector"], - install_requires=["django", "ipinfo>=5.3.0"], + install_requires=["django", "ipinfo>=5.4.0"], zip_safe=False, ) From b7a34d47b32cab0bf00abe89b95cd30f410d9944 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 23 Jan 2026 10:37:42 +0100 Subject: [PATCH 2/2] Add resproxy API support --- ipinfo_django/middleware.py | 71 +++++++++++++++++++++ tests/conftest.py | 14 +++++ tests/test_async_resproxy_middleware.py | 84 +++++++++++++++++++++++++ tests/test_resproxy_middleware.py | 70 +++++++++++++++++++++ tests/urls.py | 17 ++++- uv.lock | 3 + 6 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 tests/test_async_resproxy_middleware.py create mode 100644 tests/test_resproxy_middleware.py create mode 100644 uv.lock diff --git a/ipinfo_django/middleware.py b/ipinfo_django/middleware.py index 3d31354..2665afd 100644 --- a/ipinfo_django/middleware.py +++ b/ipinfo_django/middleware.py @@ -139,3 +139,74 @@ def __init__(self, get_response): ipinfo_token = getattr(settings, "IPINFO_TOKEN", None) ipinfo_settings = getattr(settings, "IPINFO_SETTINGS", {}) self.ipinfo = ipinfo.getHandlerAsyncPlus(ipinfo_token, **ipinfo_settings) + + +class IPinfoResproxyMiddleware: + def __init__(self, get_response=None): + """ + Initializes class while gettings user settings and creating the cache. + """ + self.get_response = get_response + self.filter = getattr(settings, "IPINFO_FILTER", self.is_bot) + + ipinfo_token = getattr(settings, "IPINFO_TOKEN", None) + ipinfo_settings = getattr(settings, "IPINFO_SETTINGS", {}) + self.ip_selector = getattr(settings, "IPINFO_IP_SELECTOR", DefaultIPSelector()) + self.ipinfo = ipinfo.getHandler(ipinfo_token, **ipinfo_settings) + + def __call__(self, request): + """Middleware hook that acts on and modifies request object.""" + try: + if self.filter and self.filter(request): + request.ipinfo_resproxy = None + else: + request.ipinfo_resproxy = self.ipinfo.getResproxy( + self.ip_selector.get_ip(request) + ) + except Exception: + request.ipinfo_resproxy = None + LOGGER.error(traceback.format_exc()) + + response = self.get_response(request) + return response + + def is_bot(self, request): + return is_bot(request) + + +class IPinfoAsyncResproxyMiddleware: + sync_capable = False + async_capable = True + + def __init__(self, get_response): + """Initialize class, get settings, and create the cache.""" + self.get_response = get_response + + self.filter = getattr(settings, "IPINFO_FILTER", self.is_bot) + + ipinfo_token = getattr(settings, "IPINFO_TOKEN", None) + ipinfo_settings = getattr(settings, "IPINFO_SETTINGS", {}) + self.ip_selector = getattr(settings, "IPINFO_IP_SELECTOR", DefaultIPSelector()) + self.ipinfo = ipinfo.getHandlerAsync(ipinfo_token, **ipinfo_settings) + + def __call__(self, request): + return self.__acall__(request) + + async def __acall__(self, request): + """Middleware hook that acts on and modifies request object.""" + try: + if self.filter and self.filter(request): + request.ipinfo_resproxy = None + else: + request.ipinfo_resproxy = await self.ipinfo.getResproxy( + self.ip_selector.get_ip(request) + ) + except Exception: + request.ipinfo_resproxy = None + LOGGER.error(traceback.format_exc()) + + response = await self.get_response(request) + return response + + def is_bot(self, request): + return is_bot(request) diff --git a/tests/conftest.py b/tests/conftest.py index 9a14bbe..79dfe9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,3 +55,17 @@ def ipinfo_async_plus_middleware(settings): settings.MIDDLEWARE = [ "ipinfo_django.middleware.IPinfoAsyncPlusMiddleware", ] + + +@pytest.fixture +def ipinfo_resproxy_middleware(settings): + settings.MIDDLEWARE = [ + "ipinfo_django.middleware.IPinfoResproxyMiddleware", + ] + + +@pytest.fixture +def ipinfo_async_resproxy_middleware(settings): + settings.MIDDLEWARE = [ + "ipinfo_django.middleware.IPinfoAsyncResproxyMiddleware", + ] diff --git a/tests/test_async_resproxy_middleware.py b/tests/test_async_resproxy_middleware.py new file mode 100644 index 0000000..f1174d2 --- /dev/null +++ b/tests/test_async_resproxy_middleware.py @@ -0,0 +1,84 @@ +from http import HTTPStatus +from unittest import mock + +import pytest +from ipinfo.details import Details + + +@pytest.mark.asyncio +async def test_middleware_appends_resproxy_info( + async_client, ipinfo_async_resproxy_middleware +): + with mock.patch("ipinfo.AsyncHandler.getResproxy") as mocked_getResproxy: + mocked_getResproxy.return_value = Details( + { + "ip": "127.0.0.1", + "last_seen": "2026-01-15", + "percent_days_seen": 100, + "service": "test_service", + } + ) + res = await async_client.get("/test_resproxy_view/") + assert res.status_code == HTTPStatus.OK + assert b"Resproxy for: 127.0.0.1" in res.content + + +@pytest.mark.asyncio +async def test_middleware_filters(async_client, ipinfo_async_resproxy_middleware): + res = await async_client.get("/test_resproxy_view/", USER_AGENT="some bot") + assert res.status_code == HTTPStatus.OK + assert b"Request filtered." in res.content + + +@pytest.mark.asyncio +async def test_middleware_behind_proxy(async_client, ipinfo_async_resproxy_middleware): + with mock.patch("ipinfo.AsyncHandler.getResproxy") as mocked_getResproxy: + mocked_getResproxy.return_value = Details( + { + "ip": "93.44.186.197", + "last_seen": "2026-01-15", + "percent_days_seen": 100, + "service": "test_service", + } + ) + res = await async_client.get( + "/test_resproxy_view/", X_FORWARDED_FOR="93.44.186.197" + ) + + mocked_getResproxy.assert_called_once_with("93.44.186.197") + assert res.status_code == HTTPStatus.OK + assert b"Resproxy for: 93.44.186.197" in res.content + + +@pytest.mark.asyncio +async def test_middleware_not_behind_proxy( + async_client, ipinfo_async_resproxy_middleware +): + with mock.patch("ipinfo.AsyncHandler.getResproxy") as mocked_getResproxy: + mocked_getResproxy.return_value = Details( + { + "ip": "127.0.0.1", + "last_seen": "2026-01-15", + "percent_days_seen": 100, + "service": "test_service", + } + ) + res = await async_client.get("/test_resproxy_view/") + + mocked_getResproxy.assert_called_once_with("127.0.0.1") + assert res.status_code == HTTPStatus.OK + assert b"Resproxy for: 127.0.0.1" in res.content + + +@pytest.mark.asyncio +async def test_middleware_empty_response( + async_client, ipinfo_async_resproxy_middleware +): + """Test that empty response from API (IP not in resproxy database) is passed through.""" + with mock.patch("ipinfo.AsyncHandler.getResproxy") as mocked_getResproxy: + mocked_getResproxy.return_value = Details({}) + res = await async_client.get("/test_resproxy_view/") + + mocked_getResproxy.assert_called_once_with("127.0.0.1") + assert res.status_code == HTTPStatus.OK + assert b"Empty resproxy response." in res.content diff --git a/tests/test_resproxy_middleware.py b/tests/test_resproxy_middleware.py new file mode 100644 index 0000000..4ab1702 --- /dev/null +++ b/tests/test_resproxy_middleware.py @@ -0,0 +1,70 @@ +from http import HTTPStatus +from unittest import mock + +from ipinfo.details import Details + + +def test_middleware_appends_resproxy_info(client, ipinfo_resproxy_middleware): + with mock.patch("ipinfo.Handler.getResproxy") as mocked_getResproxy: + mocked_getResproxy.return_value = Details( + { + "ip": "127.0.0.1", + "last_seen": "2026-01-15", + "percent_days_seen": 100, + "service": "test_service", + } + ) + res = client.get("/test_resproxy_view/") + assert res.status_code == HTTPStatus.OK + assert b"Resproxy for: 127.0.0.1" in res.content + + +def test_middleware_filters(client, ipinfo_resproxy_middleware): + res = client.get("/test_resproxy_view/", HTTP_USER_AGENT="some bot") + assert res.status_code == HTTPStatus.OK + assert b"Request filtered." in res.content + + +def test_middleware_behind_proxy(client, ipinfo_resproxy_middleware): + with mock.patch("ipinfo.Handler.getResproxy") as mocked_getResproxy: + mocked_getResproxy.return_value = Details( + { + "ip": "93.44.186.197", + "last_seen": "2026-01-15", + "percent_days_seen": 100, + "service": "test_service", + } + ) + res = client.get("/test_resproxy_view/", HTTP_X_FORWARDED_FOR="93.44.186.197") + + mocked_getResproxy.assert_called_once_with("93.44.186.197") + assert res.status_code == HTTPStatus.OK + assert b"Resproxy for: 93.44.186.197" in res.content + + +def test_middleware_not_behind_proxy(client, ipinfo_resproxy_middleware): + with mock.patch("ipinfo.Handler.getResproxy") as mocked_getResproxy: + mocked_getResproxy.return_value = Details( + { + "ip": "127.0.0.1", + "last_seen": "2026-01-15", + "percent_days_seen": 100, + "service": "test_service", + } + ) + res = client.get("/test_resproxy_view/") + + mocked_getResproxy.assert_called_once_with("127.0.0.1") + assert res.status_code == HTTPStatus.OK + assert b"Resproxy for: 127.0.0.1" in res.content + + +def test_middleware_empty_response(client, ipinfo_resproxy_middleware): + """Test that empty response from API (IP not in resproxy database) is passed through.""" + with mock.patch("ipinfo.Handler.getResproxy") as mocked_getResproxy: + mocked_getResproxy.return_value = Details({}) + res = client.get("/test_resproxy_view/") + + mocked_getResproxy.assert_called_once_with("127.0.0.1") + assert res.status_code == HTTPStatus.OK + assert b"Empty resproxy response." in res.content diff --git a/tests/urls.py b/tests/urls.py index 939e9bf..a9eb266 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -11,4 +11,19 @@ async def test_view(request): return HttpResponse("Request filtered.", status=200) -urlpatterns = [path("test_view/", test_view)] +async def test_resproxy_view(request): + ipinfo_resproxy = getattr(request, "ipinfo_resproxy", None) + + if ipinfo_resproxy: + ip = getattr(ipinfo_resproxy, "ip", None) + if ip: + return HttpResponse(f"Resproxy for: {ip}", status=200) + return HttpResponse("Empty resproxy response.", status=200) + + return HttpResponse("Request filtered.", status=200) + + +urlpatterns = [ + path("test_view/", test_view), + path("test_resproxy_view/", test_resproxy_view), +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..bda0207 --- /dev/null +++ b/uv.lock @@ -0,0 +1,3 @@ +version = 1 +revision = 3 +requires-python = ">=3.13"