Files
dickendock/backend/src/tools/paywallkiller/routes.js

156 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const router = express.Router();
const { authenticate } = require('../../middleware/auth');
const https = require('https');
const http = require('http');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
// ── Hilfsfunktion: folgt Redirects, gibt finale URL zurück ───────────────
function fetchFollowRedirects(url, redirectCount = 0) {
return new Promise((resolve, reject) => {
if (redirectCount > 8) return reject(new Error('Zu viele Redirects'));
const mod = url.startsWith('https') ? https : http;
const req = mod.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'de,en;q=0.5',
},
timeout: 12000,
}, (res) => {
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
let location = res.headers.location;
if (!location.startsWith('http')) {
try {
const base = new URL(url);
location = location.startsWith('/')
? `${base.protocol}//${base.host}${location}`
: `${base.protocol}//${base.host}/${location}`;
} catch { return reject(new Error('Ungültige Redirect-URL')); }
}
res.resume();
return fetchFollowRedirects(location, redirectCount + 1).then(resolve).catch(reject);
}
res.resume();
resolve({ statusCode: res.statusCode, finalUrl: url });
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
});
}
// ── Prüfe ob archive.is einen Snapshot hat ────────────────────────────────
// archive.is/newest/URL leitet auf archive.is/HASH weiter wenn ein Snapshot existiert.
// Wenn kein Snapshot → Redirect zurück auf archive.is/ (Root) oder /newest/... bleibt stehen.
async function checkArchive(archiveUrl) {
try {
const result = await fetchFollowRedirects(archiveUrl);
const final = result.finalUrl;
// Kein Redirect = nichts gefunden (blieb auf gleicher URL)
if (final === archiveUrl) return { found: false };
// Snapshot-URL hat die Form https://archive.is/ABCDE (alphanumeric hash, 4-8 Zeichen)
if (/https?:\/\/archive\.(is|ph|today|fo|li|vn|md|gg)\/[a-zA-Z0-9]{4,10}$/.test(final)) {
return { found: true, resolvedUrl: final };
}
// archive.is Root oder Submit-Seite = kein Treffer
if (/archive\.[a-z]+\/?$/.test(final)) return { found: false };
if (final.includes('/submit')) return { found: false };
// Sonst: irgendwohin weitergeleitet → als Treffer werten
return { found: true, resolvedUrl: final };
} catch {
return { found: false };
}
}
// ── POST /api/tools/paywallkiller/check ───────────────────────────────────
router.post('/check', authenticate, async (req, res) => {
const { url } = req.body;
if (!url || typeof url !== 'string') return res.status(400).json({ error: 'URL fehlt' });
let targetUrl = url.trim();
if (!targetUrl.startsWith('http')) targetUrl = 'https://' + targetUrl;
const newestUrl = `https://archive.is/newest/${targetUrl}`;
const oldestUrl = `https://archive.is/oldest/${targetUrl}`;
try {
const newest = await checkArchive(newestUrl);
if (newest.found) {
return res.json({ archiveUrl: newest.resolvedUrl || newestUrl, type: 'newest' });
}
const oldest = await checkArchive(oldestUrl);
if (oldest.found) {
return res.json({ archiveUrl: oldest.resolvedUrl || oldestUrl, type: 'oldest' });
}
return res.status(404).json({ error: 'Kein Archiv für diese URL gefunden' });
} catch (err) {
return res.status(500).json({ error: 'Fehler beim Prüfen: ' + err.message });
}
});
// ── POST /api/tools/paywallkiller/pdf ─────────────────────────────────────
router.post('/pdf', authenticate, async (req, res) => {
const { archiveUrl } = req.body;
if (!archiveUrl || typeof archiveUrl !== 'string') return res.status(400).json({ error: 'archiveUrl fehlt' });
if (!/^https?:\/\/archive\.(is|ph|today|fo|li|vn|md|gg)\//.test(archiveUrl)) {
return res.status(400).json({ error: 'Ungültige archive.is URL' });
}
let browser;
const tmpFile = path.join('/tmp', `pwk_${crypto.randomBytes(8).toString('hex')}.pdf`);
try {
let puppeteer, chromium;
try {
puppeteer = require('puppeteer-core');
chromium = require('@sparticuz/chromium');
} catch {
try {
puppeteer = require('puppeteer');
chromium = null;
} catch {
return res.status(500).json({ error: 'PDF-Rendering nicht verfügbar puppeteer nicht installiert' });
}
}
const launchOptions = chromium
? {
args: [...chromium.args, '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
defaultViewport: { width: 1280, height: 900 },
executablePath: process.env.CHROMIUM_PATH || await chromium.executablePath(),
headless: true,
}
: { headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] };
browser = await puppeteer.launch(launchOptions);
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
await page.goto(archiveUrl, { waitUntil: 'networkidle2', timeout: 30000 });
await page.pdf({
path: tmpFile,
format: 'A4',
printBackground: true,
margin: { top: '10mm', bottom: '10mm', left: '10mm', right: '10mm' },
});
await browser.close();
browser = null;
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="archiv_${Date.now()}.pdf"`);
const stream = fs.createReadStream(tmpFile);
stream.pipe(res);
stream.on('end', () => fs.unlink(tmpFile, () => {}));
stream.on('error', () => fs.unlink(tmpFile, () => {}));
} catch (err) {
if (browser) try { await browser.close(); } catch {}
fs.unlink(tmpFile, () => {});
return res.status(500).json({ error: 'PDF-Erstellung fehlgeschlagen: ' + err.message });
}
});
module.exports = router;