From 4f3a1829ca65ae56c9a5c0d112812990e6b85f21 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 10 Mar 2026 22:15:37 +0100 Subject: [PATCH] feat(clickTtHttpPageRoutes): add meta-refresh URL rewriting for proxy handling - Implemented a new function to rewrite meta-refresh URLs in HTML, ensuring redirects after login are routed through the proxy while maintaining session integrity. - Updated the proxy GET and POST endpoints to include the new meta-refresh handling, enhancing the overall functionality of the proxy interactions. - Improved error handling in the new function to ensure robustness in processing HTML content. --- backend/routes/clickTtHttpPageRoutes.js | 42 +++++++++++++++++++++- backend/services/clickTtHttpPageService.js | 11 +++++- frontend/src/views/ClickTtView.vue | 3 ++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/backend/routes/clickTtHttpPageRoutes.js b/backend/routes/clickTtHttpPageRoutes.js index bfb5f030..48cfa031 100644 --- a/backend/routes/clickTtHttpPageRoutes.js +++ b/backend/routes/clickTtHttpPageRoutes.js @@ -85,6 +85,38 @@ function rewriteLinksInHtml(html, proxyBaseUrl, pageBaseUrl, sid) { } } +/** + * Schreibt meta-refresh-URLs um, damit Redirects nach Login über unseren Proxy laufen (Session bleibt erhalten). + */ +function rewriteMetaRefreshInHtml(html, proxyBaseUrl, pageBaseUrl, sid) { + if (!html || !proxyBaseUrl || !pageBaseUrl || !sid) return html; + try { + const base = new URL(pageBaseUrl); + return html.replace( + /]*(?:http-equiv\s*=\s*["']refresh["'][^>]*content\s*=\s*["']([^"']+)["']|content\s*=\s*["']([^"']+)["'][^>]*http-equiv\s*=\s*["']refresh["'])[^>]*>/gi, + (match, content1, content2) => { + const content = content1 || content2; + if (!content) return match; + const urlMatch = content.match(/^\s*\d+\s*;\s*url\s*=\s*(.+)$/i); + if (!urlMatch) return match; + let targetUrl = urlMatch[1].trim(); + if ((targetUrl.startsWith("'") && targetUrl.endsWith("'")) || (targetUrl.startsWith('"') && targetUrl.endsWith('"'))) { + targetUrl = targetUrl.slice(1, -1); + } + if (targetUrl.startsWith('/') || !targetUrl.startsWith('http')) { + targetUrl = new URL(targetUrl, base.origin + base.pathname).href; + } + if (!shouldProxyUrl(targetUrl)) return match; + const proxyUrl = buildProxyUrl(proxyBaseUrl, targetUrl, sid); + const newContent = content.replace(/url\s*=\s*.+$/i, `url=${proxyUrl}`); + return match.replace(content, newContent); + } + ); + } catch { + return html; + } +} + /** * Schreibt Formular-Actions um, sodass Submissions über unseren Proxy laufen (Login etc. wird geloggt). */ @@ -198,6 +230,7 @@ router.get('/proxy', async (req, res, next) => { const proxyBase = baseUrl.replace(/\/$/, '') + '/api/clicktt/proxy'; html = rewriteLinksInHtml(html, proxyBase, targetUrl, sid); html = rewriteFormActionsInHtml(html, proxyBase, targetUrl, sid); + html = rewriteMetaRefreshInHtml(html, proxyBase, targetUrl, sid); res.set({ 'Content-Type': 'text/html; charset=utf-8', @@ -258,8 +291,14 @@ router.post('/proxy', async (req, res, next) => { } (Array.isArray(setCookies) ? setCookies : [setCookies]).forEach(c => res.append('Set-Cookie', c)); } - const location = responseHeaders.get?.('location') ?? responseHeaders['location']; + let location = responseHeaders.get?.('location') ?? responseHeaders['location']; if (location && (result.status >= 301 && result.status <= 308)) { + if (location.startsWith('/')) { + try { + const base = new URL(targetUrl); + location = base.origin + location; + } catch { /* ignore */ } + } 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'; @@ -288,6 +327,7 @@ router.post('/proxy', async (req, res, next) => { } responseBody = rewriteLinksInHtml(responseBody, proxyBase, targetUrl, sid); responseBody = rewriteFormActionsInHtml(responseBody, proxyBase, targetUrl, sid); + responseBody = rewriteMetaRefreshInHtml(responseBody, proxyBase, targetUrl, sid); } res.set({ diff --git a/backend/services/clickTtHttpPageService.js b/backend/services/clickTtHttpPageService.js index 3a439b39..9985f084 100644 --- a/backend/services/clickTtHttpPageService.js +++ b/backend/services/clickTtHttpPageService.js @@ -106,15 +106,24 @@ async function fetchWithLogging(options) { const baseDomain = extractBaseDomain(url); const startTime = Date.now(); + let origin; + try { origin = new URL(url).origin + '/'; } catch { origin = 'https://httv.click-tt.de/'; } 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', + 'Referer': origin, + 'Origin': origin.replace(/\/$/, ''), ...customHeaders, }; try { - const fetchOpts = { method, headers, timeout: 30000 }; + const fetchOpts = { + method, + headers, + timeout: 30000, + redirect: method === 'POST' ? 'manual' : 'follow', + }; if (body && (method === 'POST' || method === 'PUT')) { fetchOpts.body = body; } diff --git a/frontend/src/views/ClickTtView.vue b/frontend/src/views/ClickTtView.vue index 3735523a..fa1d825e 100644 --- a/frontend/src/views/ClickTtView.vue +++ b/frontend/src/views/ClickTtView.vue @@ -97,6 +97,7 @@ export default { const clubId = ref(''); const directUrl = ref(''); const iframeSrc = ref(''); + const sessionId = ref(''); const canLoad = computed(() => { if (pageType.value === 'url') { @@ -124,11 +125,13 @@ export default { params.set('clubId', clubId.value.trim()); } } + if (sessionId.value) params.set('sid', sessionId.value); return `${base}?${params.toString()}`; } function loadPage() { if (!canLoad.value) return; + sessionId.value = crypto.randomUUID?.() || `s${Date.now()}-${Math.random().toString(36).slice(2)}`; iframeSrc.value = buildProxyUrl(); }