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

+ {% 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 fa08dc4..e69de29 100644
Binary files a/static/logo.png and b/static/logo.png differ
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;
-}
-