Cloud with rain
.:G
G:.
0 and 1 serie, black on white
pulled card
myjsp.feelinglinux.com
ver. 1.1.9-4
Hallo, welcome to my world.
Here you can find some stuff about computer science.
<<< Enjoy your visit! >>>
0 and 1 serie, white on black

GeoLite2, Geo IP database. Caricamento dati di geolocalizzazione di IPv4 su DB MySQL ed utilizzo

        Scritto: Giansante Gabriele, 01/04/2017      Aggiornato: 23/12/2017 (BUG FIX)     

INDICE RISORSE/SOFTWARE USATI
  • GeoLite2 (IP geolocation Open Source Data, https://www.maxmind.com)
  • MySQL 5.1.73-8, Linux
  • Java 1.8.0_112
  • JDBC driver: mysql-connector-java-5.1.10-bin.jar
GLI INDIRIZZI IP PRESENTI NELL'ARTICOLO SONO STATI SCELTI IN MODO ASSOLUTAMENTE CASUALE

GeoLite2, IP geolocation Open Source Data

Quando ci capita di cercare su internet informazioni su un particolare IP, ci imbattiamo in decine di siti che forniscono informazioni dettagliate sulla rete a cui appartiene l'IP, sul provider che fornisce la connettivita' e molto altro, fino addirittura alle coordinate (piu' o meno precise) dove si suppone sia localizzato l'eventuale host su cui e' configurato l'IP.
Normalmente, questi dati vengono forniti a pagamento, almeno quelli piu' completi, ma e' possibile comunque trovare in rete raccolte di informazioni open source, diponibili a tutti per l'integrazione e l'utilizzo nelle proprie applicazioni o nei propri sistemi.

Pubblicita'
"Geolite2" e' un database open source che fornisce informazioni di base relative a blocchi di IP organizzati per nazione (macro-blocchi) o addirittura per citta' (maggiore granularita' dei dati).
Viene mantenuto e fornito da MaxMind attraverso il sito www.maxmind.com. Il database viene aggiornato una volta al mese e contiene informazioni di base come rete, coordinate, continente, nazione, regione, citta' e poco altro (una versione meno recente ed ormai obsoleta fornisce anche informazioni sui provider). Per molti scopi queste informazioni sono sufficienti, ma sono comunque fruibili dati piu' completi e ricchi sotto forma di servizio a pagamento (per chi se lo puo' permettere). Per gli scopi di questo articolo interessano solo i dati open source (free).

Il sito mette a disposizione un database di IP (IpV4 e IpV6) con i relativi dati di geolocalizzazione, forniti in diverse lingue fra cui l'inglese, il russo, ecc. Il database viene fornito in formato binario (con relative API per l'interrogazione) o in formato CSV.

In questo articolo vedremo:
  1. come sono organizzati i dati
  2. come integrare i dati in forma CSV in un proprio DB MySQL
  3. come ottenere la rete a cui appartiene un IP ed i suoi dati geografici
  4. un mini esempio di interrogazione in Java tramite JDBC
Veranno usati esclusivamente i dati in CSV, con interrogazioni SQL, tralasciando volutamente il formato binario e le API messe a disposizione da MaxMind.

Formato dati e creazione tabelle

Scarichiamo i dati dal sito che li fornisce:
https://www.maxmind.com (MaxMind)
Dal menu "Products"->"Open Source Data", scegliamo "GeoLite2 Country and City databases (New format)" (potete esplorare l'altro materiale presente, di cui pero' non parlero' nell'articolo) e scarichiamo sia "Geolite2 City" che "Geolite2 Country" in formato CSV (zip).
Dovremmo ora avere a disposizione i due archivi (con le date di ultimo aggiornamento al posto di "AAAAMMDD")
  • GeoLite2-City-CSV_AAAAMMDD.zip
    • GeoLite2-City-Blocks-IPv4.csv
    • GeoLite2-City-Blocks-IPv6.csv
    • GeoLite2-City-Locations-en.csv
    • GeoLite2-City-Locations-ru.csv
    • GeoLite2-City-Locations-[...].csv

  • GeoLite2-Country-CSV_AAAAMMDD.zip
    • GeoLite2-Country-Blocks-IPv4.csv
    • GeoLite2-Country-Blocks-IPv6.csv
    • GeoLite2-Country-Locations-en.csv
    • GeoLite2-Country-Locations-ru.csv
    • GeoLite2-Country-Locations-[...].csv
Ovviamente sono presenti tutte le lingue tranne l'italiano... A parte cio', ci concentreremo su questi file:
  • GeoLite2-City-Blocks-IPv4.csv
  • GeoLite2-City-Locations-en.csv
  • GeoLite2-Country-Blocks-IPv4.csv
  • GeoLite2-Country-Locations-en.csv
Tralascero' gli IpV6 ed i nomi in lingue diverse dall'inglese.

Guardando all'interno dei singoli file, osserviamo che la prima riga ci fornisce i nomi dei campi delle tabelle, mentre le restanti righe, quelle con i dati veri e propri, ci danno un'idea del tipo di dato da assegnare ad ogni campo:

GeoLite2-City-Blocks-IPv4.csv

 1   network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,
       is_anonymous_proxy,is_satellite_provider,postal_code,latitude,longitude,
       accuracy_radius
 2   21.0.0.0/24,2077456,2077456,,0,0,,-33.4940,143.2104,1000
 3   1.0.1.0/24,1810821,1814991,,0,0,,26.0614,119.3061,50
 4   1.0.2.0/23,1810821,1814991,,0,0,,26.0614,119.3061,50
 5   1.0.4.0/22,2077456,2077456,,0,0,,-33.4940,143.2104,1000
 6   1.0.8.0/21,1809858,1814991,,0,0,,23.1167,113.2500,50
     [...]

GeoLite2-City-Locations-en.csv

 1   geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,
       subdivision_1_iso_code,subdivision_1_name,subdivision_2_iso_code,subdivision_2_name,
       city_name,metro_code,time_zone
 2   49518,en,AF,Africa,RW,Rwanda,,,,,,,Africa/Kigali
 3   51537,en,AF,Africa,SO,Somalia,,,,,,,Africa/Mogadishu
 4   53654,en,AF,Africa,SO,Somalia,BN,Banaadir,,,Mogadishu,,Africa/Mogadishu
 5   57289,en,AF,Africa,SO,Somalia,WO,"Woqooyi Galbeed",,,Hargeisa,,Africa/Mogadishu
     [...]


L'informazione base e' data dal campo "network", ovvero un blocco di IP nella forma IP/MASCHERA, con la maschera espressa in forma di intero.
Non sono presenti singoli IP. Ogni "network e' legata alla tabella delle localita' tramite i campi "*geoname_id".
Pubblicita'
Passiamo all'importazione dei dati. Prima di tutto e' necessario generare le tabelle che conterranno i dati importati dai file CSV. E' possibile crearle in un nuovo database o integrarle in uno esistente. Il codice SQL che segue genera una possibile rappresentazione dei dati che, ovviamente, puo' essere modificata e/o adattata ai propri scopi (dimensionamento migliore dei campi, aggiunta indici, ecc.).
Ho aggiunto le chiavi sui campi "network" e "geoname_id" nell'ottica di effettuare semplici interrogazioni per IP o rete. Per qualsiasi altro scopo e' semplice aggiungerne altre. Senza queste chiavi (specialmente nel caso della divisione per citta'), l'utilizzo del DB diventa estremamente pesante in termini di tempo impiegato per eseguire le query.


Divisione per citta' (City_Blocks)

    -- Reti IPv4 divise per citta'. 
    -- Contiene coordinate geografiche.
    CREATE TABLE `City_Blocks_IPv4` (
      `network` varchar(18) NOT NULL,
      `geoname_id` varchar(50) DEFAULT NULL,
      `registered_country_geoname_id` text,
      `represented_country_geoname_id` text,
      `is_anonymous_proxy` int(11) DEFAULT NULL,
      `is_satellite_provider` int(11) DEFAULT NULL,
      `postal_code` text,
      `latitude` double DEFAULT NULL,
      `longitude` double DEFAULT NULL,
      `accuracy_radius` int(11) DEFAULT NULL,
      KEY `network_idx` (`network`),
      KEY `geoname_idx` (`geoname_id`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

    -- Nomi in inglese per le localita' associate 
    -- a blocchi di basso livello (city)
    CREATE TABLE `City_Locations_en` (
      `geoname_id` varchar(50) DEFAULT NULL,
      `locale_code` text,
      `continent_code` text,
      `continent_name` text,
      `country_iso_code` text,
      `country_name` text,
      `subdivision_1_iso_code` text,
      `subdivision_1_name` text,
      `subdivision_2_iso_code` text,
      `subdivision_2_name` text,
      `city_name` text,
      `metro_code` text,
      `time_zone` text,
      KEY `geoname_idx` (`geoname_id`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Divisione per nazione (country)

    -- Reti IPv4 divise per nazione. 
    -- NON contiene coordinate geografiche.
    CREATE TABLE `Country_Blocks_IPv4` (
      `network` varchar(18),
      `geoname_id` varchar(50),
      `registered_country_geoname_id` text,
      `represented_country_geoname_id` text,
      `is_anonymous_proxy` int(11) DEFAULT NULL,
      `is_satellite_provider` int(11) DEFAULT NULL,
      KEY `network_idx` (`network`),
      KEY `geoname_idx` (`geoname_id`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

    -- Nomi in inglese per le localita' associate 
    -- a blocchi di alto livello (country)
    CREATE TABLE `Country_Locations_en` (
      `geoname_id` varchar(50),
      `locale_code` text,
      `continent_code` text,
      `continent_name` text,
      `country_iso_code` text,
      `country_name` text,
      KEY `geoname_idx` (`geoname_id`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8;


Import dei dati da file CSV

Per importare nel modo piu' veloce possibile i dati, usiamo il costrutto "load data infile", capace di gestire molto rapidamente anche la tabella dei blocchi per citta' (piu' di 150Mb di file CSV).
L'interpretazione corretta del CSV avviene indicando al "load data infile" la seguente configurazione:
FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES
ovvero, i campi sono separati da ",", i testi complessi sono racchiusi tra virgolette, ogni riga termina con un "fine linea" semplice ("\n"), la prima riga va ignorata in quanto contenente i nomi dei campi della tabella.

Supponendo di lavorare in un sistema linux, spostarsi sulla cartella contenente i file CSV ed entrare nella console di MySQL con
mysql -u <user> -h<host> -p
o una sua variante. Una volta dentro, eseguire i seguenti comandi rispettivamente per entrare nel DB voluto, per creare le tabelle con l'SQL visto in precedenza e per caricare i dati dei CSV nelle tabelle create.
mysql> use <nomeDB>

mysql> [...inserire codice creazione tabelle...]

mysql> load data local infile 'GeoLite2-City-Blocks-IPv4.csv' into table City_Blocks_IPv4 FIELDS TERMINATED BY ','  ENCLOSED BY '"'  LINES TERMINATED BY '\n' IGNORE 1 LINES;
mysql> load data local infile 'GeoLite2-City-Locations-en.csv' into table City_Locations_en FIELDS TERMINATED BY ','  ENCLOSED BY '"'  LINES TERMINATED BY '\n' IGNORE 1 LINES;
mysql> load data local infile 'GeoLite2-Country-Locations-en.csv' into table Country_Locations_en FIELDS TERMINATED BY ','  ENCLOSED BY '"'  LINES TERMINATED BY '\n' IGNORE 1 LINES;
mysql> load data local infile 'GeoLite2-Country-Blocks-IPv4.csv' into table Country_Blocks_IPv4 FIELDS TERMINATED BY ','  ENCLOSED BY '"'  LINES TERMINATED BY '\n' IGNORE 1 LINES;
I dati a questo punto dovrebbero essere presenti nel DB. Verificarne la presenza con delle semplici query del tipo
select * from <tabella> limit 10;
Se tutto va bene, possiamo passare al prossimo paragrafo, dove si vedra' uno dei tanti modi di utilizzare queste informazioni.

Stored Procedure per ricerca a partire da un dato IP

Abbiamo visto che i dati contenuti nelle tabelle sono relativi a blocchi di IP (reti) piuttosto che a singoli IP.
Tipicamente, pero', si vogliono trovare i dati relativi ad un singolo IP. Un modo di farlo e' attraverso una stored procedure che si occupi dei calcoli e delle verifiche necessarie al posto nostro, restituendoci direttamente i dati voluti.
L'algoritmo implementato segue banalmente quello che faremmo con una ricerca "visiva".
Brevemente, un IP e' composto da 4 byte, cosi' come l'indirizzo base di una rete.
  IP: <byte 1>.<byte 2>.<byte 3>.<byte 4>
Rete: <byte 1>.<byte 2>.<byte 3>.<byte 4>/<maschera>

L'algoritmo nel dettaglio:
  1. Ricerca della piu' grande sequenza di byte dell'IP (a partire dal primo) tale che esistano delle reti il cui IP di base comincia per la stessa sequenza.
    Ad esempio, dato l'IP "116.193.154.157" la minima sequenza e' [116,193], in quanto non esiste nel DB una rete il cui IP di base inizi per "116.193.154".

  2. Ricerca di tutte le reti il cui IP di base ha in comune con l'IP cercato la sequenza minima trovata al punto precedente.
    Ad esempio, dato l'IP "116.193.154.157", sono presenti nel DB 58 blocchi che iniziano per la sequenza minima [116,193], fra cui alcuni sono

    • 116.193.0.0/21
    • 116.193.8.0/21
    • 116.193.16.0/20
    • 116.193.32.0/19
    • 116.193.64.0/20
    • 116.193.80.0/20
    • ...
    • 116.193.224.0/20
    • 116.193.240.0/20

  3. Di tutti i blocchi trovati al punto precedente, quello cercato e' quello il cui primo byte successivo alla sequenza minima si avvicina di piu' per difetto al corrispondente dell'IP che stiamo analizzando. Se sono presenti piu' blocchi che soddisfano la condizione sul byte successivo alla sequenza minima, viene preso il blocco il cui IP di base e' maggiore di tutti.
    Ad esempio, dato l'IP "116.193.154.157", il primo byte successivo alla sequenza minima e' "154".
    Esistono nel DB 32 blocchi tali che il loro 3° byte e' minore di "154", fra cui:

    • ...
    • 116.193.146.0/24
    • 116.193.147.0/25
    • 116.193.147.128/25
    • 116.193.148.0/22
    • 116.193.152.0/22

    Di questi blocchi, quello con il terzo byte piu' grande e' "116.193.152.0/22", che rappresenta il blocco cercato.
    Se per caso ci fossero stati i due blocchi {116.193.152.0/25, 116.193.152.128/25}, avrei preso il secondo in quanto maggiore.

  4. Un difetto di questo database pubblico e' che non copre tutte le possibili reti, quindi ci sono molti IP non presenti.
    Per un IP che cade all'interno di una rete non esistente, l'algoritmo (come presentato fino ad ora) restituisce un risultato sbagliato per cui e' necessario verificare se l'IP appartiene alla rete trovata. Se l'IP appartiene alla rete trovata, vengono restituiti i dati della rete stessa, altrimenti non viene prodotto alcun risultato.

Quella che segue e' una possibile implementazione (ottimizzabile ed estendibile a piacere), relativa alla ricerca sulla divisione per citta'. Puo' essere estesa facilmente alla divisione per nazione.
  • function ipV4Element(ip, posizione):byte
    Restituisce un singolo byte di un indirizzo IP in base alla posizione (1...4). Nel caso venga passata una rete (ip/maschera), la maschera viene ignorata.

  • function ipV4Compare(ip1, ip2):{-1,0,1}
    Confronta gli IP passati (byte a byte). Restituisce "-1" se l'ip1 e' minore dell'ip2, "0" se gli ip sono uguali, "1" se l'ip1 e' maggiore dell'ip2. Nel caso venga passata una rete (ip/maschera), la maschera viene ignorata.

  • function ipV4BelongsToNet(ip, rete):{0,1}
    Verifica che l'IP passato appartenga alla rete (passata come "ip/maschera"). Restituisce "0" se l'ip non appartiene alla rete, "1" se appartiene.

  • procedure findIpV4NetInfo(ip):<tupla>
    Restituisce una tupla contenente alcuni campi relativi al blocco (rete) che contiene l'ip passato (rete, nazione, latitudine, longitudine, ecc.).
    Aggiungere a piacere eventuali campi che non ho inserito.

-- elementPosition: {1...4}
-- Es. ipv4Element('10.11.12.13', 3) = 12
CREATE FUNCTION `ipV4Element` (ip varchar(18), elementPosition INTEGER)
RETURNS INTEGER
BEGIN
    -- elimino l'eventuale parte relativa alla maschera di rete
    set @temp = SUBSTRING_INDEX(ip, '/', 1);
    set @result = CAST(substring_index(SUBSTRING_INDEX(@temp, '.', elementPosition), '.', -1) AS UNSIGNED);
RETURN @result;
END

-- result: -1,0,1 (<,=,>)
-- Es. ipv4Compare('1.2.3.4','1.2.1.5') = -1
CREATE FUNCTION `ipV4Compare` (ip1 varchar(18), ip2 varchar(18))
RETURNS INTEGER
BEGIN
    set @count=1;
    while (@count<=4) do
        set @n1 = ipV4Element(ip1, @count);
        set @n2 = ipV4Element(ip2, @count);
        set @result = if (@n1<@n2, -1, if(@n1>@n2, 1, 0));
        -- break (@cont=5) se elemento del primo ip
        -- maggiore o minore dell'analogo nel secondo ip
        set @count = if (@result=0, @count+1, 5);
    end while;
RETURN @result;
END

-- result: 0,1 (non appartiene, appartiene)
-- Es. ipv4BelongsToNet('1.2.3.4','1.2.3.0/24') = 1
CREATE FUNCTION `ipV4BelongsToNet`(ip varchar(18), network varchar(22)) 
RETURNS int(1)
BEGIN
    -- ip di base della rete
    set @networkBaseIp = SUBSTRING_INDEX(network,'/',1);
    -- maschera di rete (se passo un IP come network, 
    -- ho l'IP invece della maschera)
    set @stringMask = SUBSTRING_INDEX(network,'/',-1);
    -- se e' un singolo IP, imposto la maschera a 32, 
    -- altrimenti ne faccio il cast ad intero
    set @mask = if (@stringMask=network, 32, cast(@stringMask as UNSIGNED));
    -- trasformo l'IP da sequenza di 4 byte a unico numero intero 
    -- (INET_ATON e' una funzione MySQL)
    set @intIp = INET_ATON(ip);
    -- trasformo l'IP di base della rete in unico numero intero
    -- (INET_ATON e' una funzione MySQL)
    set @intNetworkBaseIp = INET_ATON(@networkBaseIp);
    -- applico la maschera all'IP per ottenere l'IP di base 
    -- della rete di cui fa parte l'IP. Se l'IP ottenuto coincide con quello di 
    -- base della rete, allora l'IP passato appartiene alla rete.
    -- Per applicare la maschera, trasformo la maschera in IP in forma di numero intero e 
    -- la metto in AND logico con l'IP.
    -- Per trasformare la maschera in IP in forma di numero intero, creo una sequenza
    -- di '1' lunga quanto la maschera (ricordiamo che la maschera indica il numero di 1 iniziali), 
    -- converto la sequenza da binaria a numero intero decimale e ne faccio lo shift a sinistra 
    -- in modo da ottenere un numero a 32bit 
    -- (es. "/24"=11111111111111111111111100000000=‭4294967040‬).
    set @intApplyMaskToIp = @intIp & CONV(REPEAT('1', @mask), 2, 10) << 32 - @mask;
    -- Verifica dell'appartenenza mediante l'uguaglianza degli IP di base
    RETURN @intApplyMaskToIp=@intNetworkBaseIp;
END


-- xxx.xxx.xxx.xxx/xx 18 char
CREATE DEFINER=`giansante`@`XXX.XXX.XXX.XXX` PROCEDURE `findIpV4NetInfo`(IN ip varchar(18))
BEGIN
    set @counter = 3;
    -- XXX.XXX.XXX
    set @ipPart = SUBSTRING_INDEX(ip, '.', @counter);
    while (not exists (
                select network 
                from City_Blocks_IPv4  
                where network like concat(@ipPart, '%') and 
                    ipV4Compare(network, ip)<=0) and @counter>1) do
        set @counter = @counter - 1;
        set @ipPart = SUBSTRING_INDEX(ip, '.', @counter);
    end while;
    -- Risultato, prendo la rete piu' grande (ordinamento per ip di base)
    -- Ordino in base all'elemento dell'IP base alla 
    -- posizione (counter) trovata con il while
    select network, is_anonymous_proxy, continent_name, country_name, country_iso_code, 
            subdivision_1_name, subdivision_2_name, city_name, latitude, longitude 
        from City_Blocks_IPv4 b, City_Locations_en l 
        where b.geoname_id=l.geoname_id and network like concat(@ipPart, '%') and ipV4BelongsToNet(ip, network)=1
        order by ipV4Element(network, @counter+1) desc
        limit 1;
END
Pubblicita'

Esempio di ricerca da console mysql

Per eseguire la stored procedure, dopo averla creata, usare nella console mysql l'istruzione
call findIpV4NetInfo(<ip>);
Ad esempio, volendo ottenere le informazioni relative all'indirizzo "116.193.154.157" (IP preso assolutamente a caso), eseguendo la chiamata alla procedura "findIpV4NetInfo", avremmo il seguente risultato (validita' al 27/03/2017):
mysql> call findIpV4NetInfo('116.193.154.157')\G
*************************** 1. row ***************************
           network: 116.193.152.0/22
is_anonymous_proxy: 0
    continent_name: Asia
      country_name: China
  country_iso_code: CN
subdivision_1_name: Yunnan
subdivision_2_name:
         city_name: Shengping
          latitude: 28.511
         longitude: 99.0159
1 row in set (0.02 sec)

Query OK, 0 rows affected (0.02 sec)


Esempio di ricerca con Java

Vediamo infine un semplice esempio di interrogazione del database con Java.
Per eseguire il programma e' necessario inserire nel CLASSPATH il jar relativo al driver JDBC per MySQL. Se non lo si ha, e' possibile scaricarlo all'indirizzo
https://dev.mysql.com/downloads/connector/j/5.1.html
Il programma seguente apre una connessione con il database, invoca la stored procedure passandole l'indirizzo voluto e, se esiste un risultato valido, stampa le informazioni relative alla rete contenente l'ip passato.
Supponiamo di aver caricato i dati nel seguente database:

Nome : myDb
Host : 192.168.20.20
Utente : user
Password : password

e di voler cercare l'indirizzo IP "116.193.154.157".

package org.giansante.tester;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Esempio semplice (e senza troppi controlli) di chiamata alla stored procedure
 * "findIpV4NetInfo"
 * @author Giansante Gabriele
 */
public class TestFindIp {

    public static void main(String[] args) {
        try {
            //Il DriverManager carica automaticamente la classe del Driver Mysql
            Connection connection = DriverManager.getConnection(
                            "jdbc:mysql://127.0.0.1:3306/myDb", "user", "password");
            //"Preparazione" chiamata a stored procedure
            CallableStatement statement = connection.prepareCall("call findIpV4NetInfo(?)");
            //Imposto l'ip su cui cercare informazioni
            statement.setString(1, "116.193.154.157");
            //Invoco la procedura
            ResultSet result = statement.executeQuery();
            //Se esiste un risultato ed e' posisbile posizionare il cursore 
            //sulla prima riga, allora leggo i dati
            if (result.first()){
                //Campi della tabella risultato
                String[] fields = 
                    {"network", "is_anonymous_proxy", "continent_name", 
                        "country_name", "country_iso_code", "subdivision_1_name", 
                        "subdivision_2_name", "city_name", "latitude", "longitude"};
                //Stampa dei campi
                for (String field: fields){
                    System.out.println(field + ": " + result.getObject(field));
                }                
                statement.close();
                connection.close();
            } else {
                println("Nessun risultato!");
            } 
        } catch (SQLException | ClassNotFoundException ex) {
            Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

}    

Se tutto va bene, l'esecuzione del programma produce il seguente output

    network: 116.193.152.0/22
    is_anonymous_proxy: 0
    continent_name: Asia
    country_name: China
    country_iso_code: CN
    subdivision_1_name: Yunnan
    subdivision_2_name: 
    city_name: Shengping
    latitude: 28.511
    longitude: 99.0159

Hai trovato utile questo articolo?
Aiutami a condividerlo o metti un "mi piace".
Grazie mille!


Gli strumenti di condivisione (Google+, Facebook) sono visibili in alto a destra solo dopo aver accettato la policy di utilizzo dei cookie per questo sito.
FAQ - Come faccio a cambiare la mia scelta?

 

Strumenti (myjsp.feelinglinux.com)
Gioco: allenamento con la tastiera Strumenti di codifica/decodifica URI (%-encoding) e Base64 Strumenti di calcolo online per IP e Reti
QUIZ GAME
Quiz game

Cerca @myjsp.feelinglinux.com

Pubblicita'