diff --git a/backend/src/routes/proxy.js b/backend/src/routes/proxy.js index 79eb80b..15400f5 100644 --- a/backend/src/routes/proxy.js +++ b/backend/src/routes/proxy.js @@ -7,8 +7,6 @@ const qs = require('qs'); const proxyHandler = Helpers.wrapAsync(async (req, res) => { let targetUrl = req.query.url; - // 1. Handle additional query parameters - // If we have ?url=https://site.com&q=foo, we want to fetch https://site.com?q=foo if (targetUrl) { try { const urlObj = new URL(targetUrl); @@ -18,26 +16,29 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { } }); targetUrl = urlObj.href; - } catch (e) { - // Might be relative, handle below - } + } catch (e) {} } - // 2. Handle Path-based URLs (e.g. /api/proxy/https://google.com) + // Improved Path-based and Referer detection if (!targetUrl || !targetUrl.startsWith('http')) { const originalUrl = req.originalUrl; - const marker = '/api/proxy/'; - if (originalUrl.includes(marker)) { - const remainder = originalUrl.split(marker)[1]; + + // Check if URL is in the path directly (e.g. /api/proxy/https://...) + const pathMarker = '/api/proxy/'; + if (originalUrl.includes(pathMarker)) { + const parts = originalUrl.split(pathMarker); + const remainder = parts[1]; if (remainder && remainder.startsWith('http')) { targetUrl = remainder; - } else if (remainder && req.headers.referer) { - // 3. Smart Path Resolution via Referer + } else if (req.headers.referer && req.headers.referer.includes('url=')) { + // Smart reconstruction from Referer try { const refUrl = new URL(req.headers.referer); const parentTarget = refUrl.searchParams.get('url'); if (parentTarget) { - targetUrl = new URL(remainder, parentTarget).href; + // Reconstruct: parent origin + current query/path + const currentPath = req.url.replace('/api/proxy', ''); + targetUrl = new URL(currentPath, parentTarget).href; } } catch (e) {} } @@ -45,29 +46,11 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { } if (!targetUrl || !targetUrl.startsWith('http')) { - console.warn('[Proxy] No valid target URL found for:', req.originalUrl); - // If we can't find a URL, but we have a referer, maybe the browser followed a relative link - // that the proxy didn't catch. Try to redirect them back into the proxy. - if (req.headers.referer && req.headers.referer.includes('/api/proxy')) { - try { - const refUrl = new URL(req.headers.referer); - const parentTarget = refUrl.searchParams.get('url'); - if (parentTarget) { - const absoluteUrl = new URL(req.url.replace('/api/proxy', ''), parentTarget).href; - return res.redirect(`/api/proxy?url=${encodeURIComponent(absoluteUrl)}`); - } - } catch (e) {} - } - return res.status(400).send(`

URL Required

The stealth proxy needs a target URL to work.

-
- ${req.originalUrl} -
-

Try entering a full URL in the address bar, e.g., https://google.com

- +
`); } @@ -76,9 +59,8 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { const method = req.method; const headers = {}; - // Essential headers to forward const forwardHeaders = [ - 'accept', 'accept-language', 'accept-encoding', 'user-agent', 'content-type', 'range', 'authorization' + 'accept', 'accept-language', 'accept-encoding', 'user-agent', 'content-type', 'range', 'authorization', 'x-requested-with' ]; forwardHeaders.forEach(h => { @@ -87,17 +69,13 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { } }); - // Ensure a realistic User-Agent if (!headers['user-agent']) { - headers['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'; + headers['user-agent'] = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1'; } - // Strip Referer and Origin to avoid target site security blocks delete headers['referer']; delete headers['origin']; - console.log(`[Proxy] ${method} -> ${targetUrl}`); - const axiosConfig = { url: targetUrl, method: method, @@ -121,13 +99,11 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { const finalUrl = response.request.res.responseUrl || targetUrl; const contentType = response.headers['content-type'] || ''; - // Handle Redirects manually if axios didn't (though maxRedirects is 10) if (response.status >= 300 && response.status < 400 && response.headers.location) { const redirUrl = new URL(response.headers.location, finalUrl).href; return res.redirect(`/api/proxy?url=${encodeURIComponent(redirUrl)}`); } - // Forward headers from target, stripping security ones Object.keys(response.headers).forEach(key => { const lowerKey = key.toLowerCase(); if (![ @@ -146,7 +122,6 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { } }); - // Force CORS for the proxy res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); @@ -159,9 +134,7 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { try { const parsedUrl = new URL(finalUrl); origin = parsedUrl.origin; - } catch (e) { - origin = targetUrl; - } + } catch (e) { origin = targetUrl; } const headInjection = ` @@ -169,19 +142,65 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { (function() { try { var PROXY_BASE = window.location.origin + "/api/proxy?url="; + var TARGET_ORIGIN = "${origin}"; + var FINAL_URL = "${finalUrl}"; function toProxyUrl(url) { if (!url || typeof url !== 'string') return url; if (url.startsWith('javascript:') || url.startsWith('data:') || url.startsWith('mailto:') || url.startsWith('tel:')) return url; + if (url.startsWith(window.location.origin + "/api/proxy")) return url; try { - var absUrl = new URL(url, location.href).href; - // Don't proxy if already proxied - if (absUrl.includes("/api/proxy?url=")) return url; + var absUrl = new URL(url, FINAL_URL).href; return PROXY_BASE + encodeURIComponent(absUrl); } catch(e) { return url; } } - // Intercept window.open + function notifyParent() { + try { + window.parent.postMessage({ + type: 'zen-browser-url-change', + url: window.location.href, + title: document.title + }, '*'); + } catch(e) {} + } + + var originalFetch = window.fetch; + window.fetch = function(input, init) { + if (typeof input === 'string') { + input = toProxyUrl(input); + } else if (input instanceof Request) { + input = new Request(toProxyUrl(input.url), input); + } + return originalFetch(input, init); + }; + + var originalOpenXHR = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function(method, url) { + return originalOpenXHR.apply(this, [method, toProxyUrl(url), true]); + }; + + if (navigator.sendBeacon) { + var originalSendBeacon = navigator.sendBeacon; + navigator.sendBeacon = function(url, data) { + return originalSendBeacon.apply(this, [toProxyUrl(url), data]); + }; + } + + var originalPushState = history.pushState; + history.pushState = function(state, title, url) { + var res = originalPushState.apply(this, arguments); + notifyParent(); + return res; + }; + var originalReplaceState = history.replaceState; + history.replaceState = function() { + var res = originalReplaceState.apply(this, arguments); + notifyParent(); + return res; + }; + window.addEventListener('popstate', notifyParent); + var originalOpen = window.open; window.open = function(url, name, specs) { if (!url) return originalOpen(url, name, specs); @@ -189,35 +208,39 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { return null; }; - // Frame busting protection Object.defineProperty(window, 'top', { get: function() { return window.self; } }); Object.defineProperty(window, 'parent', { get: function() { return window.self; } }); - // Intercept location changes var originalReplace = window.location.replace; window.location.replace = function(url) { window.location.href = toProxyUrl(url); }; - // Intercept form submissions window.addEventListener('submit', function(e) { var form = e.target; - if (form.action && !form.action.includes("/api/proxy")) { + if (form.method.toLowerCase() === 'get') { + // For GET forms, we want to stay in the proxy. + // If we change the action to /api/proxy?url=target, the browser will append ?field=val + // resulting in /api/proxy?field=val (losing the url parameter). + // So we add a hidden input for the 'url'. + var action = form.getAttribute('action') || ''; + var absoluteAction = new URL(action, FINAL_URL).href; + + // Clean up existing url inputs + form.querySelectorAll('input[name="url"]').forEach(el => el.remove()); + + var urlInput = document.createElement('input'); + urlInput.type = 'hidden'; + urlInput.name = 'url'; + urlInput.value = absoluteAction; + form.appendChild(urlInput); + + form.action = window.location.origin + "/api/proxy"; + } else if (form.action && !form.action.includes("/api/proxy")) { form.action = toProxyUrl(form.action); } }, true); - // Intercept all link clicks (dynamic links) - window.addEventListener('click', function(e) { - var target = e.target.closest('a'); - if (target && target.href && !target.href.includes("/api/proxy")) { - if (target.target === '_blank' || target.target === '_parent' || target.target === '_top') { - target.target = '_self'; - } - } - }, true); - - // Periodic check to fix dynamically added elements setInterval(function() { document.querySelectorAll('a[href]:not([data-proxied]), form[action]:not([data-proxied])').forEach(function(el) { var attr = el.tagName === 'A' ? 'href' : 'action'; @@ -227,14 +250,15 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { el.setAttribute('data-proxied', 'true'); } }); - }, 2000); + }, 1000); + + notifyParent(); } catch(e) { console.error("Proxy injection error:", e); } })(); `; - // Inject headInjection if (html.toLowerCase().includes('')) { html = html.replace(//i, `${headInjection}`); } else if (html.toLowerCase().includes('')) { @@ -243,7 +267,6 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { html = headInjection + html; } - // Aggressive rewrite for absolute and relative URLs html = html.replace(/(href|src|action)=["'](.*?)["']/gi, (match, attr, p1) => { if (!p1 || p1.startsWith('#') || p1.startsWith('javascript:') || p1.startsWith('data:') || p1.startsWith('mailto:')) return match; @@ -251,7 +274,6 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => { const absoluteUrl = new URL(p1, finalUrl).href; const lowerAttr = attr.toLowerCase(); - // Proxy links, forms, and scripts/iframes if (lowerAttr === 'href' || lowerAttr === 'action' || match.toLowerCase().includes(' { res.status(response.status).send(html); } else { - // For non-HTML content (images, JS, CSS), just send it res.status(response.status).send(data); } } catch (error) { - console.error(`[Proxy] Critical Error:`, error.message); res.status(500).send(`
-

Proxy Error

-

Failed to load the requested page.

- ${error.message} +

Proxy Error

+ ${error.message}
`); diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 6a43583..982b0aa 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -132,36 +132,59 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { const apiBase = process.env.NEXT_PUBLIC_BACK_API || '/api'; const proxyUrl = window.location.origin + apiBase + '/proxy?url='; const content = ` + - Zen Browser + SCHOOLWORK: ELA, SCIENCE, HISTORY, MATH, LITERATURE, SOCIAL STUDIES, AND WRITING + -
+
+

SCHOOLWORK

+

ELA, SCIENCE, HISTORY, MATH, LITERATURE, SOCIAL STUDIES, AND WRITING

+
Click or press any key to hide this from your history.
+
+ +
- +
+
+
Press [ESC] to Panic
+