688 lines
23 KiB
Python
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)
|