From ff41944f2cc3a10e1daae6f9f83d22c68c822c88 Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Wed, 8 Oct 2025 19:16:52 +1300 Subject: [PATCH] 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. --- renderer/insecure.html | 84 ++++++++++++++++++++++++++++++++++++++++++ renderer/script.js | 78 +++++++++++++++++++++++++++++++++++---- 2 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 renderer/insecure.html diff --git a/renderer/insecure.html b/renderer/insecure.html new file mode 100644 index 0000000..96bb670 --- /dev/null +++ b/renderer/insecure.html @@ -0,0 +1,84 @@ + + + + +Connection Not Secure + + + + +
+

+ + Connection Not Secure http +

+

You’re about to visit a page using HTTP (unencrypted). 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.

+
+ +
+ + + +
+
Nebula Secure Navigation Interstitial
+
+ + + diff --git a/renderer/script.js b/renderer/script.js index 071dc25..9023e4a 100644 --- a/renderer/script.js +++ b/renderer/script.js @@ -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,8 +312,15 @@ 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); + createTab(targetUrl); } else { urlBox.value = targetUrl; navigate(); @@ -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') { @@ -358,14 +380,14 @@ function resolveInternalUrl(url) { console.log('[DEBUG] Resolving browser://' + page, 'plug:', plug); if (plug && (plug.fileUrl || plug.file)) { // 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; + 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 + 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(); } });