387 lines
13 KiB
Python
387 lines
13 KiB
Python
from flask import Flask, request, render_template_string
|
||
import requests
|
||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||
|
||
app = Flask(__name__)
|
||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
|
||
|
||
RIPESTAT_BASE = "https://stat.ripe.net/data"
|
||
|
||
HTML_TEMPLATE = """<!doctype html>
|
||
<html lang="it">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>SolidData – IP & RIPE Lookup</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
|
||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||
<link rel="stylesheet" href="/static/myip.css">
|
||
</head>
|
||
<body>
|
||
<div class="card">
|
||
{% if show_logo %}
|
||
<a href="https://www.soliddata.cloud" class="logo-link" target="_blank" rel="noopener">
|
||
<img src="/static/logo.png" class="logo" alt="Logo">
|
||
</a>
|
||
{% endif %}
|
||
|
||
<div class="badge">
|
||
<span class="badge-dot"></span>
|
||
<span>IP & RIPE Lookup</span>
|
||
</div>
|
||
|
||
<h1 class="title">Il tuo IP pubblico è:</h1>
|
||
|
||
<div class="ip-wrapper">
|
||
<span id="ip-value" class="ip">{{ ip }}</span>
|
||
</div>
|
||
|
||
<button id="copy-btn" class="copy-btn copy-btn-below" type="button">
|
||
<span class="copy-icon">📋</span>
|
||
<span class="copy-label">Copia IP</span>
|
||
</button>
|
||
|
||
<div class="divider"></div>
|
||
|
||
{% if ripe %}
|
||
<div class="ripe-title">Informazioni RIPE</div>
|
||
|
||
{% if ripe.holder %}
|
||
<div class="ripe-item"><strong>Provider:</strong> {{ ripe.holder }}</div>
|
||
{% endif %}
|
||
{% if ripe.routing_status %}
|
||
<div class="ripe-item"><strong>Routing Status:</strong> {{ ripe.routing_status }}</div>
|
||
{% endif %}
|
||
{% if ripe.reverse_dns %}
|
||
<div class="ripe-item"><strong>Reverse DNS:</strong> {{ ripe.reverse_dns }}</div>
|
||
{% endif %}
|
||
{% if ripe.geoloc %}
|
||
<div class="ripe-item">
|
||
<strong>GeoLocation:</strong>
|
||
{% if ripe.geoloc.code %}
|
||
<img src="https://flagsapi.com/{{ ripe.geoloc.code }}/flat/24.png" class="flag-icon" alt="{{ ripe.geoloc.code }}">
|
||
{% endif %}
|
||
{{ ripe.geoloc.country }}
|
||
</div>
|
||
{% endif %}
|
||
{% if ripe.abuse %}
|
||
<div class="ripe-item"><strong>Abuse Contact:</strong> {{ ripe.abuse }}</div>
|
||
{% endif %}
|
||
|
||
{% if ripe.netname %}
|
||
<div class="ripe-item"><strong>Netname:</strong> {{ ripe.netname }}</div>
|
||
{% endif %}
|
||
|
||
{% if ripe.org_name %}
|
||
<div class="ripe-item"><strong>Organisation:</strong> {{ ripe.org_name }}</div>
|
||
{% endif %}
|
||
|
||
{% if ripe.rir %}
|
||
<div class="ripe-item"><strong>RIR:</strong> {{ ripe.rir }}</div>
|
||
{% endif %}
|
||
|
||
{% if ripe.block %}
|
||
<div class="ripe-item"><strong>IP Block:</strong> {{ ripe.block }}</div>
|
||
{% endif %}
|
||
|
||
{% if ripe.rir %}
|
||
<div class="ripe-item"><strong>RIR:</strong> {{ ripe.rir }}</div>
|
||
{% endif %}
|
||
|
||
<div class="ripe-muted">Dati ottenuti tramite RIPEstat Data API.</div>
|
||
{% else %}
|
||
<div class="ripe-title">Informazioni RIPE</div>
|
||
<div class="ripe-muted">
|
||
Non è stato possibile recuperare i dettagli RIPE per questo indirizzo.
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
var card = document.querySelector(".card");
|
||
if (card) card.classList.add("card-visible");
|
||
|
||
var btn = document.getElementById("copy-btn");
|
||
var ipSpan = document.getElementById("ip-value");
|
||
if (!btn || !ipSpan) return;
|
||
|
||
btn.addEventListener("click", function () {
|
||
var ip = ipSpan.textContent.trim();
|
||
if (!ip) return;
|
||
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(ip).then(function () {
|
||
var labelEl = btn.querySelector(".copy-label");
|
||
var original = labelEl.textContent;
|
||
btn.classList.add("copy-btn-success");
|
||
labelEl.textContent = "Copiato!";
|
||
setTimeout(function () {
|
||
btn.classList.remove("copy-btn-success");
|
||
labelEl.textContent = original;
|
||
}, 1500);
|
||
}).catch(console.error);
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
import ipaddress
|
||
from flask import Flask, request, render_template_string
|
||
# ...
|
||
|
||
def _is_private_ip(ip: str) -> bool:
|
||
try:
|
||
return ipaddress.ip_address(ip).is_private
|
||
except ValueError:
|
||
# se non è un IP valido, lo consideriamo "non utilizzabile"
|
||
return True
|
||
|
||
|
||
def get_client_ip():
|
||
"""
|
||
Prova a determinare l'IP pubblico del client:
|
||
- prende la lista da X-Forwarded-For
|
||
- sceglie il primo IP NON privato (non 10.x/192.168/172.16/127.x ecc.)
|
||
- fallback su X-Real-IP
|
||
- fallback su request.remote_addr
|
||
"""
|
||
# 1) X-Forwarded-For: "client, proxy1, proxy2..."
|
||
xff = request.headers.get("X-Forwarded-For", "")
|
||
if xff:
|
||
parts = [p.strip() for p in xff.split(",") if p.strip()]
|
||
# cerco il primo IP pubblico nella lista
|
||
for ip in parts:
|
||
if not _is_private_ip(ip):
|
||
return ip
|
||
# se proprio non trovo nulla di pubblico, prendo il primo
|
||
if parts:
|
||
return parts[0]
|
||
|
||
# 2) X-Real-IP
|
||
xri = request.headers.get("X-Real-IP")
|
||
if xri and not _is_private_ip(xri):
|
||
return xri.strip()
|
||
|
||
# 3) Fallback: remote_addr così com'è
|
||
return request.remote_addr or "Sconosciuto"
|
||
def get_effective_ip():
|
||
"""
|
||
Restituisce (ip, overridden)
|
||
- ip: indirizzo IP da usare per la lookup
|
||
- overridden: True se l'IP è stato forzato via ?checkip=
|
||
"""
|
||
override = request.args.get("checkip")
|
||
if override:
|
||
override = override.strip()
|
||
try:
|
||
# valida che sia un IP v4/v6
|
||
ipaddress.ip_address(override)
|
||
return override, True
|
||
except ValueError:
|
||
# se non è valido, ignora e usa il flusso normale
|
||
pass
|
||
|
||
# default: uso l'IP reale del client
|
||
return get_client_ip(), False
|
||
|
||
def fetch_ripe_info(ip: str):
|
||
"""Recupera informazioni RIPEstat estese per l'IP, compatibili col template."""
|
||
|
||
info = {
|
||
"prefix": None,
|
||
"asns": [],
|
||
"holder": None,
|
||
"routing_status": None,
|
||
"reverse_dns": None,
|
||
"geoloc": None,
|
||
"abuse": None,
|
||
# campi extra che stai già usando in pagina
|
||
"block": None,
|
||
"rir": None,
|
||
"country": None, # non usato ora in UI
|
||
"country_whois": None, # usato in "Country (WHOIS)"
|
||
"netname": None, # usato in "Netname"
|
||
"org_name": None,
|
||
"raw": {}, # solo per debug
|
||
}
|
||
|
||
try:
|
||
# 0) PREFIX-OVERVIEW: blocco IP + RIR + stato announced/routed
|
||
po_resp = requests.get(
|
||
f"{RIPESTAT_BASE}/prefix-overview/data.json",
|
||
params={"resource": ip},
|
||
timeout=2,
|
||
)
|
||
po = po_resp.json().get("data", {})
|
||
info["raw"]["prefix_overview"] = po
|
||
|
||
if po:
|
||
# es: 77.43.64.0/18
|
||
info["block"] = po.get("resource")
|
||
# es: block.desc = "RIPE NCC (Status: ALLOCATED)"
|
||
block = po.get("block") or {}
|
||
desc = block.get("desc")
|
||
if desc:
|
||
# prendiamo la parte prima della parentesi
|
||
info["rir"] = desc.split("(")[0].strip()
|
||
|
||
# announced / routed (bool)
|
||
info["routing_status"] = None
|
||
if po.get("announced") is not None:
|
||
info["routing_status"] = "Announced" if po.get("announced") else "Not announced"
|
||
|
||
# prefix principale
|
||
if not info["prefix"]:
|
||
info["prefix"] = po.get("resource")
|
||
|
||
# 1) NETWORK-INFO: prefisso + ASNs
|
||
ni_resp = requests.get(
|
||
f"{RIPESTAT_BASE}/network-info/data.json",
|
||
params={"resource": ip},
|
||
timeout=2,
|
||
)
|
||
ni = ni_resp.json().get("data", {})
|
||
info["raw"]["network_info"] = ni
|
||
|
||
if ni:
|
||
if not info["prefix"]:
|
||
info["prefix"] = ni.get("prefix")
|
||
asns = ni.get("asns") or []
|
||
info["asns"] = [f"AS{a}" for a in asns]
|
||
|
||
# 2) AS-OVERVIEW: holder/provider (es: "AS-IRIDEOS - Retelit Digital Services S.p.A.")
|
||
if ni and ni.get("asns"):
|
||
ao_resp = requests.get(
|
||
f"{RIPESTAT_BASE}/as-overview/data.json",
|
||
params={"resource": ni["asns"][0]},
|
||
timeout=2,
|
||
)
|
||
ao = ao_resp.json().get("data", {})
|
||
info["raw"]["as_overview"] = ao
|
||
if ao:
|
||
info["holder"] = ao.get("holder") or info["holder"]
|
||
|
||
# 3) ROUTING-STATUS: origin AS + visibilità
|
||
rs_resp = requests.get(
|
||
f"{RIPESTAT_BASE}/routing-status/data.json",
|
||
params={"resource": info["prefix"] or ip},
|
||
timeout=2,
|
||
)
|
||
rs = rs_resp.json().get("data", {})
|
||
info["raw"]["routing_status"] = rs
|
||
|
||
if rs:
|
||
origins = rs.get("origins") or []
|
||
origin_as = None
|
||
if origins:
|
||
origin_as = origins[0].get("origin")
|
||
v4 = (rs.get("visibility") or {}).get("v4") or {}
|
||
peers = v4.get("ris_peers_seeing")
|
||
total = v4.get("total_ris_peers")
|
||
|
||
if origin_as and peers is not None and total is not None:
|
||
info["routing_status"] = f"Origin AS{origin_as}, visibility {peers}/{total} peers"
|
||
elif origin_as:
|
||
info["routing_status"] = f"Origin AS{origin_as}"
|
||
|
||
# 4) REVERSE-DNS
|
||
rd_resp = requests.get(
|
||
f"{RIPESTAT_BASE}/reverse-dns/data.json",
|
||
params={"resource": ip},
|
||
timeout=2,
|
||
)
|
||
rd = rd_resp.json().get("data", {}).get("result", [])
|
||
info["raw"]["reverse_dns"] = rd
|
||
if rd:
|
||
info["reverse_dns"] = rd[0].get("name")
|
||
|
||
# 5) GEOLOC (usa located_resources → locations)
|
||
gl_resp = requests.get(
|
||
f"{RIPESTAT_BASE}/geoloc/data.json",
|
||
params={"resource": ip},
|
||
timeout=2,
|
||
)
|
||
gl = gl_resp.json().get("data", {})
|
||
info["raw"]["geoloc"] = gl
|
||
|
||
located_resources = gl.get("located_resources") or []
|
||
if located_resources:
|
||
first_res = located_resources[0]
|
||
locs = first_res.get("locations") or []
|
||
if locs:
|
||
loc0 = locs[0]
|
||
city = loc0.get("city")
|
||
country_code = loc0.get("country")
|
||
# flagsapi vuole il codice (es: IT)
|
||
label = country_code
|
||
if city and country_code:
|
||
label = f"{city}, {country_code}"
|
||
|
||
info["geoloc"] = {
|
||
"country": label,
|
||
"code": country_code,
|
||
}
|
||
|
||
# 6) ABUSE CONTACT (abuse_contacts, non emails)
|
||
ac_resp = requests.get(
|
||
f"{RIPESTAT_BASE}/abuse-contact-finder/data.json",
|
||
params={"resource": ip},
|
||
timeout=2,
|
||
)
|
||
ac = ac_resp.json().get("data", {})
|
||
info["raw"]["abuse"] = ac
|
||
|
||
abuse_contacts = ac.get("abuse_contacts") or []
|
||
if abuse_contacts:
|
||
info["abuse"] = abuse_contacts[0]
|
||
|
||
# 7) WHOIS: netname, country (WHOIS), org / descr
|
||
whois_resp = requests.get(
|
||
f"{RIPESTAT_BASE}/whois/data.json",
|
||
params={"resource": ip},
|
||
timeout=3,
|
||
)
|
||
whois = whois_resp.json().get("data", {})
|
||
info["raw"]["whois"] = whois
|
||
|
||
records = whois.get("records") or []
|
||
for block in records:
|
||
for entry in block:
|
||
k = entry.get("key", "").lower()
|
||
v = entry.get("value", "")
|
||
|
||
if k == "netname" and not info["netname"]:
|
||
info["netname"] = v
|
||
elif k == "country" and not info["country_whois"]:
|
||
info["country_whois"] = v
|
||
elif k in ("org-name", "organisation", "descr") and not info["org_name"]:
|
||
info["org_name"] = v
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
return info
|
||
|
||
@app.route("/")
|
||
def index():
|
||
ip, overridden = get_effective_ip()
|
||
ripe_info = fetch_ripe_info(ip) if ip != "Sconosciuto" else None
|
||
return render_template_string(
|
||
HTML_TEMPLATE,
|
||
ip=ip,
|
||
ripe=ripe_info,
|
||
show_logo=True,
|
||
override=overridden,
|
||
)
|
||
|
||
if __name__ == "__main__":
|
||
app.run(host="0.0.0.0", port=8000)
|