const http = require("http"); const { buildPdfHtml, buildComparisonPdfHtml } = require("../pdf-export/templates"); const { renderHtmlToPdf, pdfCacheGet, pdfCacheSet } = require("../pdf-export/renderer"); const PORT = process.env.PORT || 3101; // Fetch JSON from an internal endpoint on this same process (loopback) -- // re-triggers the full HTTP/caching/rate-limit path for /api/validate, // /api/aspa, /api/lookup rather than calling their handler logic directly. async function fetchInternal(path) { return new Promise((resolve, reject) => { const opts = { hostname: '127.0.0.1', port: PORT, path, method: 'GET' }; const req2 = http.request(opts, res2 => { let body = ''; res2.on('data', c => body += c); res2.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { reject(new Error('JSON parse failed: ' + body.slice(0, 200))); } }); }); req2.on('error', reject); req2.setTimeout(30000, () => { req2.destroy(); reject(new Error('Internal request timeout')); }); req2.end(); }); } // GET /api/export/pdf?asn=X&format=report|executive|technical async function handleExportPdf(req, res, url) { const rawAsn = (url.searchParams.get('asn') || '').replace(/[^0-9]/g, ''); const format = ['executive', 'technical', 'report'].includes(url.searchParams.get('format')) ? url.searchParams.get('format') : 'report'; if (!rawAsn) { res.writeHead(400, {'Content-Type':'application/json'}); return res.end(JSON.stringify({ error: 'Missing asn parameter' })); } const cacheKey = `pdf:${rawAsn}:${format}`; const cached = pdfCacheGet(cacheKey); if (cached) { res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${rawAsn}-${format}.pdf"`, 'X-Cache': 'HIT', 'Content-Length': cached.length }); return res.end(cached); } try { const [validateData, aspaData] = await Promise.all([ fetchInternal(`/api/validate?asn=${rawAsn}`), fetchInternal(`/api/aspa?asn=${rawAsn}`).catch(() => null), ]); if (validateData.error) { res.writeHead(400, {'Content-Type':'application/json'}); return res.end(JSON.stringify({ error: validateData.error })); } const html = buildPdfHtml(validateData, aspaData, format); const pdfBuf = await renderHtmlToPdf(html); pdfCacheSet(cacheKey, pdfBuf); res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${rawAsn}-${format}.pdf"`, 'Content-Length': pdfBuf.length }); return res.end(pdfBuf); } catch (err) { console.error('[PDF] Error:', err.message); res.writeHead(503, {'Content-Type':'application/json'}); return res.end(JSON.stringify({ error: 'PDF generation failed', message: err.message })); } } // GET /api/export/pdf/compare?asn1=X&asn2=Y async function handleExportPdfCompare(req, res, url) { const asn1 = (url.searchParams.get('asn1') || '').replace(/[^0-9]/g, ''); const asn2 = (url.searchParams.get('asn2') || '').replace(/[^0-9]/g, ''); if (!asn1 || !asn2) { res.writeHead(400, {'Content-Type':'application/json'}); return res.end(JSON.stringify({ error: 'Missing asn1 or asn2 parameter' })); } if (asn1 === asn2) { res.writeHead(400, {'Content-Type':'application/json'}); return res.end(JSON.stringify({ error: 'asn1 and asn2 must be different' })); } const cacheKey = `pdf:compare:${Math.min(+asn1,+asn2)}:${Math.max(+asn1,+asn2)}`; const cached = pdfCacheGet(cacheKey); if (cached) { res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${asn1}-vs-AS${asn2}.pdf"`, 'X-Cache': 'HIT', 'Content-Length': cached.length }); return res.end(cached); } try { const [v1, a1, l1, v2, a2, l2] = await Promise.all([ fetchInternal(`/api/validate?asn=${asn1}`), fetchInternal(`/api/aspa?asn=${asn1}`).catch(() => null), fetchInternal(`/api/lookup?asn=${asn1}`).catch(() => null), fetchInternal(`/api/validate?asn=${asn2}`), fetchInternal(`/api/aspa?asn=${asn2}`).catch(() => null), fetchInternal(`/api/lookup?asn=${asn2}`).catch(() => null), ]); if (v1.error || v2.error) { res.writeHead(400, {'Content-Type':'application/json'}); return res.end(JSON.stringify({ error: v1.error || v2.error })); } const html = buildComparisonPdfHtml(v1, a1, l1, v2, a2, l2); const pdfBuf = await renderHtmlToPdf(html); pdfCacheSet(cacheKey, pdfBuf); res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${asn1}-vs-AS${asn2}.pdf"`, 'Content-Length': pdfBuf.length }); return res.end(pdfBuf); } catch (err) { console.error('[PDF] Error:', err.message); res.writeHead(503, {'Content-Type':'application/json'}); return res.end(JSON.stringify({ error: 'PDF generation failed', message: err.message })); } } // CORS preflight for PDF export (already handled globally above, belt-and-suspenders) function handleExportOptions(req, res) { res.writeHead(204); return res.end(); } module.exports = { handleExportPdf, handleExportPdfCompare, handleExportOptions };