diff --git a/backend/routes/clickTtHttpPageRoutes.js b/backend/routes/clickTtHttpPageRoutes.js index 25f606a7..7372356a 100644 --- a/backend/routes/clickTtHttpPageRoutes.js +++ b/backend/routes/clickTtHttpPageRoutes.js @@ -47,6 +47,32 @@ function rewriteLinksInHtml(html, proxyBaseUrl, pageBaseUrl) { } } +/** + * Schreibt Formular-Actions um, sodass Submissions über unseren Proxy laufen (Login etc. wird geloggt). + */ +function rewriteFormActionsInHtml(html, proxyBaseUrl, pageBaseUrl) { + if (!html || !proxyBaseUrl || !pageBaseUrl) return html; + try { + const base = new URL(pageBaseUrl); + return html.replace( + /]*?)action\s*=\s*["']([^"']*)["']([^>]*)>/gi, + (match, before, action, after) => { + const actionTrim = action.trim(); + if (!actionTrim) return match; // action="" = same URL + let absoluteUrl = actionTrim; + if (actionTrim.startsWith('/') || !actionTrim.startsWith('http')) { + absoluteUrl = new URL(actionTrim, base.origin + base.pathname).href; + } + if (!shouldProxyUrl(absoluteUrl)) return match; + const proxyUrl = `${proxyBaseUrl}${proxyBaseUrl.includes('?') ? '&' : '?'}url=${encodeURIComponent(absoluteUrl)}`; + return `
`; + } + ); + } catch { + return html; + } +} + /** * GET /api/clicktt/proxy * Proxy für iframe-Einbettung – liefert HTML direkt (ohne Auth, für iframe src). @@ -117,6 +143,7 @@ router.get('/proxy', async (req, res, next) => { || `${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); res.set({ 'Content-Type': 'text/html; charset=utf-8', @@ -134,6 +161,73 @@ router.get('/proxy', async (req, res, next) => { } }); +/** + * POST /api/clicktt/proxy + * Formular-Weiterleitung (Login etc.) – POST wird an Ziel-URL weitergeleitet und geloggt. + * Query: url (Ziel-URL, erforderlich) + */ +router.post('/proxy', async (req, res, next) => { + try { + 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); + + const result = await clickTtHttpPageService.fetchWithLogging({ + url: targetUrl, + fetchType: 'formPost', + method: 'POST', + body: body || undefined, + headers: { + 'Content-Type': contentType, + ...(cookie && { Cookie: cookie }), + }, + userId: null, + }); + + const responseHeaders = result.headers; + if (responseHeaders) { + const raw = responseHeaders.raw?.(); + const setCookies = raw?.['set-cookie'] ?? responseHeaders.get?.('set-cookie') ?? responseHeaders['set-cookie']; + if (setCookies) { + (Array.isArray(setCookies) ? setCookies : [setCookies]).forEach(c => res.append('Set-Cookie', c)); + } + const location = responseHeaders.get?.('location') ?? responseHeaders['location']; + if (location && (result.status >= 301 && result.status <= 308)) { + 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'; + if (shouldProxyUrl(location)) { + res.redirect(302, `${proxyBase}?url=${encodeURIComponent(location)}`); + return; + } + res.redirect(result.status, location); + return; + } + } + + res.set({ + 'Content-Type': result.contentType || 'text/html; charset=utf-8', + 'Access-Control-Allow-Origin': '*', + 'X-Frame-Options': 'ALLOWALL', + 'Content-Security-Policy': 'frame-ancestors *;', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + }); + res.status(result.status).send(result.body); + } catch (error) { + console.error('ClickTT Proxy POST Fehler:', error); + res.status(500).send( + `

Fehler beim Senden

${String(error.message)}

` + ); + } +}); + /** * GET /api/clicktt/league-page * Ruft die Ligenübersicht ab (leaguePage) diff --git a/backend/services/clickTtHttpPageService.js b/backend/services/clickTtHttpPageService.js index fbb8061c..3a439b39 100644 --- a/backend/services/clickTtHttpPageService.js +++ b/backend/services/clickTtHttpPageService.js @@ -99,27 +99,32 @@ async function fetchWithLogging(options) { clubIdParam = null, userId = null, method = 'GET', + body = null, + headers: customHeaders = {}, } = options; const baseDomain = extractBaseDomain(url); const startTime = Date.now(); + const headers = { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) 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,image/webp,*/*;q=0.8', + 'Accept-Language': 'de,en-US;q=0.7,en;q=0.3', + ...customHeaders, + }; + try { - const response = await fetch(url, { - method, - headers: { - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) 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,image/webp,*/*;q=0.8', - 'Accept-Language': 'de,en-US;q=0.7,en;q=0.3', - }, - timeout: 30000, - }); + const fetchOpts = { method, headers, timeout: 30000 }; + if (body && (method === 'POST' || method === 'PUT')) { + fetchOpts.body = body; + } + const response = await fetch(url, fetchOpts); const executionTimeMs = Date.now() - startTime; const contentType = response.headers.get('content-type') || null; - const body = await response.text(); + const responseBody = await response.text(); const success = response.ok; - const snippet = createResponseSnippet(body); + const snippet = createResponseSnippet(responseBody); await HttpPageFetchLog.create({ userId, @@ -143,8 +148,9 @@ async function fetchWithLogging(options) { success, status: response.status, contentType, - body, + body: responseBody, executionTimeMs, + headers: response.headers, }; } catch (error) { const executionTimeMs = Date.now() - startTime;