Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions ipinfo_django/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
author_email="[email protected]",
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,
)
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
84 changes: 84 additions & 0 deletions tests/test_async_resproxy_middleware.py
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions tests/test_resproxy_middleware.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 16 additions & 1 deletion tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]
3 changes: 3 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.