nginx und caddy hinzugefügt
This commit is contained in:
parent
7a0639dc38
commit
a02eb7d905
7 changed files with 625 additions and 0 deletions
31
docker-compose/nginx-proxy/docker-compose.yml
Normal file
31
docker-compose/nginx-proxy/docker-compose.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
services:
|
||||
reverseproxy:
|
||||
image: jwilder/nginx-proxy:latest
|
||||
container_name: reverseproxy
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DEFAULT_HOST: default.vhost
|
||||
ENABLE_IPV6: "true"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./zertifikate:/etc/nginx/certs:ro
|
||||
- ./vhost.d:/etc/nginx/vhost.d
|
||||
- ./html:/usr/share/nginx/html
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
networks:
|
||||
- reverseproxy_netz
|
||||
|
||||
letsencrypt-nginx-proxy-companion:
|
||||
image: jrcs/letsencrypt-nginx-proxy-companion
|
||||
volumes:
|
||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
||||
- './zertifikate:/etc/nginx/certs:rw'
|
||||
volumes_from:
|
||||
- reverseproxy
|
||||
|
||||
|
||||
networks:
|
||||
reverseproxy_netz:
|
||||
external: true
|
||||
4
docker-compose/nginx-proxy/vhost.d/vorlage
Normal file
4
docker-compose/nginx-proxy/vhost.d/vorlage
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
add_header X-Real-IP $remote_addr always;
|
||||
add_header X-Forwarded-For $proxy_add_x_forwarded_for always;
|
||||
|
||||
location = /ip { default_type text/plain; return 200 "$remote_addr\n"; }
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
add_header X-Real-IP $remote_addr always;
|
||||
add_header X-Forwarded-For $proxy_add_x_forwarded_for always;
|
||||
|
||||
location = /ip { default_type text/plain; return 200 "$remote_addr\n"; }
|
||||
17
docker-compose/webserver1/docker-compose.yml
Normal file
17
docker-compose/webserver1/docker-compose.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
networks:
|
||||
reverseproxy_netz:
|
||||
external: true
|
||||
|
||||
services:
|
||||
meinwebserver2:
|
||||
image: nginx
|
||||
expose:
|
||||
- 80
|
||||
networks:
|
||||
- reverseproxy_netz
|
||||
environment:
|
||||
VIRTUAL_HOST: webserver1.fobix.benbex.de
|
||||
VIRTUAL_PORT: "80"
|
||||
LETSENCRYPT_HOST: webserver1.fobix.benbex.de
|
||||
volumes:
|
||||
- ./html:/usr/share/nginx/html/
|
||||
276
docker-compose/webserver1/html/index.html
Normal file
276
docker-compose/webserver1/html/index.html
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<!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>
|
||||
|
||||
17
docker-compose/webserver2/docker-compose.yml
Normal file
17
docker-compose/webserver2/docker-compose.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
networks:
|
||||
reverseproxy_netz:
|
||||
external: true
|
||||
|
||||
services:
|
||||
meinwebserver2:
|
||||
image: nginx
|
||||
expose:
|
||||
- 80
|
||||
networks:
|
||||
- reverseproxy_netz
|
||||
environment:
|
||||
VIRTUAL_HOST: webserver2.fobix.benbex.de
|
||||
VIRTUAL_PORT: "80"
|
||||
LETSENCRYPT_HOST: webserver2.fobix.benbex.de
|
||||
volumes:
|
||||
- ./html:/usr/share/nginx/html/
|
||||
276
docker-compose/webserver2/html/index.html
Normal file
276
docker-compose/webserver2/html/index.html
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<!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>
|
||||
|
||||
Loading…
Add table
Reference in a new issue