E ora basta! E che diamine! Pare impossibile che ormai si paghi pure l’aria che si respira.
E dire che anni fa esistevano dei servizi robusti, affidabili e pressoché gratuiti che funzionavano alla grande.
Adesso invece una qualsiasi stronzata te la fanno pagare con una sottoscrizione e sottolineo: qualsiasi cagata!
E ora basta! E che diamine! Pare impossibile che ormai si paghi pure l’aria che si respira.
E dire che anni fa esistevano dei servizi robusti, affidabili e pressoché gratuiti che funzionavano alla grande.
Adesso invece una qualsiasi fesseria te la fanno pagare con una sottoscrizione e sottolineo: qualsiasi cosa!
Sembra la corsa all’oro dell’approssimazione. Tutti vogliono soldi ma pochi offrono servizi decenti.
La mia ultima vicissitudine? Un servizio di geolocalizzazione tramite indirizzo IP.
Avrete senz’altro notato che se visitate il mio sito durante la notte, questo assume una graziosa opacità scura con un cerchio luminoso intorno al cursore a mo’ di torcia (c’è pure un interruttore che vi consente di disattivarlo se volete).
Quell’effetto è dato da un piccolo script in jQuery che recupera (recuperava ormai) tramite un servizio, la latitudine e la longitudine della città dove il vostro IP è assegnato e successivamente tramite un altro servizio, calcola le ore di luce rimaste al tramonto e al crepuscolo, settando un’oscurità progressiva alla homepage.
Ecco, quello che mi ha fatto girare altamente le palle, è il limite farlocco imposto da questi servizi.
Non ho nulla incontrario sulle limitazioni dei piani free, purché ci siano spiegati i limiti in modo chiaro e che vengano rispettati.
Quando però mi scrivete che ci sono a disposizione 300 chiamate al giorno e già dopo 3 o 4 mi appare un’errore sul limite raggiunto, mi salta la vena.
Non tollero nella maniera più assoluta essere preso per il culo!
Ecco, quello che mi ha fatto parecchio arrabbiare, è il limite farlocco imposto da questi servizi.
Non ho nulla incontrario sulle limitazioni dei piani free, purché ci siano spiegati i limiti in modo chiaro e che vengano rispettati.
Quando però mi scrivete che ci sono a disposizione 300 chiamate al giorno e già dopo 3 o 4 mi appare un’errore sul limite raggiunto, mi salta la vena.
Non tollero nella maniera più assoluta essere preso in giro!
Siccome queste sono le cose che mi toccano nel profondo e mi fanno partire l’embolo, adotto la buona e vecchia massima dell’informatica e della vita in generale:
se vuoi una cosa faccia quello che vuoi te, devi fartela da solo.
Quindi adesso il buon caro e vecchio Matteo, vi insegna a creare una piccola app che restituisce la geolocalizzazione di chi ci si connette, in modo da mandare a quel paese chi cerca di fare il furbo per spillarvi soldi.
Per l’occasione utilizzeremo Visual Studio e metteremo in piedi un progetto di tipo WebApi in .Net Core 8.
Una piccola premessa: ovviamente questo metodo funziona se non avete grandi pretese e vi accontentate di una posizione approssimativa.
Se avete bisogno di qualcosa di più preciso e affidabile questa guida non è per voi, il perché ve lo spiegherò più avanti.
Se invece ve ne fregate di geolocalizzare il tipo che si collega da uno sperduto villaggio del Burkina Faso, sicuramente troverete utile quanto vi sto per spiegare.
Primi steps
Do per scontato che un minimo di programmazione mastichiate, specie su alcune tecnologie .NET e\o .NET Core.
Se non siete esperti niente paura, dopo tutto nulla vi vieta comunque di scriverlo in un altro linguaggio come Java o PHP, i concetti sono identici in tutti i linguaggi.
Scarichiamo prima di tutto Visual Studio Community (se preferite, potete usare anche Visual Studio Code. Io per praticità lo evito come la peste) da https://visualstudio.microsoft.com/it/vs/community/ e installatelo.

Alla schermata di selezione delle funzionalità selezionate Sviluppo ASP.NET e Web e nei Dettagli di installazione sulla sinistra, spuntate .Net 8.0 WebAssembly Build Tools.
Questo ci darà le funzionalità che ci serviranno per sviluppare la nostra piccola app.
Terminiamo l’installazione e avviamo il programma.

Cliccate su Create a new project o l’equivalente in italiano se l’avete installato nella lingua di Dante.

Tenete selezionato C#, All platforms e Web nelle dropdown in alto e scrollate la lista fino a trovare “ASP.NET Core Web Api“.
Sarà questa la tecnologia che useremo per il coding della nostra app. Clicchiamo su Next in basso a destra e verremo proiettati alla schermata di configurazione del progetto.

Le opzioni sono abbastanza intuitive. Scrivete GeoAPI come nome del progetto, settate una posizione per la creazione dei files e delle cartelle e tenete spuntato Place solution and project in the same directory.
Contando che sarà un’applicazione minuscola non ha senso strutturarla su più progetti.
Cliccate ancora su Next in basso a destra e il wizard vi mostrerà l’ultima schermata.
L’elemento chiave dell’app
Prima di tutto ci serve la parte più importante: il database dove sono memorizzati i range di indirizzi IP e la loro geolocalizzazione.
In soldoni, l’applicazione non fa altro che cercare all’interno di esso l’indirizzo IP della vostra macchina per restituirvi una posizione approssimativa con le sue coordinate.
Possiamo ringraziare db-ip (https://db-ip.com/) che ci mette a disposizione una versione leggera e gratuita del suo database che possiamo utilizzare per i nostri scopi.
Viene aggiornato a cadenza mensile per cui la nostra preoccupazione sarà solo quella di sostituirlo ogni mese con quello presente all’interno dell’app che andremo a scrivere.
Potete pure scrivervi una routine che vi fa il lavoro sporco a cadenza mensile o settimanale, vedete voi.
Ovviamente per ragioni di tempo per ora non sto a spiegarvi come farlo, magari un’altra volta.
Puntiamo il nostro browser su https://db-ip.com/db/download/ip-to-city-lite e scrolliamo verso il basso, troveremo la sezione download sia per la versione CSV che la versione MMDB.
Siccome a noi piace fare le cose per bene, accettiamo i termini e le condizioni poco più in alto e clicchiamo su Download IP to City Lite MMDB.
Scompattatelo dall’archivio e tenetelo da parte.
Prima Visual Studio.
Importare il necessario
Per poter utilizzare correttamente il database di db-ip abbiamo bisogno di installare un pacchetto nuget aggiuntivo che ci mette a disposizione i metodi necessari per connetterci e recuperare i dati. Visual Studio ci rende la procedura facile e indolore.

Facciamo click col tasto destro del mouse sul progetto nella lista a destra e clicchiamo su “Manage NuGet Packages“
Una volta aperta la finestra di gestione dei pacchetti, clicchiamo sul tab Browse e nella finestra di ricerca scriviamo MaxMind.GeoIP2.

La finestra ci restituirà alcuni risultati ma a noi interessa solamente MaxMind.GeoIP2 (eventuali dipendenze verranno installate in automatico)

Clicchiamoci sopra destra apparirà la possibilità di installare il pacchetto. Scegliamo l’ultima versione stabile (al momento è la 5.4.1) e clicchiamo su Install.
Accettiamo i termini ed eventuali pacchetti aggiuntivi e aspettiamo che finisca l’installazione dell’add-on.
Spostiamo il database

Dobbiamo in primis provvedere a includere il database nell’app.
Per cui facciamo click col tasto tasto destro sul progetto GeoAPI e clicchiamo su New Folder.
Ci verrà chiesto di inserire un nome per la cartella. Chiamiamola Data e confermiamo.
Il servizio
Prima di tutto creiamo la classe con il servizio.
Facciamo click col tasto destro sul progetto GeoAPI e creiamo una cartella chiamata Services.

Facciamo la stessa sulla cartella Services appena creata e aggiungiamo una classe chiamandola GeopositionService: tasto destro su Services -> Add -> New Item.
Visual Studio dovrebbe aprirci automaticamente il file. Vediamo la struttura:
namespace GeoAPI.Services
{
public class GeopositionService
{
}
}
Code language: C# (cs)
Dobbiamo creare il servizio che recupera i dati dal db che verrà consumato dal controller.
Importiamo prima di tutto le direttive using per usare il pacchetto aggiunto in precedenza:
using MaxMind.Db;
using MaxMind.GeoIP2;
Code language: C# (cs)
E scriviamo la logica usando DatabaseReader di MaxMind:
using MaxMind.Db;
using MaxMind.GeoIP2;
namespace GeoAPI.Services
{
public class GeopositionService
{
private readonly DatabaseReader _reader;
public GeopositionService(IWebHostEnvironment environment)
{
var path = Path.Combine(environment.ContentRootPath, "Data", "dbip-city-lite.mmdb");
_reader = new DatabaseReader(path, FileAccessMode.Memory);
}
public DatabaseReader GetReader()
{
return _reader;
}
}
}
Code language: C# (cs)
A risolvere la dipendenza lasciamo che se ne occupi la D.I. di .Net Core.
Noi comunque vogliamo che il nostro servizio venga eseguito solo ed esclusivamente una volta durante tutto il ciclo dell’applicazione per cui lo aggiungeremo come Singleton.
Apriamo il file Program.cs e aggiungiamo la seguente riga:
builder.Services.AddSingleton<GeopositionService>();
Code language: C# (cs)
dopo
var builder = WebApplication.CreateBuilder(args);Code language: JavaScript (javascript)
In questo modo il nostro Program.cs diventa
using GeoAPI.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddSingleton<GeopositionService>(); //Il nostro servizio
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Code language: C# (cs)
Da notare come Visual Studio ci ha aggiunto automaticamente la direttiva using del servizio appena creato.
Il controller
Ora che abbiamo il servizio, dobbiamo scrivere il controller che ci restituirà il risultato.
Quindi facciamo click col tasto destro sulla cartella Controllers della soluzione -> Add -> New Item.

Questa volta anzichè scegliere Class scegliamo Api Controller – Empty (non che cambi molto ma almeno abbiamo già il controller pronto). Su Name scriviamo GeopositionController.cs e successivamente su Add.
Dopo che Visual Studio farà il suo lavoro, ci restituirà il controller pronto per lavorarci:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace GeoAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class GeopositionController : ControllerBase
{
}
}
Code language: C# (cs)
Iniettiamo il servizio al costruttore del controller come prima cosa:
using GeoAPI.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace GeoAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class GeopositionController : ControllerBase
{
private readonly GeopositionService _geopositionService;
public GeopositionController(GeopositionService geopositionService)
{
_geopositionService = geopositionService;
}
}
}
Code language: C# (cs)
Il fulcro centrare del controller è di ottenere le coordinate tramite indirizzo IP della persona che richiede l’API.
Come prima cosa creiamoci la funzione che restituisce l’indirizzo IP dell’utente:
private string getIp()
{
var cfIp = Request.Headers["CF-Connecting-IP"].FirstOrDefault();
var forwarded = Request.Headers["X-Forwarded-For"].FirstOrDefault();
var remoteIp = HttpContext.Connection.RemoteIpAddress?.ToString();
var realIp = cfIp ?? forwarded?.Split(',').FirstOrDefault()?.Trim() ?? remoteIp ?? "0.0.0.0";
return realIp;
}
Code language: C# (cs)
Molti di voi staranno pensando che sarebbe bastato semplicemente usare
var remoteIp = HttpContext.Connection.RemoteIpAddress?.ToString()
Code language: C# (cs)
Che non sarebbe stato sbagliato, anzi.
Il casino però sorge se decidete di hostare l’app su di un dominio protetto da una CDN (in questo caso CloudFlare).
In tal caso il Context su RemoteIpAddress passa la patata bollente alla CDN restituendo non l’IP dell’utente ma il suo.
Per ovviare al problema dobbiamo ottenere l’indirizzo IP tramite headers lasciandoci comunque l’opportunità di restituire l’indirizzo tramite RemoteIpAddress.
Ora abbiamo la metà dei pezzi del puzzle:
using GeoAPI.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace GeoAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class GeopositionController : ControllerBase
{
private readonly GeopositionService _geopositionService;
public GeopositionController(GeopositionService geopositionService)
{
_geopositionService = geopositionService;
}
private string getIp()
{
var cfIp = Request.Headers["CF-Connecting-IP"].FirstOrDefault();
var forwarded = Request.Headers["X-Forwarded-For"].FirstOrDefault();
var remoteIp = HttpContext.Connection.RemoteIpAddress?.ToString();
var realIp = cfIp ?? forwarded?.Split(',').FirstOrDefault()?.Trim() ?? remoteIp ?? "0.0.0.0";
return realIp;
}
}
}
Code language: C# (cs)
Non ci rimane che scrivere il metodo che fa da tramite tra servizio e la funzione getIp():
[HttpGet("GetCoords")]
public async Task<IActionResult> GetCoords(string? ip = null)
{
ip ??= getIp();
try
{
DatabaseReader reader = _geopositionService.GetReader();
CityResponse response = reader.City(ip);
return Ok(new
{
response.City,
response.Location,
response.Continent,
response.Country
});
}
catch (Exception ex)
{
return BadRequest($"Errore durante il recupero delle coordinate nel db: {ex.Message}");
}
}
Code language: C# (cs)
Con questo metodo ci lasciamo comunque la possibilità di poter fare una chiamata specificando un’altro IP al posto del nostro, utile per fare alcuni test.
Come spiegato in precedenza, il lavoro sporco lo fa tutto l’add-on di MaxMind importata con NuGet:
DatabaseReader reader = _geopositionService.GetReader();
CityResponse response = reader.City(ip);
Code language: C# (cs)
Il risultato in caso di codice 200 lo lasciamo anonimo, non ha molto senso in questo caso scrivere un DTO solamente per la restituzione di alcuni campi.
In caso di errore invece, il catch farà in modo di restituire un BadRequest col messaggio.
La forma definitiva del nostro controller sarà questa:
using GeoAPI.Services;
using MaxMind.GeoIP2;
using MaxMind.GeoIP2.Responses;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace GeoAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class GeopositionController : ControllerBase
{
private readonly GeopositionService _geopositionService;
public GeopositionController(GeopositionService geopositionService)
{
_geopositionService = geopositionService;
}
[HttpGet("GetCoords")]
public async Task<IActionResult> GetCoords(string? ip = null)
{
ip ??= getIp();
try
{
DatabaseReader reader = _geopositionService.GetReader();
CityResponse response = reader.City(ip);
return Ok(new
{
response.City,
response.Location,
response.Continent,
response.Country
});
}
catch (Exception ex)
{
return BadRequest($"Errore durante il recupero delle coordinate nel db: {ex.Message}");
}
}
private string getIp()
{
var cfIp = Request.Headers["CF-Connecting-IP"].FirstOrDefault();
var forwarded = Request.Headers["X-Forwarded-For"].FirstOrDefault();
var remoteIp = HttpContext.Connection.RemoteIpAddress?.ToString();
var realIp = cfIp ?? forwarded?.Split(',').FirstOrDefault()?.Trim() ?? remoteIp ?? "0.0.0.0";
return realIp;
}
}
}
Code language: C# (cs)Proviamola in locale!
Facciamo partire l’app senza debug, cliccando su
e vediamo se il tutto funziona come dovrebbe.

Se vedete Swagger avete fatto centro. Se non lo vedete beh, dovete ricontrollare qualche passaggio. Cliccate su /api/Geoposition/GetCoords per espandere l’accordion e poi su Try it out.

Il risultato è abbastanza ovvio. Restituisce un errore ma è giusto che sia così.
Sta cercando l’indirizzo IP della macchina in locale (::1).
Giustamente la query non lo trova nel database e ci restituisce un errore 400 (Bad Request).
Facciamo una prova inserendo un indirizzo IP nella casella di testo, come 8.8.4.4 e premiamo di nuovo Execute:

8.8.4.4 è un indirizzo IP di un dns di Google ed effettivamente la risposta dell’API è quella che ci aspettiamo.
Per avere la certezza che la nostra applicazione funzioni come si deve, abbiamo bisogno di pubblicarla per poi portarla (hosting) online.
{
"city": {
"confidence": null,
"names": {
"en": "Mountain View"
},
"geoname_id": null
},
"location": {
"accuracy_radius": null,
"average_income": null,
"latitude": 37.422,
"longitude": -122.085,
"metro_code": null,
"population_density": null,
"time_zone": null
},
"continent": {
"code": "NA",
"names": {
"de": "Nordamerika",
"en": "North America",
"es": "Norteamérica",
"fa": " امریکای شمالی",
"fr": "Amérique Du Nord",
"ja": "北アメリカ大陸",
"ko": "북아메리카",
"pt-BR": "América Do Norte",
"ru": "Северная Америка",
"zh-CN": "北美洲"
},
"geoname_id": 6255149
},
"country": {
"confidence": null,
"is_in_european_union": false,
"iso_code": "US",
"names": {
"de": "Vereinigte Staaten von Amerika",
"en": "United States",
"es": "Estados Unidos de América (los)",
"fa": "ایالات متحدهٔ امریکا",
"fr": "États-Unis",
"ja": "アメリカ合衆国",
"ko": "미국",
"pt-BR": "Estados Unidos",
"ru": "США",
"zh-CN": "美国"
},
"geoname_id": 6252001
}
}
Code language: JSON / JSON with Comments (json)Il json restituito è corretto. Non rimane che provare l’applicazione online.
Lo spettro di C.O.R.S.
Quando si parla di API è inevitabile che prima o poi il nodo CORS salti fuori.
Supponiamo di pubblicare la nostra applicazione su un nostro dominio chiamato miaapp.miodominio.it e di integrarla nel nostro portale principale su miodominio.it.
Nel momento in cui il nostro script su miodominio.it andrà a fare una richiesta all’API miaapp.miodominio.it, otterrà un errore di Cross-Origin.
Questo perché a livello di sicurezza ogni applicazione deve effettuare richieste attraverso lo stesso dominio in cui si trova.
Potreste pensare che entrambe essendo parte dello stesso dominio di secondo livello (miodominio.it) ve la scampate ma no, anche in questo caso vengono trattati come due domini distinti. Non si scappa, sappiatelo.
Come si risolve tutto questo casino? Dobbiamo impostare la nostra applicazione affinché le richieste da determinati domini, vengano accettate.
In .NET Core 8 è abbastanza semplice. Apriamo di nuovo il nostro Program.cs e aggiungiamo il seguente codice:
builder.Services.AddCors(options =>
{
string? allowedOrigin = builder.Configuration.GetValue<string>("AppOptions:CorsOrigins");
string[] allowedOrigins = allowedOrigin?.Split(',') ?? [];
if (allowedOrigin is not null)
{
options.AddPolicy("AllowPolicy", policy =>
{
policy.WithOrigins(allowedOrigins)
.AllowAnyHeader()
.AllowAnyMethod();
});
}
});
Code language: C# (cs)
Prima di
var app = builder.Build();
Code language: C# (cs)
inseriamo inoltre
app.UseCors("AllowPolicy");
Code language: C# (cs)
dopo
app.UseAuthorization();
Code language: C# (cs)
AllowPolicy è il nome che ho dato alla policy da integrare ma sentitevi liberi di usare quello che più vi aggrada.
Come i più scafati avranno notato, ho inserito la lista di domini da mettere in whitelist tramite stringa di configurazione in appsettings.json e appsettings.Development.json:
string? allowedOrigin = builder.Configuration.GetValue<string>("AppOptions:CorsOrigins");
Code language: C# (cs)
I valori sono separati da virgola quindi il codice con .Split(‘,’) imposta correttamente ogni dominio da mettere in whitelist.
Ci basta solo aprire i file appsettings.json e appsettings.Development.json nella soluzione e dopo la prima graffa inserire
"AppOptions": {
"CorsOrigins": "https://miodominio.it,https://www.miodominio.it"
},
Code language: JSON / JSON with Comments (json)
Ovviamente miodominio.it andrà sostituito con i domini che volete mettere abilitare.
Perché in tutti e due i files? Perchè uno è quello che andrà caricato in produzione e uno è quello che si usa per lo sviluppo.
In progetti più complessi è vitale separare i due layer sia a livello di sicurezza che a livello di consistenza di dati. Il server sa distinguere un progetto tra i due ambienti dandoci la possibilità di avere due configurazioni distinte (ma con gli stessi campi).
In questo caso però potremmo anche evitare di utilizzare appsettings.Development.json ma come ho detto all’inizio: a noi piace fare le cose per bene.
Siate sempre pragmatici. Se pensate che una cosa si possa fare meglio, fatela.
Risparmierete casini di livello 9 e superiori. La vera bravura di uno sviluppatore è capire quando una feature debba essere configurabile e\o espandibile senza compromettere la leggibilità, l’usabilità e la scalabilità del codice.
Usare 8 classi, 9 servizi e 2 interfacce per un Hello World è da ergastolo ma anche scrivere un monoblocco utilizzabile solo e comunque per una determinata riga di codice lo è.
Pubblichiamo!
Se avete impostato i domini da mettere in whitelist, è possibile pubblicare l’applicazione.
Spoiler: è possibile farlo anche dopo ma è meglio caricare tutti i file aggiornati.

bin\Release\net8.0\publish\ su Folder Location, sarà il path dove saranno salvati tutti i files da copiare nella root folder online.
Cliccate su Finish.

Assicuratevi solamente che Configuration sia impostato su Release e potete pubblicare cliccando su Pubblish.

Cliccando su “Navigate” si aprirà esplora file con il contenuto da caricare online (tranne la cartella Data col file del database che ci occuperemo di caricare prima di tutto il resto).
Avendo il file del database con impostazione Do not copy di default, non è stato copiato direttamente in fase di pubblicazione. Poco male comunque, caricate direttamente la cartella Data prima di tutti i files.
La nostra app online!
E finalmente una volta che la nostra app sarà online, riusciremo ad avere le nostre tanto amate coordinate:

Nulla ci vieta di includere anche l’indirizzo IP all’interno del response o altre informazioni.
Abbiamo la massima libertà di configurarla come più ci aggrada.
Vediamo a livello di C.O.R.S. se funziona tutto. In via del tutto esclusiva provvedo io a fare delle prove su dei miei domini implementando un piccolo file di test in un’altra applicazione.
Prima di tutto la configurazione deve fare in modo che accetti richieste dal mio dominio:
"AppOptions": {
"CorsOrigins": "https://matteosaba.it,https://www.matteosaba.it"
}
Code language: JSON / JSON with Comments (json)
Scrivo un piccolo file html di test con una chiamata ajax:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Test Page</title>
</head>
<body>
<div id="response"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
fetch("https://testapp.tryasp.net/api/Geoposition/GetCoords")
.then(res => res.json())
.then(data => {
const responseBox = document.getElementById('response');
responseBox.append(JSON.stringify(data));
})
.catch(error => console.log("Si e' verificato un errore!"));
});
</script>
</body>
</html>
Code language: HTML, XML (xml)
Una volta caricato sulla cartella public di matteosaba.it, punto il browser verso la pagina html appena caricata e…

FLAMBEEEEEEEEEEE’!

Direi che l’app funziona. Ora avete tra le mani un sistema semplice ma versatile. Dovete solo aggiornare il database di tanto in tanto (vi consiglio una volta al mese) ma per il resto ci siamo.
Ovviamente senza servizi esterni e le loro patetiche strategie.
Nel caso siate pigri fino all’ennesima potenza vi lascio il link del repository dove potete scaricare l’app completa.
https://github.com/theoxd1/GeoAPI-Demo
Buon coding!








