From 921e4054e0c33616b24d522d8b7bf9d92b3b7b33 Mon Sep 17 00:00:00 2001 From: Alessio Bleggi Date: Wed, 26 Nov 2025 21:50:52 +0000 Subject: [PATCH] latest RIPE additions --- README.md | 191 +----------------------------------------------- app.py | 174 ++++++++++++++++++++++++++++++++----------- static/myip.css | 60 ++++++--------- 3 files changed, 156 insertions(+), 269 deletions(-) diff --git a/README.md b/README.md index 2025dda..d37c36f 100644 --- a/README.md +++ b/README.md @@ -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 l’indirizzo 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 dell’IP reale - -L’app 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). diff --git a/app.py b/app.py index cdb6b04..b0a7c5f 100644 --- a/app.py +++ b/app.py @@ -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 = """ - SolidData – IP & RIPE Lookup + myip – IP & RIPE Lookup - +
{% if show_logo %} - + {% endif %} +
IP & RIPE Lookup
+

Il tuo IP pubblico Γ¨:

-
- {{ ip }} -
- - +
+ {{ ip }} +
+
{% if ripe %}
Informazioni RIPE
+ {% if ripe.holder %}
Provider: {{ ripe.holder }}
{% endif %} {% if ripe.prefix %} -
Prefisso: {{ ripe.prefix }}
+
Prefisso annunciato: {{ ripe.prefix }}
{% endif %} {% if ripe.asns %}
ASN: {{ ripe.asns | join(', ') }}
{% endif %} + {% if ripe.routing_status %} +
Routing Status: {{ ripe.routing_status }}
+ {% endif %} + {% if ripe.reverse_dns %} +
Reverse DNS: {{ ripe.reverse_dns }}
+ {% endif %} + {% if ripe.geoloc %} +
+ GeoLocation: + {% if ripe.geoloc.code %} + {{ ripe.geoloc.code }} + {% endif %} + {{ ripe.geoloc.country }} +
+ {% endif %} + {% if ripe.abuse %} +
Abuse Contact: {{ ripe.abuse }}
+ {% endif %} +
Dati ottenuti tramite RIPEstat Data API.
{% else %}
Informazioni RIPE
@@ -77,15 +100,18 @@ HTML_TEMPLATE = """ 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); + } }); }); @@ -93,50 +119,112 @@ HTML_TEMPLATE = """ """ -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) diff --git a/static/myip.css b/static/myip.css index 3ab505b..f864122 100644 --- a/static/myip.css +++ b/static/myip.css @@ -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; -} -