57 lines
1.7 KiB
TypeScript
57 lines
1.7 KiB
TypeScript
import { Router } from 'express';
|
|
import { resolveAndValidateHost, safeFetch } from './ssrf-guard';
|
|
|
|
const router = Router();
|
|
|
|
router.get('/image-proxy', async (req, res) => {
|
|
try {
|
|
const url = String(req.query.url || '');
|
|
|
|
if (!/^https?:\/\//i.test(url)) {
|
|
return res.status(400).json({ error: 'Invalid URL' });
|
|
}
|
|
|
|
const hostAllowed = await resolveAndValidateHost(url);
|
|
|
|
if (!hostAllowed) {
|
|
return res.status(400).json({ error: 'URL resolves to a blocked address' });
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
const response = await safeFetch(url, { signal: controller.signal });
|
|
|
|
clearTimeout(timeout);
|
|
|
|
if (!response || !response.ok) {
|
|
return res.status(response?.status ?? 502).end();
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type') || '';
|
|
|
|
if (!contentType.toLowerCase().startsWith('image/')) {
|
|
return res.status(415).json({ error: 'Unsupported content type' });
|
|
}
|
|
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
const MAX_BYTES = 8 * 1024 * 1024;
|
|
|
|
if (arrayBuffer.byteLength > MAX_BYTES) {
|
|
return res.status(413).json({ error: 'Image too large' });
|
|
}
|
|
|
|
res.setHeader('Content-Type', contentType);
|
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
res.send(Buffer.from(arrayBuffer));
|
|
} catch (err) {
|
|
if ((err as { name?: string })?.name === 'AbortError') {
|
|
return res.status(504).json({ error: 'Timeout fetching image' });
|
|
}
|
|
|
|
console.error('Image proxy error:', err);
|
|
res.status(502).json({ error: 'Failed to fetch image' });
|
|
}
|
|
});
|
|
|
|
export default router;
|