latest RIPE additions

This commit is contained in:
2025-11-26 21:50:52 +00:00
parent 0371893af9
commit 921e4054e0
3 changed files with 156 additions and 269 deletions

191
README.md
View File

@@ -1,190 +1,3 @@
# 🌐 myip IP & RIPE Lookup WebApp
# myip IP & RIPE Lookup (extended)
myip è una webapp minimale e containerizzata che mostra:
- il **tuo IP pubblico**
- informazioni RIPE utili (ASN, Provider/Holder, Prefisso annunciato)
- UI moderna e responsiva
- pulsante **Copia IP**
- integrazione con ingress-nginx e PROXY protocol
- design personalizzabile tramite CSS dedicato
Pensata per essere deployata come micro-servizio in Kubernetes.
---
## ✨ Screenshot
> ![screenshot](./assets/screenshot.png)
---
## 🚀 Funzionalità
### 🔍 Identificazione del client
- Recupero del **vero IP del client** anche dietro più proxy/load balancer.
- Compatibile con:
- `X-Forwarded-For`
- `X-Real-IP`
- PROXY protocol v2
### 🛰️ Lookup RIPEstat
Per lindirizzo IP viene mostrato:
- ASN
- Provider (holder)
- Prefisso annunciato
### 🎨 UI moderna
- Font Montserrat
- Layout centrato
- Logo cliccabile
- Animazioni CSS
- Copy-to-clipboard
- Modalità dark automatica
### 🔧 Semplice da deployare
- Dockerfile incluso
- Configurazione Helm-ready
- Compatibile con ingress-nginx e cert-manager
---
## 📂 Struttura del progetto
```
myip-webapp/
├─ app.py
├─ requirements.txt
├─ Dockerfile
├─ static/
│ ├─ myip.css
│ ├─ logo.png
│ └─ favicon.ico
└─ README.md
```
---
## 🐳 Deploy con Docker
### Build
```bash
docker build -t myip:latest .
```
### Run
```bash
docker run -p 8080:8000 myip
```
Apri:
```
http://localhost:8080
```
---
## ☸️ Deploy in Kubernetes (Helm)
### values.yaml minimale
```yaml
ingress:
enabled: true
className: nginx
host: myip.example.com
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/use-forwarded-headers: "true"
nginx.ingress.kubernetes.io/real-ip-header: "X-Forwarded-For"
nginx.ingress.kubernetes.io/compute-full-forwarded-for: "true"
tls:
enabled: true
```
### ingress-nginx PROXY protocol (fondamentale)
Nella ConfigMap del controller:
```yaml
data:
use-proxy-protocol: "true"
real-ip-header: "proxy_protocol"
set-real-ip-from: "xxx.xxx.xxx.0/24"
use-forwarded-headers: "true"
```
### HAProxy (L4) davanti ad ingress-nginx
```haproxy
server backend1 xxx.xxx.xxx.xxx:443 send-proxy-v2 check
server backend2 xxx.xxx.xxx.xxx:443 send-proxy-v2 check
server backend3 xxx.xxx.xxx.xxx:443 send-proxy-v2 check
```
---
## 🧠 Note Tecniche
### Determinazione dellIP reale
Lapp utilizza:
```python
X-Forwarded-For X-Real-IP remote_addr
```
ed è compatibile con proxy multipli e ingress-nginx.
### Lookup RIPE
Usa la API ufficiale RIPEstat:
- `/network-info/`
- `/as-overview/`
Timeout veloce (2s) per non bloccare la UI.
---
## 🎨 Personalizzazioni
Tutto il design è modificabile in:
```
static/myip.css
```
Puoi sostituire:
- `logo.png` → per branding
- `favicon.ico` → icona personalizzata
---
## 🏁 Roadmap
- [ ] Endpoint `/api/ip`
- [ ] Multi-theme (light/dark manuale)
- [ ] Mini-widget JavaScript includibile in altri siti
- [ ] Supporto IPv6-only
- [ ] Caching locale del lookup RIPE
---
## 🤝 Credits
- Frontend & Styling by ChatGPT + AB style guidelines
- Backend Python + Flask
- Lookup dati: **RIPEstat Data API**
- Supporto PROXY prot. v2: HAProxy + ingress-nginx
---
## 📄 Licenza
MIT (o altra licenza a tua scelta)
UI moderna + IP reale + RIPE esteso (ASN, Provider, Prefix, Routing Status, Reverse DNS, GeoLocation, Abuse Contact).

174
app.py
View File

@@ -5,56 +5,79 @@ 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>
<title>myip 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/x-icon" href="/static/favicon.png">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/myip.css">
</head>
<body>
<div class="card">
{% if show_logo %}
<a href="https://soliddata.cloud" class="logo-link" target="_blank" rel="noopener">
<a href="https://www.netech-solution.it" 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 &amp; 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="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.prefix %}
<div class="ripe-item"><strong>Prefisso:</strong> {{ ripe.prefix }}</div>
<div class="ripe-item"><strong>Prefisso annunciato:</strong> {{ ripe.prefix }}</div>
{% endif %}
{% if ripe.asns %}
<div class="ripe-item"><strong>ASN:</strong> {{ ripe.asns | join(', ') }}</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 %}
<div class="ripe-muted">Dati ottenuti tramite RIPEstat Data API.</div>
{% else %}
<div class="ripe-title">Informazioni RIPE</div>
@@ -77,15 +100,18 @@ HTML_TEMPLATE = """<!doctype html>
var ip = ipSpan.textContent.trim();
if (!ip) return;
navigator.clipboard.writeText(ip).then(function () {
var original = btn.querySelector(".copy-label").textContent;
btn.classList.add("copy-btn-success");
btn.querySelector(".copy-label").textContent = "Copiato!";
setTimeout(function () {
btn.classList.remove("copy-btn-success");
btn.querySelector(".copy-label").textContent = original;
}, 1500);
}).catch(console.error);
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>
@@ -93,50 +119,112 @@ HTML_TEMPLATE = """<!doctype html>
</html>
"""
RIPESTAT_BASE = "https://stat.ripe.net/data"
def get_client_ip():
"""Determina l'IP reale del client dietro proxy / load balancer."""
xff = request.headers.get("X-Forwarded-For", "")
if xff:
parts = [p.strip() for p in xff.split(",") if p.strip()]
if parts:
return parts[0]
xri = request.headers.get("X-Real-IP")
if xri:
return xri.strip()
return request.remote_addr or "Sconosciuto"
def fetch_ripe_info(ip: str):
"""Recupera informazioni RIPEstat estese per l'IP."""
info = {
"prefix": None,
"asns": [],
"holder": None,
"routing_status": None,
"reverse_dns": None,
"geoloc": None,
"abuse": None,
}
try:
ni_resp = requests.get(f"{RIPESTAT_BASE}/network-info/data.json", params={"resource": ip}, timeout=2)
ni_resp.raise_for_status()
ni_data = ni_resp.json().get("data", {})
# 1. Network info (prefisso + ASN)
ni = requests.get(
f"{RIPESTAT_BASE}/network-info/data.json",
params={"resource": ip},
timeout=2,
).json().get("data", {})
prefix = ni_data.get("prefix")
asns = ni_data.get("asns") or []
holder = None
info["prefix"] = ni.get("prefix")
asns = ni.get("asns") or []
info["asns"] = [f"AS{a}" for a in asns]
# 2. AS overview (holder/provider)
if asns:
first_asn = asns[0]
ao_resp = requests.get(f"{RIPESTAT_BASE}/as-overview/data.json", params={"resource": first_asn}, timeout=2)
ao_resp.raise_for_status()
holder = ao_resp.json().get("data", {}).get("holder")
ao = requests.get(
f"{RIPESTAT_BASE}/as-overview/data.json",
params={"resource": asns[0]},
timeout=2,
).json().get("data", {})
info["holder"] = ao.get("holder")
class R: pass
r = R()
r.prefix = prefix
r.asns = [f"AS{x}" for x in asns] if asns else []
r.holder = holder
# 3. Routing status / visibility
rs = requests.get(
f"{RIPESTAT_BASE}/routing-status/data.json",
params={"resource": ip},
timeout=2,
).json().get("data", {})
info["routing_status"] = rs.get("status")
# 4. Reverse DNS
rd = requests.get(
f"{RIPESTAT_BASE}/reverse-dns/data.json",
params={"resource": ip},
timeout=2,
).json().get("data", {}).get("result", [])
info["reverse_dns"] = rd[0].get("name") if rd else None
# 5. GeoLocation
gl = requests.get(
f"{RIPESTAT_BASE}/geoloc/data.json",
params={"resource": ip},
timeout=2,
).json().get("data", {})
country = None
if gl.get("locations"):
country = gl["locations"][0].get("country", {})
if country:
info["geoloc"] = {
"country": country.get("name"),
"code": country.get("code"),
}
# 6. Abuse Contact
ac = requests.get(
f"{RIPESTAT_BASE}/abuse-contact-finder/data.json",
params={"resource": ip},
timeout=2,
).json().get("data", {})
emails = ac.get("emails") or []
info["abuse"] = emails[0] if emails else None
except Exception:
# In caso di errore lasciamo info parziali / vuote
pass
return info
return r if any([r.prefix, r.asns, r.holder]) else None
except:
return None
@app.route("/")
def index():
ip = get_client_ip()
ripe = fetch_ripe_info(ip) if ip != "Sconosciuto" else None
return render_template_string(HTML_TEMPLATE, ip=ip, ripe=ripe, show_logo=True)
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,
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@@ -19,19 +19,11 @@ body {
align-items: center;
text-align: center;
}
.badge { margin-bottom: 1rem; }
.title {
font-size: 1.25rem;
color: #101F2D;
margin-bottom: 0.5rem;
}
.ip-wrapper { margin-top: 1rem; margin-bottom: 1rem; }
.ip { font-size: 3em; font-weight: 600; color: #0181C4; }
.divider {
border-bottom: 1px solid #0181C4;
width: 100%;
margin-bottom: 30px;
}
.ripe-title {
font-size: 1.1rem;
font-weight: 600;
@@ -43,11 +35,30 @@ body {
color: #101F2D;
margin: 0.2rem 0;
}
.ripe-muted {
font-size: .8rem;
margin-top: 30px;
/* Logo link wrapper */
.logo-link {
display: inline-block;
text-decoration: none;
}
/* Logo ridimensionato correttamente */
/* Bandierina geoloc */
.flag-icon {
width: 24px;
height: 24px;
border-radius: 4px;
margin-right: 6px;
vertical-align: middle;
box-shadow: 0 0 3px rgba(0,0,0,0.25);
}
/* Bottone copia IP sotto l'indirizzo */
.copy-btn-below {
margin-top: 0.5rem;
margin-bottom: 1.5rem;
}
/* Logo size constraints */
.logo {
max-width: 260px;
max-height: 100px;
@@ -57,28 +68,3 @@ body {
margin-bottom: 1.5rem;
display: block;
}
/* bottone sotto l'IP */
.copy-btn-below {
font-family: 'Montserrat', Helvetica, Arial, sans-serif;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
background: #101F2D;
color: #ffffff;
font-size: 1.2em;
padding: 8px 20px;
transition: all .3s;
border:0;
}
.copy-btn-below:hover, .copy-btn-below:active {
font-family: 'Montserrat', Helvetica, Arial, sans-serif;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
background: #79858B;
color: #ffffff;
font-size: 1.2em;
padding: 8px 20px;
transition: all .3s;
border:0;
}