ip-show/app.py
2025-12-13 15:50:01 +01:00

688 lines
23 KiB
Python

import uvicorn
import httpx
import subprocess
import re
import shutil
import json
from fastapi import FastAPI, WebSocket, Request, WebSocketDisconnect
from fastapi.responses import HTMLResponse, PlainTextResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
translations = {
"nl": {
"title_home": "Jouw Publieke IP Adressen",
"title_expert": "Expert Verbindingsanalyse",
"grp_v6": "Jouw IPv6-gegevens",
"grp_v4": "Jouw IPv4-gegevens",
"lbl_ip6": "Jouw IPv6-adres:",
"lbl_ip4": "Jouw IPv4-adres:",
"lbl_isp": "Provider:",
"lbl_lat": "Latency:",
"lbl_lat_v6": "Jouw IPv6-latency:",
"lbl_lat_v4": "Jouw IPv4-latency:",
"btn_copy": "Kopieer",
"link_text": "Text-Only",
"link_expert": "Expert Mode",
"link_home": "Terug naar Home",
"ana_v6": "IPv6 Analyse",
"ana_v4": "IPv4 Analyse",
"lbl_ip": "IP Adres:",
"lbl_asn": "AS Nummer:",
"lbl_tcp": "TCP Latency (Min/Avg/Max):",
"lbl_jit": "Jitter (Stabiliteit):",
"lbl_mss": "Gemeten TCP MSS:",
"stat_load": "Laden...",
"stat_conn": "Verbinden...",
"stat_err": "Fout",
"stat_unkn": "Onbekend",
"est": "(Real-time)",
"404_title": "Pagina Niet Gevonden",
"404_msg": "De opgevraagde pagina bestaat niet.",
"404_btn": "Terug naar Home"
},
"en": {
"title_home": "Your Public IP Addresses",
"title_expert": "Expert Connection Analysis",
"grp_v6": "Your IPv6 Details",
"grp_v4": "Your IPv4 Details",
"lbl_ip6": "Your IPv6 Address:",
"lbl_ip4": "Your IPv4 Address:",
"lbl_isp": "Provider:",
"lbl_lat": "Latency:",
"lbl_lat_v6": "Your IPv6 Latency:",
"lbl_lat_v4": "Your IPv4 Latency:",
"btn_copy": "Copy",
"link_text": "Text-Only",
"link_expert": "Expert Mode",
"link_home": "Back to Home",
"ana_v6": "IPv6 Analysis",
"ana_v4": "IPv4 Analysis",
"lbl_ip": "IP Address:",
"lbl_asn": "AS Number:",
"lbl_tcp": "TCP Latency (Min/Avg/Max):",
"lbl_jit": "Jitter (Stability):",
"lbl_mss": "Measured TCP MSS:",
"stat_load": "Loading...",
"stat_conn": "Connecting...",
"stat_err": "Error",
"stat_unkn": "Unknown",
"est": "(Real-time)",
"404_title": "Page Not Found",
"404_msg": "The requested page does not exist.",
"404_btn": "Back to Home"
}
}
style_css = """
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #F0F8FF;
color: #2c3e50;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
}
.container {
background-color: #ffffff;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
width: 90%;
max-width: 900px;
position: relative;
z-index: 10;
margin-bottom: 20px;
box-sizing: border-box;
}
h1 { font-size: 2.2em; color: #34495e; margin-bottom: 30px; word-wrap: break-word; }
.data-group {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-top: 25px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
text-align: left;
}
.data-title {
font-size: 1.4em;
color: #34495e;
margin-bottom: 15px;
text-align: center;
font-weight: bold;
}
.data-item {
font-size: 1.1em;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
}
.data-label {
font-weight: bold;
color: #555;
display: flex;
align-items: center;
margin-right: 10px;
}
.data-value {
font-weight: normal;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-style: normal;
color: #95a5a6;
text-align: right;
flex: 1;
overflow-wrap: anywhere;
}
.data-icon { margin-right: 8px; font-size: 1.2em; }
.copy-button {
background-color: #07AAF9; color: white; border: none; padding: 12px;
border-radius: 5px; cursor: pointer; font-size: 1em; width: 100%; margin-top: 15px;
transition: background 0.2s;
}
.copy-button:hover { background-color: #0588c8; }
.footer-links {
color: #bdc3c7;
font-size: 0.9em;
margin-top: 10px;
}
.footer-links a { color: #bdc3c7; text-decoration: none; margin: 0 10px; transition: color 0.2s; }
.footer-links a:hover { color: #7f8c8d; text-decoration: underline; }
.loading { color: #bdc3c7; font-style: italic; }
.success { color: #27ae60; }
.expert-card h3 { margin-top: 0; color: #34495e; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px; }
.expert-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.95em;
align-items: baseline;
}
.expert-label {
font-weight: bold;
color: #2c3e50;
min-width: 140px;
flex-shrink: 0;
}
.expert-val {
font-family: monospace;
color: #34495e;
text-align: right;
overflow-wrap: anywhere;
}
.hidden { display: none; }
.btn-home {
background-color: #34495e; color: white; border: none; padding: 12px 24px;
border-radius: 5px; cursor: pointer; font-size: 1em; text-decoration: none;
display: inline-block; margin-top: 20px;
}
.btn-home:hover { background-color: #2c3e50; }
#toast {
visibility: hidden; min-width: 200px; background-color: #333;
color: #fff; text-align: center; border-radius: 4px; padding: 12px;
position: fixed; z-index: 100; bottom: 30px; left: 50%; transform: translateX(-50%);
}
#toast.show { visibility: visible; animation: fadein 0.5s, fadeout 0.5s 2.5s; }
@keyframes fadein { from {bottom: 0; opacity: 0;} to {bottom: 30px; opacity: 1;} }
@keyframes fadeout { from {bottom: 30px; opacity: 1;} to {bottom: 0; opacity: 0;} }
@media (max-width: 500px) {
.expert-row.stack-mobile {
flex-direction: column;
align-items: flex-start;
margin-bottom: 15px;
border-bottom: 1px dashed #eee;
padding-bottom: 5px;
}
.expert-row.stack-mobile .expert-val {
text-align: left;
margin-top: 4px;
width: 100%;
}
.data-item {
flex-direction: column;
align-items: flex-start;
border-bottom: 1px dashed #f0f0f0;
padding-bottom: 8px;
}
.data-value {
text-align: left;
width: 100%;
margin-top: 5px;
}
}
</style>
"""
def get_real_ip(request: Request):
cf = request.headers.get("cf-connecting-ip")
if cf: return cf
fwd = request.headers.get("x-forwarded-for")
if fwd: return fwd.split(",")[0].strip()
return request.client.host
def detect_language(request: Request):
accept_language = request.headers.get("accept-language", "").lower()
if "nl" in accept_language:
return "nl"
return "en"
def get_real_tcp_mss(ip: str, port: str):
if not ip or not port:
return None
if not shutil.which("ss"):
return "SS tool missing"
try:
target = f"[{ip}]:{port}" if ":" in ip else f"{ip}:{port}"
cmd = ["ss", "-n", "-i", "dst", target]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=1)
output = result.stdout
match = re.search(r'mss:(\d+)', output)
if match:
return f"{match.group(1)}b"
return "Unknown"
except Exception:
return "Error"
async def get_isp_home(ip: str):
try:
url = f"http://ip-api.com/json/{ip}?fields=isp"
async with httpx.AsyncClient() as client:
resp = await client.get(url, timeout=2.0)
return resp.json()
except:
return {}
async def get_isp_expert(ip: str):
try:
url = f"http://ip-api.com/json/{ip}?fields=isp,as"
async with httpx.AsyncClient() as client:
resp = await client.get(url, timeout=2.0)
data = resp.json()
raw_as = data.get("as", "")
data["as_number"] = raw_as.split(" ")[0] if raw_as else "-"
return data
except:
return {}
@app.get("/ping", response_class=PlainTextResponse)
async def ping():
return "pong"
@app.exception_handler(StarletteHTTPException)
async def custom_404_handler(request: Request, exc):
if exc.status_code == 404:
lang = detect_language(request)
t = translations[lang]
html = f"""
<!DOCTYPE html>
<html lang="{lang}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="https://pixelhosting.nl/img/logos/logo-icon.png" sizes="32x32">
<title>404 - {t['404_title']}</title>
{style_css}
</head>
<body>
<div class="container">
<h1 style="color: #e74c3c;">404</h1>
<h2>{t['404_title']}</h2>
<p>{t['404_msg']}</p>
<a href="/" class="btn-home">{t['404_btn']}</a>
</div>
</body>
</html>
"""
return HTMLResponse(content=html, status_code=404)
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
lang = detect_language(request)
t = translations[lang]
return f"""
<!DOCTYPE html>
<html lang="{lang}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="https://pixelhosting.nl/img/logos/logo-icon.png" sizes="32x32">
<title>{t['title_home']}</title>
{style_css}
</head>
<body>
<div class="container">
<h1>{t['title_home']}</h1>
<div class="data-group">
<h2 class="data-title">{t['grp_v6']}</h2>
<div class="data-item">
<span class="data-label"><span class="data-icon">🌐</span>{t['lbl_ip6']}</span>
<span class="data-value loading" id="ip6">{t['stat_load']}</span>
</div>
<div class="data-item">
<span class="data-label"><span class="data-icon">🏢</span>{t['lbl_isp']}</span>
<span class="data-value loading" id="isp6">...</span>
</div>
<div class="data-item">
<span class="data-label"><span class="data-icon">⚡</span>{t['lbl_lat_v6']}</span>
<span class="data-value loading" id="ping6">...</span>
</div>
<button class="copy-button" id="btn6" style="display:none" onclick="copyIP('ip6')">{t['btn_copy']} IPv6</button>
</div>
<div class="data-group">
<h2 class="data-title">{t['grp_v4']}</h2>
<div class="data-item">
<span class="data-label"><span class="data-icon">🌐</span>{t['lbl_ip4']}</span>
<span class="data-value loading" id="ip4">{t['stat_load']}</span>
</div>
<div class="data-item">
<span class="data-label"><span class="data-icon">🏢</span>{t['lbl_isp']}</span>
<span class="data-value loading" id="isp4">...</span>
</div>
<div class="data-item">
<span class="data-label"><span class="data-icon">⚡</span>{t['lbl_lat_v4']}</span>
<span class="data-value loading" id="ping4">...</span>
</div>
<button class="copy-button" id="btn4" style="display:none" onclick="copyIP('ip4')">{t['btn_copy']} IPv4</button>
</div>
</div>
<div class="footer-links">
<a href="https://ip-v4.pixelhosting.nl/api/home?format=text" target="_blank">{t['link_text']} IPv4</a>
<a href="https://ip-v6.pixelhosting.nl/api/home?format=text" target="_blank">{t['link_text']} IPv6</a>
<a href="/expert"><strong>{t['link_expert']}</strong></a>
</div>
<div id="toast">{t['btn_copy']}!</div>
<script>
const CONFIG = {{
4: {{ api: "https://ip-v4.pixelhosting.nl/api/home", ws: "wss://ip-v4.pixelhosting.nl/ws" }},
6: {{ api: "https://ip-v6.pixelhosting.nl/api/home", ws: "wss://ip-v6.pixelhosting.nl/ws" }}
}};
async function init() {{
checkStack(4, 'ip4', 'isp4', 'ping4', 'btn4');
checkStack(6, 'ip6', 'isp6', 'ping6', 'btn6');
}}
async function checkStack(ver, ipId, ispId, pingId, btnId) {{
const ipEl = document.getElementById(ipId);
const ispEl = document.getElementById(ispId);
const pingEl = document.getElementById(pingId);
const btn = document.getElementById(btnId);
try {{
const res = await fetch(CONFIG[ver].api);
const data = await res.json();
if(data.ip) {{
ipEl.textContent = data.ip;
if(data.isp) {{
ispEl.textContent = data.isp;
ispEl.classList.remove('loading');
}} else {{
ispEl.textContent = "{t['stat_unkn']}";
}}
ipEl.classList.remove('loading');
btn.style.display = 'block';
measureLatency(CONFIG[ver].ws, pingEl);
}} else {{
throw new Error("No IP");
}}
}} catch (e) {{
ipEl.textContent = "N/A";
ipEl.classList.remove('loading');
if(ispEl) ispEl.textContent = "-";
pingEl.textContent = "-";
}}
}}
function measureLatency(wsUrl, el) {{
el.textContent = "{t['stat_conn']}";
try {{
const socket = new WebSocket(wsUrl);
let start;
socket.onopen = () => {{ start = performance.now(); socket.send("ping"); }};
socket.onmessage = (event) => {{
try {{
const msg = JSON.parse(event.data);
if (msg.type === "mss_report") return;
}} catch(e) {{
}}
const lat = Math.round(performance.now() - start);
el.textContent = lat + " MS";
el.classList.remove('loading');
socket.close();
}};
socket.onerror = () => {{ el.textContent = "{t['stat_err']}"; }};
}} catch(e) {{ el.textContent = "{t['stat_err']}"; }}
}}
function copyIP(id) {{
const text = document.getElementById(id).textContent;
navigator.clipboard.writeText(text).then(() => {{
const t = document.getElementById("toast");
t.className = "show";
setTimeout(() => t.className = t.className.replace("show", ""), 3000);
}});
}}
window.onload = init;
</script>
</body>
</html>
"""
@app.get("/expert", response_class=HTMLResponse)
async def expert_page(request: Request):
lang = detect_language(request)
t = translations[lang]
return f"""
<!DOCTYPE html>
<html lang="{lang}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{t['title_expert']}</title>
{style_css}
</head>
<body>
<div class="container" style="max-width: 800px;">
<h1>{t['title_expert']}</h1>
<div id="results6" class="data-group hidden">
<h3>{t['ana_v6']}</h3>
<div class="expert-row stack-mobile"><span class="expert-label">{t['lbl_ip']}</span> <span id="e_ip6" class="expert-val">...</span></div>
<div class="expert-row stack-mobile"><span class="expert-label">{t['lbl_isp']}</span> <span id="e_isp6" class="expert-val">...</span></div>
<div class="expert-row"><span class="expert-label">{t['lbl_asn']}</span> <span id="e_asn6" class="expert-val">...</span></div>
<hr style="border: 0; border-top: 1px dashed #ccc; margin: 15px 0;">
<div class="expert-row stack-mobile"><span class="expert-label">{t['lbl_tcp']}</span> <span id="e_lat6" class="expert-val">{t['stat_conn']}</span></div>
<div class="expert-row"><span class="expert-label">{t['lbl_jit']}</span> <span id="e_jit6" class="expert-val">...</span></div>
<div class="expert-row"><span class="expert-label">{t['lbl_mss']}</span> <span id="e_mss6" class="expert-val">... ({t['est']})</span></div>
</div>
<div id="results4" class="data-group hidden">
<h3>{t['ana_v4']}</h3>
<div class="expert-row stack-mobile"><span class="expert-label">{t['lbl_ip']}</span> <span id="e_ip4" class="expert-val">...</span></div>
<div class="expert-row stack-mobile"><span class="expert-label">{t['lbl_isp']}</span> <span id="e_isp4" class="expert-val">...</span></div>
<div class="expert-row"><span class="expert-label">{t['lbl_asn']}</span> <span id="e_asn4" class="expert-val">...</span></div>
<hr style="border: 0; border-top: 1px dashed #ccc; margin: 15px 0;">
<div class="expert-row stack-mobile"><span class="expert-label">{t['lbl_tcp']}</span> <span id="e_lat4" class="expert-val">{t['stat_conn']}</span></div>
<div class="expert-row"><span class="expert-label">{t['lbl_jit']}</span> <span id="e_jit4" class="expert-val">...</span></div>
<div class="expert-row"><span class="expert-label">{t['lbl_mss']}</span> <span id="e_mss4" class="expert-val">... ({t['est']})</span></div>
</div>
<div class="footer-links">
<a href="/">{t['link_home']}</a>
</div>
</div>
<script>
const CONFIG = {{
4: {{ api: "https://ip-v4.pixelhosting.nl/api/expert", ws: "wss://ip-v4.pixelhosting.nl/ws" }},
6: {{ api: "https://ip-v6.pixelhosting.nl/api/expert", ws: "wss://ip-v6.pixelhosting.nl/ws" }}
}};
window.onload = function() {{
runExpertTest();
}};
function runExpertTest() {{
testStack(4);
testStack(6);
}}
async function testStack(ver) {{
const container = document.getElementById('results'+ver);
try {{
const res = await fetch(CONFIG[ver].api);
const data = await res.json();
if(!data.ip) throw new Error("Offline");
container.classList.remove('hidden');
document.getElementById('e_ip'+ver).innerText = data.ip;
document.getElementById('e_isp'+ver).innerText = data.isp || "{t['stat_unkn']}";
document.getElementById('e_asn'+ver).innerText = data.as_number || "-";
document.getElementById('e_mss'+ver).innerText = data.mss || "-";
runLatencyTest(CONFIG[ver].ws, ver);
}} catch (e) {{
console.log("v"+ver+" N/A");
}}
}}
function runLatencyTest(wsUrl, ver) {{
const latEl = document.getElementById('e_lat'+ver);
const jitEl = document.getElementById('e_jit'+ver);
const mssEl = document.getElementById('e_mss'+ver);
const socket = new WebSocket(wsUrl);
const pings = [];
let count = 0;
const maxPings = 10;
socket.onopen = () => {{
sendPing();
}};
function sendPing() {{
socket.lastStart = performance.now();
socket.send("ping");
}}
socket.onmessage = (event) => {{
try {{
const msg = JSON.parse(event.data);
if (msg.type === "mss_report") {{
if(msg.value && msg.value !== "Unknown") {{
mssEl.innerText = msg.value;
}}
return;
}}
}} catch(e) {{
}}
const duration = performance.now() - socket.lastStart;
pings.push(duration);
count++;
if(count < maxPings) {{
setTimeout(sendPing, 50);
}} else {{
finish();
}}
}};
socket.onerror = () => {{ latEl.innerText = "Error"; socket.close(); }};
function finish() {{
socket.close();
const min = Math.min(...pings).toFixed(1);
const max = Math.max(...pings).toFixed(1);
const avg = (pings.reduce((a, b) => a + b, 0) / pings.length).toFixed(1);
let jitterSum = 0;
for(let i=0; i<pings.length-1; i++) {{
jitterSum += Math.abs(pings[i] - pings[i+1]);
}}
const jitter = (pings.length > 1) ? (jitterSum / (pings.length-1)).toFixed(2) : "0.00";
latEl.innerText = `${{min}}ms / ${{avg}}ms / ${{max}}ms`;
jitEl.innerText = `${{jitter}}ms`;
}}
}}
</script>
</body>
</html>
"""
@app.get("/api/home")
async def api_home(request: Request, format: str = None):
ip = get_real_ip(request)
ua = request.headers.get("user-agent", "").lower()
if format == "text" or "curl" in ua or "wget" in ua:
return PlainTextResponse(ip)
details = await get_isp_home(ip)
return JSONResponse({
"ip": ip,
"isp": details.get("isp"),
"status": "ok"
})
@app.get("/api/expert")
async def api_expert(request: Request):
ip = get_real_ip(request)
port = request.headers.get("x-remote-port")
details = await get_isp_expert(ip)
return JSONResponse({
"ip": ip,
"isp": details.get("isp"),
"as_number": details.get("as_number"),
"status": "ok"
})
@app.websocket("/ws")
async def ws_endpoint(websocket: WebSocket):
await websocket.accept()
real_ip = websocket.headers.get("x-real-ip") or websocket.client.host
real_port = websocket.headers.get("x-remote-port")
mss_value = "Unknown"
if real_port:
mss_value = get_real_tcp_mss(real_ip, real_port)
await websocket.send_text(json.dumps({
"type": "mss_report",
"value": mss_value
}))
try:
while True:
data = await websocket.receive_text()
if data == "ping":
await websocket.send_text(data)
except WebSocketDisconnect:
pass
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)