diff --git a/backend/src/routes/proxy.js b/backend/src/routes/proxy.js
index c4de94d..79eb80b 100644
--- a/backend/src/routes/proxy.js
+++ b/backend/src/routes/proxy.js
@@ -2,39 +2,132 @@ const express = require('express');
const router = express.Router();
const axios = require('axios');
const Helpers = require('../helpers');
+const qs = require('qs');
-router.all('/', Helpers.wrapAsync(async (req, res) => {
- const targetUrl = req.query.url;
- if (!targetUrl) return res.status(400).send('URL is required');
+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);
+ Object.keys(req.query).forEach(key => {
+ if (key !== 'url') {
+ urlObj.searchParams.set(key, req.query[key]);
+ }
+ });
+ targetUrl = urlObj.href;
+ } catch (e) {
+ // Might be relative, handle below
+ }
+ }
+
+ // 2. Handle Path-based URLs (e.g. /api/proxy/https://google.com)
+ if (!targetUrl || !targetUrl.startsWith('http')) {
+ const originalUrl = req.originalUrl;
+ const marker = '/api/proxy/';
+ if (originalUrl.includes(marker)) {
+ const remainder = originalUrl.split(marker)[1];
+ if (remainder && remainder.startsWith('http')) {
+ targetUrl = remainder;
+ } else if (remainder && req.headers.referer) {
+ // 3. Smart Path Resolution via Referer
+ try {
+ const refUrl = new URL(req.headers.referer);
+ const parentTarget = refUrl.searchParams.get('url');
+ if (parentTarget) {
+ targetUrl = new URL(remainder, parentTarget).href;
+ }
+ } catch (e) {}
+ }
+ }
+ }
+
+ 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
+
+
+ `);
+ }
try {
const method = req.method;
- const headers = { ...req.headers };
+ const headers = {};
- // Remove host and other headers that might cause issues
- delete headers.host;
- delete headers.origin;
- delete headers.referer;
+ // Essential headers to forward
+ const forwardHeaders = [
+ 'accept', 'accept-language', 'accept-encoding', 'user-agent', 'content-type', 'range', 'authorization'
+ ];
- // Set a common 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';
+ forwardHeaders.forEach(h => {
+ if (req.headers[h]) {
+ headers[h] = req.headers[h];
+ }
+ });
- const response = await axios({
+ // 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';
+ }
+
+ // 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,
headers: headers,
- data: req.body,
- params: req.params,
responseType: 'arraybuffer',
- validateStatus: () => true,
+ validateStatus: () => true,
maxRedirects: 10,
- timeout: 15000,
- });
+ timeout: 30000,
+ decompress: true
+ };
- const contentType = response.headers['content-type'] || '';
+ if (method !== 'GET' && method !== 'HEAD' && req.body) {
+ if (headers['content-type'] && headers['content-type'].includes('application/x-www-form-urlencoded')) {
+ axiosConfig.data = typeof req.body === 'string' ? req.body : qs.stringify(req.body);
+ } else {
+ axiosConfig.data = req.body;
+ }
+ }
+
+ const response = await axios(axiosConfig);
const finalUrl = response.request.res.responseUrl || targetUrl;
-
- // Copy headers but strip security and encoding ones
+ 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 (![
@@ -45,88 +138,147 @@ router.all('/', Helpers.wrapAsync(async (req, res) => {
'transfer-encoding',
'connection',
'strict-transport-security',
- 'x-content-type-options'
+ 'x-content-type-options',
+ 'set-cookie',
+ 'access-control-allow-origin'
].includes(lowerKey)) {
res.setHeader(key, response.headers[key]);
}
});
- // Ensure we don't have caching issues during dev
- res.setHeader('Cache-Control', 'no-store');
+ // Force CORS for the proxy
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
let data = response.data;
if (contentType.includes('text/html')) {
let html = data.toString('utf-8');
- const parsedUrl = new URL(finalUrl);
- const origin = parsedUrl.origin;
- const baseUrl = origin + parsedUrl.pathname;
-
- // Inject and frame-busting neutralization
+ let origin = '';
+ try {
+ const parsedUrl = new URL(finalUrl);
+ origin = parsedUrl.origin;
+ } catch (e) {
+ origin = targetUrl;
+ }
+
const headInjection = `
`;
- if (html.includes('')) {
- html = html.replace('', `${headInjection}`);
- } else if (html.includes('')) {
- html = html.replace('', `${headInjection}`);
+ // Inject headInjection
+ if (html.toLowerCase().includes('')) {
+ html = html.replace(//i, `${headInjection}`);
+ } else if (html.toLowerCase().includes('')) {
+ html = html.replace(//i, `${headInjection}`);
} else {
html = headInjection + html;
}
- // More aggressive link and resource rewriting
- // This helps with relative paths that might miss in some cases
- // especially in scripts or attributes we don't handle well
-
- // Rewrite links to stay in proxy
- html = html.replace(/(href|src|action)="\s*((?!#|javascript|mailto|tel|data:|https?:\/\/).*?)\s*"/gi, (match, attr, p1) => {
+ // 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;
+
try {
const absoluteUrl = new URL(p1, finalUrl).href;
- if (attr.toLowerCase() === 'href' || attr.toLowerCase() === 'action') {
- return `${attr}="/api/proxy?url=${encodeURIComponent(absoluteUrl)}"`;
- }
- // For src, we might not want to proxy everything (images/scripts) as it adds load
- // but if the site blocks cross-origin, we must.
- // Let's proxy scripts and iframes.
- if (match.toLowerCase().includes('