From c5a88324c3dc124032d501e58cab942d6087b1aa Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 10 Mar 2026 22:08:20 +0100 Subject: [PATCH] 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. --- backend/routes/clickTtHttpPageRoutes.js | 98 ++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/backend/routes/clickTtHttpPageRoutes.js b/backend/routes/clickTtHttpPageRoutes.js index 7372356a..21aee5df 100644 --- a/backend/routes/clickTtHttpPageRoutes.js +++ b/backend/routes/clickTtHttpPageRoutes.js @@ -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 ``; } ); @@ -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 `
`; } ); @@ -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('

Fehler

Parameter url (click-tt.de oder httv.de) erforderlich.

'); } 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(/]*http-equiv=["']content-security-policy["'][^>]*>/gi, '') + .replace(/]*http-equiv=["']x-frame-options["'][^>]*>/gi, ''); + const pageOrigin = (() => { try { return new URL(targetUrl).origin + '/'; } catch { return null; } })(); + if (pageOrigin) { + body = body.replace(/]*>/gi, ''); + body = body.replace(/]*)>/i, ``); + } + 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(