Skip to content

Commit

Permalink
Fixed #35091 -- Allowed GeoIP2 querying using IPv4Address/IPv6Address.
Browse files Browse the repository at this point in the history
  • Loading branch information
ngnpope authored Jan 7, 2024
1 parent 53fc6ac commit cc56c22
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 17 deletions.
7 changes: 4 additions & 3 deletions django/contrib/gis/geoip2.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
directory corresponding to settings.GEOIP_PATH.
"""

import ipaddress
import socket
import warnings

Expand Down Expand Up @@ -172,10 +173,10 @@ def __repr__(self):

def _check_query(self, query, city=False, city_or_country=False):
"Check the query and database availability."
# Making sure a string was passed in for the query.
if not isinstance(query, str):
if not isinstance(query, (str, ipaddress.IPv4Address, ipaddress.IPv6Address)):
raise TypeError(
"GeoIP query must be a string, not type %s" % type(query).__name__
"GeoIP query must be a string or instance of IPv4Address or "
"IPv6Address, not type %s" % type(query).__name__,
)

# Extra checks for the existence of country and city databases.
Expand Down
9 changes: 5 additions & 4 deletions docs/ref/contrib/gis/geoip2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@ and given cache setting.
Querying
--------

All the following querying routines may take either a string IP address
or a fully qualified domain name (FQDN). For example, both
``'205.186.163.125'`` and ``'djangoproject.com'`` would be valid query
parameters.
All the following querying routines may take an instance of
:class:`~ipaddress.IPv4Address` or :class:`~ipaddress.IPv6Address`, a string IP
address, or a fully qualified domain name (FQDN). For example,
``IPv4Address("205.186.163.125")``, ``"205.186.163.125"``, and
``"djangoproject.com"`` would all be valid query parameters.

.. method:: GeoIP2.city(query)

Expand Down
3 changes: 3 additions & 0 deletions docs/releases/5.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ Minor features
* :class:`~django.contrib.gis.db.models.Collect` is now supported on MySQL
8.0.24+.

* :class:`~django.contrib.gis.geoip2.GeoIP2` now allows querying using
:class:`ipaddress.IPv4Address` or :class:`ipaddress.IPv6Address` objects.

:mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
31 changes: 21 additions & 10 deletions tests/gis_tests/test_geoip2.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ipaddress
import itertools
import pathlib
from unittest import mock, skipUnless
Expand Down Expand Up @@ -25,15 +26,20 @@ def build_geoip_path(*parts):
)
class GeoLite2Test(SimpleTestCase):
fqdn = "sky.uk"
ipv4 = "2.125.160.216"
ipv6 = "::ffff:027d:a0d8"
ipv4_str = "2.125.160.216"
ipv6_str = "::ffff:027d:a0d8"
ipv4_addr = ipaddress.ip_address(ipv4_str)
ipv6_addr = ipaddress.ip_address(ipv6_str)
query_values = (fqdn, ipv4_str, ipv6_str, ipv4_addr, ipv6_addr)

@classmethod
def setUpClass(cls):
# Avoid referencing __file__ at module level.
cls.enterClassContext(override_settings(GEOIP_PATH=build_geoip_path()))
# Always mock host lookup to avoid test breakage if DNS changes.
cls.enterClassContext(mock.patch("socket.gethostbyname", return_value=cls.ipv4))
cls.enterClassContext(
mock.patch("socket.gethostbyname", return_value=cls.ipv4_str)
)

super().setUpClass()

Expand Down Expand Up @@ -86,15 +92,18 @@ def test_bad_query(self):

functions += (g.country, g.country_code, g.country_name)
values = (123, 123.45, b"", (), [], {}, set(), frozenset(), GeoIP2)
msg = "GeoIP query must be a string, not type"
msg = (
"GeoIP query must be a string or instance of IPv4Address or IPv6Address, "
"not type"
)
for function, value in itertools.product(functions, values):
with self.subTest(function=function.__qualname__, type=type(value)):
with self.assertRaisesMessage(TypeError, msg):
function(value)

def test_country(self):
g = GeoIP2(city="<invalid>")
for query in (self.fqdn, self.ipv4, self.ipv6):
for query in self.query_values:
with self.subTest(query=query):
self.assertEqual(
g.country(query),
Expand All @@ -108,7 +117,7 @@ def test_country(self):

def test_city(self):
g = GeoIP2(country="<invalid>")
for query in (self.fqdn, self.ipv4, self.ipv6):
for query in self.query_values:
with self.subTest(query=query):
self.assertEqual(
g.city(query),
Expand Down Expand Up @@ -188,15 +197,17 @@ def test_repr(self):

def test_check_query(self):
g = GeoIP2()
self.assertEqual(g._check_query(self.ipv4), self.ipv4)
self.assertEqual(g._check_query(self.ipv6), self.ipv6)
self.assertEqual(g._check_query(self.fqdn), self.ipv4)
self.assertEqual(g._check_query(self.fqdn), self.ipv4_str)
self.assertEqual(g._check_query(self.ipv4_str), self.ipv4_str)
self.assertEqual(g._check_query(self.ipv6_str), self.ipv6_str)
self.assertEqual(g._check_query(self.ipv4_addr), self.ipv4_addr)
self.assertEqual(g._check_query(self.ipv6_addr), self.ipv6_addr)

def test_coords_deprecation_warning(self):
g = GeoIP2()
msg = "GeoIP2.coords() is deprecated. Use GeoIP2.lon_lat() instead."
with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
e1, e2 = g.coords(self.ipv4)
e1, e2 = g.coords(self.ipv4_str)
self.assertIsInstance(e1, float)
self.assertIsInstance(e2, float)

Expand Down

0 comments on commit cc56c22

Please sign in to comment.