Geolocalización IP en Python3 con MaxMind y Nominatim

Actualizado el 7/4/2020. / Actualizado el 11/6/2020.

Introducción

GeoIp es un programa destinado a la geolocalización de una dirección IP con licencia libre para su uso sin restricción alguna.
No solo permite la obtención de la Geolicalización, sino que también permite obtener el Sistema Autónomo (ASN) al que pertenece la IP y la organización que lo tiene registrado.

Se encuentra escrito en Python3, adjuntando también un script escrito en Bash para actualizar las bases de datos de MaxMind periódicamente desde el Cron del sistema.

Para ello se utilizan las siguientes herramientas:

MaxMind

Bases de datos de MaxMind, en concreto las bases de datos City y ASN de su producto gratuito GeoLite2.

ACTUALIZACIÓN
Tras los últimos cambios aplicados el 30 de Diciembre del 2019 es necesario disponer de una cuenta gratuita en esta plataforma, pudiendo registrarse en el siguiente enlace.

Una vez creada la cuenta hay que generar un código o token para poder descargarse las bases de datos. Este código lo generamos, una vez validados en el portal, en el siguiente enlace.

Las instrucciones para descargar los archivos a partir de su URL se pueden consultar aquí.

Toda la información de estos cambios se pueden consultar en el siguiente enlace.

Nominatim

A partir de las coordenadas de latitud y longitud obtenidas con las bases de datos de MaxMind obtenemos la ubicación con la herramienta Nominatim, la cual hace uso de la información de OpenStreetMap.

Esta herramienta tiene una serie de restricciones y limitaciones que debemos de tener en cuenta para su uso. Se encuentran aquí.


Instalación

Se recomienda hacer uso de entornos virtuales de Python para desplegar la herramienta. Puedes consultar como instalarlo en entornos Linux aquí.

Nos ubicamos en el directorio en donde vayamos a desplegar el programa.

cd /path/to/your/program

Descarga con Git

Descargamos el programa con el siguiente comando:

git clone https://sergiobr@bitbucket.org/sergiobr/geoip.git

Instalación manual

Debemos de crear el directorio dbs en donde se ubicarán las bases de datos de MaxMind.

mkdir -p dbs

Creamos el archivo geoIp.py con el contenido del programa:

import maxminddb
import sys
from geopy.geocoders import Nominatim
from optparse import OptionParser
from IPy import IP

######################
# Funciones y Clases #
######################

# Clase para interactuar con MaxMind y Nominatim
class getIpInfo:

    # Constructor de la clase
    def __init__(self,ip):
        self.ip = str(ip)

    # Obtener la info de MaxMind
    def getMaxMind(self):
        try:
            # Obtener la info de la base de datos City de MaxMind
            reader = maxminddb.open_database('dbs/GeoLite2-City.mmdb')
            res = reader.get(self.ip)
            # Devolver el resultado de MaxMind
            return res
        except Exception as e:
            raise ValueError('class - error getting MaxMind info: %s' % (str(e)))

    # Funcion para obtener la direccion a partir de las coordenadas Latitud y Longitud
    def getAddress(self,coordenates):
        try:
            # Crear la aplicacion de Nominatim
            geolocator = Nominatim(user_agent="my-application")
            # Obtener la localizacion a partir de la Latitud y Longitud
            location = geolocator.reverse(coordenates)
            # Devolver la informacion con la localizacion
            return location.raw
        except Exception as e:
            raise ValueError('class - error getting Nominatim info from coordenates: %s' % (str(e)))


    # Obtener la info del ASN de MaxMind
    def getAsnInfo(self):
        try:
            # Obtener la info de la base de datos ASN de MaxMind
            reader = maxminddb.open_database('dbs/GeoLite2-ASN.mmdb')
            res = reader.get(self.ip)
            # Devolver el resutlado de MaxMind
            return res
        except Exception as e:
            raise ValueError('class - getting MaxMind ASN info: %s' % (str(e)))


# Mostar el resultado final
def printResult(info,arguments):
    try:
        if info == 'default':
            keys = ['IP','Latitude','Longitude','ContCode','Continent','CounCode','Country']
            for key in keys:
                print('%-15s  -  %-15s' % (str(key), str(arguments[key])))
        elif info == 'full':
            keys = ['Region','Address','Timezone','Licence']
            for key in keys:
                print('%-15s  -  %-15s' % (str(key), str(arguments[key])))
        elif info == 'asn':
            keys = ['ASN','ASN Org']
            for key in keys:
                print('%-15s  -  %-15s' % (str(key), str(arguments[key])))
        else:
            print('Nothing to show.')
    except Exception as e:
        raise ValueError('function - error printing results: %s' % (str(e)))

# Creacion de las opciones e info del programa
def programParser():
    try:
        usage = "Address info of an IP address.\nUsage: %prog [options] ip_address."
        description = 'IP geolocation info. Add only one IP address. Ranges are not allowed.'
        version = '%prog: version 1.0'
        parser = OptionParser(usage,version=version,description=description,add_help_option=True)
        parser.add_option('-a', '--asn', dest = 'asn', help = 'Get only ASN info.', action='store_true')
        parser.add_option('-f', '--full', dest = 'full', help = 'Get full address info.', action='store_true')
        return parser
    except Exception as e:
        raise ValueError('function - creating program parser: %s' % (str(e)))


############
# Programa #
############

try:
    # Crear el Parser del programa
    parser = programParser()

    # Obtener los argumentos y opciones
    (options, args) = parser.parse_args()

    # Confirmar el total de argumentos. Solo se permite uno
    if len(args) == 0:
        raise ValueError('No argument has been indicated.')
    elif len(args) > 1 or len(args) < 0:
        raise ValueError('Only one argument is allowed.')
    else:

        # Confirmar si el argumento es una IP valida o no
        try:
            ip = IP(args[0])
        except Exception as e:
            raise ValueError('The IP %s is not valid. %s' % (str(args[0]),str(e)))

        # Confirmar si la IP es publica o no
        if ip.iptype() == 'PUBLIC':

            # Crear la clase para obtener la informacion de la IP
            geoIpCommands = getIpInfo(ip)

            # Declaramos las variables con los resultados
            finalResult = {}
            finalResultFull = {}
            finalResultAsn = {}

            # Obtener la direccion de la IP dependiendo de los argumentos indicados
            # Obtener la info de MaxMind
            maxMindRes = geoIpCommands.getMaxMind()

            # Info por defecto
            finalResult = {'IP':str(ip)}
            finalResult['Latitude'] = str(maxMindRes['location']['latitude'])
            finalResult['Longitude'] = str(maxMindRes['location']['longitude'])
            finalResult['ContCode'] = str(maxMindRes['continent']['code'])
            finalResult['Continent'] = str(maxMindRes['continent']['names']['en'])
            if 'country' in maxMindRes:
                finalResult['CounCode'] = str(maxMindRes['country']['iso_code'])
                finalResult['Country'] = str(maxMindRes['country']['names']['en'])
            elif 'registered_country' in maxMindRes:
                finalResult['CounCode'] = str(maxMindRes['registered_country']['iso_code'])
                finalResult['Country'] = str(maxMindRes['registered_country']['names']['en'])
            else:
                finalResult['CounCode'] = 'None'
                finalResult['Country'] = 'None'

            # Info completa, opcion '-f' o '--full'
            if options.full:
                # Obtener las coordenadas (Latitud y Longitud)
                coordenates = finalResult['Latitude'],finalResult['Longitude']
                # Obtener la info de Nominatim a partir de las cordenadas
                nominatimRes = geoIpCommands.getAddress(coordenates)
                # Info completa
                if 'state' in nominatimRes['address']:
                    finalResultFull['Region'] = nominatimRes['address']['state']
                elif 'state_district' in nominatimRes['address']:
                    finalResultFull['Region'] = nominatimRes['address']['state_district']
                else:
                    finalResultFull['Region'] = 'None'
                finalResultFull['Address'] = nominatimRes['display_name']
                finalResultFull['Timezone'] = str(maxMindRes['location']['time_zone'])
                finalResultFull['Licence'] = nominatimRes['licence']
                #finalResultFull['City'] = str(maxMindRes['city']['names']['en'])
                #finalResultFull['Postcode'] = str(maxMindRes['postal']['code']
                #finalResultFull['Postcode'] = nominatimRes['address']['postcode']

            # Info del ASN, opcion '-a' o '--asn'
            if options.asn:
                # Obtener la info del ASN
                asnRes = geoIpCommands.getAsnInfo()
                # Info del ASN
                finalResultAsn['ASN'] = asnRes['autonomous_system_number']
                finalResultAsn['ASN Org'] = asnRes['autonomous_system_organization']

            # Mostrar los resultados 'autonomous_system_number' y 'autonomous_system_organization'
            printResult('default',finalResult)
            if finalResultFull:
                printResult('full',finalResultFull)
            if finalResultAsn:
                printResult('asn',finalResultAsn)

        else:
            raise ValueError('The IP %s is not public.' % (str(ip)))

except Exception as e:
    print('ERROR - Reason: %s' % (str(e)))
    sys.exit(1)
finally:
    print('\nPowered by https://blog.tiraquelibras.com\n')

Creamos el archivo updateMaxMind.sh para descargar y actualizar las bases de datos de MaxMind, con el siguiente contenido, sustituyendo YOUR_MAXMIND_TOKEN por el token que debes haber obtenido en tu cuenta de MaxMind:

#!/bin/bash

### Variables

# DB files path
db_path='dbs'

# DB files name
city_file='GeoLite2-City.mmdb'
country_file='GeoLite2-Country.mmdb'
as_file='GeoLite2-ASN.mmdb'

# DB files downloaded
city_file_gz='GeoLite2-City.tar.gz'
country_file_gz='GeoLite2-Country.tar.gz'
as_file_gz='GeoLite2-ASN.tar.gz'

# Max Mind token
maxMind_token='YOUR_MAXMIND_TOKEN'

# URLs to download all databases (City, Country, ASN)
city_url="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=$maxMind_token&suffix=tar.gz
"
country_url="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&date=20191231&license_key=$maxMind_token&suffix=tar.gz"
as_url="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=$maxMind_token&suffix=tar.gz"


### Getting all files

# City
wget -c $city_url -O $city_file_gz
if [ $? != 0 ];then
        echo "Failed gettig $city_file_gz MaxMind database"
        exit 1
fi

# Country
wget -c $country_url -O $country_file_gz
if [ $? != 0 ];then
        echo "Failed gettig $country_file_gz MaxMind database"
        exit 1
fi

# ASN
wget -c $as_url -O $as_file_gz
if [ $? != 0 ];then
        echo "Failed gettig $as_file_gz MaxMind database"
        exit 1
fi

### Deleting all stored databases (.mmdb)
rm -rf $db_path/*

### Uncompress all files downloaded

# City
tar xzvf $city_file_gz
# Country
tar xzvf $country_file_gz
# ASN
tar xzvf $as_file_gz

### Moving all db files (.mmdb) to the final path

# City
path=$( find . -name $city_file -exec dirname {} \;)
mv $path'/'$city_file $db_path'/'

# Country
path2=$( find . -name $country_file -exec dirname {} \;)
mv $path2'/'$country_file $db_path'/'

# ASN
path3=$( find . -name $as_file -exec dirname {} \;)
mv $path3'/'$as_file $db_path'/'

rm -rf ./Geo*

Le damos permisos de ejecución a este archivo:

chmod 744 updateMaxMind.sh

Instalación de módulos necesarios

Instalamos los módulos necesarios indicados en el archivo requirements.txt con el siguiente comando:

pip install -r requirements.txt

Se instalarán los siguientes módulos de Python3:

  • maxminddb==1.5.1
  • pprint==0.1
  • geopy==1.20.0
  • IPy==1.00

Descarga/Actualización de las bases de datos de MaxMind

Primero editamos el archivo updateMaxMind.she indicamos nuestro token en la siguiente línea:

# Max Mind token
maxMind_token='YOUR_MAXMIND_TOKEN'

Sustituimos YOUR_MAXMIND_TOKEN por nuestro token creado.

Ejecutamos el script de Bash para descargar las bases de datos de MaxMind que vamos a utilizar:

./updateMaxMind.sh

Podemos configurar este en el Cron de nuestro sistema, por ejemplo para que se ejecute los lunes y viernes:

02 19 * * 1,5 cd /path/to/your/program; ./updateMaxMind.sh

Ejecución del programa

Importante ejecutar el programa en un entorno con Python3 instalado.

El programa dispone de una ayuda contextual descriptiva:

# python geoIp.py --help
Usage: Address info of an IP address.
Usage: geoIp.py [options] ip_address.

IP geolocation info. Add only one IP address. Ranges are not allowed.

Options:
  --version   show program's version number and exit
  -h, --help  show this help message and exit
  -a, --asn   Get only ASN info.
  -f, --full  Get full address info.

Powered by https://blog.tiraquelibras.com


Las opciones disponibles son:

  • -h o help: permite mostrar la ayuda del programa.
  • -a o asn: muestra la información del ASN junto a la información por defecto.
  • -f o full: muestra la información por defecto ampliada.
  • version: muestra la versión desplegada del programa.

Un ejemplo de ejecución con información por defecto:

# python geoIp.py 8.8.8.8
IP               -  8.8.8.8
Latitude         -  37.751
Longitude        -  -97.822
ContCode         -  NA
Continent        -  North America
CounCode         -  US
Country          -  United States

Powered by https://blog.tiraquelibras.com

Otro ejemplo con información del ASN:

# python geoIp.py 8.8.8.8 -a
IP               -  8.8.8.8
Latitude         -  37.751
Longitude        -  -97.822
ContCode         -  NA
Continent        -  North America
CounCode         -  US
Country          -  United States
ASN              -  15169
ASN Org          -  Google LLC

Powered by https://blog.tiraquelibras.com

Y por último con información ampliada:

# python geoIp.py 8.8.8.8 -f
IP               -  8.8.8.8
Latitude         -  37.751
Longitude        -  -97.822
ContCode         -  NA
Continent        -  North America
CounCode         -  US
Country          -  United States
Region           -  Kansas
Address          -  Reno County, Kansas, United States
Timezone         -  America/Chicago
Licence          -  Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright

Powered by https://blog.tiraquelibras.com

Versión del programa:

# python geoIp.py --version
geoIp.py: version 1.0

Powered by https://blog.tiraquelibras.com

Enlaces de interés

Enlace al proyecto en Bitbucket pincha aquí.

MaxMind pincha aquí

GeoLite2 de MaxMind pincha aquí

Nominatim Wiki pincha aquí

Nominatim política de uso pincha aquí

Nominatim OpenStreetMap tool pincha aquí

OpenStreetMap pincha aquí.

Actualización de Max Mind para el acceso pincha aquí.