feat(clickTtHttpPageRoutes): enhance proxy functionality with session management and cookie handling

- Introduced session ID management to maintain user sessions across proxy requests, improving tracking and logging capabilities.
- Added functions to extract and format cookies, enabling proper cookie handling for requests and responses.
- Updated link and form action rewriting functions to include session IDs, ensuring seamless integration with the proxy.
- Enhanced the proxy GET and POST endpoints to manage cookies effectively, improving the overall robustness of the proxy interactions.
This commit is contained in:
Torsten Schulz (local)
2026-03-10 22:08:20 +01:00
parent cab06f9ad6
commit c5a88324c3

View File

@@ -4,12 +4,42 @@
*/
import express from 'express';
import crypto from 'crypto';
import clickTtHttpPageService from '../services/clickTtHttpPageService.js';
import HttpPageFetchLog from '../models/HttpPageFetchLog.js';
import { authenticate } from '../middleware/authMiddleware.js';
const router = express.Router();
// Cookie-Store pro Session (sid) Cookies vom Ursprungsserver werden hier gehalten und weitergeleitet
const cookieStore = new Map();
function extractCookies(setCookieHeaders) {
const cookies = {};
if (setCookieHeaders) {
(Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders]).forEach(cookie => {
const [nameValue] = String(cookie).split(';');
const eq = nameValue.indexOf('=');
if (eq > 0) {
const name = nameValue.substring(0, eq).trim();
const value = nameValue.substring(eq + 1).trim();
if (name) cookies[name] = value;
}
});
}
return cookies;
}
function formatCookies(cookies) {
return Object.entries(cookies)
.map(([name, value]) => `${name}=${value}`)
.join('; ');
}
function getOrCreateSid(req) {
return req.query.sid || crypto.randomBytes(16).toString('hex');
}
/** Domains, deren Links durch den Proxy umgeleitet werden (für Folge-Logs) */
const PROXY_DOMAINS = ['click-tt.de', 'httv.de'];
@@ -23,10 +53,18 @@ function shouldProxyUrl(href) {
return PROXY_DOMAINS.some(d => h.includes(d));
}
/**
* Baut die Proxy-URL inkl. Session-ID für Cookie-Weitergabe
*/
function buildProxyUrl(proxyBase, targetUrl, sid) {
const sep = proxyBase.includes('?') ? '&' : '?';
return `${proxyBase}${sep}url=${encodeURIComponent(targetUrl)}${sid ? `&sid=${sid}` : ''}`;
}
/**
* Schreibt Links im HTML um, sodass Klicks im iframe über unseren Proxy laufen (Folge-Logs).
*/
function rewriteLinksInHtml(html, proxyBaseUrl, pageBaseUrl) {
function rewriteLinksInHtml(html, proxyBaseUrl, pageBaseUrl, sid) {
if (!html || !proxyBaseUrl || !pageBaseUrl) return html;
try {
const base = new URL(pageBaseUrl);
@@ -38,7 +76,7 @@ function rewriteLinksInHtml(html, proxyBaseUrl, pageBaseUrl) {
absoluteUrl = new URL(href, base.origin + base.pathname).href;
}
if (!shouldProxyUrl(absoluteUrl)) return match;
const proxyUrl = `${proxyBaseUrl}${proxyBaseUrl.includes('?') ? '&' : '?'}url=${encodeURIComponent(absoluteUrl)}`;
const proxyUrl = buildProxyUrl(proxyBaseUrl, absoluteUrl, sid);
return `<a ${before}href="${proxyUrl}"${after}>`;
}
);
@@ -50,7 +88,7 @@ function rewriteLinksInHtml(html, proxyBaseUrl, pageBaseUrl) {
/**
* Schreibt Formular-Actions um, sodass Submissions über unseren Proxy laufen (Login etc. wird geloggt).
*/
function rewriteFormActionsInHtml(html, proxyBaseUrl, pageBaseUrl) {
function rewriteFormActionsInHtml(html, proxyBaseUrl, pageBaseUrl, sid) {
if (!html || !proxyBaseUrl || !pageBaseUrl) return html;
try {
const base = new URL(pageBaseUrl);
@@ -64,7 +102,7 @@ function rewriteFormActionsInHtml(html, proxyBaseUrl, pageBaseUrl) {
absoluteUrl = new URL(actionTrim, base.origin + base.pathname).href;
}
if (!shouldProxyUrl(absoluteUrl)) return match;
const proxyUrl = `${proxyBaseUrl}${proxyBaseUrl.includes('?') ? '&' : '?'}url=${encodeURIComponent(absoluteUrl)}`;
const proxyUrl = buildProxyUrl(proxyBaseUrl, absoluteUrl, sid);
return `<form ${before}action="${proxyUrl}"${after}>`;
}
);
@@ -82,6 +120,9 @@ function rewriteFormActionsInHtml(html, proxyBaseUrl, pageBaseUrl) {
*/
router.get('/proxy', async (req, res, next) => {
try {
const sid = getOrCreateSid(req);
const cookies = cookieStore.get(sid) || {};
const { type, association, championship, clubId, url } = req.query;
let targetUrl = null;
let fetchType = 'proxy';
@@ -121,8 +162,21 @@ router.get('/proxy', async (req, res, next) => {
championship: championship || null,
clubIdParam: clubId || null,
userId: null,
headers: Object.keys(cookies).length > 0 ? { Cookie: formatCookies(cookies) } : {},
});
const responseHeaders = result.headers;
if (responseHeaders) {
const raw = responseHeaders.raw?.();
const setCookies = raw?.['set-cookie'];
if (setCookies) {
const newCookies = extractCookies(setCookies);
if (Object.keys(newCookies).length > 0) {
cookieStore.set(sid, { ...cookies, ...newCookies });
}
}
}
let html = result.body;
// CSP/X-Frame-Options entfernen für iframe-Einbettung
html = (html || '')
@@ -142,8 +196,8 @@ router.get('/proxy', async (req, res, next) => {
const baseUrl = process.env.BACKEND_BASE_URL || process.env.BASE_URL
|| `${req.protocol || 'http'}://${req.get('host') || 'localhost:' + (process.env.PORT || 3005)}`;
const proxyBase = baseUrl.replace(/\/$/, '') + '/api/clicktt/proxy';
html = rewriteLinksInHtml(html, proxyBase, targetUrl);
html = rewriteFormActionsInHtml(html, proxyBase, targetUrl);
html = rewriteLinksInHtml(html, proxyBase, targetUrl, sid);
html = rewriteFormActionsInHtml(html, proxyBase, targetUrl, sid);
res.set({
'Content-Type': 'text/html; charset=utf-8',
@@ -168,13 +222,15 @@ router.get('/proxy', async (req, res, next) => {
*/
router.post('/proxy', async (req, res, next) => {
try {
const sid = getOrCreateSid(req);
const cookies = cookieStore.get(sid) || {};
const targetUrl = req.query.url;
if (!targetUrl || (!targetUrl.includes('click-tt.de') && !targetUrl.includes('httv.de'))) {
return res.status(400).send('<html><body><h1>Fehler</h1><p>Parameter url (click-tt.de oder httv.de) erforderlich.</p></body></html>');
}
const contentType = req.get('content-type') || 'application/x-www-form-urlencoded';
const cookie = req.get('cookie');
const body = typeof req.body === 'string' ? req.body : (req.body && Object.keys(req.body).length
? new URLSearchParams(req.body).toString()
: null);
@@ -186,7 +242,7 @@ router.post('/proxy', async (req, res, next) => {
body: body || undefined,
headers: {
'Content-Type': contentType,
...(cookie && { Cookie: cookie }),
...(Object.keys(cookies).length > 0 && { Cookie: formatCookies(cookies) }),
},
userId: null,
});
@@ -196,6 +252,10 @@ router.post('/proxy', async (req, res, next) => {
const raw = responseHeaders.raw?.();
const setCookies = raw?.['set-cookie'] ?? responseHeaders.get?.('set-cookie') ?? responseHeaders['set-cookie'];
if (setCookies) {
const newCookies = extractCookies(setCookies);
if (Object.keys(newCookies).length > 0) {
cookieStore.set(sid, { ...cookies, ...newCookies });
}
(Array.isArray(setCookies) ? setCookies : [setCookies]).forEach(c => res.append('Set-Cookie', c));
}
const location = responseHeaders.get?.('location') ?? responseHeaders['location'];
@@ -204,7 +264,7 @@ router.post('/proxy', async (req, res, next) => {
|| `${req.protocol || 'http'}://${req.get('host') || 'localhost:' + (process.env.PORT || 3005)}`;
const proxyBase = baseUrl.replace(/\/$/, '') + '/api/clicktt/proxy';
if (shouldProxyUrl(location)) {
res.redirect(302, `${proxyBase}?url=${encodeURIComponent(location)}`);
res.redirect(302, buildProxyUrl(proxyBase, location, sid));
return;
}
res.redirect(result.status, location);
@@ -212,6 +272,24 @@ router.post('/proxy', async (req, res, next) => {
}
}
let body = result.body;
const contentType = result.contentType || '';
if (body && contentType.includes('text/html')) {
const baseUrl = process.env.BACKEND_BASE_URL || process.env.BASE_URL
|| `${req.protocol || 'http'}://${req.get('host') || 'localhost:' + (process.env.PORT || 3005)}`;
const proxyBase = baseUrl.replace(/\/$/, '') + '/api/clicktt/proxy';
body = body
.replace(/<meta[^>]*http-equiv=["']content-security-policy["'][^>]*>/gi, '')
.replace(/<meta[^>]*http-equiv=["']x-frame-options["'][^>]*>/gi, '');
const pageOrigin = (() => { try { return new URL(targetUrl).origin + '/'; } catch { return null; } })();
if (pageOrigin) {
body = body.replace(/<base[^>]*>/gi, '');
body = body.replace(/<head([^>]*)>/i, `<head$1><base href="${pageOrigin}">`);
}
body = rewriteLinksInHtml(body, proxyBase, targetUrl, sid);
body = rewriteFormActionsInHtml(body, proxyBase, targetUrl, sid);
}
res.set({
'Content-Type': result.contentType || 'text/html; charset=utf-8',
'Access-Control-Allow-Origin': '*',
@@ -219,7 +297,7 @@ router.post('/proxy', async (req, res, next) => {
'Content-Security-Policy': 'frame-ancestors *;',
'Cache-Control': 'no-cache, no-store, must-revalidate',
});
res.status(result.status).send(result.body);
res.status(result.status).send(body);
} catch (error) {
console.error('ClickTT Proxy POST Fehler:', error);
res.status(500).send(