PageRenderTime 51ms CodeModel.GetById 36ms app.highlight 11ms RepoModel.GetById 1ms app.codeStats 1ms

/django/contrib/gis/utils/geoip.py

https://code.google.com/p/mango-py/
Python | 361 lines | 341 code | 0 blank | 20 comment | 8 complexity | 187ea5801ac7d86bbf16b350ae0c7d49 MD5 | raw file
  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"""
 41import os, re
 42from ctypes import c_char_p, c_float, c_int, Structure, CDLL, POINTER
 43from ctypes.util import find_library
 44from django.conf import settings
 45if not settings.configured: settings.configure()
 46
 47# Creating the settings dictionary with any settings, if needed.
 48GEOIP_SETTINGS = dict((key, getattr(settings, key))
 49                      for key in ('GEOIP_PATH', 'GEOIP_LIBRARY_PATH', 'GEOIP_COUNTRY', 'GEOIP_CITY')
 50                      if hasattr(settings, key))
 51lib_path = GEOIP_SETTINGS.get('GEOIP_LIBRARY_PATH', None)
 52
 53# GeoIP Exception class.
 54class 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/
 58if lib_path:
 59    lib_name = None
 60else:
 61    # TODO: Is this really the library name for Windows?
 62    lib_name = 'GeoIP'
 63
 64# Getting the path to the GeoIP library.
 65if lib_name: lib_path = find_library(lib_name)
 66if 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)
 68lgeoip = CDLL(lib_path)
 69
 70# Regular expressions for recognizing IP addresses and the GeoIP
 71# free database editions.
 72ipregex = re.compile(r'^(?P<w>\d\d?\d?)\.(?P<x>\d\d?\d?)\.(?P<y>\d\d?\d?)\.(?P<z>\d\d?\d?)$')
 73free_regex = re.compile(r'^GEO-\d{3}FREE')
 74lite_regex = re.compile(r'^GEO-\d{3}LITE')
 75
 76#### GeoIP C Structure definitions ####
 77class 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                ]
 98class GeoIPTag(Structure): pass
 99
100#### ctypes function prototypes ####
101RECTYPE = POINTER(GeoIPRecord)
102DBTYPE = POINTER(GeoIPTag)
103
104# For retrieving records by name or address.
105def record_output(func):
106    func.restype = RECTYPE
107    return func
108rec_by_addr = record_output(lgeoip.GeoIP_record_by_addr)
109rec_by_name = record_output(lgeoip.GeoIP_record_by_name)
110
111# For opening & closing GeoIP database files.
112geoip_open = lgeoip.GeoIP_open
113geoip_open.restype = DBTYPE
114geoip_close = lgeoip.GeoIP_delete
115geoip_close.argtypes = [DBTYPE]
116geoip_close.restype = None
117
118# String output routines.
119def string_output(func):
120    func.restype = c_char_p
121    return func
122geoip_dbinfo = string_output(lgeoip.GeoIP_database_info)
123cntry_code_by_addr = string_output(lgeoip.GeoIP_country_code_by_addr)
124cntry_code_by_name = string_output(lgeoip.GeoIP_country_code_by_name)
125cntry_name_by_addr = string_output(lgeoip.GeoIP_country_name_by_addr)
126cntry_name_by_name = string_output(lgeoip.GeoIP_country_name_by_name)
127
128#### GeoIP class ####
129class 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