diff --git a/server.js b/server.js index cf78c22..7fd0d5c 100644 --- a/server.js +++ b/server.js @@ -37,8 +37,27 @@ let state = loadState(); // ---------- Image proxy with retry + memory + disk cache ---------- const imgCache = new Map(); +const UPSTREAM_HOSTS = [ + /(^|\.)travelapi\.com$/, /(^|\.)hotelbeds\.com$/, /(^|\.)paximum\.com$/, + /(^|\.)hotelston\.com$/, /(^|\.)sunhotels\.net$/, /(^|\.)giatamedia\.com$/, + /(^|\.)tui\.com$/, /(^|\.)bstatic\.com$/, /(^|\.)dnatatravel\.com$/ +]; +function isCheck24(host) { return /(^|\.)urlaub\.check24\.de$/.test(host); } function hostAllowed(host) { - return /(^|\.)urlaub\.check24\.de$/.test(host) || host === 'files.ahoi-schiff.de'; + return isCheck24(host) || host === 'files.ahoi-schiff.de' || UPSTREAM_HOSTS.some(re => re.test(host)); +} +// Check24 wraps the real image URL (base64) inside its CDN path. Their CDN blocks +// server-side hotlinking, but the underlying source CDNs (travelapi, hotelbeds, …) +// serve freely — so we unwrap and fetch the original directly. +function unwrap(u) { + try { + const m = u.match(/source=([^!\/]+)/); + if (m) { + const real = Buffer.from(m[1].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'); + if (/^https?:\/\//.test(real)) return real; + } + } catch (e) {} + return u; } const keyFor = u => crypto.createHash('sha1').update(u).digest('hex'); function diskGet(u) { @@ -52,14 +71,17 @@ function diskPut(u, buf, type) { try { fs.writeFileSync(f, buf); fs.writeFileSync(f + '.t', type); } catch (e) {} } async function fetchImg(u, tries = 3) { - let host; try { host = new URL(u).host; } catch (e) { return null; } - const referer = host.endsWith('ahoi-schiff.de') ? 'https://www.ahoi-schiff.de/' : 'https://urlaub.check24.de/'; + let host0; try { host0 = new URL(u).host; } catch (e) { return null; } + // If it's a Check24 CDN wrapper, fetch the original source URL directly. + const target = isCheck24(host0) ? unwrap(u) : u; + let thost; try { thost = new URL(target).host; } catch (e) { thost = host0; } + const referer = thost.endsWith('ahoi-schiff.de') ? 'https://www.ahoi-schiff.de/' : ('https://' + thost + '/'); for (let i = 0; i < tries; i++) { try { const ac = new AbortController(); const t = setTimeout(() => ac.abort(), 9000); - const r = await fetch(u, { signal: ac.signal, headers: { - 'Referer': referer, 'User-Agent': 'Mozilla/5.0 (compatible; HeidrichReise/1.0)', 'Accept': 'image/*' + const r = await fetch(target, { signal: ac.signal, headers: { + 'Referer': referer, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', 'Accept': 'image/avif,image/webp,image/*,*/*' }}); clearTimeout(t); if (r.ok) return { buf: Buffer.from(await r.arrayBuffer()), type: r.headers.get('content-type') || 'image/jpeg' };