You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

base.py 8.8 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import os
  2. import socket
  3. import geoip2.database
  4. from django.conf import settings
  5. from django.core.validators import ipv4_re
  6. from django.utils import six
  7. from django.utils.ipv6 import is_valid_ipv6_address
  8. from .resources import City, Country
  9. # Creating the settings dictionary with any settings, if needed.
  10. GEOIP_SETTINGS = {
  11. 'GEOIP_PATH': getattr(settings, 'GEOIP_PATH', None),
  12. 'GEOIP_CITY': getattr(settings, 'GEOIP_CITY', 'GeoLite2-City.mmdb'),
  13. 'GEOIP_COUNTRY': getattr(settings, 'GEOIP_COUNTRY', 'GeoLite2-Country.mmdb'),
  14. }
  15. class GeoIP2Exception(Exception):
  16. pass
  17. class GeoIP2(object):
  18. # The flags for GeoIP memory caching.
  19. # Try MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that order.
  20. MODE_AUTO = 0
  21. # Use the C extension with memory map.
  22. MODE_MMAP_EXT = 1
  23. # Read from memory map. Pure Python.
  24. MODE_MMAP = 2
  25. # Read database as standard file. Pure Python.
  26. MODE_FILE = 4
  27. # Load database into memory. Pure Python.
  28. MODE_MEMORY = 8
  29. cache_options = {opt: None for opt in (0, 1, 2, 4, 8)}
  30. # Paths to the city & country binary databases.
  31. _city_file = ''
  32. _country_file = ''
  33. # Initially, pointers to GeoIP file references are NULL.
  34. _city = None
  35. _country = None
  36. def __init__(self, path=None, cache=0, country=None, city=None):
  37. """
  38. Initialize the GeoIP object. No parameters are required to use default
  39. settings. Keyword arguments may be passed in to customize the locations
  40. of the GeoIP datasets.
  41. * path: Base directory to where GeoIP data is located or the full path
  42. to where the city or country data files (*.mmdb) are located.
  43. Assumes that both the city and country data sets are located in
  44. this directory; overrides the GEOIP_PATH setting.
  45. * cache: The cache settings when opening up the GeoIP datasets. May be
  46. an integer in (0, 1, 2, 4, 8) corresponding to the MODE_AUTO,
  47. MODE_MMAP_EXT, MODE_MMAP, MODE_FILE, and MODE_MEMORY,
  48. `GeoIPOptions` C API settings, respectively. Defaults to 0,
  49. meaning MODE_AUTO.
  50. * country: The name of the GeoIP country data file. Defaults to
  51. 'GeoLite2-Country.mmdb'; overrides the GEOIP_COUNTRY setting.
  52. * city: The name of the GeoIP city data file. Defaults to
  53. 'GeoLite2-City.mmdb'; overrides the GEOIP_CITY setting.
  54. """
  55. # Checking the given cache option.
  56. if cache in self.cache_options:
  57. self._cache = cache
  58. else:
  59. raise GeoIP2Exception('Invalid GeoIP caching option: %s' % cache)
  60. # Getting the GeoIP data path.
  61. if not path:
  62. path = GEOIP_SETTINGS['GEOIP_PATH']
  63. if not path:
  64. raise GeoIP2Exception('GeoIP path must be provided via parameter or the GEOIP_PATH setting.')
  65. if not isinstance(path, six.string_types):
  66. raise TypeError('Invalid path type: %s' % type(path).__name__)
  67. if os.path.isdir(path):
  68. # Constructing the GeoIP database filenames using the settings
  69. # dictionary. If the database files for the GeoLite country
  70. # and/or city datasets exist, then try to open them.
  71. country_db = os.path.join(path, country or GEOIP_SETTINGS['GEOIP_COUNTRY'])
  72. if os.path.isfile(country_db):
  73. self._country = geoip2.database.Reader(country_db, mode=cache)
  74. self._country_file = country_db
  75. city_db = os.path.join(path, city or GEOIP_SETTINGS['GEOIP_CITY'])
  76. if os.path.isfile(city_db):
  77. self._city = geoip2.database.Reader(city_db, mode=cache)
  78. self._city_file = city_db
  79. elif os.path.isfile(path):
  80. # Otherwise, some detective work will be needed to figure out
  81. # whether the given database path is for the GeoIP country or city
  82. # databases.
  83. reader = geoip2.database.Reader(path, mode=cache)
  84. db_type = reader.metadata().database_type
  85. if db_type.endswith('City'):
  86. # GeoLite City database detected.
  87. self._city = reader
  88. self._city_file = path
  89. elif db_type.endswith('Country'):
  90. # GeoIP Country database detected.
  91. self._country = reader
  92. self._country_file = path
  93. else:
  94. raise GeoIP2Exception('Unable to recognize database edition: %s' % db_type)
  95. else:
  96. raise GeoIP2Exception('GeoIP path must be a valid file or directory.')
  97. @property
  98. def _reader(self):
  99. if self._country:
  100. return self._country
  101. else:
  102. return self._city
  103. @property
  104. def _country_or_city(self):
  105. if self._country:
  106. return self._country.country
  107. else:
  108. return self._city.city
  109. def __del__(self):
  110. # Cleanup any GeoIP file handles lying around.
  111. if self._reader:
  112. self._reader.close()
  113. def __repr__(self):
  114. meta = self._reader.metadata()
  115. version = '[v%s.%s]' % (meta.binary_format_major_version, meta.binary_format_minor_version)
  116. return '<%(cls)s %(version)s _country_file="%(country)s", _city_file="%(city)s">' % {
  117. 'cls': self.__class__.__name__,
  118. 'version': version,
  119. 'country': self._country_file,
  120. 'city': self._city_file,
  121. }
  122. def _check_query(self, query, country=False, city=False, city_or_country=False):
  123. "Helper routine for checking the query and database availability."
  124. # Making sure a string was passed in for the query.
  125. if not isinstance(query, six.string_types):
  126. raise TypeError('GeoIP query must be a string, not type %s' % type(query).__name__)
  127. # Extra checks for the existence of country and city databases.
  128. if city_or_country and not (self._country or self._city):
  129. raise GeoIP2Exception('Invalid GeoIP country and city data files.')
  130. elif country and not self._country:
  131. raise GeoIP2Exception('Invalid GeoIP country data file: %s' % self._country_file)
  132. elif city and not self._city:
  133. raise GeoIP2Exception('Invalid GeoIP city data file: %s' % self._city_file)
  134. # Return the query string back to the caller. GeoIP2 only takes IP addresses.
  135. if not (ipv4_re.match(query) or is_valid_ipv6_address(query)):
  136. query = socket.gethostbyname(query)
  137. return query
  138. def city(self, query):
  139. """
  140. Return a dictionary of city information for the given IP address or
  141. Fully Qualified Domain Name (FQDN). Some information in the dictionary
  142. may be undefined (None).
  143. """
  144. enc_query = self._check_query(query, city=True)
  145. return City(self._city.city(enc_query))
  146. def country_code(self, query):
  147. "Return the country code for the given IP Address or FQDN."
  148. enc_query = self._check_query(query, city_or_country=True)
  149. return self.country(enc_query)['country_code']
  150. def country_name(self, query):
  151. "Return the country name for the given IP Address or FQDN."
  152. enc_query = self._check_query(query, city_or_country=True)
  153. return self.country(enc_query)['country_name']
  154. def country(self, query):
  155. """
  156. Return a dictionary with the country code and name when given an
  157. IP address or a Fully Qualified Domain Name (FQDN). For example, both
  158. '24.124.1.80' and 'djangoproject.com' are valid parameters.
  159. """
  160. # Returning the country code and name
  161. enc_query = self._check_query(query, city_or_country=True)
  162. return Country(self._country_or_city(enc_query))
  163. # #### Coordinate retrieval routines ####
  164. def coords(self, query, ordering=('longitude', 'latitude')):
  165. cdict = self.city(query)
  166. if cdict is None:
  167. return None
  168. else:
  169. return tuple(cdict[o] for o in ordering)
  170. def lon_lat(self, query):
  171. "Return a tuple of the (longitude, latitude) for the given query."
  172. return self.coords(query)
  173. def lat_lon(self, query):
  174. "Return a tuple of the (latitude, longitude) for the given query."
  175. return self.coords(query, ('latitude', 'longitude'))
  176. def geos(self, query):
  177. "Return a GEOS Point object for the given query."
  178. ll = self.lon_lat(query)
  179. if ll:
  180. from django.contrib.gis.geos import Point
  181. return Point(ll, srid=4326)
  182. else:
  183. return None
  184. # #### GeoIP Database Information Routines ####
  185. @property
  186. def info(self):
  187. "Return information about the GeoIP library and databases in use."
  188. meta = self._reader.metadata()
  189. return 'GeoIP Library:\n\t%s.%s\n' % (meta.binary_format_major_version, meta.binary_format_minor_version)
  190. @classmethod
  191. def open(cls, full_path, cache):
  192. return GeoIP2(full_path, cache)