geoip2.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. """
  2. This module houses the GeoIP2 object, a wrapper for the MaxMind GeoIP2(R)
  3. Python API (https://geoip2.readthedocs.io/). This is an alternative to the
  4. Python GeoIP2 interface provided by MaxMind.
  5. GeoIP(R) is a registered trademark of MaxMind, Inc.
  6. For IP-based geolocation, this module requires the GeoLite2 Country and City
  7. datasets, in binary format (CSV will not work!). The datasets may be
  8. downloaded from MaxMind at https://dev.maxmind.com/geoip/geoip2/geolite2/.
  9. Grab GeoLite2-Country.mmdb.gz and GeoLite2-City.mmdb.gz, and unzip them in the
  10. directory corresponding to settings.GEOIP_PATH.
  11. """
  12. import ipaddress
  13. import socket
  14. import warnings
  15. from django.conf import settings
  16. from django.core.exceptions import ValidationError
  17. from django.core.validators import validate_ipv46_address
  18. from django.utils._os import to_path
  19. from django.utils.deprecation import RemovedInDjango60Warning
  20. from django.utils.functional import cached_property
  21. __all__ = ["HAS_GEOIP2"]
  22. try:
  23. import geoip2.database
  24. except ImportError: # pragma: no cover
  25. HAS_GEOIP2 = False
  26. else:
  27. HAS_GEOIP2 = True
  28. __all__ += ["GeoIP2", "GeoIP2Exception"]
  29. class GeoIP2Exception(Exception):
  30. pass
  31. class GeoIP2:
  32. # The flags for GeoIP memory caching.
  33. # Try MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that order.
  34. MODE_AUTO = 0
  35. # Use the C extension with memory map.
  36. MODE_MMAP_EXT = 1
  37. # Read from memory map. Pure Python.
  38. MODE_MMAP = 2
  39. # Read database as standard file. Pure Python.
  40. MODE_FILE = 4
  41. # Load database into memory. Pure Python.
  42. MODE_MEMORY = 8
  43. cache_options = frozenset(
  44. (MODE_AUTO, MODE_MMAP_EXT, MODE_MMAP, MODE_FILE, MODE_MEMORY)
  45. )
  46. _path = None
  47. _reader = None
  48. def __init__(self, path=None, cache=0, country=None, city=None):
  49. """
  50. Initialize the GeoIP object. No parameters are required to use default
  51. settings. Keyword arguments may be passed in to customize the locations
  52. of the GeoIP datasets.
  53. * path: Base directory to where GeoIP data is located or the full path
  54. to where the city or country data files (*.mmdb) are located.
  55. Assumes that both the city and country data sets are located in
  56. this directory; overrides the GEOIP_PATH setting.
  57. * cache: The cache settings when opening up the GeoIP datasets. May be
  58. an integer in (0, 1, 2, 4, 8) corresponding to the MODE_AUTO,
  59. MODE_MMAP_EXT, MODE_MMAP, MODE_FILE, and MODE_MEMORY,
  60. `GeoIPOptions` C API settings, respectively. Defaults to 0,
  61. meaning MODE_AUTO.
  62. * country: The name of the GeoIP country data file. Defaults to
  63. 'GeoLite2-Country.mmdb'; overrides the GEOIP_COUNTRY setting.
  64. * city: The name of the GeoIP city data file. Defaults to
  65. 'GeoLite2-City.mmdb'; overrides the GEOIP_CITY setting.
  66. """
  67. if cache not in self.cache_options:
  68. raise GeoIP2Exception("Invalid GeoIP caching option: %s" % cache)
  69. path = path or getattr(settings, "GEOIP_PATH", None)
  70. city = city or getattr(settings, "GEOIP_CITY", "GeoLite2-City.mmdb")
  71. country = country or getattr(settings, "GEOIP_COUNTRY", "GeoLite2-Country.mmdb")
  72. if not path:
  73. raise GeoIP2Exception(
  74. "GeoIP path must be provided via parameter or the GEOIP_PATH setting."
  75. )
  76. path = to_path(path)
  77. # Try the path first in case it is the full path to a database.
  78. for path in (path, path / city, path / country):
  79. if path.is_file():
  80. self._path = path
  81. self._reader = geoip2.database.Reader(path, mode=cache)
  82. break
  83. else:
  84. raise GeoIP2Exception(
  85. "Path must be a valid database or directory containing databases."
  86. )
  87. database_type = self._metadata.database_type
  88. if not database_type.endswith(("City", "Country")):
  89. raise GeoIP2Exception(f"Unable to handle database edition: {database_type}")
  90. def __del__(self):
  91. # Cleanup any GeoIP file handles lying around.
  92. if self._reader:
  93. self._reader.close()
  94. def __repr__(self):
  95. m = self._metadata
  96. version = f"v{m.binary_format_major_version}.{m.binary_format_minor_version}"
  97. return f"<{self.__class__.__name__} [{version}] _path='{self._path}'>"
  98. @cached_property
  99. def _metadata(self):
  100. return self._reader.metadata()
  101. def _query(self, query, *, require_city=False):
  102. if not isinstance(query, (str, ipaddress.IPv4Address, ipaddress.IPv6Address)):
  103. raise TypeError(
  104. "GeoIP query must be a string or instance of IPv4Address or "
  105. "IPv6Address, not type %s" % type(query).__name__,
  106. )
  107. is_city = self._metadata.database_type.endswith("City")
  108. if require_city and not is_city:
  109. raise GeoIP2Exception(f"Invalid GeoIP city data file: {self._path}")
  110. try:
  111. validate_ipv46_address(query)
  112. except ValidationError:
  113. # GeoIP2 only takes IP addresses, so try to resolve a hostname.
  114. query = socket.gethostbyname(query)
  115. function = self._reader.city if is_city else self._reader.country
  116. return function(query)
  117. def city(self, query):
  118. """
  119. Return a dictionary of city information for the given IP address or
  120. Fully Qualified Domain Name (FQDN). Some information in the dictionary
  121. may be undefined (None).
  122. """
  123. response = self._query(query, require_city=True)
  124. region = response.subdivisions[0] if response.subdivisions else None
  125. return {
  126. "accuracy_radius": response.location.accuracy_radius,
  127. "city": response.city.name,
  128. "continent_code": response.continent.code,
  129. "continent_name": response.continent.name,
  130. "country_code": response.country.iso_code,
  131. "country_name": response.country.name,
  132. "is_in_european_union": response.country.is_in_european_union,
  133. "latitude": response.location.latitude,
  134. "longitude": response.location.longitude,
  135. "metro_code": response.location.metro_code,
  136. "postal_code": response.postal.code,
  137. "region_code": region.iso_code if region else None,
  138. "region_name": region.name if region else None,
  139. "time_zone": response.location.time_zone,
  140. # Kept for backward compatibility.
  141. "dma_code": response.location.metro_code,
  142. "region": region.iso_code if region else None,
  143. }
  144. def country_code(self, query):
  145. "Return the country code for the given IP Address or FQDN."
  146. return self.country(query)["country_code"]
  147. def country_name(self, query):
  148. "Return the country name for the given IP Address or FQDN."
  149. return self.country(query)["country_name"]
  150. def country(self, query):
  151. """
  152. Return a dictionary with the country code and name when given an
  153. IP address or a Fully Qualified Domain Name (FQDN). For example, both
  154. '24.124.1.80' and 'djangoproject.com' are valid parameters.
  155. """
  156. response = self._query(query, require_city=False)
  157. return {
  158. "continent_code": response.continent.code,
  159. "continent_name": response.continent.name,
  160. "country_code": response.country.iso_code,
  161. "country_name": response.country.name,
  162. "is_in_european_union": response.country.is_in_european_union,
  163. }
  164. def coords(self, query, ordering=("longitude", "latitude")):
  165. warnings.warn(
  166. "GeoIP2.coords() is deprecated. Use GeoIP2.lon_lat() instead.",
  167. RemovedInDjango60Warning,
  168. stacklevel=2,
  169. )
  170. data = self.city(query)
  171. return tuple(data[o] for o in ordering)
  172. def lon_lat(self, query):
  173. "Return a tuple of the (longitude, latitude) for the given query."
  174. data = self.city(query)
  175. return data["longitude"], data["latitude"]
  176. def lat_lon(self, query):
  177. "Return a tuple of the (latitude, longitude) for the given query."
  178. data = self.city(query)
  179. return data["latitude"], data["longitude"]
  180. def geos(self, query):
  181. "Return a GEOS Point object for the given query."
  182. # Allows importing and using GeoIP2() when GEOS is not installed.
  183. from django.contrib.gis.geos import Point
  184. return Point(self.lon_lat(query), srid=4326)
  185. @classmethod
  186. def open(cls, full_path, cache):
  187. warnings.warn(
  188. "GeoIP2.open() is deprecated. Use GeoIP2() instead.",
  189. RemovedInDjango60Warning,
  190. stacklevel=2,
  191. )
  192. return GeoIP2(full_path, cache)