From f069058c98b029f85a7f11407526ba9f10a5501a Mon Sep 17 00:00:00 2001 From: Raven Scott Date: Sun, 1 Mar 2026 04:00:29 -0500 Subject: [PATCH] evert: remove experimental my.dash.board PWA virtual host Reverts the experimental feature that attempted to serve the dashboard via a local HTTP server registered as a hardcoded `my.dash.board` virtual host, intended to satisfy Chrome's PWA "secure origin" installability requirement. The approach caused proxy connection errors and file-not-found issues that were not worth resolving. Removed: native-host/dashboard-server.js, native-host/test-chain.mjs Reverted: startup.js, message-router.js, virtual-hosts.js, index.js, build-distributable.js, extension/pages/virtual-hosts.js, CI workflows (ci.yml, release.yml) --- .gitea/workflows/ci.yml | 8 - .gitea/workflows/release.yml | 7 - extension/dashboard/pages/virtual-hosts.js | 13 +- native-host/dashboard-server.js | 249 ------------------ native-host/holesail-manager/index.js | 2 - native-host/holesail-manager/virtual-hosts.js | 26 +- native-host/host/message-router.js | 2 - native-host/host/startup.js | 12 - native-host/test-chain.mjs | 11 - scripts/build-distributable.js | 53 ---- 10 files changed, 6 insertions(+), 377 deletions(-) delete mode 100644 native-host/dashboard-server.js delete mode 100644 native-host/test-chain.mjs diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index af4c28f..1cf6e42 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -76,7 +76,6 @@ jobs: node --check native-host/https-proxy.js node --check native-host/connect-proxy.js node --check native-host/messenger.js - node --check native-host/dashboard-server.js node --check native-host/host/paths.js node --check native-host/host/logger.js node --check native-host/host/startup.js @@ -149,13 +148,6 @@ jobs: echo "$OUTPUT" | grep -i "not available" fi - if echo "$OUTPUT" | grep -q "\[dashboard-server\] listening on"; then - echo "PASS: dashboard server started (my.dash.board virtual host registered)" - else - echo "FAIL: dashboard server did not start — bundle assets may be missing" - exit 1 - fi - echo "PASS: smoke test complete" - name: Generate checksums diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 4a20042..40e1a85 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -71,13 +71,6 @@ jobs: exit 1 fi - if echo "$OUTPUT" | grep -q "\[dashboard-server\] listening on"; then - echo "PASS: dashboard server started (my.dash.board virtual host registered)" - else - echo "FAIL: dashboard server did not start — bundle assets may be missing" - exit 1 - fi - - name: List release artifacts run: | echo "=== releases/ ===" diff --git a/extension/dashboard/pages/virtual-hosts.js b/extension/dashboard/pages/virtual-hosts.js index e087eec..4e864f3 100644 --- a/extension/dashboard/pages/virtual-hosts.js +++ b/extension/dashboard/pages/virtual-hosts.js @@ -54,23 +54,20 @@ function updateConnectionsTable(state) { const openUrl = `https://${hostname}`; const safeHostname = hostname.replace(/"/g, '"'); const needsReconnect = v.state === 'error' || v.state === 'closed'; - const isBuiltIn = v.type === 'local'; return ` - ${isBuiltIn ? '' : ``} + ${escapeHtml(hostname)} - ${isBuiltIn ? 'built-in' : ''} - ${isBuiltIn ? '' : `
${truncate(hsUrl, 28)} -
`} + ${escapeHtml(backend)} @@ -83,14 +80,14 @@ function updateConnectionsTable(state) { Open - ${!isBuiltIn && needsReconnect ? `` : ''} - ${isBuiltIn ? '' : ``} + `; diff --git a/native-host/dashboard-server.js b/native-host/dashboard-server.js deleted file mode 100644 index 566607a..0000000 --- a/native-host/dashboard-server.js +++ /dev/null @@ -1,249 +0,0 @@ -/** - * Local HTTP server that serves the Holesail dashboard as a static site. - * - * In distribution mode (running as a standalone Bare binary), dashboard files - * are read from the bare-bundle assets embedded at build time via `require.bundle()`. - * - * In development mode (`bare index.mjs`), `require.bundle()` is not available, - * so files are read from disk relative to the source directory. - * - * The server is registered as the backend for the `my.dash.board` virtual host - * so the HTTPS proxy can serve the dashboard over a trusted HTTPS origin, - * satisfying Chrome's PWA installability requirement. - */ - -const http1 = require('bare-http1'); -const tcp = require('bare-tcp'); -const path = require('bare-path'); -const EventEmitter = require('bare-events'); - -const { allocateTunnelPort, releaseTunnelPort } = require('./holesail-manager/port-allocator.js'); - -// Resolve the extension/dashboard directory for dev-mode file serving. -// Uses the same logic as host/paths.js: in dev mode __dirname is the real -// native-host/ directory; in distribution mode the bundle read takes priority -// and this path is only used as a last-resort fallback. -function _resolveDevDashboardDir() { - // In dev mode __dirname is the real native-host/ path - if (process.stderr) process.stderr.write('[dashboard-server] __dirname=' + __dirname + '\n'); - if (__dirname && !__dirname.startsWith('bare:')) { - const d = path.join(__dirname, '..', 'extension', 'dashboard'); - if (process.stderr) process.stderr.write('[dashboard-server] DEV_DASHBOARD_DIR (from __dirname)=' + d + '\n'); - return d; - } - // Distribution mode: try executable directory as fallback - try { - const os = require('bare-os'); - const execPath = os.execPath(); - if (process.stderr) process.stderr.write('[dashboard-server] execPath=' + execPath + '\n'); - if (execPath && !execPath.startsWith('bare:')) { - const d = path.join(path.dirname(execPath), 'extension', 'dashboard'); - if (process.stderr) process.stderr.write('[dashboard-server] DEV_DASHBOARD_DIR (from execPath)=' + d + '\n'); - return d; - } - } catch (e) { - if (process.stderr) process.stderr.write('[dashboard-server] execPath error: ' + e.message + '\n'); - } - if (process.stderr) process.stderr.write('[dashboard-server] DEV_DASHBOARD_DIR=null\n'); - return null; -} -const DEV_DASHBOARD_DIR = _resolveDevDashboardDir(); - -const DASHBOARD_HOSTNAME = 'my.dash.board'; - -const MIME_TYPES = { - '.html': 'text/html; charset=utf-8', - '.css': 'text/css; charset=utf-8', - '.js': 'application/javascript; charset=utf-8', - '.json': 'application/json; charset=utf-8', - '.webmanifest': 'application/manifest+json; charset=utf-8', - '.png': 'image/png', - '.ico': 'image/x-icon', - '.svg': 'image/svg+xml; charset=utf-8', - '.woff': 'font/woff', - '.woff2': 'font/woff2' -}; - -let _server = null; -let _port = null; - -function _getMimeType(filePath) { - const ext = path.extname(filePath).toLowerCase(); - return MIME_TYPES[ext] || 'application/octet-stream'; -} - -/** - * Read a file by its bundle key and disk-relative path. - * In distribution mode, reads from the bare-bundle asset embedded at build time. - * In development mode, reads from disk relative to DEV_DASHBOARD_DIR. - * @param {string} bundleKey - Key in the bare-bundle (e.g. '/dashboard/dashboard.html'). - * @param {string} diskRelPath - Path relative to DEV_DASHBOARD_DIR (e.g. 'dashboard.html' or '../icons/48.png'). - * @returns {Buffer|null} File contents or null if not found. - */ -function _readFileByKey(bundleKey, diskRelPath) { - // Distribution mode: read from bare-bundle assets - try { - if (typeof require.bundle === 'function') { - const b = require.bundle(); - if (process.stderr) process.stderr.write('[dashboard-server] bundle=' + (b ? 'object' : 'null') + ' key=' + bundleKey + '\n'); - if (b) { - const buf = b.read(bundleKey); - if (process.stderr) process.stderr.write('[dashboard-server] bundle.read=' + (buf ? buf.length + ' bytes' : 'null') + '\n'); - if (buf !== null) return buf; - } - } else { - if (process.stderr) process.stderr.write('[dashboard-server] require.bundle not a function (type=' + typeof require.bundle + ')\n'); - } - } catch (e) { - if (process.stderr) process.stderr.write('[dashboard-server] bundle error: ' + e.message + '\n'); - } - - // Disk fallback (dev mode, or distribution mode without bundle assets) - if (DEV_DASHBOARD_DIR) { - try { - const fs = require('bare-fs'); - const fullPath = path.join(DEV_DASHBOARD_DIR, diskRelPath); - if (process.stderr) process.stderr.write('[dashboard-server] disk read: ' + fullPath + '\n'); - const buf = fs.readFileSync(fullPath); - if (process.stderr) process.stderr.write('[dashboard-server] disk read OK: ' + buf.length + ' bytes\n'); - return buf; - } catch (e) { - if (process.stderr) process.stderr.write('[dashboard-server] disk read error: ' + e.message + '\n'); - } - } else { - if (process.stderr) process.stderr.write('[dashboard-server] DEV_DASHBOARD_DIR is null, skipping disk read\n'); - } - - return null; -} - -/** - * Start the dashboard HTTP server. - * @returns {Promise<{port: number}>} - */ -function start() { - return new Promise((resolve, reject) => { - if (_server) { - resolve({ port: _port }); - return; - } - - _port = allocateTunnelPort(); - - const tcpServer = tcp.createServer(); - - // Minimal server object required by bare-http1 ServerConnection. - // ServerConnection emits 'request' on this object, not on itself. - const httpServer = new EventEmitter(); - httpServer.timeout = 0; - httpServer.closing = false; - httpServer.on('request', (req, res) => { - let urlPath = (req.url || '/').split('?')[0]; - if (urlPath === '/') urlPath = '/dashboard.html'; - - // Serve a dynamically generated manifest with absolute https://my.dash.board/ URLs - // so Chrome accepts start_url, id, scope, and protocol_handlers (same-origin requirement). - if (urlPath === '/manifest.webmanifest') { - const manifest = { - id: 'https://' + DASHBOARD_HOSTNAME + '/', - name: 'Holesail Browser', - short_name: 'Holesail', - description: 'P2P Holesail tunnels in the browser', - start_url: 'https://' + DASHBOARD_HOSTNAME + '/', - scope: 'https://' + DASHBOARD_HOSTNAME + '/', - display: 'standalone', - display_override: ['window-controls-overlay', 'standalone'], - background_color: '#0f1117', - theme_color: '#0f1117', - protocol_handlers: [ - { protocol: 'web+holesail', url: 'https://' + DASHBOARD_HOSTNAME + '/?lookup=%s' } - ], - screenshots: [ - { src: 'https://' + DASHBOARD_HOSTNAME + '/screenshots/wide.png', sizes: '1024x515', type: 'image/png', form_factor: 'wide', label: 'Holesail Browser Dashboard' }, - { src: 'https://' + DASHBOARD_HOSTNAME + '/screenshots/narrow.png', sizes: '390x844', type: 'image/png', form_factor: 'narrow', label: 'Holesail Browser Dashboard' } - ], - icons: [ - { src: 'https://' + DASHBOARD_HOSTNAME + '/icons/48.png', sizes: '48x48', type: 'image/png', purpose: 'any' }, - { src: 'https://' + DASHBOARD_HOSTNAME + '/icons/128.png', sizes: '128x128', type: 'image/png', purpose: 'any' }, - { src: 'https://' + DASHBOARD_HOSTNAME + '/icons/192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, - { src: 'https://' + DASHBOARD_HOSTNAME + '/icons/512.png', sizes: '512x512', type: 'image/png', purpose: 'any' } - ] - }; - const body = Buffer.from(JSON.stringify(manifest, null, 2)); - res.statusCode = 200; - res.setHeader('Content-Type', 'application/manifest+json; charset=utf-8'); - res.setHeader('Content-Length', body.length); - res.end(body); - return; - } - - // Strip leading slash for file lookup - let relPath = urlPath.replace(/^\/+/, ''); - - // Icons are stored in extension/icons/ (one level up from dashboard/). - // Remap /icons/ → ../icons/ for the bundle key and disk path. - let bundleKey = '/dashboard/' + relPath; - let diskRelPath = relPath; - if (relPath.startsWith('icons/')) { - bundleKey = '/' + relPath; // stored as /icons/ in bundle - diskRelPath = '../icons/' + relPath.slice('icons/'.length); - } - - const buf = _readFileByKey(bundleKey, diskRelPath); - - if (!buf) { - res.statusCode = 404; - res.setHeader('Content-Type', 'text/plain'); - res.end('Not found: ' + relPath); - return; - } - - res.statusCode = 200; - res.setHeader('Content-Type', _getMimeType(relPath)); - res.setHeader('Content-Length', buf.length); - res.end(buf); - }); - - tcpServer.on('connection', (socket) => { - // eslint-disable-next-line no-new - new http1.ServerConnection(httpServer, socket, {}); - }); - - tcpServer.on('error', (err) => { - releaseTunnelPort(_port); - _port = null; - _server = null; - reject(err); - }); - - tcpServer.listen(_port, '127.0.0.1', () => { - _server = tcpServer; - if (process.stderr) { - process.stderr.write('[dashboard-server] listening on 127.0.0.1:' + _port + '\n'); - } - resolve({ port: _port }); - }); - }); -} - -/** - * Stop the dashboard HTTP server and release its port. - */ -function stop() { - if (_server) { - try { _server.close(); } catch (_) {} - _server = null; - } - if (_port != null) { - releaseTunnelPort(_port); - _port = null; - } -} - -/** @returns {string} The fixed hostname for the dashboard virtual host. */ -function getHostname() { return DASHBOARD_HOSTNAME; } - -/** @returns {number|null} The port the server is listening on, or null if not started. */ -function getPort() { return _port; } - -module.exports = { start, stop, getHostname, getPort }; diff --git a/native-host/holesail-manager/index.js b/native-host/holesail-manager/index.js index dbc38bf..9fc49e4 100644 --- a/native-host/holesail-manager/index.js +++ b/native-host/holesail-manager/index.js @@ -30,7 +30,6 @@ function saveState() { id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || '' })); const virtualHostsList = vhostsModule.getVirtualHosts() - .filter(v => v.type !== 'local') // never persist built-in local hosts .map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl })); const serviceTunnelsList = svcModule.getServiceTunnels().map(t => ({ id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort @@ -125,7 +124,6 @@ module.exports = { getServers: serversModule.getServers, // Virtual hosts setVirtualHost: vhostsModule.setVirtualHost, - setLocalVirtualHost: vhostsModule.setLocalVirtualHost, removeVirtualHost: vhostsModule.removeVirtualHost, getVirtualHosts: vhostsModule.getVirtualHosts, getLocalPortForHostname: vhostsModule.getLocalPortForHostname, diff --git a/native-host/holesail-manager/virtual-hosts.js b/native-host/holesail-manager/virtual-hosts.js index dea109b..f2ed1c8 100644 --- a/native-host/holesail-manager/virtual-hosts.js +++ b/native-host/holesail-manager/virtual-hosts.js @@ -129,29 +129,6 @@ async function setVirtualHost(payload) { } } -/** - * Register a built-in local virtual host that is served by an internal HTTP - * server rather than a Holesail P2P tunnel. The entry is marked `type: 'local'` - * so it is never persisted to state.json and cannot be removed by the user. - * @param {string} hostname - The hostname (e.g. `my.dash.board`). - * @param {number} localPort - The local port the internal server is listening on. - */ -function setLocalVirtualHost(hostname, localPort) { - virtualHosts.set(hostname, { - hostname, - hsUrl: null, - holesail: null, - localHost: TUNNEL_HOST, - localPort, - state: 'ready', - type: 'local', - createdAt: Date.now(), - reconnectTimer: null, - reconnectDelay: 0 - }); - debugLog('setLocalVirtualHost: hostname=', hostname, 'localPort=', localPort); -} - /** * Remove a virtual host, close its tunnel, and release its local port. * @param {object} payload @@ -163,7 +140,6 @@ async function removeVirtualHost(payload) { debugLog('removeVirtualHost: hostname=', hostname); const entry = virtualHosts.get(hostname); if (!entry) return { ok: false, error: 'Virtual host not found' }; - if (entry.type === 'local') return { ok: false, error: 'Built-in host cannot be removed' }; if (entry.reconnectTimer) { clearTimeout(entry.reconnectTimer); entry.reconnectTimer = null; } if (entry.localPort) releaseTunnelPort(entry.localPort); if (entry.holesail) { try { await entry.holesail.close(); } catch (_) {} } @@ -229,4 +205,4 @@ async function cleanupVirtualHosts() { virtualHosts.clear(); } -module.exports = { init, setVirtualHost, setLocalVirtualHost, removeVirtualHost, getVirtualHosts, getLocalPortForHostname, getLocalBackend, getVirtualHostMap, cleanupVirtualHosts, RECONNECT_BASE_MS, RECONNECT_MAX_MS }; +module.exports = { init, setVirtualHost, removeVirtualHost, getVirtualHosts, getLocalPortForHostname, getLocalBackend, getVirtualHostMap, cleanupVirtualHosts, RECONNECT_BASE_MS, RECONNECT_MAX_MS }; diff --git a/native-host/host/message-router.js b/native-host/host/message-router.js index c77bf15..c8dbef5 100644 --- a/native-host/host/message-router.js +++ b/native-host/host/message-router.js @@ -16,7 +16,6 @@ const certificateAuthority = require('../certificate-authority.js'); const httpsProxy = require('../https-proxy.js'); const connectProxy = require('../connect-proxy.js'); const sshManager = require('../ssh-manager.js'); -const dashboardServer = require('../dashboard-server.js'); const backupManager = require('../backup-manager.js'); backupManager.setStoragePath(STORAGE_PATH); backupManager.setCertsPath(certificateAuthority.getCertsDir()); @@ -344,7 +343,6 @@ async function handleMessageAsync(send, msg) { function cleanup() { if (_scheduledBackupTimer) { clearTimeout(_scheduledBackupTimer); _scheduledBackupTimer = null; } - dashboardServer.stop(); httpsProxy.stop(() => {}); connectProxy.stop(() => {}); sshManager.cleanup(); diff --git a/native-host/host/startup.js b/native-host/host/startup.js index b82cc6c..9a319fe 100644 --- a/native-host/host/startup.js +++ b/native-host/host/startup.js @@ -5,7 +5,6 @@ */ const { log, debugLog } = require('./logger.js'); -const dashboardServer = require('../dashboard-server.js'); const PROXY_PORT = 8443; const CONNECT_PROXY_PORT = 8442; @@ -113,17 +112,6 @@ function initStartup(holesailManager, certificateAuthority, httpsProxy, connectP log('Proxies startup complete: HTTPS', savedProxyPort, 'CONNECT', connectProxy.getPort() ?? 'FAILED'); - // Start the dashboard HTTP server and register it as the my.dash.board - // local virtual host so the HTTPS proxy can serve it over a trusted - // HTTPS origin (satisfying Chrome's PWA installability requirement). - try { - const { port: dashPort } = await dashboardServer.start(); - holesailManager.setLocalVirtualHost(dashboardServer.getHostname(), dashPort); - log('Dashboard server ready on port', dashPort, '→ https://' + dashboardServer.getHostname() + '/'); - } catch (e) { - log('Dashboard server startup failed:', e.message); - } - // Assign tunnelsRestoredPromise BEFORE resolving proxiesReadyPromise so // that any getState call awaiting proxiesReadyPromise immediately sees the // non-null promise rather than the one-microtask window where it is null. diff --git a/native-host/test-chain.mjs b/native-host/test-chain.mjs deleted file mode 100644 index 58db7a9..0000000 --- a/native-host/test-chain.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import 'bare-process/global'; -import { createRequire } from 'bare-module'; -const require = createRequire(import.meta.url); - -// Simulate: startup.js (in host/) requires dashboard-server.js (in native-host/) -const startup_require = createRequire(new URL('./host/startup.js', import.meta.url)); -const dashServer = startup_require('../dashboard-server.js'); -console.log('dashServer.getHostname():', dashServer.getHostname()); -const result = await dashServer.start(); -console.log('start result:', result); -await dashServer.stop(); diff --git a/scripts/build-distributable.js b/scripts/build-distributable.js index 37ee374..8df0896 100644 --- a/scripts/build-distributable.js +++ b/scripts/build-distributable.js @@ -36,8 +36,6 @@ const ROOT = path.join(__dirname, '..') const NATIVE_HOST_DIR = path.join(ROOT, 'native-host') const RELEASES_DIR = path.join(ROOT, 'releases') const ENTRY = path.join(NATIVE_HOST_DIR, 'index.mjs') -const EXTENSION_DASHBOARD_DIR = path.join(ROOT, 'extension', 'dashboard') -const EXTENSION_ICONS_DIR = path.join(ROOT, 'extension', 'icons') const ALL_HOSTS = [ 'darwin-arm64', @@ -100,23 +98,6 @@ function getPlatformModule(host) { } } -/** - * Recursively list all files under a directory. - * @param {string} dir - Absolute path to the directory. - * @returns {string[]} Absolute paths to all files. - */ -function walkDir(dir) { - const results = [] - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name) - if (entry.isDirectory()) { - results.push(...walkDir(full)) - } else { - results.push(full) - } - } - return results -} /** * Patch bare-build's apple/sign.js to skip codesign when not on macOS. @@ -266,40 +247,6 @@ module.exports = EventEmitter; console.log(' Patched events module for node-rdpjs-2 compatibility') } - // ── Embed dashboard static files as bundle assets ─────────────────────────── - // The dashboard-server.js reads these via require.bundle().read('/dashboard/') - // at runtime, so no separate file copy step is needed in the installer. - if (fs.existsSync(EXTENSION_DASHBOARD_DIR)) { - const dashFiles = walkDir(EXTENSION_DASHBOARD_DIR) - let dashCount = 0 - for (const file of dashFiles) { - const rel = path.relative(EXTENSION_DASHBOARD_DIR, file).replace(/\\/g, '/') - const key = '/dashboard/' + rel - bundle.write(key, fs.readFileSync(file), { asset: true }) - dashCount++ - } - console.log(` Embedded ${dashCount} dashboard assets into bundle`) - } else { - console.warn(' WARNING: extension/dashboard not found — dashboard assets not embedded') - } - - // ── Embed extension icons as bundle assets ─────────────────────────────────── - // Icons are served at https://my.dash.board/icons/ by dashboard-server.js. - // They live in extension/icons/ (outside the dashboard dir) so they need a - // separate embedding pass with keys like /icons/. - if (fs.existsSync(EXTENSION_ICONS_DIR)) { - const iconFiles = walkDir(EXTENSION_ICONS_DIR) - let iconCount = 0 - for (const file of iconFiles) { - const rel = path.relative(EXTENSION_ICONS_DIR, file).replace(/\\/g, '/') - const key = '/icons/' + rel - bundle.write(key, fs.readFileSync(file), { asset: true }) - iconCount++ - } - console.log(` Embedded ${iconCount} icon assets into bundle`) - } else { - console.warn(' WARNING: extension/icons not found — icon assets not embedded') - } } async function build(hosts, doPackage) {