""" Pure Python GeoIP API. The API is based off of U{MaxMind's C-based Python API}, but the code itself is based on the U{pure PHP5 API} by Jim Winstead and Hans Lellelid. It is mostly a drop-in replacement, except the C{new} and C{open} methods are gone. You should instantiate the L{GeoIP} class yourself: C{gi = GeoIP('/path/to/GeoIP.dat', pygeoip.MEMORY_CACHE)} @author: Jennifer Ennis @license: Copyright(C) 2004 MaxMind LLC This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . """ from __future__ import with_statement, absolute_import, division import os import math import socket import mmap import gzip import codecs from StringIO import StringIO from . import const from .util import ip2long from .timezone import time_zone_by_country_and_region import six MMAP_CACHE = const.MMAP_CACHE MEMORY_CACHE = const.MEMORY_CACHE STANDARD = const.STANDARD class GeoIPError(Exception): pass class GeoIPMetaclass(type): def __new__(cls, *args, **kwargs): """ Singleton method to gets an instance without reparsing the db. Unique instances are instantiated based on the filename of the db. Flags are ignored for this, i.e. if you initialize one with STANDARD flag (default) and then try later to initialize with MEMORY_CACHE, it will still return the STANDARD one. """ if not hasattr(cls, '_instances'): cls._instances = {} if len(args) > 0: filename = args[0] elif 'filename' in kwargs: filename = kwargs['filename'] if not filename in cls._instances: cls._instances[filename] = type.__new__(cls, *args, **kwargs) return cls._instances[filename] GeoIPBase = GeoIPMetaclass('GeoIPBase', (object,), {}) class GeoIP(GeoIPBase): def __init__(self, filename, flags=0): """ Initialize the class. @param filename: path to a geoip database. If MEMORY_CACHE is used, the file can be gzipped. @type filename: str @param flags: flags that affect how the database is processed. Currently the only supported flags are STANDARD (the default), MEMORY_CACHE (preload the whole file into memory), and MMAP_CACHE (access the file via mmap). @type flags: int """ self._filename = filename self._flags = flags if self._flags & const.MMAP_CACHE: with open(filename, 'rb') as f: self._filehandle = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) elif self._flags & const.MEMORY_CACHE: if filename.endswith('.gz'): opener = gzip.open else: opener = open with opener(filename, 'rb') as f: self._memoryBuffer = f.read() self._filehandle = StringIO(self._memoryBuffer) else: self._filehandle = codecs.open(filename, 'rb','latin_1') self._setup_segments() def _setup_segments(self): """ Parses the database file to determine what kind of database is being used and setup segment sizes and start points that will be used by the seek*() methods later. """ self._databaseType = const.COUNTRY_EDITION self._recordLength = const.STANDARD_RECORD_LENGTH filepos = self._filehandle.tell() self._filehandle.seek(-3, os.SEEK_END) for i in range(const.STRUCTURE_INFO_MAX_SIZE): delim = self._filehandle.read(3) if delim == six.u(chr(255) * 3): self._databaseType = ord(self._filehandle.read(1)) if (self._databaseType >= 106): # backwards compatibility with databases from April 2003 and earlier self._databaseType -= 105 if self._databaseType == const.REGION_EDITION_REV0: self._databaseSegments = const.STATE_BEGIN_REV0 elif self._databaseType == const.REGION_EDITION_REV1: self._databaseSegments = const.STATE_BEGIN_REV1 elif self._databaseType in (const.CITY_EDITION_REV0, const.CITY_EDITION_REV1, const.ORG_EDITION, const.ISP_EDITION, const.ASNUM_EDITION): self._databaseSegments = 0 buf = self._filehandle.read(const.SEGMENT_RECORD_LENGTH) for j in range(const.SEGMENT_RECORD_LENGTH): self._databaseSegments += (ord(buf[j]) << (j * 8)) if self._databaseType in (const.ORG_EDITION, const.ISP_EDITION): self._recordLength = const.ORG_RECORD_LENGTH break else: self._filehandle.seek(-4, os.SEEK_CUR) if self._databaseType == const.COUNTRY_EDITION: self._databaseSegments = const.COUNTRY_BEGIN self._filehandle.seek(filepos, os.SEEK_SET) def _lookup_country_id(self, addr): """ Get the country index. This method is called by the _lookupCountryCode and _lookupCountryName methods. It looks up the index ('id') for the country which is the key for the code and name. @param addr: The IP address @type addr: str @return: network byte order 32-bit integer @rtype: int """ ipnum = ip2long(addr) if not ipnum: raise ValueError("Invalid IP address: %s" % addr) if self._databaseType != const.COUNTRY_EDITION: raise GeoIPError('Invalid database type; country_* methods expect '\ 'Country database') return self._seek_country(ipnum) - const.COUNTRY_BEGIN def _seek_country(self, ipnum): """ Using the record length and appropriate start points, seek to the country that corresponds to the converted IP address integer. @param ipnum: result of ip2long conversion @type ipnum: int @return: offset of start of record @rtype: int """ offset = 0 for depth in range(31, -1, -1): if self._flags & const.MEMORY_CACHE: startIndex = 2 * self._recordLength * offset length = 2 * self._recordLength endIndex = startIndex + length buf = self._memoryBuffer[startIndex:endIndex] else: self._filehandle.seek(2 * self._recordLength * offset, os.SEEK_SET) buf = self._filehandle.read(2 * self._recordLength) x = [0,0] for i in range(2): for j in range(self._recordLength): x[i] += ord(buf[self._recordLength * i + j]) << (j * 8) if ipnum & (1 << depth): if x[1] >= self._databaseSegments: return x[1] offset = x[1] else: if x[0] >= self._databaseSegments: return x[0] offset = x[0] raise Exception('Error traversing database - perhaps it is corrupt?') def _get_org(self, ipnum): """ Seek and return organization (or ISP) name for converted IP addr. @param ipnum: Converted IP address @type ipnum: int @return: org/isp name @rtype: str """ seek_org = self._seek_country(ipnum) if seek_org == self._databaseSegments: return None record_pointer = seek_org + (2 * self._recordLength - 1) * self._databaseSegments self._filehandle.seek(record_pointer, os.SEEK_SET) org_buf = self._filehandle.read(const.MAX_ORG_RECORD_LENGTH) return org_buf[:org_buf.index(chr(0))] def _get_region(self, ipnum): """ Seek and return the region info (dict containing country_code and region_name). @param ipnum: converted IP address @type ipnum: int @return: dict containing country_code and region_name @rtype: dict """ country_code = '' region = '' if self._databaseType == const.REGION_EDITION_REV0: seek_country = self._seek_country(ipnum) seek_region = seek_country - const.STATE_BEGIN_REV0 if seek_region >= 1000: country_code = 'US' region = ''.join([chr((seek_region // 1000) // 26 + 65), chr((seek_region // 1000) % 26 + 65)]) else: country_code = const.COUNTRY_CODES[seek_region] region = '' elif self._databaseType == const.REGION_EDITION_REV1: seek_country = self._seek_country(ipnum) seek_region = seek_country - const.STATE_BEGIN_REV1 if seek_region < const.US_OFFSET: country_code = ''; region = '' elif seek_region < const.CANADA_OFFSET: country_code = 'US' region = ''.join([chr((seek_region - const.US_OFFSET) // 26 + 65), chr((seek_region - const.US_OFFSET) % 26 + 65)]) elif seek_region < const.WORLD_OFFSET: country_code = 'CA' region = ''.join([chr((seek_region - const.CANADA_OFFSET) // 26 + 65), chr((seek_region - const.CANADA_OFFSET) % 26 + 65)]) else: i = (seek_region - const.WORLD_OFFSET) // const.FIPS_RANGE if i < len(const.COUNTRY_CODES): #country_code = const.COUNTRY_CODES[(seek_region - const.WORLD_OFFSET) // const.FIPS_RANGE] country_code = const.COUNTRY_CODES[i] else: country_code = '' region = '' elif self._databaseType in (const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): rec = self._get_record(ipnum) country_code = rec['country_code'] if 'country_code' in rec else '' region = rec['region_name'] if 'region_name' in rec else '' return {'country_code' : country_code, 'region_name' : region } def _get_record(self, ipnum): """ Populate location dict for converted IP. @param ipnum: converted IP address @type ipnum: int @return: dict with country_code, country_code3, country_name, region, city, postal_code, latitude, longitude, dma_code, metro_code, area_code, region_name, time_zone @rtype: dict """ seek_country = self._seek_country(ipnum) if seek_country == self._databaseSegments: return None record_pointer = seek_country + (2 * self._recordLength - 1) * self._databaseSegments self._filehandle.seek(record_pointer, os.SEEK_SET) record_buf = self._filehandle.read(const.FULL_RECORD_LENGTH) record = {} record_buf_pos = 0 char = ord(record_buf[record_buf_pos]) #char = record_buf[record_buf_pos] if six.PY3 else ord(record_buf[record_buf_pos]) record['country_code'] = const.COUNTRY_CODES[char] record['country_code3'] = const.COUNTRY_CODES3[char] record['country_name'] = const.COUNTRY_NAMES[char] record_buf_pos += 1 str_length = 0 # get region char = ord(record_buf[record_buf_pos+str_length]) while (char != 0): str_length += 1 char = ord(record_buf[record_buf_pos+str_length]) if str_length > 0: record['region_name'] = record_buf[record_buf_pos:record_buf_pos+str_length] record_buf_pos += str_length + 1 str_length = 0 # get city char = ord(record_buf[record_buf_pos+str_length]) while (char != 0): str_length += 1 char = ord(record_buf[record_buf_pos+str_length]) if str_length > 0: record['city'] = record_buf[record_buf_pos:record_buf_pos+str_length] else: record['city'] = '' record_buf_pos += str_length + 1 str_length = 0 # get the postal code char = ord(record_buf[record_buf_pos+str_length]) while (char != 0): str_length += 1 char = ord(record_buf[record_buf_pos+str_length]) if str_length > 0: record['postal_code'] = record_buf[record_buf_pos:record_buf_pos+str_length] else: record['postal_code'] = None record_buf_pos += str_length + 1 str_length = 0 latitude = 0 longitude = 0 for j in range(3): char = ord(record_buf[record_buf_pos]) record_buf_pos += 1 latitude += (char << (j * 8)) record['latitude'] = (latitude/10000.0) - 180.0 for j in range(3): char = ord(record_buf[record_buf_pos]) record_buf_pos += 1 longitude += (char << (j * 8)) record['longitude'] = (longitude/10000.0) - 180.0 if self._databaseType == const.CITY_EDITION_REV1: dmaarea_combo = 0 if record['country_code'] == 'US': for j in range(3): char = ord(record_buf[record_buf_pos]) record_buf_pos += 1 dmaarea_combo += (char << (j*8)) record['dma_code'] = int(math.floor(dmaarea_combo/1000)) record['area_code'] = dmaarea_combo%1000 else: record['dma_code'] = 0 record['area_code'] = 0 if 'dma_code' in record and record['dma_code'] in const.DMA_MAP: record['metro_code'] = const.DMA_MAP[record['dma_code']] else: record['metro_code'] = '' if 'country_code' in record: record['time_zone'] = time_zone_by_country_and_region( record['country_code'], record.get('region_name')) or '' else: record['time_zone'] = '' return record def country_code_by_addr(self, addr): """ Returns 2-letter country code (e.g. 'US') for specified IP address. Use this method if you have a Country, Region, or City database. @param addr: IP address @type addr: str @return: 2-letter country code @rtype: str """ try: if self._databaseType == const.COUNTRY_EDITION: country_id = self._lookup_country_id(addr) return const.COUNTRY_CODES[country_id] elif self._databaseType in (const.REGION_EDITION_REV0, const.REGION_EDITION_REV1, const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): return self.region_by_addr(addr)['country_code'] else: raise GeoIPError('Invalid database type; country_* methods expect '\ 'Country, City, or Region database') except ValueError: raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def country_code_by_name(self, hostname): """ Returns 2-letter country code (e.g. 'US') for specified hostname. Use this method if you have a Country, Region, or City database. @param hostname: host name @type hostname: str @return: 2-letter country code @rtype: str """ addr = socket.gethostbyname(hostname) return self.country_code_by_addr(addr) def country_name_by_addr(self, addr): """ Returns full country name for specified IP address. Use this method if you have a Country or City database. @param addr: IP address @type addr: str @return: country name @rtype: str """ try: if self._databaseType == const.COUNTRY_EDITION: country_id = self._lookup_country_id(addr) return const.COUNTRY_NAMES[country_id] elif self._databaseType in (const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): return self.record_by_addr(addr)['country_name'] else: raise GeoIPError('Invalid database type; country_* methods expect '\ 'Country or City database') except ValueError: raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def country_name_by_name(self, hostname): """ Returns full country name for specified hostname. Use this method if you have a Country database. @param hostname: host name @type hostname: str @return: country name @rtype: str """ addr = socket.gethostbyname(hostname) return self.country_name_by_addr(addr) def org_by_addr(self, addr): """ Lookup the organization (or ISP) for given IP address. Use this method if you have an Organization/ISP database. @param addr: IP address @type addr: str @return: organization or ISP name @rtype: str """ try: ipnum = ip2long(addr) if not ipnum: raise ValueError("Invalid IP address: %s" % addr) if self._databaseType not in (const.ORG_EDITION, const.ISP_EDITION, const.ASNUM_EDITION): raise GeoIPError('Invalid database type; org_* methods expect '\ 'Org/ISP database') return self._get_org(ipnum) except ValueError: raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def org_by_name(self, hostname): """ Lookup the organization (or ISP) for hostname. Use this method if you have an Organization/ISP database. @param hostname: host name @type hostname: str @return: organization or ISP name @rtype: str """ addr = socket.gethostbyname(hostname) return self.org_by_addr(addr) def record_by_addr(self, addr): """ Look up the record for a given IP address. Use this method if you have a City database. @param addr: IP address @type addr: str @return: dict with country_code, country_code3, country_name, region, city, postal_code, latitude, longitude, dma_code, metro_code, area_code, region_name, time_zone @rtype: dict """ try: ipnum = ip2long(addr) if not ipnum: raise ValueError("Invalid IP address: %s" % addr) if not self._databaseType in (const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): raise GeoIPError('Invalid database type; record_* methods expect City database') return self._get_record(ipnum) except ValueError: raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def record_by_name(self, hostname): """ Look up the record for a given hostname. Use this method if you have a City database. @param hostname: host name @type hostname: str @return: dict with country_code, country_code3, country_name, region, city, postal_code, latitude, longitude, dma_code, metro_code, area_code, region_name, time_zone @rtype: dict """ addr = socket.gethostbyname(hostname) return self.record_by_addr(addr) def region_by_addr(self, addr): """ Lookup the region for given IP address. Use this method if you have a Region database. @param addr: IP address @type addr: str @return: dict containing country_code, region, and region_name @rtype: dict """ try: ipnum = ip2long(addr) if not ipnum: raise ValueError("Invalid IP address: %s" % addr) if not self._databaseType in (const.REGION_EDITION_REV0, const.REGION_EDITION_REV1, const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): raise GeoIPError('Invalid database type; region_* methods expect '\ 'Region or City database') return self._get_region(ipnum) except ValueError: raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def region_by_name(self, hostname): """ Lookup the region for given hostname. Use this method if you have a Region database. @param hostname: host name @type hostname: str @return: dict containing country_code, region, and region_name @rtype: dict """ addr = socket.gethostbyname(hostname) return self.region_by_addr(addr) def time_zone_by_addr(self, addr): """ Look up the time zone for a given IP address. Use this method if you have a Region or City database. @param hostname: IP address @type hostname: str @return: Time zone @rtype: str """ try: ipnum = ip2long(addr) if not ipnum: raise ValueError("Invalid IP address: %s" % addr) if not self._databaseType in (const.REGION_EDITION_REV0, const.REGION_EDITION_REV1, const.CITY_EDITION_REV0, const.CITY_EDITION_REV1): raise GeoIPError('Invalid database type; region_* methods expect '\ 'Region or City database') return self._get_record(ipnum)['time_zone'] except ValueError: raise GeoIPError('*_by_addr methods only accept IP addresses. Use *_by_name for hostnames. (Address: %s)' % addr) def time_zone_by_name(self, hostname): """ Look up the time zone for a given hostname. Use this method if you have a Region or City database. @param hostname: host name @type hostname: str @return: Time zone @rtype: str """ addr = socket.gethostbyname(hostname) return self.time_zone_by_addr(addr)