3
This commit is contained in:
parent
2cefb14500
commit
3efcff95c6
@ -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(`
|
||||
<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>
|
||||
<button onclick="window.history.back()" style="background:#333; color:#fff; border:none; padding:8px 16px; border-radius:4px; cursor:pointer; margin-top:15px;">Go Back</button>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
@ -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 = `
|
||||
<base href="${origin}/">
|
||||
@ -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); }
|
||||
})();
|
||||
</script>
|
||||
`;
|
||||
|
||||
// Inject headInjection
|
||||
if (html.toLowerCase().includes('<head>')) {
|
||||
html = html.replace(/<head>/i, `<head>${headInjection}`);
|
||||
} else if (html.toLowerCase().includes('<html>')) {
|
||||
@ -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('<script') || match.toLowerCase().includes('<iframe')) {
|
||||
if (p1.includes('/api/proxy')) return match;
|
||||
return `${attr}="/api/proxy?url=${encodeURIComponent(absoluteUrl)}"`;
|
||||
@ -262,16 +284,13 @@ const proxyHandler = Helpers.wrapAsync(async (req, res) => {
|
||||
|
||||
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(`
|
||||
<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>
|
||||
<h2>Proxy Error</h2>
|
||||
<code>${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>
|
||||
`);
|
||||
|
||||
@ -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 = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Zen Browser</title>
|
||||
<title>SCHOOLWORK: ELA, SCIENCE, HISTORY, MATH, LITERATURE, SOCIAL STUDIES, AND WRITING</title>
|
||||
<link rel="icon" href="https://www.google.com/favicon.ico">
|
||||
<style>
|
||||
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: 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: #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: 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; pointer-events: none; }
|
||||
form { flex-grow: 1; position: relative; }
|
||||
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; }
|
||||
body { background: #fff; margin: 0; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden; color: #333; }
|
||||
|
||||
/* Splash Screen */
|
||||
#splash { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #fff; z-index: 9999; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; cursor: pointer; transition: opacity 0.5s; }
|
||||
#splash h1 { font-size: 24px; color: #1a73e8; margin-bottom: 20px; font-weight: 500; }
|
||||
#splash p { font-size: 16px; color: #5f6368; }
|
||||
#splash .hint { margin-top: 50px; font-size: 14px; color: #999; border: 1px solid #eee; padding: 10px 20px; border-radius: 4px; }
|
||||
|
||||
/* Main UI */
|
||||
.header { background: #f8f9fa; border-bottom: 1px solid #dadce0; display: flex; flex-direction: column; z-index: 10; display: none; }
|
||||
.tab-bar { display: flex; padding: 10px 10px 0; gap: 4px; overflow-x: auto; background: #dee2e6; align-items: flex-end; }
|
||||
.tab { padding: 8px 16px; background: #e9ecef; border-radius: 8px 8px 0 0; cursor: pointer; display: flex; align-items: center; gap: 10px; font-size: 12px; min-width: 100px; max-width: 180px; white-space: nowrap; overflow: hidden; border: 1px solid #dee2e6; border-bottom: none; transition: all 0.2s; color: #495057; position: relative; }
|
||||
.tab:hover { background: #f1f3f4; }
|
||||
.tab.active { background: #fff; color: #202124; font-weight: 500; border-color: #dadce0; border-bottom-color: #fff; margin-bottom: -1px; z-index: 1; border-top: 3px solid #1a73e8; }
|
||||
.tab .close { font-size: 16px; color: #70757a; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
|
||||
.tab .close:hover { background: #dadce0; color: #202124; }
|
||||
|
||||
.search-bar { padding: 8px 12px; display: flex; gap: 8px; align-items: center; background: #fff; border-bottom: 1px solid #eee; }
|
||||
.nav-btns { display: flex; gap: 4px; }
|
||||
.nav-btn { cursor: pointer; color: #5f6368; padding: 6px; border-radius: 50%; display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; transition: 0.2s; }
|
||||
.nav-btn:hover { background: #f1f3f4; color: #202124; }
|
||||
.nav-btn.disabled { color: #bdc1c6; cursor: default; pointer-events: none; }
|
||||
|
||||
form { flex-grow: 1; position: relative; max-width: 800px; margin: 0 auto; width: 100%; }
|
||||
input { padding: 10px 20px; font-size: 14px; border: 1px solid #dfe1e5; border-radius: 24px; width: 100%; outline: none; background: #f1f3f4; color: #202124; transition: all 0.2s; box-sizing: border-box; }
|
||||
input:focus { background: #fff; box-shadow: 0 1px 6px rgba(32,33,36,.28); border-color: rgba(223,225,229,0); }
|
||||
|
||||
.new-tab-btn { cursor: pointer; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; color: #5f6368; margin-left: 8px; margin-bottom: 8px; transition: 0.2s; }
|
||||
.new-tab-btn:hover { background: #dadce0; color: #202124; }
|
||||
|
||||
.iframe-container { flex-grow: 1; position: relative; background: #fff; display: none; }
|
||||
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: 3px; background: #3ea6ff; transition: width 0.4s; width: 0; z-index: 100; box-shadow: 0 0 8px rgba(62, 166, 255, 0.5); }
|
||||
|
||||
.loader { position: absolute; bottom: 0; left: 0; height: 2px; background: #1a73e8; transition: width 0.3s; width: 0; z-index: 100; }
|
||||
|
||||
.panic-hint { position: fixed; bottom: 10px; right: 10px; font-size: 10px; color: #ccc; z-index: 1000; pointer-events: none; opacity: 0.5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='header'>
|
||||
<div id="splash" onclick="unlock()">
|
||||
<h1>SCHOOLWORK</h1>
|
||||
<p>ELA, SCIENCE, HISTORY, MATH, LITERATURE, SOCIAL STUDIES, AND WRITING</p>
|
||||
<div class="hint">Click or press any key to hide this from your history.</div>
|
||||
</div>
|
||||
|
||||
<div class='header' id="header">
|
||||
<div class='tab-bar' id='tabBar'>
|
||||
<div class='new-tab-btn' onclick='addTab()' title='New Tab'>+</div>
|
||||
</div>
|
||||
@ -173,23 +196,49 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<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' />
|
||||
<input id='searchInput' placeholder='Search Google or type a URL' autofocus autocomplete='off' />
|
||||
<div id='loader' class='loader'></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='iframe-container' id='iframeContainer'></div>
|
||||
<div class="panic-hint">Press [ESC] to Panic</div>
|
||||
|
||||
<script>
|
||||
var tabs = [];
|
||||
var activeTabId = null;
|
||||
const PROXY_BASE = "${proxyUrl}";
|
||||
const DEFAULT_TITLE = "SCHOOLWORK: ELA, SCIENCE, HISTORY, MATH, LITERATURE, SOCIAL STUDIES, AND WRITING";
|
||||
|
||||
function unlock() {
|
||||
document.getElementById('splash').style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
document.getElementById('splash').style.display = 'none';
|
||||
document.getElementById('header').style.display = 'flex';
|
||||
document.getElementById('iframeContainer').style.display = 'block';
|
||||
// History Cloaking
|
||||
history.replaceState(null, '', 'https://www.google.com/search?q=educational+resources+for+students');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', function(e) {
|
||||
if (document.getElementById('splash').style.display !== 'none') {
|
||||
unlock();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
window.location.href = 'https://classroom.google.com';
|
||||
}
|
||||
});
|
||||
|
||||
function getRawUrl(proxiedUrl) {
|
||||
try {
|
||||
var url = new URL(proxiedUrl);
|
||||
var raw = url.searchParams.get('url');
|
||||
if (raw) return raw;
|
||||
// Handle path-based
|
||||
if (proxiedUrl.includes('/api/proxy?url=')) {
|
||||
return decodeURIComponent(proxiedUrl.split('/api/proxy?url=')[1]);
|
||||
}
|
||||
if (proxiedUrl.includes('/api/proxy/')) {
|
||||
return proxiedUrl.split('/api/proxy/')[1];
|
||||
}
|
||||
@ -270,6 +319,33 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
return id;
|
||||
}
|
||||
|
||||
window.addEventListener('message', function(e) {
|
||||
if (e.data && e.data.type === 'zen-browser-url-change') {
|
||||
var rawUrl = getRawUrl(e.data.url);
|
||||
var iframe = Array.from(document.querySelectorAll('iframe')).find(f => f.contentWindow === e.source);
|
||||
if (!iframe) return;
|
||||
var id = iframe.id.replace('frame-', '');
|
||||
var tab = tabs.find(t => t.id === id);
|
||||
|
||||
if (tab && rawUrl && rawUrl !== 'about:blank') {
|
||||
if (tab.history[tab.historyIndex] !== rawUrl) {
|
||||
tab.history = tab.history.slice(0, tab.historyIndex + 1);
|
||||
tab.history.push(rawUrl);
|
||||
tab.historyIndex++;
|
||||
}
|
||||
tab.url = rawUrl;
|
||||
if (activeTabId === tab.id) {
|
||||
document.getElementById('searchInput').value = rawUrl;
|
||||
updateNavButtons();
|
||||
}
|
||||
if (e.data.title) {
|
||||
var tabEl = document.getElementById('el-' + tab.id);
|
||||
if (tabEl) tabEl.querySelector('.title').innerText = e.data.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function switchTab(id) {
|
||||
activeTabId = id;
|
||||
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
|
||||
@ -285,12 +361,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
document.getElementById('searchInput').value = tab.url;
|
||||
updateNavButtons();
|
||||
}
|
||||
document.title = DEFAULT_TITLE;
|
||||
}
|
||||
|
||||
function updateNavButtons() {
|
||||
var tab = tabs.find(t => t.id === activeTabId);
|
||||
if (!tab) return;
|
||||
|
||||
document.getElementById('backBtn').classList.toggle('disabled', tab.historyIndex <= 0);
|
||||
document.getElementById('forwardBtn').classList.toggle('disabled', tab.historyIndex >= tab.history.length - 1);
|
||||
}
|
||||
@ -298,11 +374,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
function removeTab(id) {
|
||||
var index = tabs.findIndex(t => t.id === id);
|
||||
if (index === -1) return;
|
||||
|
||||
tabs = tabs.filter(t => t.id !== id);
|
||||
document.getElementById('el-' + id).remove();
|
||||
document.getElementById('frame-' + id).remove();
|
||||
|
||||
if (activeTabId === id) {
|
||||
if (tabs.length > 0) {
|
||||
var nextTab = tabs[index] || tabs[index - 1];
|
||||
@ -382,6 +456,8 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
}
|
||||
|
||||
addTab();
|
||||
// Initial title set
|
||||
document.title = DEFAULT_TITLE;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user