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('