From 3169c790233ae270fe3ddea5c3481251c4fe3c90 Mon Sep 17 00:00:00 2001 From: Alessio Bleggi Date: Wed, 26 Nov 2025 21:41:05 +0000 Subject: [PATCH] Added some detail taken from RIPE --- README.md | 191 +----------------------------------------------- app.py | 174 ++++++++++++++++++++++++++++++++----------- static/logo.png | Bin 11603 -> 0 bytes static/myip.css | 60 ++++++--------- 4 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/logo.png b/static/logo.png index fa08dc46603d60887130a062a481fd1d765791c7..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 11603 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 RD>zFUK~7ppssaQF{0|Fh6x09! 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; -} -