diff --git a/pygeoip/__init__.py b/pygeoip/__init__.py index 4237ee3..3e89422 100644 --- a/pygeoip/__init__.py +++ b/pygeoip/__init__.py @@ -1,641 +1,647 @@ -""" -Pure Python GeoIP API. The API is based off of U{MaxMind's C-based Python API<http://www.maxmind.com/app/python>}, -but the code itself is based on the U{pure PHP5 API<http://pear.php.net/package/Net_GeoIP/>} -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 <zaylea at gmail dot com> - -@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 <http://www.gnu.org/licenses/lgpl.txt>. -""" - -from __future__ import with_statement, absolute_import, division -import os -import math -import socket -import mmap -import gzip -import codecs - -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: - self._filehandle = gzip.open(filename, 'rb') - self._memoryBuffer = self._filehandle.read() - - 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) +""" +Pure Python GeoIP API. The API is based off of U{MaxMind's C-based Python API<http://www.maxmind.com/app/python>}, +but the code itself is based on the U{pure PHP5 API<http://pear.php.net/package/Net_GeoIP/>} +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 <zaylea at gmail dot com> + +@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 <http://www.gnu.org/licenses/lgpl.txt>. +""" + +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)