docker-fobi/docker-compose/webserver2/html/index.html
2025-11-03 14:22:31 +01:00

276 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>