feat: Paywall-Killer Tool (archive.is + PDF-Export)
This commit is contained in:
156
backend/src/tools/paywallkiller/routes.js
Normal file
156
backend/src/tools/paywallkiller/routes.js
Normal file
@@ -0,0 +1,156 @@
|
||||
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: HTTP GET mit Follow-Redirect ────────────────────────────
|
||||
function fetchUrl(url, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const mod = url.startsWith('https') ? https : http;
|
||||
const req = mod.get(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; DickenDock/1.0)',
|
||||
...options.headers,
|
||||
},
|
||||
timeout: 10000,
|
||||
}, (res) => {
|
||||
// Redirects manuell folgen (max 5)
|
||||
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && (options.redirects || 0) < 5) {
|
||||
const location = res.headers.location.startsWith('http')
|
||||
? res.headers.location
|
||||
: new URL(res.headers.location, url).href;
|
||||
res.resume();
|
||||
return fetchUrl(location, { ...options, redirects: (options.redirects || 0) + 1 })
|
||||
.then(resolve).catch(reject);
|
||||
}
|
||||
resolve({ statusCode: res.statusCode, url, finalUrl: url, headers: res.headers, res });
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
||||
});
|
||||
}
|
||||
|
||||
// ── Prüfe ob archive.is-URL existiert (HEAD-ähnlich via GET + sofortiges Abbrechen) ──
|
||||
async function checkArchive(archiveUrl) {
|
||||
try {
|
||||
const result = await fetchUrl(archiveUrl);
|
||||
result.res.destroy();
|
||||
// archive.is gibt 200 zurück wenn Archiv existiert, 404 oder Redirect zu / wenn nicht
|
||||
if (result.statusCode === 200) return true;
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST /api/tools/paywallkiller/check ───────────────────────────────────
|
||||
// Body: { url: "https://..." }
|
||||
// Response: { archiveUrl: "https://archive.is/...", type: "newest"|"oldest" }
|
||||
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 newestOk = await checkArchive(newestUrl);
|
||||
if (newestOk) {
|
||||
return res.json({ archiveUrl: newestUrl, type: 'newest' });
|
||||
}
|
||||
const oldestOk = await checkArchive(oldestUrl);
|
||||
if (oldestOk) {
|
||||
return res.json({ archiveUrl: 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 ─────────────────────────────────────
|
||||
// Body: { archiveUrl: "https://archive.is/..." }
|
||||
// Response: PDF als Download-Stream, Datei danach gelöscht
|
||||
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 (!archiveUrl.startsWith('https://archive.is/')) 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;
|
||||
|
||||
// Versuche puppeteer-core + @sparticuz/chromium (Alpine-kompatibel)
|
||||
try {
|
||||
puppeteer = require('puppeteer-core');
|
||||
chromium = require('@sparticuz/chromium');
|
||||
} catch {
|
||||
// Fallback: normales puppeteer (falls installiert)
|
||||
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;
|
||||
|
||||
const filename = 'archiv_' + Date.now() + '.pdf';
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user