base.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import os
  2. import re
  3. from ctypes import c_char_p
  4. from django.core.validators import ipv4_re
  5. from django.contrib.gis.geoip.libgeoip import GEOIP_SETTINGS
  6. from django.contrib.gis.geoip.prototypes import (
  7. GeoIP_open, GeoIP_delete, GeoIP_database_info,
  8. GeoIP_lib_version, GeoIP_record_by_addr, GeoIP_record_by_name,
  9. GeoIP_country_code_by_addr, GeoIP_country_code_by_name,
  10. GeoIP_country_name_by_addr, GeoIP_country_name_by_name)
  11. from django.utils import six
  12. from django.utils.encoding import force_bytes
  13. # Regular expressions for recognizing the GeoIP free database editions.
  14. free_regex = re.compile(r'^GEO-\d{3}FREE')
  15. lite_regex = re.compile(r'^GEO-\d{3}LITE')
  16. #### GeoIP classes ####
  17. class GeoIPException(Exception):
  18. pass
  19. class GeoIP(object):
  20. # The flags for GeoIP memory caching.
  21. # GEOIP_STANDARD - read database from filesystem, uses least memory.
  22. #
  23. # GEOIP_MEMORY_CACHE - load database into memory, faster performance
  24. # but uses more memory
  25. #
  26. # GEOIP_CHECK_CACHE - check for updated database. If database has been
  27. # updated, reload filehandle and/or memory cache. This option
  28. # is not thread safe.
  29. #
  30. # GEOIP_INDEX_CACHE - just cache the most frequently accessed index
  31. # portion of the database, resulting in faster lookups than
  32. # GEOIP_STANDARD, but less memory usage than GEOIP_MEMORY_CACHE -
  33. # useful for larger databases such as GeoIP Organization and
  34. # GeoIP City. Note, for GeoIP Country, Region and Netspeed
  35. # databases, GEOIP_INDEX_CACHE is equivalent to GEOIP_MEMORY_CACHE
  36. #
  37. # GEOIP_MMAP_CACHE - load database into mmap shared memory ( not available
  38. # on Windows).
  39. GEOIP_STANDARD = 0
  40. GEOIP_MEMORY_CACHE = 1
  41. GEOIP_CHECK_CACHE = 2
  42. GEOIP_INDEX_CACHE = 4
  43. GEOIP_MMAP_CACHE = 8
  44. cache_options = dict((opt, None) for opt in (0, 1, 2, 4, 8))
  45. # Paths to the city & country binary databases.
  46. _city_file = ''
  47. _country_file = ''
  48. # Initially, pointers to GeoIP file references are NULL.
  49. _city = None
  50. _country = None
  51. def __init__(self, path=None, cache=0, country=None, city=None):
  52. """
  53. Initializes the GeoIP object, no parameters are required to use default
  54. settings. Keyword arguments may be passed in to customize the locations
  55. of the GeoIP data sets.
  56. * path: Base directory to where GeoIP data is located or the full path
  57. to where the city or country data files (*.dat) are located.
  58. Assumes that both the city and country data sets are located in
  59. this directory; overrides the GEOIP_PATH settings attribute.
  60. * cache: The cache settings when opening up the GeoIP datasets,
  61. and may be an integer in (0, 1, 2, 4, 8) corresponding to
  62. the GEOIP_STANDARD, GEOIP_MEMORY_CACHE, GEOIP_CHECK_CACHE,
  63. GEOIP_INDEX_CACHE, and GEOIP_MMAP_CACHE, `GeoIPOptions` C API
  64. settings, respectively. Defaults to 0, meaning that the data is read
  65. from the disk.
  66. * country: The name of the GeoIP country data file. Defaults to
  67. 'GeoIP.dat'; overrides the GEOIP_COUNTRY settings attribute.
  68. * city: The name of the GeoIP city data file. Defaults to
  69. 'GeoLiteCity.dat'; overrides the GEOIP_CITY settings attribute.
  70. """
  71. # Checking the given cache option.
  72. if cache in self.cache_options:
  73. self._cache = cache
  74. else:
  75. raise GeoIPException('Invalid GeoIP caching option: %s' % cache)
  76. # Getting the GeoIP data path.
  77. if not path:
  78. path = GEOIP_SETTINGS.get('GEOIP_PATH', None)
  79. if not path:
  80. raise GeoIPException('GeoIP path must be provided via parameter or the GEOIP_PATH setting.')
  81. if not isinstance(path, six.string_types):
  82. raise TypeError('Invalid path type: %s' % type(path).__name__)
  83. if os.path.isdir(path):
  84. # Constructing the GeoIP database filenames using the settings
  85. # dictionary. If the database files for the GeoLite country
  86. # and/or city datasets exist, then try and open them.
  87. country_db = os.path.join(path, country or GEOIP_SETTINGS.get('GEOIP_COUNTRY', 'GeoIP.dat'))
  88. if os.path.isfile(country_db):
  89. self._country = GeoIP_open(force_bytes(country_db), cache)
  90. self._country_file = country_db
  91. city_db = os.path.join(path, city or GEOIP_SETTINGS.get('GEOIP_CITY', 'GeoLiteCity.dat'))
  92. if os.path.isfile(city_db):
  93. self._city = GeoIP_open(force_bytes(city_db), cache)
  94. self._city_file = city_db
  95. elif os.path.isfile(path):
  96. # Otherwise, some detective work will be needed to figure
  97. # out whether the given database path is for the GeoIP country
  98. # or city databases.
  99. ptr = GeoIP_open(force_bytes(path), cache)
  100. info = GeoIP_database_info(ptr)
  101. if lite_regex.match(info):
  102. # GeoLite City database detected.
  103. self._city = ptr
  104. self._city_file = path
  105. elif free_regex.match(info):
  106. # GeoIP Country database detected.
  107. self._country = ptr
  108. self._country_file = path
  109. else:
  110. raise GeoIPException('Unable to recognize database edition: %s' % info)
  111. else:
  112. raise GeoIPException('GeoIP path must be a valid file or directory.')
  113. def __del__(self):
  114. # Cleaning any GeoIP file handles lying around.
  115. if GeoIP_delete is None:
  116. return
  117. if self._country:
  118. GeoIP_delete(self._country)
  119. if self._city:
  120. GeoIP_delete(self._city)
  121. def _check_query(self, query, country=False, city=False, city_or_country=False):
  122. "Helper routine for checking the query and database availability."
  123. # Making sure a string was passed in for the query.
  124. if not isinstance(query, six.string_types):
  125. raise TypeError('GeoIP query must be a string, not type %s' % type(query).__name__)
  126. # Extra checks for the existence of country and city databases.
  127. if city_or_country and not (self._country or self._city):
  128. raise GeoIPException('Invalid GeoIP country and city data files.')
  129. elif country and not self._country:
  130. raise GeoIPException('Invalid GeoIP country data file: %s' % self._country_file)
  131. elif city and not self._city:
  132. raise GeoIPException('Invalid GeoIP city data file: %s' % self._city_file)
  133. # Return the query string back to the caller. GeoIP only takes bytestrings.
  134. return force_bytes(query)
  135. def city(self, query):
  136. """
  137. Returns a dictionary of city information for the given IP address or
  138. Fully Qualified Domain Name (FQDN). Some information in the dictionary
  139. may be undefined (None).
  140. """
  141. enc_query = self._check_query(query, city=True)
  142. if ipv4_re.match(query):
  143. # If an IP address was passed in
  144. return GeoIP_record_by_addr(self._city, c_char_p(enc_query))
  145. else:
  146. # If a FQDN was passed in.
  147. return GeoIP_record_by_name(self._city, c_char_p(enc_query))
  148. def country_code(self, query):
  149. "Returns the country code for the given IP Address or FQDN."
  150. enc_query = self._check_query(query, city_or_country=True)
  151. if self._country:
  152. if ipv4_re.match(query):
  153. return GeoIP_country_code_by_addr(self._country, enc_query)
  154. else:
  155. return GeoIP_country_code_by_name(self._country, enc_query)
  156. else:
  157. return self.city(query)['country_code']
  158. def country_name(self, query):
  159. "Returns the country name for the given IP Address or FQDN."
  160. enc_query = self._check_query(query, city_or_country=True)
  161. if self._country:
  162. if ipv4_re.match(query):
  163. return GeoIP_country_name_by_addr(self._country, enc_query)
  164. else:
  165. return GeoIP_country_name_by_name(self._country, enc_query)
  166. else:
  167. return self.city(query)['country_name']
  168. def country(self, query):
  169. """
  170. Returns a dictionary with the country code and name when given an
  171. IP address or a Fully Qualified Domain Name (FQDN). For example, both
  172. '24.124.1.80' and 'djangoproject.com' are valid parameters.
  173. """
  174. # Returning the country code and name
  175. return {'country_code': self.country_code(query),
  176. 'country_name': self.country_name(query),
  177. }
  178. #### Coordinate retrieval routines ####
  179. def coords(self, query, ordering=('longitude', 'latitude')):
  180. cdict = self.city(query)
  181. if cdict is None:
  182. return None
  183. else:
  184. return tuple(cdict[o] for o in ordering)
  185. def lon_lat(self, query):
  186. "Returns a tuple of the (longitude, latitude) for the given query."
  187. return self.coords(query)
  188. def lat_lon(self, query):
  189. "Returns a tuple of the (latitude, longitude) for the given query."
  190. return self.coords(query, ('latitude', 'longitude'))
  191. def geos(self, query):
  192. "Returns a GEOS Point object for the given query."
  193. ll = self.lon_lat(query)
  194. if ll:
  195. from django.contrib.gis.geos import Point
  196. return Point(ll, srid=4326)
  197. else:
  198. return None
  199. #### GeoIP Database Information Routines ####
  200. @property
  201. def country_info(self):
  202. "Returns information about the GeoIP country database."
  203. if self._country is None:
  204. ci = 'No GeoIP Country data in "%s"' % self._country_file
  205. else:
  206. ci = GeoIP_database_info(self._country)
  207. return ci
  208. @property
  209. def city_info(self):
  210. "Retuns information about the GeoIP city database."
  211. if self._city is None:
  212. ci = 'No GeoIP City data in "%s"' % self._city_file
  213. else:
  214. ci = GeoIP_database_info(self._city)
  215. return ci
  216. @property
  217. def info(self):
  218. "Returns information about the GeoIP library and databases in use."
  219. info = ''
  220. if GeoIP_lib_version:
  221. info += 'GeoIP Library:\n\t%s\n' % GeoIP_lib_version()
  222. return info + 'Country:\n\t%s\nCity:\n\t%s' % (self.country_info, self.city_info)
  223. #### Methods for compatibility w/the GeoIP-Python API. ####
  224. @classmethod
  225. def open(cls, full_path, cache):
  226. return GeoIP(full_path, cache)
  227. def _rec_by_arg(self, arg):
  228. if self._city:
  229. return self.city(arg)
  230. else:
  231. return self.country(arg)
  232. region_by_addr = city
  233. region_by_name = city
  234. record_by_addr = _rec_by_arg
  235. record_by_name = _rec_by_arg
  236. country_code_by_addr = country_code
  237. country_code_by_name = country_code
  238. country_name_by_addr = country_name
  239. country_name_by_name = country_name