diff --git a/backend/src/index.js b/backend/src/index.js index efe7970..ec06481 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -15,6 +15,7 @@ const authRoutes = require('./routes/auth'); const fileRoutes = require('./routes/file'); const searchRoutes = require('./routes/search'); const sqlRoutes = require('./routes/sql'); +const proxyRoutes = require('./routes/proxy'); const pexelsRoutes = require('./routes/pexels'); const openaiRoutes = require('./routes/openai'); @@ -88,6 +89,7 @@ app.use(cors({origin: true})); require('./auth/auth'); app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); app.use('/api/auth', authRoutes); app.use('/api/file', fileRoutes); @@ -96,6 +98,7 @@ app.enable('trust proxy'); app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes); +app.use('/api/proxy', proxyRoutes); app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoutes); diff --git a/backend/src/routes/proxy.js b/backend/src/routes/proxy.js new file mode 100644 index 0000000..c4de94d --- /dev/null +++ b/backend/src/routes/proxy.js @@ -0,0 +1,132 @@ +const express = require('express'); +const router = express.Router(); +const axios = require('axios'); +const Helpers = require('../helpers'); + +router.all('/', Helpers.wrapAsync(async (req, res) => { + const targetUrl = req.query.url; + if (!targetUrl) return res.status(400).send('URL is required'); + + try { + const method = req.method; + const headers = { ...req.headers }; + + // Remove host and other headers that might cause issues + delete headers.host; + delete headers.origin; + delete headers.referer; + + // 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'; + + const response = await axios({ + url: targetUrl, + method: method, + headers: headers, + data: req.body, + params: req.params, + responseType: 'arraybuffer', + validateStatus: () => true, + maxRedirects: 10, + timeout: 15000, + }); + + const contentType = response.headers['content-type'] || ''; + const finalUrl = response.request.res.responseUrl || targetUrl; + + // Copy headers but strip security and encoding ones + Object.keys(response.headers).forEach(key => { + const lowerKey = key.toLowerCase(); + if (![ + 'x-frame-options', + 'content-security-policy', + 'content-security-policy-report-only', + 'content-length', + 'transfer-encoding', + 'connection', + 'strict-transport-security', + 'x-content-type-options' + ].includes(lowerKey)) { + res.setHeader(key, response.headers[key]); + } + }); + + // Ensure we don't have caching issues during dev + res.setHeader('Cache-Control', 'no-store'); + + 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 + const headInjection = ` + + + `; + + if (html.includes('')) { + html = html.replace('', `${headInjection}`); + } else if (html.includes('')) { + html = html.replace('', `${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) => { + 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(' { let keyPresses: number[] = []; const handleKeyDown = (e: KeyboardEvent) => { - // Ignore if user is typing in an input or textarea const target = e.target as HTMLElement; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { return; @@ -163,104 +162,167 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { if (keyPresses.length >= 5) { const win = window.open('about:blank', '_blank'); if (win) { + const proxyUrl = window.location.origin + '/api/proxy?url='; const content = ` - Search + Zen Browser -
-
-
+
+
+
+
+
-
-
-
-
+
-
+
+
+