web/lib/django/contrib/gis/utils/geoip.py
changeset 0 0d40e90630ef
equal deleted inserted replaced
-1:000000000000 0:0d40e90630ef
       
     1 """
       
     2  This module houses the GeoIP object, a ctypes wrapper for the MaxMind GeoIP(R)
       
     3  C API (http://www.maxmind.com/app/c).  This is an alternative to the GPL
       
     4  licensed Python GeoIP interface provided by MaxMind.
       
     5 
       
     6  GeoIP(R) is a registered trademark of MaxMind, LLC of Boston, Massachusetts.
       
     7 
       
     8  For IP-based geolocation, this module requires the GeoLite Country and City
       
     9  datasets, in binary format (CSV will not work!).  The datasets may be
       
    10  downloaded from MaxMind at http://www.maxmind.com/download/geoip/database/.
       
    11  Grab GeoIP.dat.gz and GeoLiteCity.dat.gz, and unzip them in the directory
       
    12  corresponding to settings.GEOIP_PATH.  See the GeoIP docstring and examples
       
    13  below for more details.
       
    14 
       
    15  TODO: Verify compatibility with Windows.
       
    16 
       
    17  Example:
       
    18 
       
    19  >>> from django.contrib.gis.utils import GeoIP
       
    20  >>> g = GeoIP()
       
    21  >>> g.country('google.com')
       
    22  {'country_code': 'US', 'country_name': 'United States'}
       
    23  >>> g.city('72.14.207.99')
       
    24  {'area_code': 650,
       
    25  'city': 'Mountain View',
       
    26  'country_code': 'US',
       
    27  'country_code3': 'USA',
       
    28  'country_name': 'United States',
       
    29  'dma_code': 807,
       
    30  'latitude': 37.419200897216797,
       
    31  'longitude': -122.05740356445312,
       
    32  'postal_code': '94043',
       
    33  'region': 'CA'}
       
    34  >>> g.lat_lon('salon.com')
       
    35  (37.789798736572266, -122.39420318603516)
       
    36  >>> g.lon_lat('uh.edu')
       
    37  (-95.415199279785156, 29.77549934387207)
       
    38  >>> g.geos('24.124.1.80').wkt
       
    39  'POINT (-95.2087020874023438 39.0392990112304688)'
       
    40 """
       
    41 import os, re
       
    42 from ctypes import c_char_p, c_float, c_int, Structure, CDLL, POINTER
       
    43 from ctypes.util import find_library
       
    44 from django.conf import settings
       
    45 if not settings.configured: settings.configure()
       
    46 
       
    47 # Creating the settings dictionary with any settings, if needed.
       
    48 GEOIP_SETTINGS = dict((key, getattr(settings, key))
       
    49                       for key in ('GEOIP_PATH', 'GEOIP_LIBRARY_PATH', 'GEOIP_COUNTRY', 'GEOIP_CITY')
       
    50                       if hasattr(settings, key))
       
    51 lib_path = GEOIP_SETTINGS.get('GEOIP_LIBRARY_PATH', None)
       
    52 
       
    53 # GeoIP Exception class.
       
    54 class GeoIPException(Exception): pass
       
    55 
       
    56 # The shared library for the GeoIP C API.  May be downloaded
       
    57 #  from http://www.maxmind.com/download/geoip/api/c/
       
    58 if lib_path:
       
    59     lib_name = None
       
    60 else:
       
    61     # TODO: Is this really the library name for Windows?
       
    62     lib_name = 'GeoIP'
       
    63 
       
    64 # Getting the path to the GeoIP library.
       
    65 if lib_name: lib_path = find_library(lib_name)
       
    66 if lib_path is None: raise GeoIPException('Could not find the GeoIP library (tried "%s"). '
       
    67                                           'Try setting GEOIP_LIBRARY_PATH in your settings.' % lib_name)
       
    68 lgeoip = CDLL(lib_path)
       
    69 
       
    70 # Regular expressions for recognizing IP addresses and the GeoIP
       
    71 # free database editions.
       
    72 ipregex = re.compile(r'^(?P<w>\d\d?\d?)\.(?P<x>\d\d?\d?)\.(?P<y>\d\d?\d?)\.(?P<z>\d\d?\d?)$')
       
    73 free_regex = re.compile(r'^GEO-\d{3}FREE')
       
    74 lite_regex = re.compile(r'^GEO-\d{3}LITE')
       
    75 
       
    76 #### GeoIP C Structure definitions ####
       
    77 class GeoIPRecord(Structure):
       
    78     _fields_ = [('country_code', c_char_p),
       
    79                 ('country_code3', c_char_p),
       
    80                 ('country_name', c_char_p),
       
    81                 ('region', c_char_p),
       
    82                 ('city', c_char_p),
       
    83                 ('postal_code', c_char_p),
       
    84                 ('latitude', c_float),
       
    85                 ('longitude', c_float),
       
    86                 # TODO: In 1.4.6 this changed from `int dma_code;` to
       
    87                 # `union {int metro_code; int dma_code;};`.  Change
       
    88                 # to a `ctypes.Union` in to accomodate in future when
       
    89                 # pre-1.4.6 versions are no longer distributed.
       
    90                 ('dma_code', c_int),
       
    91                 ('area_code', c_int),
       
    92                 # TODO: The following structure fields were added in 1.4.3 --
       
    93                 # uncomment these fields when sure previous versions are no
       
    94                 # longer distributed by package maintainers.
       
    95                 #('charset', c_int),
       
    96                 #('continent_code', c_char_p),
       
    97                 ]
       
    98 class GeoIPTag(Structure): pass
       
    99 
       
   100 #### ctypes function prototypes ####
       
   101 RECTYPE = POINTER(GeoIPRecord)
       
   102 DBTYPE = POINTER(GeoIPTag)
       
   103 
       
   104 # For retrieving records by name or address.
       
   105 def record_output(func):
       
   106     func.restype = RECTYPE
       
   107     return func
       
   108 rec_by_addr = record_output(lgeoip.GeoIP_record_by_addr)
       
   109 rec_by_name = record_output(lgeoip.GeoIP_record_by_name)
       
   110 
       
   111 # For opening & closing GeoIP database files.
       
   112 geoip_open = lgeoip.GeoIP_open
       
   113 geoip_open.restype = DBTYPE
       
   114 geoip_close = lgeoip.GeoIP_delete
       
   115 geoip_close.argtypes = [DBTYPE]
       
   116 geoip_close.restype = None
       
   117 
       
   118 # String output routines.
       
   119 def string_output(func):
       
   120     func.restype = c_char_p
       
   121     return func
       
   122 geoip_dbinfo = string_output(lgeoip.GeoIP_database_info)
       
   123 cntry_code_by_addr = string_output(lgeoip.GeoIP_country_code_by_addr)
       
   124 cntry_code_by_name = string_output(lgeoip.GeoIP_country_code_by_name)
       
   125 cntry_name_by_addr = string_output(lgeoip.GeoIP_country_name_by_addr)
       
   126 cntry_name_by_name = string_output(lgeoip.GeoIP_country_name_by_name)
       
   127 
       
   128 #### GeoIP class ####
       
   129 class GeoIP(object):
       
   130     # The flags for GeoIP memory caching.
       
   131     # GEOIP_STANDARD - read database from filesystem, uses least memory.
       
   132     #
       
   133     # GEOIP_MEMORY_CACHE - load database into memory, faster performance
       
   134     #        but uses more memory
       
   135     #
       
   136     # GEOIP_CHECK_CACHE - check for updated database.  If database has been updated,
       
   137     #        reload filehandle and/or memory cache.
       
   138     #
       
   139     # GEOIP_INDEX_CACHE - just cache
       
   140     #        the most frequently accessed index portion of the database, resulting
       
   141     #        in faster lookups than GEOIP_STANDARD, but less memory usage than
       
   142     #        GEOIP_MEMORY_CACHE - useful for larger databases such as
       
   143     #        GeoIP Organization and GeoIP City.  Note, for GeoIP Country, Region
       
   144     #        and Netspeed databases, GEOIP_INDEX_CACHE is equivalent to GEOIP_MEMORY_CACHE
       
   145     #
       
   146     GEOIP_STANDARD = 0
       
   147     GEOIP_MEMORY_CACHE = 1
       
   148     GEOIP_CHECK_CACHE = 2
       
   149     GEOIP_INDEX_CACHE = 4
       
   150     cache_options = dict((opt, None) for opt in (0, 1, 2, 4))
       
   151     _city_file = ''
       
   152     _country_file = ''
       
   153 
       
   154     # Initially, pointers to GeoIP file references are NULL.
       
   155     _city = None
       
   156     _country = None
       
   157 
       
   158     def __init__(self, path=None, cache=0, country=None, city=None):
       
   159         """
       
   160         Initializes the GeoIP object, no parameters are required to use default
       
   161         settings.  Keyword arguments may be passed in to customize the locations
       
   162         of the GeoIP data sets.
       
   163 
       
   164         * path: Base directory to where GeoIP data is located or the full path
       
   165             to where the city or country data files (*.dat) are located.
       
   166             Assumes that both the city and country data sets are located in
       
   167             this directory; overrides the GEOIP_PATH settings attribute.
       
   168 
       
   169         * cache: The cache settings when opening up the GeoIP datasets,
       
   170             and may be an integer in (0, 1, 2, 4) corresponding to
       
   171             the GEOIP_STANDARD, GEOIP_MEMORY_CACHE, GEOIP_CHECK_CACHE,
       
   172             and GEOIP_INDEX_CACHE `GeoIPOptions` C API settings,
       
   173             respectively.  Defaults to 0, meaning that the data is read
       
   174             from the disk.
       
   175 
       
   176         * country: The name of the GeoIP country data file.  Defaults to
       
   177             'GeoIP.dat'; overrides the GEOIP_COUNTRY settings attribute.
       
   178 
       
   179         * city: The name of the GeoIP city data file.  Defaults to
       
   180             'GeoLiteCity.dat'; overrides the GEOIP_CITY settings attribute.
       
   181         """
       
   182         # Checking the given cache option.
       
   183         if cache in self.cache_options:
       
   184             self._cache = self.cache_options[cache]
       
   185         else:
       
   186             raise GeoIPException('Invalid caching option: %s' % cache)
       
   187 
       
   188         # Getting the GeoIP data path.
       
   189         if not path:
       
   190             path = GEOIP_SETTINGS.get('GEOIP_PATH', None)
       
   191             if not path: raise GeoIPException('GeoIP path must be provided via parameter or the GEOIP_PATH setting.')
       
   192         if not isinstance(path, basestring):
       
   193             raise TypeError('Invalid path type: %s' % type(path).__name__)
       
   194 
       
   195         if os.path.isdir(path):
       
   196             # Constructing the GeoIP database filenames using the settings
       
   197             # dictionary.  If the database files for the GeoLite country
       
   198             # and/or city datasets exist, then try and open them.
       
   199             country_db = os.path.join(path, country or GEOIP_SETTINGS.get('GEOIP_COUNTRY', 'GeoIP.dat'))
       
   200             if os.path.isfile(country_db):
       
   201                 self._country = geoip_open(country_db, cache)
       
   202                 self._country_file = country_db
       
   203 
       
   204             city_db = os.path.join(path, city or GEOIP_SETTINGS.get('GEOIP_CITY', 'GeoLiteCity.dat'))
       
   205             if os.path.isfile(city_db):
       
   206                 self._city = geoip_open(city_db, cache)
       
   207                 self._city_file = city_db
       
   208         elif os.path.isfile(path):
       
   209             # Otherwise, some detective work will be needed to figure
       
   210             # out whether the given database path is for the GeoIP country
       
   211             # or city databases.
       
   212             ptr = geoip_open(path, cache)
       
   213             info = geoip_dbinfo(ptr)
       
   214             if lite_regex.match(info):
       
   215                 # GeoLite City database detected.
       
   216                 self._city = ptr
       
   217                 self._city_file = path
       
   218             elif free_regex.match(info):
       
   219                 # GeoIP Country database detected.
       
   220                 self._country = ptr
       
   221                 self._country_file = path
       
   222             else:
       
   223                 raise GeoIPException('Unable to recognize database edition: %s' % info)
       
   224         else:
       
   225             raise GeoIPException('GeoIP path must be a valid file or directory.')
       
   226 
       
   227     def __del__(self):
       
   228         # Cleaning any GeoIP file handles lying around.
       
   229         if self._country: geoip_close(self._country)
       
   230         if self._city: geoip_close(self._city)
       
   231 
       
   232     def _check_query(self, query, country=False, city=False, city_or_country=False):
       
   233         "Helper routine for checking the query and database availability."
       
   234         # Making sure a string was passed in for the query.
       
   235         if not isinstance(query, basestring):
       
   236             raise TypeError('GeoIP query must be a string, not type %s' % type(query).__name__)
       
   237 
       
   238         # Extra checks for the existence of country and city databases.
       
   239         if city_or_country and not (self._country or self._city):
       
   240             raise GeoIPException('Invalid GeoIP country and city data files.')
       
   241         elif country and not self._country:
       
   242             raise GeoIPException('Invalid GeoIP country data file: %s' % self._country_file)
       
   243         elif city and not self._city:
       
   244             raise GeoIPException('Invalid GeoIP city data file: %s' % self._city_file)
       
   245 
       
   246     def city(self, query):
       
   247         """
       
   248         Returns a dictionary of city information for the given IP address or
       
   249         Fully Qualified Domain Name (FQDN).  Some information in the dictionary
       
   250         may be undefined (None).
       
   251         """
       
   252         self._check_query(query, city=True)
       
   253         if ipregex.match(query):
       
   254             # If an IP address was passed in
       
   255             ptr = rec_by_addr(self._city, c_char_p(query))
       
   256         else:
       
   257             # If a FQDN was passed in.
       
   258             ptr = rec_by_name(self._city, c_char_p(query))
       
   259 
       
   260         # Checking the pointer to the C structure, if valid pull out elements
       
   261         # into a dicionary and return.
       
   262         if bool(ptr):
       
   263             record = ptr.contents
       
   264             return dict((tup[0], getattr(record, tup[0])) for tup in record._fields_)
       
   265         else:
       
   266             return None
       
   267 
       
   268     def country_code(self, query):
       
   269         "Returns the country code for the given IP Address or FQDN."
       
   270         self._check_query(query, city_or_country=True)
       
   271         if self._country:
       
   272             if ipregex.match(query): return cntry_code_by_addr(self._country, query)
       
   273             else: return cntry_code_by_name(self._country, query)
       
   274         else:
       
   275             return self.city(query)['country_code']
       
   276 
       
   277     def country_name(self, query):
       
   278         "Returns the country name for the given IP Address or FQDN."
       
   279         self._check_query(query, city_or_country=True)
       
   280         if self._country:
       
   281             if ipregex.match(query): return cntry_name_by_addr(self._country, query)
       
   282             else: return cntry_name_by_name(self._country, query)
       
   283         else:
       
   284             return self.city(query)['country_name']
       
   285 
       
   286     def country(self, query):
       
   287         """
       
   288         Returns a dictonary with with the country code and name when given an
       
   289         IP address or a Fully Qualified Domain Name (FQDN).  For example, both
       
   290         '24.124.1.80' and 'djangoproject.com' are valid parameters.
       
   291         """
       
   292         # Returning the country code and name
       
   293         return {'country_code' : self.country_code(query),
       
   294                 'country_name' : self.country_name(query),
       
   295                 }
       
   296 
       
   297     #### Coordinate retrieval routines ####
       
   298     def coords(self, query, ordering=('longitude', 'latitude')):
       
   299         cdict = self.city(query)
       
   300         if cdict is None: return None
       
   301         else: return tuple(cdict[o] for o in ordering)
       
   302 
       
   303     def lon_lat(self, query):
       
   304         "Returns a tuple of the (longitude, latitude) for the given query."
       
   305         return self.coords(query)
       
   306 
       
   307     def lat_lon(self, query):
       
   308         "Returns a tuple of the (latitude, longitude) for the given query."
       
   309         return self.coords(query, ('latitude', 'longitude'))
       
   310 
       
   311     def geos(self, query):
       
   312         "Returns a GEOS Point object for the given query."
       
   313         ll = self.lon_lat(query)
       
   314         if ll:
       
   315             from django.contrib.gis.geos import Point
       
   316             return Point(ll, srid=4326)
       
   317         else:
       
   318             return None
       
   319 
       
   320     #### GeoIP Database Information Routines ####
       
   321     def country_info(self):
       
   322         "Returns information about the GeoIP country database."
       
   323         if self._country is None:
       
   324             ci = 'No GeoIP Country data in "%s"' % self._country_file
       
   325         else:
       
   326             ci = geoip_dbinfo(self._country)
       
   327         return ci
       
   328     country_info = property(country_info)
       
   329 
       
   330     def city_info(self):
       
   331         "Retuns information about the GeoIP city database."
       
   332         if self._city is None:
       
   333             ci = 'No GeoIP City data in "%s"' % self._city_file
       
   334         else:
       
   335             ci = geoip_dbinfo(self._city)
       
   336         return ci
       
   337     city_info = property(city_info)
       
   338 
       
   339     def info(self):
       
   340         "Returns information about all GeoIP databases in use."
       
   341         return 'Country:\n\t%s\nCity:\n\t%s' % (self.country_info, self.city_info)
       
   342     info = property(info)
       
   343 
       
   344     #### Methods for compatibility w/the GeoIP-Python API. ####
       
   345     @classmethod
       
   346     def open(cls, full_path, cache):
       
   347         return GeoIP(full_path, cache)
       
   348 
       
   349     def _rec_by_arg(self, arg):
       
   350         if self._city:
       
   351             return self.city(arg)
       
   352         else:
       
   353             return self.country(arg)
       
   354     region_by_addr = city
       
   355     region_by_name = city
       
   356     record_by_addr = _rec_by_arg
       
   357     record_by_name = _rec_by_arg
       
   358     country_code_by_addr = country_code
       
   359     country_code_by_name = country_code
       
   360     country_name_by_addr = country_name
       
   361     country_name_by_name = country_name