Add HTTP interstitial warning and bypass support
Introduces an insecure.html interstitial page that warns users before navigating to unencrypted HTTP sites, except for localhost and previously bypassed hosts. Updates script.js to intercept HTTP navigations, display the warning, and allow session-based bypasses when the user chooses to proceed.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Connection Not Secure</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style>
|
||||
:root {
|
||||
--bg:#121212; --panel:#1e1e1e; --warn:#d97706; --danger:#dc2626; --text:#f5f5f5; --muted:#9ca3af; --accent:#6366f1;
|
||||
color-scheme: dark;
|
||||
}
|
||||
body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,Inter,Ubuntu,sans-serif; background:var(--bg); color:var(--text); display:flex; min-height:100vh; align-items:center; justify-content:center; padding:32px; }
|
||||
.card { max-width:780px; width:100%; background:linear-gradient(145deg,#1c1c1c,#242424); border:1px solid #2c2c2c; border-radius:20px; padding:40px 46px 48px; box-shadow:0 8px 28px -6px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,0.04); position:relative; overflow:hidden; }
|
||||
.card:before { content:""; position:absolute; inset:0; background:radial-gradient(circle at 18% 15%,rgba(255,255,255,.08),transparent 55%), radial-gradient(circle at 82% 78%,rgba(255,255,255,.05),transparent 60%); pointer-events:none; }
|
||||
h1 { font-size: clamp(1.9rem, 2.6vw, 2.6rem); margin:0 0 12px; letter-spacing:-.5px; display:flex; align-items:center; gap:.6rem; }
|
||||
h1 span.badge { font-size:12px; letter-spacing:1px; padding:4px 8px; border:1px solid var(--warn); color:var(--warn); border-radius:999px; text-transform:uppercase; background:rgba(217,119,6,0.1); }
|
||||
p.lede { font-size:1.05rem; line-height:1.55; margin:0 0 22px; color:var(--muted); }
|
||||
code { background:#252525; padding:3px 6px; border-radius:6px; font-size:.9rem; color:#e0e0e0; }
|
||||
.url-box { font-family:monospace; font-size:.92rem; padding:10px 12px; background:#181818; border:1px solid #2a2a2a; border-radius:10px; word-break:break-all; margin:0 0 22px; display:flex; align-items:center; gap:.75rem; }
|
||||
.url-box svg { flex:0 0 auto; width:22px; height:22px; stroke:var(--warn); }
|
||||
ul { margin:0 0 26px 1.1rem; padding:0; line-height:1.5; color:var(--muted); }
|
||||
ul li { margin-bottom:6px; }
|
||||
.actions { display:flex; flex-wrap:wrap; gap:14px; }
|
||||
button { cursor:pointer; font-size:.95rem; letter-spacing:.4px; font-weight:500; border-radius:12px; padding:14px 26px; border:1px solid transparent; background:linear-gradient(135deg,#303030,#252525); color:#fff; position:relative; overflow:hidden; transition:.25s; }
|
||||
button.primary { background:linear-gradient(135deg,#6366f1,#5145cd); box-shadow:0 4px 18px -4px rgba(99,102,241,.5); }
|
||||
button.danger { background:linear-gradient(135deg,#b91c1c,#7f1d1d); border-color:#dc2626; }
|
||||
button.outline { background:transparent; border-color:#444; }
|
||||
button:hover { filter:brightness(1.12); transform:translateY(-2px); }
|
||||
button:active { transform:translateY(0); filter:brightness(.9); }
|
||||
.mini { font-size:.75rem; text-transform:uppercase; letter-spacing:1px; opacity:.8; margin-top:24px; }
|
||||
.fade-in { animation:fade .5s ease .05s both; }
|
||||
@keyframes fade { from { opacity:0; transform: translateY(6px); } to { opacity:1; transform:none; } }
|
||||
.grid { display:grid; gap:40px; }
|
||||
@media (max-width:760px){ .card{padding:34px 28px 40px;} h1{font-size:2rem;} }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card fade-in">
|
||||
<h1>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M12 2 2 7l10 5 10-5-10-5Z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
Connection Not Secure <span class="badge">http</span>
|
||||
</h1>
|
||||
<p class="lede">You’re about to visit a page using <strong>HTTP (unencrypted)</strong>. Information you send or view can potentially be intercepted or modified. If this is a site you trust and you understand the risks, you can continue anyway.</p>
|
||||
<div class="url-box" id="targetBox" title="Target URL"></div>
|
||||
<ul>
|
||||
<li>No TLS encryption – data (including passwords or forms) travels in plain text.</li>
|
||||
<li>Attackers on the same network (café Wi‑Fi, school, workplace) could tamper with or read content.</li>
|
||||
<li>The site might support HTTPS. Try manually changing to <code>https://</code> first.</li>
|
||||
<li>Proceed only if necessary and you have a reason to trust this destination.</li>
|
||||
</ul>
|
||||
<div class="actions">
|
||||
<button id="backBtn" class="outline" aria-label="Go Back">Go Back</button>
|
||||
<button id="tryHttps" class="primary" aria-label="Retry with HTTPS">Try HTTPS</button>
|
||||
<button id="continueBtn" class="danger" aria-label="Continue (HTTP)">Continue Anyway</button>
|
||||
</div>
|
||||
<div class="mini">Nebula Secure Navigation Interstitial</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const params = new URLSearchParams(location.search);
|
||||
const target = params.get('target');
|
||||
const box = document.getElementById('targetBox');
|
||||
if (target) box.textContent = target;
|
||||
function sendNavigate(url, opts){
|
||||
if (window.electronAPI && window.electronAPI.sendToHost){
|
||||
window.electronAPI.sendToHost('navigate', url, opts||{});
|
||||
} else if (window.parent) {
|
||||
window.parent.postMessage({ type:'navigate', url, opts }, '*');
|
||||
}
|
||||
}
|
||||
document.getElementById('backBtn').onclick = () => history.length > 1 ? history.back() : sendNavigate('browser://home');
|
||||
document.getElementById('tryHttps').onclick = () => {
|
||||
if (!target) return; try {
|
||||
const u = new URL(target.replace(/^http:/,'https:'));
|
||||
sendNavigate(u.href);
|
||||
} catch { sendNavigate(target.replace(/^http:/,'https:')); }
|
||||
};
|
||||
document.getElementById('continueBtn').onclick = () => {
|
||||
if (!target) return; sendNavigate(target, { insecureBypass:true });
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+67
-5
@@ -52,7 +52,9 @@ urlBox.addEventListener('keydown', (e) => {
|
||||
|
||||
let tabs = [];
|
||||
let activeTabId = null;
|
||||
const allowedInternalPages = ['settings', 'home', 'downloads', 'nebot'];
|
||||
const allowedInternalPages = ['settings', 'home', 'downloads', 'nebot', 'insecure'];
|
||||
// Session-scoped allowlist of HTTP hosts the user explicitly chose to proceed with.
|
||||
const insecureBypassedHosts = new Set();
|
||||
let pluginPages = []; // { id, file, fileUrl, pluginId }
|
||||
let pluginPagesReady = false;
|
||||
const pendingInternalNavigations = [];
|
||||
@@ -65,6 +67,16 @@ window.addEventListener('message', (e) => {
|
||||
if (data.type === 'open-internal-page' && typeof data.url === 'string') {
|
||||
console.log('[DEBUG] Message request to open internal page:', data.url);
|
||||
createTab(data.url);
|
||||
} else if (data.type === 'navigate' && typeof data.url === 'string') {
|
||||
// Fallback navigation from pages (like insecure.html) when electronAPI.sendToHost is unavailable
|
||||
try {
|
||||
if (data.opts && data.opts.insecureBypass && /^http:\/\//i.test(data.url)) {
|
||||
const h = new URL(data.url).hostname;
|
||||
insecureBypassedHosts.add(h);
|
||||
}
|
||||
} catch {}
|
||||
urlBox.value = data.url;
|
||||
navigate();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[DEBUG] open-internal-page handler error', err);
|
||||
@@ -300,6 +312,13 @@ function createTab(inputUrl) {
|
||||
if (e.channel === 'navigate' && e.args[0]) {
|
||||
const targetUrl = e.args[0];
|
||||
const opts = e.args[1] || {};
|
||||
// If user accepted insecure warning, record host to bypass for session
|
||||
try {
|
||||
if (opts.insecureBypass && /^http:\/\//i.test(targetUrl)) {
|
||||
const h = new URL(targetUrl).hostname;
|
||||
insecureBypassedHosts.add(h);
|
||||
}
|
||||
} catch {}
|
||||
if (opts.newTab) {
|
||||
createTab(targetUrl);
|
||||
} else {
|
||||
@@ -338,7 +357,10 @@ try { window.createTab = createTab; } catch {}
|
||||
function resolveInternalUrl(url) {
|
||||
console.log('[DEBUG] resolveInternalUrl called with:', url);
|
||||
if (url.startsWith('browser://')) {
|
||||
const page = url.replace('browser://', '');
|
||||
// Support query / hash on internal pages (e.g., browser://insecure?target=...)
|
||||
const tail = url.replace('browser://', '');
|
||||
const page = tail.split(/[?#]/)[0];
|
||||
const suffix = tail.slice(page.length); // includes ? and/or # if present
|
||||
console.log('[DEBUG] Extracted page:', page);
|
||||
// Fast path: if user typed browser://nebot and plugin page exists, return immediately
|
||||
if (page === 'nebot') {
|
||||
@@ -360,12 +382,12 @@ function resolveInternalUrl(url) {
|
||||
// Prefer pre-built fileUrl for correctness across platforms
|
||||
const resolved = plug.fileUrl ? plug.fileUrl : (plug.file.startsWith('file://') ? plug.file : 'file://' + plug.file.replace(/\\/g,'/'));
|
||||
console.log('[DEBUG] Resolved plugin page', page, '->', resolved);
|
||||
return resolved;
|
||||
return resolved + suffix;
|
||||
}
|
||||
// Fallback: built-in renderer copy (e.g., renderer/nebot.html)
|
||||
console.log('[DEBUG] Using fallback for page:', page);
|
||||
if (page === 'nebot') return 'nebot.html';
|
||||
return `${page}.html`;
|
||||
if (page === 'nebot') return 'nebot.html' + suffix;
|
||||
return `${page}.html${suffix}`;
|
||||
}
|
||||
console.log('[DEBUG] Page not in allowedInternalPages, returning 404');
|
||||
return '404.html';
|
||||
@@ -415,6 +437,35 @@ function performNavigation(input, originalInputForHistory) {
|
||||
|
||||
console.log('[DEBUG] performNavigation input:', input, 'resolved:', resolved, 'tab.isHome:', tab.isHome, 'isInternal:', isInternal);
|
||||
|
||||
// Intercept plain HTTP (not HTTPS) navigations (excluding localhost / 127.* / internal pages)
|
||||
try {
|
||||
if (!isInternal && /^http:\/\//i.test(resolved)) {
|
||||
const u = new URL(resolved);
|
||||
const host = u.hostname;
|
||||
const isLoopback = /^(localhost|127\.0\.0\.1|::1)$/.test(host);
|
||||
if (!isLoopback && !insecureBypassedHosts.has(host)) {
|
||||
const encoded = encodeURIComponent(resolved);
|
||||
// Directly load insecure.html (avoid custom scheme so OS doesn't try to resolve an external handler)
|
||||
const interstitial = `insecure.html?target=${encoded}`;
|
||||
// For a fresh home tab, convert directly to webview showing the interstitial
|
||||
if (tab.isHome) {
|
||||
convertHomeTabToWebview(tab.id, originalInputForHistory, interstitial);
|
||||
return;
|
||||
}
|
||||
// Navigate existing webview to interstitial instead
|
||||
const webviewExisting = document.getElementById(`tab-${activeTabId}`);
|
||||
if (webviewExisting) webviewExisting.src = interstitial;
|
||||
tab.history = tab.history.slice(0, tab.historyIndex + 1);
|
||||
tab.history.push(originalInputForHistory);
|
||||
tab.historyIndex++;
|
||||
tab.url = originalInputForHistory;
|
||||
scheduleRenderTabs();
|
||||
scheduleUpdateNavButtons();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) { debug('[DEBUG] HTTP interception error', e); }
|
||||
|
||||
if (tab.isHome && !isInternal) {
|
||||
convertHomeTabToWebview(tab.id, originalInputForHistory, resolved);
|
||||
return;
|
||||
@@ -528,6 +579,17 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
|
||||
if (e.channel === 'theme-update') {
|
||||
const home = document.getElementById('home-webview');
|
||||
if (home) home.send('theme-update', ...e.args);
|
||||
} else if (e.channel === 'navigate' && e.args[0]) {
|
||||
const targetUrl = e.args[0];
|
||||
const opts = e.args[1] || {};
|
||||
try {
|
||||
if (opts.insecureBypass && /^http:\/\//i.test(targetUrl)) {
|
||||
const h = new URL(targetUrl).hostname;
|
||||
insecureBypassedHosts.add(h);
|
||||
}
|
||||
} catch {}
|
||||
urlBox.value = targetUrl;
|
||||
navigate();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user