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.
 
 
 
 

230 lines
8.8 KiB

  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)