diff --git a/README.md b/README.md index d37c36f..2025dda 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,190 @@ -# myip – IP & RIPE Lookup (extended) +# 🌐 myip – IP & RIPE Lookup WebApp -UI moderna + IP reale + RIPE esteso (ASN, Provider, Prefix, Routing Status, Reverse DNS, GeoLocation, Abuse Contact). +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) diff --git a/app.py b/app.py index b0a7c5f..cdb6b04 100644 --- a/app.py +++ b/app.py @@ -5,79 +5,56 @@ 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 = """ - myip – IP & RIPE Lookup + SolidData – 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 annunciato: {{ ripe.prefix }}
+
Prefisso: {{ 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
@@ -100,18 +77,15 @@ HTML_TEMPLATE = """ 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); - } + 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); }); }); @@ -119,112 +93,50 @@ 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: - # 1. Network info (prefisso + ASN) - ni = requests.get( - f"{RIPESTAT_BASE}/network-info/data.json", - params={"resource": ip}, - timeout=2, - ).json().get("data", {}) + 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", {}) - info["prefix"] = ni.get("prefix") - asns = ni.get("asns") or [] - info["asns"] = [f"AS{a}" for a in asns] + prefix = ni_data.get("prefix") + asns = ni_data.get("asns") or [] + holder = None - # 2. AS overview (holder/provider) if asns: - ao = requests.get( - f"{RIPESTAT_BASE}/as-overview/data.json", - params={"resource": asns[0]}, - timeout=2, - ).json().get("data", {}) - info["holder"] = ao.get("holder") + 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") - # 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 + class R: pass + r = R() + r.prefix = prefix + r.asns = [f"AS{x}" for x in asns] if asns else [] + r.holder = holder + return r if any([r.prefix, r.asns, r.holder]) else None + except: + return None @app.route("/") def index(): ip = get_client_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, - ) - + ripe = fetch_ripe_info(ip) if ip != "Sconosciuto" else None + return render_template_string(HTML_TEMPLATE, ip=ip, ripe=ripe, 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 e69de29..fa08dc4 100644 Binary files a/static/logo.png and b/static/logo.png differ diff --git a/static/myip.css b/static/myip.css index f864122..3ab505b 100644 --- a/static/myip.css +++ b/static/myip.css @@ -19,11 +19,19 @@ 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; @@ -35,30 +43,11 @@ body { color: #101F2D; margin: 0.2rem 0; } - -/* Logo link wrapper */ -.logo-link { - display: inline-block; - text-decoration: none; +.ripe-muted { + font-size: .8rem; + margin-top: 30px; } - -/* 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 ridimensionato correttamente */ .logo { max-width: 260px; max-height: 100px; @@ -68,3 +57,28 @@ 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; +} +