/django/contrib/gis/utils/geoip.py
Python | 361 lines | 341 code | 0 blank | 20 comment | 8 complexity | 187ea5801ac7d86bbf16b350ae0c7d49 MD5 | raw file
Possible License(s): BSD-3-Clause
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