276 lines
11 KiB
HTML
276 lines
11 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="robots" content="noindex,nofollow" />
|
||
<title>Webserver-Testseite</title>
|
||
<style>
|
||
:root{--bg:#0b0c10;--card:#11131a;--fg:#e8edf2;--muted:#9aa7b4;--ok:#18a957;--warn:#e6a700;--err:#d64545;--accent:#7c4dff;--grid:rgba(124,77,255,.08)}
|
||
@media (prefers-color-scheme: light){
|
||
:root{--bg:#f6f8fb;--card:#ffffff;--fg:#0e1320;--muted:#556171;--accent:#5b4bff;--grid:rgba(91,75,255,.06)}
|
||
}
|
||
*{box-sizing:border-box}
|
||
html,body{height:100%}
|
||
body{
|
||
margin:0;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;
|
||
background:radial-gradient(1200px 600px at 10% -10%,var(--grid),transparent 70%),
|
||
radial-gradient(900px 500px at 110% 10%,var(--grid),transparent 70%),var(--bg);
|
||
color:var(--fg);line-height:1.45;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility
|
||
}
|
||
.wrap{max-width:980px;margin:auto;padding:24px;display:grid;gap:24px}
|
||
header{display:grid;gap:12px}
|
||
.brand{display:flex;align-items:center;gap:12px}
|
||
.logo{inline-size:48px;block-size:48px;border-radius:12px;background:
|
||
conic-gradient(from 210deg,var(--accent),transparent 40%),
|
||
radial-gradient(70% 70% at 30% 30%,var(--accent),transparent 60%),
|
||
linear-gradient(135deg,rgba(255,255,255,.15),rgba(255,255,255,0));
|
||
box-shadow:0 8px 24px rgba(0,0,0,.25), inset 0 0 0 1px rgba(255,255,255,.06)
|
||
}
|
||
.title{font-size:clamp(22px,3.2vw,30px);font-weight:700}
|
||
.subtitle{color:var(--muted);font-size:14px}
|
||
.card{background:var(--card);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:18px;box-shadow:0 6px 20px rgba(0,0,0,.15)}
|
||
.grid{display:grid;gap:16px}
|
||
@media (min-width:780px){.two{grid-template-columns:1fr 1fr}}
|
||
.kv{display:grid;grid-template-columns:160px 1fr;gap:8px 12px;font-size:14px}
|
||
.kv dt{color:var(--muted)}.kv dd{margin:0;word-break:break-word}
|
||
.pill{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;
|
||
font-size:12px;border:1px solid rgba(255,255,255,.12)}
|
||
.ok{background:color-mix(in oklab,var(--ok) 18%,transparent)}
|
||
.warn{background:color-mix(in oklab,var(--warn) 18%,transparent)}
|
||
.err{background:color-mix(in oklab,var(--err) 18%,transparent)}
|
||
.list{display:grid;gap:8px}
|
||
.item{display:flex;gap:8px;align-items:center;justify-content:space-between;padding:8px 10px;border:1px dashed rgba(255,255,255,.12);border-radius:12px}
|
||
.badges{display:flex;flex-wrap:wrap;gap:8px}
|
||
.btn{appearance:none;border:1px solid rgba(255,255,255,.16);background:transparent;color:var(--fg);
|
||
padding:8px 12px;border-radius:10px;font-size:14px;cursor:pointer}
|
||
.btn:hover{border-color:rgba(255,255,255,.32)}
|
||
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace}
|
||
.table{width:100%;border-collapse:separate;border-spacing:0 6px;font-size:13px}
|
||
.tr{display:grid;grid-template-columns:220px 1fr;gap:10px;align-items:start}
|
||
.key{color:var(--muted)}
|
||
.val{word-break:break-word}
|
||
footer{color:var(--muted);font-size:12px;text-align:center;padding:6px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="wrap">
|
||
<header>
|
||
<div class="brand">
|
||
<div class="logo" aria-hidden="true"></div>
|
||
<div>
|
||
<div class="title">Webserver-Testseite</div>
|
||
<div class="subtitle">Keine externen Dienste. IP nur aus Response-Headern oder optional /ip.</div>
|
||
</div>
|
||
</div>
|
||
<div class="badges" id="badges"></div>
|
||
</header>
|
||
|
||
<section class="card">
|
||
<h2 style="margin:0 0 10px;font-size:18px">Anfrage-Kontext</h2>
|
||
<dl class="kv" id="kv"></dl>
|
||
</section>
|
||
|
||
<section class="grid two">
|
||
<div class="card">
|
||
<h3 style="margin:0 0 10px;font-size:16px">Anfragende Adresse (ohne STUN)</h3>
|
||
<div id="ipList" class="list"></div>
|
||
<div style="display:flex;gap:8px;margin-top:10px">
|
||
<button class="btn" style="display:none;" id="rerun">Neu laden</button>
|
||
<button class="btn" style="display:none;" id="copy">Kopieren</button>
|
||
</div>
|
||
<p style="margin:10px 0 0"><small class="mono" id="srcNote"></small></p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3 style="margin:0 0 10px;font-size:16px">Alle Response-Header</h3>
|
||
<div id="hdrTable" class="table"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<footer>Nur same-origin. Kein WebRTC. Keine externen HTTP-Aufrufe.</footer>
|
||
</main>
|
||
|
||
<script>
|
||
(function(){
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
// --- utils ---
|
||
const isIPv4 = (s) => /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/.test(s);
|
||
const isIPv6 = (s) => /^[0-9a-fA-F:]+$/.test(s) && s.includes(':');
|
||
const isIP = (s) => isIPv4(s) || isIPv6(s);
|
||
const splitCSV = (s) => s.split(',').map(x=>x.trim()).filter(Boolean);
|
||
const uniq = (arr) => Array.from(new Set(arr));
|
||
const addRow = (left, right, cls) => {
|
||
const div = document.createElement('div'); div.className='item';
|
||
const L = document.createElement('div'); L.textContent = left;
|
||
const R = document.createElement('div'); R.className = 'pill ' + (cls||''); R.textContent = right;
|
||
div.append(L,R); return div;
|
||
};
|
||
|
||
// --- context ---
|
||
function renderContext(){
|
||
const kv = $('kv'); kv.innerHTML='';
|
||
const {protocol, hostname, port, pathname, search, hash, href} = location;
|
||
const dpr = devicePixelRatio || 1;
|
||
const rows = [
|
||
['URL', href],
|
||
['Protokoll', protocol.replace(':','')],
|
||
['Host', hostname],
|
||
['Port', port || (protocol==='https:'?'443':'80')],
|
||
['Pfad', pathname || '/'],
|
||
['Query', search || ''],
|
||
['Hash', hash || ''],
|
||
['Zeit', new Date().toISOString()],
|
||
['Viewport', `${innerWidth}×${innerHeight} @${dpr}x`],
|
||
['UA', navigator.userAgent],
|
||
];
|
||
for(const [k,v] of rows){
|
||
const dt=document.createElement('dt'); dt.textContent=k;
|
||
const dd=document.createElement('dd'); dd.textContent=String(v??'—');
|
||
kv.append(dt,dd);
|
||
}
|
||
const badges = $('badges'); badges.innerHTML='';
|
||
const b = (t)=>{const s=document.createElement('span'); s.className='pill'; s.textContent=t; return s;};
|
||
badges.append(
|
||
b(protocol.toUpperCase().replace(':','')),
|
||
b(hostname),
|
||
b(port?(':'+port):(protocol==='https:'?':443':':80')),
|
||
b(/(curl|wget|httpie)/i.test(navigator.userAgent)?'CLI':'Browser')
|
||
);
|
||
}
|
||
|
||
// --- header parsing for IPs ---
|
||
function firstFromForwarded(val){
|
||
// Forwarded: for=1.2.3.4;proto=https;by=...
|
||
if(!val) return null;
|
||
const re=/for=(?:"|\[)?([0-9a-fA-F:.]+)(?:"|])?/g; let m;
|
||
while((m=re.exec(val))){ if(isIP(m[1])) return m[1]; }
|
||
return null;
|
||
}
|
||
function firstFromXFF(val){
|
||
if(!val) return null;
|
||
const ip = val.split(',')[0].trim().split(':')[0].trim();
|
||
return isIP(ip) ? ip : null;
|
||
}
|
||
function allIPs(val){
|
||
if(!val) return [];
|
||
const m = val.match(/([0-9a-fA-F:.]+)/g) || [];
|
||
return uniq(m.filter(isIP));
|
||
}
|
||
|
||
async function scanHeaders(){
|
||
const ipOut = $('ipList'); ipOut.innerHTML = '…';
|
||
const srcNote = $('srcNote'); srcNote.textContent = '';
|
||
const hdrTbl = $('hdrTable'); hdrTbl.innerHTML = '…';
|
||
|
||
let headersArray = [];
|
||
let found = [];
|
||
|
||
// HEAD derselben Seite
|
||
let headOk = false;
|
||
try{
|
||
const r = await fetch(location.href, { method:'HEAD', cache:'no-store' });
|
||
headOk = true;
|
||
// alle Header sammeln
|
||
r.headers.forEach((v,k)=>{ headersArray.push([k, v]); });
|
||
|
||
// Kandidaten prüfen
|
||
const candidates = [];
|
||
const h = (n)=>r.headers.get(n);
|
||
|
||
const fwd = h('forwarded');
|
||
const xff = h('x-forwarded-for');
|
||
const xri = h('x-real-ip');
|
||
const cf = h('cf-connecting-ip');
|
||
const tci = h('true-client-ip');
|
||
const xci = h('x-client-ip');
|
||
const fci = h('fastly-client-ip') || h('fly-client-ip');
|
||
|
||
if(firstFromForwarded(fwd)) candidates.push(['Forwarded', firstFromForwarded(fwd)]);
|
||
if(firstFromXFF(xff)) candidates.push(['X-Forwarded-For', firstFromXFF(xff)]);
|
||
if(isIP(xri)) candidates.push(['X-Real-IP', xri]);
|
||
if(isIP(cf)) candidates.push(['CF-Connecting-IP', cf]);
|
||
if(isIP(tci)) candidates.push(['True-Client-IP', tci]);
|
||
if(isIP(xci)) candidates.push(['X-Client-IP', xci]);
|
||
if(isIP(fci)) candidates.push(['Fastly/Fly', fci]);
|
||
|
||
found = candidates;
|
||
srcNote.textContent = candidates.length
|
||
? 'Quelle: Response-Header (same-origin)'
|
||
: '';
|
||
}catch(_){
|
||
srcNote.textContent = 'Quelle: HEAD fehlgeschlagen';
|
||
}
|
||
|
||
// Optionaler /ip Endpunkt, wenn keine Header-IP
|
||
if(found.length===0){
|
||
try{
|
||
const r = await fetch('/ip', { method:'GET', cache:'no-store' });
|
||
const text = await r.text();
|
||
let ip = null;
|
||
try{
|
||
const j = JSON.parse(text);
|
||
const v = j?.ip || j?.remote || j?.address;
|
||
if(typeof v === 'string' && isIP(v)) ip = v;
|
||
}catch{
|
||
ip = (text.match(/([0-9a-fA-F:.]+)/g)||[]).find(isIP) || null;
|
||
}
|
||
if(ip){
|
||
found = [['/ip', ip]];
|
||
srcNote.textContent = 'Quelle: /ip (same-origin)';
|
||
}
|
||
}catch(_){}
|
||
}
|
||
|
||
// Ausgabe IPs
|
||
ipOut.innerHTML = '';
|
||
if(found.length===0){
|
||
ipOut.append(addRow('keine IP gefunden', '—', 'warn'));
|
||
}else{
|
||
const map = new Map(); // ip -> Set(sources)
|
||
for(const [src, ip] of found){
|
||
if(!map.has(ip)) map.set(ip, new Set());
|
||
map.get(ip).add(src);
|
||
}
|
||
for(const [ip, srcs] of map.entries()){
|
||
ipOut.append(addRow(ip, Array.from(srcs).join(', '), 'ok'));
|
||
}
|
||
}
|
||
|
||
// Ausgabe aller Header
|
||
hdrTbl.innerHTML = '';
|
||
if(headOk && headersArray.length){
|
||
headersArray.sort((a,b)=>a[0].localeCompare(b[0]));
|
||
for(const [k,v] of headersArray){
|
||
const row = document.createElement('div'); row.className='tr';
|
||
const key = document.createElement('div'); key.className='key'; key.textContent=k;
|
||
const val = document.createElement('div'); val.className='val mono'; val.textContent=v;
|
||
row.append(key,val); hdrTbl.append(row);
|
||
}
|
||
}else{
|
||
const row = document.createElement('div'); row.className='tr';
|
||
const key = document.createElement('div'); key.className='key'; key.textContent='—';
|
||
const val = document.createElement('div'); val.className='val mono'; val.textContent='keine Header verfügbar';
|
||
row.append(key,val); hdrTbl.append(row);
|
||
}
|
||
}
|
||
|
||
function wire(){
|
||
$('rerun').addEventListener('click', scanHeaders);
|
||
$('copy').addEventListener('click', ()=>{
|
||
const items = Array.from(document.querySelectorAll('#ipList .item'));
|
||
const lines = items.map(i=>i.firstChild.textContent.trim());
|
||
if(lines.length) navigator.clipboard?.writeText(lines.join('\n')).catch(()=>{});
|
||
});
|
||
renderContext();
|
||
window.addEventListener('resize', renderContext);
|
||
scanHeaders();
|
||
}
|
||
|
||
wire();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|