Bild-Proxy (same-origin, defeats hotlink protection) + klareres Reset-Feedback

This commit is contained in:
2026-06-04 08:52:41 +00:00
parent 737e7021fd
commit bebe2353de
2 changed files with 40 additions and 5 deletions
+6 -5
View File
@@ -1,4 +1,4 @@
const C24 = u => u; // check24 proxy urls already absolute const IMG = u => '/img?u=' + encodeURIComponent(u); // route images through same-origin proxy
const REG = { const REG = {
kroatien:{flag:'🇭🇷',name:'Kroatien',color:'var(--kro)',mk:'#e8643c'}, kroatien:{flag:'🇭🇷',name:'Kroatien',color:'var(--kro)',mk:'#e8643c'},
kanaren:{flag:'🇮🇨',name:'Kanaren',color:'var(--kan)',mk:'#f1a23b'}, kanaren:{flag:'🇮🇨',name:'Kanaren',color:'var(--kan)',mk:'#f1a23b'},
@@ -119,7 +119,7 @@ let STATE = {votes:{}};
/* ---------- hero ---------- */ /* ---------- hero ---------- */
document.getElementById('herobg').style.backgroundImage = document.getElementById('herobg').style.backgroundImage =
"url('"+byId('bluesun').imgs[0]+"')"; "url('"+IMG(byId('bluesun').imgs[0])+"')";
/* ---------- voter selector ---------- */ /* ---------- voter selector ---------- */
const whoEl = document.getElementById('who'); const whoEl = document.getElementById('who');
@@ -166,7 +166,7 @@ function avgFor(id){
return {avg:n?sum/n:0,n,per}; return {avg:n?sum/n:0,n,per};
} }
function cardHTML(o){ function cardHTML(o){
const imgs=o.imgs.map((s,i)=>`<img src="${s}" class="${i===0?'on':''}" loading="lazy" alt="${o.name}" onerror="this.remove()">`).join(''); const imgs=o.imgs.map((s,i)=>`<img src="${IMG(s)}" class="${i===0?'on':''}" loading="lazy" alt="${o.name}" onerror="this.closest('.ph')&&this.remove()">`).join('');
const dots=o.imgs.length>1?`<div class="dots">${o.imgs.map((_,i)=>`<span class="dot ${i===0?'on':''}"></span>`).join('')}</div>`:''; const dots=o.imgs.length>1?`<div class="dots">${o.imgs.map((_,i)=>`<span class="dot ${i===0?'on':''}"></span>`).join('')}</div>`:'';
const nav=o.imgs.length>1?'<div class="nav l"></div><div class="nav r"></div>':''; const nav=o.imgs.length>1?'<div class="nav l"></div><div class="nav r"></div>':'';
const rate=o.rate?`<span class="crate"><b>${o.rate}</b> ${o.rlabel}${o.bew?(' · '+o.bew+' Bew.'):''}</span>`:(o.bew?`<span class="crate">${o.bew} Bewertungen</span>`:''); const rate=o.rate?`<span class="crate"><b>${o.rate}</b> ${o.rlabel}${o.bew?(' · '+o.bew+' Bew.'):''}</span>`:(o.bew?`<span class="crate">${o.bew} Bewertungen</span>`:'');
@@ -256,10 +256,11 @@ async function sendVote(option,stars){
try{const r=await fetch('/api/vote',{method:'POST',headers:{'Content-Type':'application/json'}, try{const r=await fetch('/api/vote',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({voter:me_,option,stars})});STATE=await r.json();renderVotes();}catch(e){} body:JSON.stringify({voter:me_,option,stars})});STATE=await r.json();renderVotes();}catch(e){}
} }
function flashSaved(){const s=document.getElementById('saved');s.classList.add('on');setTimeout(()=>s.classList.remove('on'),1400);} function flashSaved(msg){const s=document.getElementById('saved');s.textContent=msg||'✓ gespeichert';s.classList.add('on');setTimeout(()=>s.classList.remove('on'),1800);}
document.getElementById('resetBtn').onclick=async()=>{ document.getElementById('resetBtn').onclick=async()=>{
if(!confirm('Wirklich ALLE Bewertungen von Till, Lea und Astrid zurücksetzen?'))return; if(!confirm('Wirklich ALLE Bewertungen von Till, Lea und Astrid zurücksetzen?'))return;
try{const r=await fetch('/api/reset',{method:'POST'});STATE=await r.json();renderVotes();flashSaved();}catch(e){} try{const r=await fetch('/api/reset',{method:'POST'});STATE=await r.json();renderVotes();flashSaved('✓ Alle Bewertungen gelöscht');}
catch(e){alert('Zurücksetzen fehlgeschlagen bitte nochmal versuchen.');}
}; };
function renderAll(){renderWho();renderRecos();renderRegions();} function renderAll(){renderWho();renderRecos();renderRegions();}
+34
View File
@@ -30,6 +30,12 @@ function saveState(state) {
} }
let state = loadState(); let state = loadState();
// In-memory image cache for the proxy
const imgCache = new Map();
function hostAllowed(host) {
return /(^|\.)urlaub\.check24\.de$/.test(host) || host === 'files.ahoi-schiff.de';
}
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml',
'.png': 'image/png', '.jpg': 'image/jpeg', '.ico': 'image/x-icon', '.webmanifest': 'application/manifest+json' }; '.png': 'image/png', '.jpg': 'image/jpeg', '.ico': 'image/x-icon', '.webmanifest': 'application/manifest+json' };
@@ -72,6 +78,34 @@ const server = http.createServer(async (req, res) => {
} }
if (p === '/health') { return sendJSON(res, 200, { ok: true }); } if (p === '/health') { return sendJSON(res, 200, { ok: true }); }
// ---- Image proxy (defeats hotlink/referrer protection, same-origin = always loads) ----
if (p === '/img' && req.method === 'GET') {
const u = url.searchParams.get('u');
let host;
try { host = new URL(u).host; } catch (e) { res.writeHead(400); return res.end('bad url'); }
if (!hostAllowed(host)) { res.writeHead(403); return res.end('host not allowed'); }
if (imgCache.has(u)) {
const c = imgCache.get(u);
res.writeHead(200, { 'Content-Type': c.type, 'Cache-Control': 'public, max-age=604800' });
return res.end(c.buf);
}
try {
const referer = host.endsWith('ahoi-schiff.de') ? 'https://www.ahoi-schiff.de/' : 'https://urlaub.check24.de/';
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), 8000);
const r = await fetch(u, { signal: ac.signal, headers: {
'Referer': referer, 'User-Agent': 'Mozilla/5.0 (compatible; HeidrichReise/1.0)', 'Accept': 'image/*'
}});
clearTimeout(t);
if (!r.ok) { res.writeHead(502); return res.end('upstream ' + r.status); }
const type = r.headers.get('content-type') || 'image/jpeg';
const buf = Buffer.from(await r.arrayBuffer());
if (imgCache.size < 300) imgCache.set(u, { buf, type });
res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'public, max-age=604800' });
return res.end(buf);
} catch (e) { res.writeHead(502); return res.end('proxy error'); }
}
// ---- Static ---- // ---- Static ----
let file = p === '/' ? '/index.html' : decodeURIComponent(p); let file = p === '/' ? '/index.html' : decodeURIComponent(p);
const full = path.join(PUBLIC, path.normalize(file).replace(/^(\.\.[\/\\])+/, '')); const full = path.join(PUBLIC, path.normalize(file).replace(/^(\.\.[\/\\])+/, ''));