2
This commit is contained in:
parent
afb69ac6af
commit
2cefb14500
@ -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(`
|
||||
<div style="background:#1a1a1a; color:#ccc; padding:20px; font-family:sans-serif; border-radius:8px; margin:20px; border:1px solid #333;">
|
||||
<h2 style="color:#ff4d4d; margin-top:0;">URL Required</h2>
|
||||
<p>The stealth proxy needs a target URL to work.</p>
|
||||
<div style="background:#000; padding:15px; border-radius:4px; border:1px solid #444;">
|
||||
<code>${req.originalUrl}</code>
|
||||
</div>
|
||||
<p style="font-size:14px; color:#888; margin-top:20px;">Try entering a full URL in the address bar, e.g., <b>https://google.com</b></p>
|
||||
<button onclick="window.history.back()" style="background:#333; color:#fff; border:none; padding:8px 16px; border-radius:4px; cursor:pointer;">Go Back</button>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
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 <base> and frame-busting neutralization
|
||||
let origin = '';
|
||||
try {
|
||||
const parsedUrl = new URL(finalUrl);
|
||||
origin = parsedUrl.origin;
|
||||
} catch (e) {
|
||||
origin = targetUrl;
|
||||
}
|
||||
|
||||
const headInjection = `
|
||||
<base href="${origin}/">
|
||||
<script>
|
||||
// Neutralize frame-busting
|
||||
(function() {
|
||||
var originalOpen = window.open;
|
||||
window.open = function(url, name, specs) {
|
||||
if (!url) return originalOpen(url, name, specs);
|
||||
window.location.href = "/api/proxy?url=" + encodeURIComponent(new URL(url, location.href).href);
|
||||
return null;
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'top', { get: function() { return window.self; } });
|
||||
Object.defineProperty(window, 'parent', { get: function() { return window.self; } });
|
||||
Object.defineProperty(document, 'referrer', { get: function() { return ''; } });
|
||||
|
||||
// Override location.replace and assign to stay in proxy
|
||||
var originalReplace = window.location.replace;
|
||||
window.location.replace = function(url) {
|
||||
window.location.href = "/api/proxy?url=" + encodeURIComponent(new URL(url, location.href).href);
|
||||
};
|
||||
try {
|
||||
var PROXY_BASE = window.location.origin + "/api/proxy?url=";
|
||||
|
||||
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;
|
||||
try {
|
||||
var absUrl = new URL(url, location.href).href;
|
||||
// Don't proxy if already proxied
|
||||
if (absUrl.includes("/api/proxy?url=")) return url;
|
||||
return PROXY_BASE + encodeURIComponent(absUrl);
|
||||
} catch(e) { return url; }
|
||||
}
|
||||
|
||||
// Intercept window.open
|
||||
var originalOpen = window.open;
|
||||
window.open = function(url, name, specs) {
|
||||
if (!url) return originalOpen(url, name, specs);
|
||||
window.location.href = toProxyUrl(url);
|
||||
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")) {
|
||||
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';
|
||||
var val = el.getAttribute(attr);
|
||||
if (val && !val.includes('/api/proxy') && !val.startsWith('#') && !val.startsWith('javascript:')) {
|
||||
el.setAttribute(attr, toProxyUrl(val));
|
||||
el.setAttribute('data-proxied', 'true');
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
} catch(e) { console.error("Proxy injection error:", e); }
|
||||
})();
|
||||
</script>
|
||||
`;
|
||||
|
||||
if (html.includes('<head>')) {
|
||||
html = html.replace('<head>', `<head>${headInjection}`);
|
||||
} else if (html.includes('<html>')) {
|
||||
html = html.replace('<html>', `<html><head>${headInjection}</head>`);
|
||||
// Inject headInjection
|
||||
if (html.toLowerCase().includes('<head>')) {
|
||||
html = html.replace(/<head>/i, `<head>${headInjection}`);
|
||||
} else if (html.toLowerCase().includes('<html>')) {
|
||||
html = html.replace(/<html>/i, `<html><head>${headInjection}</head>`);
|
||||
} else {
|
||||
html = headInjection + html;
|
||||
}
|
||||
|
||||
// More aggressive link and resource rewriting
|
||||
// This helps with relative paths that <base> 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('<script') || match.toLowerCase().includes('<iframe')) {
|
||||
const lowerAttr = attr.toLowerCase();
|
||||
|
||||
// Proxy links, forms, and scripts/iframes
|
||||
if (lowerAttr === 'href' || lowerAttr === 'action' || match.toLowerCase().includes('<script') || match.toLowerCase().includes('<iframe')) {
|
||||
if (p1.includes('/api/proxy')) return match;
|
||||
return `${attr}="/api/proxy?url=${encodeURIComponent(absoluteUrl)}"`;
|
||||
}
|
||||
return match;
|
||||
} catch(e) {
|
||||
return match;
|
||||
}
|
||||
} catch(e) { return match; }
|
||||
});
|
||||
|
||||
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) {
|
||||
res.status(500).send(`Proxy error: ${error.message}`);
|
||||
console.error(`[Proxy] Critical Error:`, error.message);
|
||||
res.status(500).send(`
|
||||
<div style="background:#1a1a1a; color:#ff4d4d; padding:20px; font-family:sans-serif; border-radius:8px; margin:20px; border:1px solid #333;">
|
||||
<h2 style="margin-top:0;">Proxy Error</h2>
|
||||
<p>Failed to load the requested page.</p>
|
||||
<code style="background:#000; padding:10px; display:block; border-radius:4px;">${error.message}</code>
|
||||
<button onclick="window.location.reload()" style="margin-top:15px; background:#3ea6ff; color:#fff; border:none; padding:8px 16px; border-radius:4px; cursor:pointer;">Retry</button>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
router.all('/', proxyHandler);
|
||||
router.all('/:any*', proxyHandler);
|
||||
|
||||
module.exports = router;
|
||||
@ -33,7 +33,6 @@ type AppPropsWithLayout = AppProps & {
|
||||
}
|
||||
|
||||
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
// Use the layout defined at the page level, if available
|
||||
const getLayout = Component.getLayout || ((page) => page);
|
||||
const router = useRouter();
|
||||
const [stepsEnabled, setStepsEnabled] = React.useState(false);
|
||||
@ -57,7 +56,6 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: Remove this code in future releases
|
||||
React.useEffect(() => {
|
||||
const allowedOrigin = (() => {
|
||||
if (!document.referrer) {
|
||||
@ -112,40 +110,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Tour is disabled by default in generated projects.
|
||||
return;
|
||||
const isCompleted = (stepKey: string) => {
|
||||
return localStorage.getItem(`completed_${stepKey}`) === 'true';
|
||||
};
|
||||
if (router.pathname === '/login' && !isCompleted('loginSteps')) {
|
||||
setSteps(loginSteps);
|
||||
setStepName('loginSteps');
|
||||
setStepsEnabled(true);
|
||||
}else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) {
|
||||
setTimeout(() => {
|
||||
setSteps(appSteps);
|
||||
setStepName('appSteps');
|
||||
setStepsEnabled(true);
|
||||
}, 1000);
|
||||
} else if (router.pathname === '/users/users-list' && !isCompleted('usersSteps')) {
|
||||
setTimeout(() => {
|
||||
setSteps(usersSteps);
|
||||
setStepName('usersSteps');
|
||||
setStepsEnabled(true);
|
||||
}, 1000);
|
||||
} else if (router.pathname === '/roles/roles-list' && !isCompleted('rolesSteps')) {
|
||||
setTimeout(() => {
|
||||
setSteps(rolesSteps);
|
||||
setStepName('rolesSteps');
|
||||
setStepsEnabled(true);
|
||||
}, 1000);
|
||||
} else {
|
||||
setSteps([]);
|
||||
setStepsEnabled(false);
|
||||
}
|
||||
}, [router.pathname]);
|
||||
|
||||
// Stealth search trigger: press 'g' 5 times in 5 seconds
|
||||
React.useEffect(() => {
|
||||
let keyPresses: number[] = [];
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@ -162,34 +129,35 @@ 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 apiBase = process.env.NEXT_PUBLIC_BACK_API || '/api';
|
||||
const proxyUrl = window.location.origin + apiBase + '/proxy?url=';
|
||||
const content = `
|
||||
<html>
|
||||
<head>
|
||||
<title>Zen Browser</title>
|
||||
<style>
|
||||
body { background: #0f0f0f; margin: 0; font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden; color: #f1f1f1; }
|
||||
.header { background: #212121; border-bottom: 1px solid #333; display: flex; flex-direction: column; }
|
||||
body { background: #0f0f0f; margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden; color: #f1f1f1; }
|
||||
.header { background: #1e1e1e; border-bottom: 1px solid #333; display: flex; flex-direction: column; box-shadow: 0 4px 12px rgba(0,0,0,0.5); z-index: 10; }
|
||||
.tab-bar { display: flex; padding: 8px 8px 0; gap: 4px; overflow-x: auto; background: #121212; align-items: flex-end; }
|
||||
.tab { padding: 8px 16px; background: #2a2a2a; border-radius: 10px 10px 0 0; cursor: pointer; display: flex; align-items: center; gap: 12px; font-size: 13px; min-width: 140px; max-width: 220px; white-space: nowrap; overflow: hidden; border: 1px solid transparent; border-bottom: none; transition: all 0.2s; color: #999; position: relative; }
|
||||
.tab { padding: 8px 16px; background: #2a2a2a; border-radius: 8px 8px 0 0; cursor: pointer; display: flex; align-items: center; gap: 12px; font-size: 13px; min-width: 120px; max-width: 200px; white-space: nowrap; overflow: hidden; border: 1px solid transparent; border-bottom: none; transition: all 0.2s; color: #888; position: relative; }
|
||||
.tab:hover { background: #333; color: #ccc; }
|
||||
.tab.active { background: #212121; color: #fff; font-weight: 500; border-color: #333; border-bottom-color: #212121; margin-bottom: -1px; z-index: 1; }
|
||||
.tab .close { font-size: 14px; color: #666; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: 0.2s; }
|
||||
.tab.active { background: #1e1e1e; color: #fff; font-weight: 500; border-color: #333; border-bottom-color: #1e1e1e; margin-bottom: -1px; z-index: 1; border-top: 2px solid #3ea6ff; }
|
||||
.tab .close { font-size: 14px; color: #666; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: 0.2s; }
|
||||
.tab .close:hover { background: #444; color: #fff; }
|
||||
.search-bar { padding: 12px 20px; display: flex; gap: 16px; align-items: center; background: #212121; }
|
||||
.nav-btns { display: flex; gap: 8px; }
|
||||
.nav-btn { cursor: pointer; color: #aaa; padding: 6px; border-radius: 50%; display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; transition: 0.2s; }
|
||||
.search-bar { padding: 8px 16px; display: flex; gap: 10px; align-items: center; background: #1e1e1e; }
|
||||
.nav-btns { display: flex; gap: 2px; }
|
||||
.nav-btn { cursor: pointer; color: #aaa; padding: 6px; border-radius: 6px; display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; transition: 0.2s; }
|
||||
.nav-btn:hover { background: #333; color: #fff; }
|
||||
.nav-btn.disabled { color: #444; cursor: default; }
|
||||
.nav-btn.disabled { color: #444; cursor: default; pointer-events: none; }
|
||||
form { flex-grow: 1; position: relative; }
|
||||
input { padding: 10px 20px; font-size: 14px; border: 1px solid #333; border-radius: 24px; width: 100%; outline: none; background: #121212; color: #fff; transition: all 0.3s; box-sizing: border-box; }
|
||||
input:focus { background: #181818; border-color: #555; box-shadow: 0 0 0 2px rgba(255,255,255,0.05); }
|
||||
.new-tab-btn { cursor: pointer; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; color: #aaa; margin-left: 8px; margin-bottom: 4px; transition: 0.2s; }
|
||||
input { padding: 8px 16px; font-size: 13px; border: 1px solid #333; border-radius: 18px; width: 100%; outline: none; background: #121212; color: #fff; transition: all 0.3s; box-sizing: border-box; }
|
||||
input:focus { border-color: #3ea6ff; background: #000; box-shadow: 0 0 0 2px rgba(62, 166, 255, 0.2); }
|
||||
.new-tab-btn { cursor: pointer; width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; color: #888; margin-left: 6px; margin-bottom: 6px; transition: 0.2s; }
|
||||
.new-tab-btn:hover { background: #333; color: #fff; }
|
||||
.iframe-container { flex-grow: 1; position: relative; background: #fff; }
|
||||
iframe { border: none; width: 100%; height: 100%; position: absolute; top: 0; left: 0; display: none; background: #fff; }
|
||||
iframe.active { display: block; }
|
||||
.loader { position: absolute; bottom: 0; left: 0; height: 2px; background: #3ea6ff; transition: width 0.3s; width: 0; z-index: 100; }
|
||||
.loader { position: absolute; bottom: 0; left: 0; height: 3px; background: #3ea6ff; transition: width 0.4s; width: 0; z-index: 100; box-shadow: 0 0 8px rgba(62, 166, 255, 0.5); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -200,26 +168,32 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<div class='search-bar'>
|
||||
<div class='nav-btns'>
|
||||
<div class='nav-btn' id='backBtn' onclick='goBack()' title='Back'>←</div>
|
||||
<div class='nav-btn' id='forwardBtn' onclick='goForward()' title='Forward'>→</div>
|
||||
<div class='nav-btn' onclick='reloadTab()' title='Reload'>↻</div>
|
||||
<div class='nav-btn' onclick='goHome()' title='Home'>🏠</div>
|
||||
</div>
|
||||
<form onsubmit='handleSearch(event); return false;'>
|
||||
<input id='searchInput' placeholder='Search or type a URL' autofocus autocomplete='off' />
|
||||
<div id='loader' class='loader'></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class='iframe-container' id='iframeContainer'>
|
||||
<div id='loader' class='loader'></div>
|
||||
</div>
|
||||
<div class='iframe-container' id='iframeContainer'></div>
|
||||
<script>
|
||||
var tabs = [];
|
||||
var activeTabId = null;
|
||||
const PROXY_BASE = "${proxyUrl}";
|
||||
|
||||
function getRawUrl(proxiedUrl) {
|
||||
if (proxiedUrl.startsWith(PROXY_BASE)) {
|
||||
return decodeURIComponent(proxiedUrl.substring(PROXY_BASE.length));
|
||||
}
|
||||
try {
|
||||
var url = new URL(proxiedUrl);
|
||||
var raw = url.searchParams.get('url');
|
||||
if (raw) return raw;
|
||||
// Handle path-based
|
||||
if (proxiedUrl.includes('/api/proxy/')) {
|
||||
return proxiedUrl.split('/api/proxy/')[1];
|
||||
}
|
||||
} catch(e) {}
|
||||
return proxiedUrl;
|
||||
}
|
||||
|
||||
@ -265,20 +239,25 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.id = 'frame-' + id;
|
||||
iframe.src = PROXY_BASE + encodeURIComponent(url);
|
||||
iframe.setAttribute('allow', 'autoplay; encrypted-media; camera; microphone; fullscreen; clipboard-write');
|
||||
iframe.setAttribute('allow', 'autoplay; encrypted-media; camera; microphone; fullscreen; clipboard-write; geolocation');
|
||||
|
||||
iframe.onload = function() {
|
||||
document.getElementById('loader').style.width = '0';
|
||||
try {
|
||||
// Try to update title and URL if same-origin (proxy)
|
||||
var frameUrl = iframe.contentWindow.location.href;
|
||||
var rawUrl = getRawUrl(frameUrl);
|
||||
if (rawUrl && rawUrl !== 'about:blank') {
|
||||
|
||||
if (rawUrl && rawUrl !== 'about:blank' && !rawUrl.includes('error-overlay')) {
|
||||
if (tabObj.history[tabObj.historyIndex] !== rawUrl) {
|
||||
tabObj.history = tabObj.history.slice(0, tabObj.historyIndex + 1);
|
||||
tabObj.history.push(rawUrl);
|
||||
tabObj.historyIndex++;
|
||||
}
|
||||
tabObj.url = rawUrl;
|
||||
if (activeTabId === id) {
|
||||
document.getElementById('searchInput').value = rawUrl;
|
||||
updateNavButtons();
|
||||
}
|
||||
// Attempt to get page title
|
||||
if (iframe.contentDocument && iframe.contentDocument.title) {
|
||||
titleSpan.innerText = iframe.contentDocument.title;
|
||||
}
|
||||
@ -303,19 +282,17 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
|
||||
var tab = tabs.find(t => t.id === id);
|
||||
if (tab) {
|
||||
document.getElementById('searchInput').value = (tab.url.includes('google.com/search?igu=1')) ? '' : tab.url;
|
||||
document.getElementById('searchInput').value = tab.url;
|
||||
updateNavButtons();
|
||||
}
|
||||
}
|
||||
|
||||
function updateNavButtons() {
|
||||
var tab = tabs.find(t => t.id === activeTabId);
|
||||
var backBtn = document.getElementById('backBtn');
|
||||
if (tab && tab.historyIndex > 0) {
|
||||
backBtn.classList.remove('disabled');
|
||||
} else {
|
||||
backBtn.classList.add('disabled');
|
||||
}
|
||||
if (!tab) return;
|
||||
|
||||
document.getElementById('backBtn').classList.toggle('disabled', tab.historyIndex <= 0);
|
||||
document.getElementById('forwardBtn').classList.toggle('disabled', tab.historyIndex >= tab.history.length - 1);
|
||||
}
|
||||
|
||||
function removeTab(id) {
|
||||
@ -340,14 +317,29 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
var tab = tabs.find(t => t.id === activeTabId);
|
||||
if (tab && tab.historyIndex > 0) {
|
||||
tab.historyIndex--;
|
||||
var prevUrl = tab.history[tab.historyIndex];
|
||||
tab.url = prevUrl;
|
||||
document.getElementById('frame-' + activeTabId).src = PROXY_BASE + encodeURIComponent(prevUrl);
|
||||
document.getElementById('searchInput').value = prevUrl;
|
||||
updateNavButtons();
|
||||
loadUrlInTab(tab, tab.history[tab.historyIndex], true);
|
||||
}
|
||||
}
|
||||
|
||||
function goForward() {
|
||||
var tab = tabs.find(t => t.id === activeTabId);
|
||||
if (tab && tab.historyIndex < tab.history.length - 1) {
|
||||
tab.historyIndex++;
|
||||
loadUrlInTab(tab, tab.history[tab.historyIndex], true);
|
||||
}
|
||||
}
|
||||
|
||||
function loadUrlInTab(tab, url, skipHistory) {
|
||||
tab.url = url;
|
||||
var iframe = document.getElementById('frame-' + tab.id);
|
||||
iframe.src = PROXY_BASE + encodeURIComponent(url);
|
||||
if (activeTabId === tab.id) {
|
||||
document.getElementById('searchInput').value = url;
|
||||
}
|
||||
document.getElementById('loader').style.width = '40%';
|
||||
updateNavButtons();
|
||||
}
|
||||
|
||||
function reloadTab() {
|
||||
if (activeTabId) {
|
||||
var frame = document.getElementById('frame-' + activeTabId);
|
||||
@ -357,59 +349,33 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
var url = 'https://www.google.com/search?igu=1';
|
||||
if (activeTabId) {
|
||||
var tab = tabs.find(t => t.id === activeTabId);
|
||||
tab.url = url;
|
||||
document.getElementById('frame-' + activeTabId).src = PROXY_BASE + encodeURIComponent(url);
|
||||
document.getElementById('searchInput').value = '';
|
||||
document.getElementById('el-' + activeTabId).querySelector('.title').innerText = 'Google';
|
||||
}
|
||||
handleSearch({ preventDefault: () => {}, target: null }, 'https://www.google.com/search?igu=1');
|
||||
}
|
||||
|
||||
function handleSearch(e) {
|
||||
function handleSearch(e, forcedUrl) {
|
||||
if (e) e.preventDefault();
|
||||
var input = document.getElementById('searchInput');
|
||||
var query = input.value.trim();
|
||||
var query = forcedUrl || input.value.trim();
|
||||
if (!query) return false;
|
||||
|
||||
document.getElementById('loader').style.width = '30%';
|
||||
|
||||
var url;
|
||||
if (query.match(/^(https?:\\/\\/)/i)) {
|
||||
url = query;
|
||||
} else if (query.includes('.') && !query.includes(' ')) {
|
||||
url = 'https://' + query;
|
||||
} else {
|
||||
url = "https://www.google.com/search?igu=1&q=" + encodeURIComponent(query);
|
||||
var url = query;
|
||||
if (!query.match(/^(https?:\\/\\/)/i)) {
|
||||
if (query.includes('.') && !query.includes(' ')) {
|
||||
url = 'https://' + query;
|
||||
} else {
|
||||
url = "https://www.google.com/search?igu=1&q=" + encodeURIComponent(query);
|
||||
}
|
||||
}
|
||||
|
||||
// YouTube specific optimization: prefer mobile version for better proxy compatibility
|
||||
if (url.includes('youtube.com') && !url.includes('m.youtube.com')) {
|
||||
url = url.replace('www.youtube.com', 'm.youtube.com');
|
||||
}
|
||||
|
||||
if (!activeTabId) {
|
||||
addTab(url, query);
|
||||
addTab(url);
|
||||
} else {
|
||||
var tab = tabs.find(t => t.id === activeTabId);
|
||||
var iframe = document.getElementById('frame-' + activeTabId);
|
||||
|
||||
// Push to history
|
||||
tab.history = tab.history.slice(0, tab.historyIndex + 1);
|
||||
tab.history.push(url);
|
||||
tab.historyIndex++;
|
||||
|
||||
tab.url = url;
|
||||
iframe.src = PROXY_BASE + encodeURIComponent(url);
|
||||
var tabEl = document.getElementById('el-' + activeTabId);
|
||||
if (tabEl) {
|
||||
var displayTitle = query;
|
||||
if (url.includes('google.com/search')) displayTitle = query;
|
||||
else if (url.length > 20) displayTitle = url.substring(0, 17) + '...';
|
||||
tabEl.querySelector('.title').innerText = displayTitle;
|
||||
}
|
||||
updateNavButtons();
|
||||
loadUrlInTab(tab, url);
|
||||
}
|
||||
input.blur();
|
||||
return false;
|
||||
@ -445,12 +411,11 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
const imageHeight = '960'
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Provider store={store}>
|
||||
{getLayout(
|
||||
<>
|
||||
<Head>
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:site_name" content="https://flatlogic.com/" />
|
||||
<meta property="og:title" content={title} />
|
||||
@ -459,17 +424,14 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content={imageWidth} />
|
||||
<meta property="og:image:height" content={imageHeight} />
|
||||
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image:src" content={image} />
|
||||
<meta property="twitter:image:width" content={imageWidth} />
|
||||
<meta property="twitter:image:height" content={imageHeight} />
|
||||
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
</Head>
|
||||
|
||||
<ErrorBoundary>
|
||||
<Component {...pageProps} />
|
||||
</ErrorBoundary>
|
||||
@ -486,4 +448,4 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
)
|
||||
}
|
||||
|
||||
export default appWithTranslation(MyApp);
|
||||
export default appWithTranslation(MyApp);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user