'use strict'; // Download a picture from a web address (for a page's cover). Runs in the main process, and has the same rule as // link previews: only public http(s) addresses, every redirect checked again (see linkpreview.cjs). const path = require('node:path'); const { allowed, hooks } = require('./linkpreview.cjs'); const MAX_BYTES = 50 * 1024 * 1024; const TIMEOUT = 25000; const MAX_REDIRECTS = 5; const EXT = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp', 'image/avif': 'avif', 'image/svg+xml': 'svg', 'image/bmp': 'bmp' }; const KNOWN = /\.(png|jpe?g|gif|webp|avif|svg|bmp)$/i; // A file name for it: the address's own (when it looks like one), else "cover" with the right extension. function nameFor(u, type) { let base = ''; try { base = decodeURIComponent(path.posix.basename(u.pathname)); } catch { base = path.posix.basename(u.pathname); } base = base.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '-').trim().slice(0, 80); if (KNOWN.test(base)) return base; const ext = EXT[type] || 'jpg'; return `${base && !base.startsWith('.') ? base.replace(/\.[a-z0-9]{1,5}$/i, '') : 'cover'}.${ext}`; } async function readLimited(res, max) { const chunks = []; let size = 0; const reader = res.body.getReader(); try { for (;;) { const { done, value } = await reader.read(); if (done) break; size += value.length; if (size > max) throw new Error('That image is too big (50 MB at most).'); chunks.push(Buffer.from(value)); } } finally { reader.cancel().catch(() => {}); } return Buffer.concat(chunks); } // -> { bytes: Buffer, name, type }. Throws an Error whose message is meant for the person. async function fetchImage(url, { fetch: doFetch, lookup } = {}) { const get = doFetch || hooks.fetch || globalThis.fetch; const look = lookup || hooks.lookup; const signal = AbortSignal.timeout(TIMEOUT); let u = await allowed(String(url || '').trim(), look); if (!u) throw new Error('That is not a public web address (http or https).'); for (let hop = 0; hop <= MAX_REDIRECTS; hop++) { let res; try { res = await get(u.href, { redirect: 'manual', signal, headers: { 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', accept: 'image/*,*/*;q=0.1' } }); } catch { throw new Error('Could not reach that address.'); } if (res.status >= 300 && res.status < 400) { const loc = res.headers.get('location'); const next = loc ? await allowed(new URL(loc, u).href, look) : null; if (!next) throw new Error('That address redirects somewhere that is not allowed.'); u = next; continue; } if (!res.ok) throw new Error(`The server answered ${res.status}.`); const type = String(res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase(); if (!type.startsWith('image/')) throw new Error('That address is not a picture.'); const len = Number(res.headers.get('content-length')); if (len > MAX_BYTES) throw new Error('That image is too big (50 MB at most).'); if (!res.body) throw new Error('That address sent nothing.'); const bytes = await readLimited(res, MAX_BYTES); if (!bytes.length) throw new Error('That address sent nothing.'); return { bytes, name: nameFor(u, type), type }; } throw new Error('Too many redirects.'); } module.exports = { fetchImage, nameFor };