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
+
+> 
+
+---
+
+## π 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 = """
{% 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 %}
-

- {% 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;
+}
+