fix(pwa): resolve proxy connection reset and manifest same-origin warnings
CI / Build & Test (push) Successful in 2m47s
CI / Build & Test (push) Successful in 2m47s
Fix "Proxy error: connection reset by peer" caused by dashboard-server.js looking for files in native-host/ instead of extension/dashboard/. Remove the devBasePath parameter and resolve DEV_DASHBOARD_DIR directly from __dirname so dev and distribution mode both find the correct files. Fix all manifest warnings (start_url/id/scope/protocol_handlers ignored) by reverting manifest.webmanifest to relative/extension-origin URLs for the chrome-extension:// context, and having dashboard-server.js dynamically generate a manifest at /manifest.webmanifest with absolute https://my.dash.board/ URLs — satisfying Chrome's same-origin requirement for PWA installability. Also serve extension icons at https://my.dash.board/icons/<file> with correct bundle key mapping, and embed them in the distributable binary via build-distributable.js.
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"id": "https://my.dash.board/",
|
"id": "/dashboard/dashboard.html",
|
||||||
"name": "Holesail Browser",
|
"name": "Holesail Browser",
|
||||||
"short_name": "Holesail",
|
"short_name": "Holesail",
|
||||||
"description": "P2P Holesail tunnels in the browser",
|
"description": "P2P Holesail tunnels in the browser",
|
||||||
"start_url": "https://my.dash.board/",
|
"start_url": "dashboard.html",
|
||||||
"scope": "https://my.dash.board/",
|
"scope": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"display_override": ["window-controls-overlay", "standalone"],
|
"display_override": ["window-controls-overlay", "standalone"],
|
||||||
"background_color": "#0f1117",
|
"background_color": "#0f1117",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"protocol_handlers": [
|
"protocol_handlers": [
|
||||||
{
|
{
|
||||||
"protocol": "web+holesail",
|
"protocol": "web+holesail",
|
||||||
"url": "https://my.dash.board/?lookup=%s"
|
"url": "dashboard.html?lookup=%s"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ const path = require('bare-path');
|
|||||||
|
|
||||||
const { allocateTunnelPort, releaseTunnelPort } = require('./holesail-manager/port-allocator.js');
|
const { allocateTunnelPort, releaseTunnelPort } = require('./holesail-manager/port-allocator.js');
|
||||||
|
|
||||||
|
// Resolve the extension/dashboard directory for dev-mode file serving.
|
||||||
|
// In dev mode, __dirname is native-host/ so the dashboard is one level up.
|
||||||
|
// In distribution mode this path is never used (files come from the bundle).
|
||||||
|
const DEV_DASHBOARD_DIR = path.join(__dirname, '..', 'extension', 'dashboard');
|
||||||
|
|
||||||
const DASHBOARD_HOSTNAME = 'my.dash.board';
|
const DASHBOARD_HOSTNAME = 'my.dash.board';
|
||||||
|
|
||||||
const MIME_TYPES = {
|
const MIME_TYPES = {
|
||||||
@@ -35,7 +40,6 @@ const MIME_TYPES = {
|
|||||||
|
|
||||||
let _server = null;
|
let _server = null;
|
||||||
let _port = null;
|
let _port = null;
|
||||||
let _baseDir = null; // fallback for dev mode
|
|
||||||
|
|
||||||
function _getMimeType(filePath) {
|
function _getMimeType(filePath) {
|
||||||
const ext = path.extname(filePath).toLowerCase();
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
@@ -43,51 +47,46 @@ function _getMimeType(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a dashboard file. In distribution mode, reads from the bare-bundle asset
|
* Read a file by its bundle key and disk-relative path.
|
||||||
* embedded at build time. In development mode, reads from disk.
|
* In distribution mode, reads from the bare-bundle asset embedded at build time.
|
||||||
* @param {string} relPath - Relative path within the dashboard directory (e.g. 'dashboard.html').
|
* 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.
|
* @returns {Buffer|null} File contents or null if not found.
|
||||||
*/
|
*/
|
||||||
function _readFile(relPath) {
|
function _readFileByKey(bundleKey, diskRelPath) {
|
||||||
// Normalise path separators and strip leading slashes/dots
|
|
||||||
const safe = relPath.replace(/\\/g, '/').replace(/^[./]+/, '');
|
|
||||||
|
|
||||||
// Distribution mode: read from bare-bundle assets
|
// Distribution mode: read from bare-bundle assets
|
||||||
try {
|
try {
|
||||||
if (typeof require.bundle === 'function') {
|
if (typeof require.bundle === 'function') {
|
||||||
const b = require.bundle();
|
const b = require.bundle();
|
||||||
if (b) {
|
if (b) {
|
||||||
const buf = b.read('/dashboard/' + safe);
|
const buf = b.read(bundleKey);
|
||||||
if (buf !== null) return buf;
|
if (buf !== null) return buf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
// Development mode: read from disk
|
// Development mode: read from disk
|
||||||
if (_baseDir) {
|
|
||||||
try {
|
try {
|
||||||
const fs = require('bare-fs');
|
const fs = require('bare-fs');
|
||||||
const fullPath = path.join(_baseDir, safe);
|
const fullPath = path.join(DEV_DASHBOARD_DIR, diskRelPath);
|
||||||
return fs.readFileSync(fullPath);
|
return fs.readFileSync(fullPath);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the dashboard HTTP server.
|
* Start the dashboard HTTP server.
|
||||||
* @param {string} [devBasePath] - Base directory for dev-mode file serving (optional).
|
|
||||||
* @returns {Promise<{port: number}>}
|
* @returns {Promise<{port: number}>}
|
||||||
*/
|
*/
|
||||||
function start(devBasePath) {
|
function start() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (_server) {
|
if (_server) {
|
||||||
resolve({ port: _port });
|
resolve({ port: _port });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_baseDir = devBasePath || null;
|
|
||||||
_port = allocateTunnelPort();
|
_port = allocateTunnelPort();
|
||||||
|
|
||||||
const tcpServer = tcp.createServer();
|
const tcpServer = tcp.createServer();
|
||||||
@@ -98,9 +97,55 @@ function start(devBasePath) {
|
|||||||
let urlPath = (req.url || '/').split('?')[0];
|
let urlPath = (req.url || '/').split('?')[0];
|
||||||
if (urlPath === '/') urlPath = '/dashboard.html';
|
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
|
// Strip leading slash for file lookup
|
||||||
const relPath = urlPath.replace(/^\/+/, '');
|
let relPath = urlPath.replace(/^\/+/, '');
|
||||||
const buf = _readFile(relPath);
|
|
||||||
|
// Icons are stored in extension/icons/ (one level up from dashboard/).
|
||||||
|
// Remap /icons/<file> → ../icons/<file> for the bundle key and disk path.
|
||||||
|
let bundleKey = '/dashboard/' + relPath;
|
||||||
|
let diskRelPath = relPath;
|
||||||
|
if (relPath.startsWith('icons/')) {
|
||||||
|
bundleKey = '/' + relPath; // stored as /icons/<file> in bundle
|
||||||
|
diskRelPath = '../icons/' + relPath.slice('icons/'.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = _readFileByKey(bundleKey, diskRelPath);
|
||||||
|
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
@@ -112,8 +157,6 @@ function start(devBasePath) {
|
|||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Type', _getMimeType(relPath));
|
res.setHeader('Content-Type', _getMimeType(relPath));
|
||||||
res.setHeader('Content-Length', buf.length);
|
res.setHeader('Content-Length', buf.length);
|
||||||
// Allow the extension's chrome.* APIs to work when loaded in a PWA window
|
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
||||||
res.end(buf);
|
res.end(buf);
|
||||||
});
|
});
|
||||||
conn.on('error', () => {});
|
conn.on('error', () => {});
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { log, debugLog } = require('./logger.js');
|
const { log, debugLog } = require('./logger.js');
|
||||||
const { BASE_DIR } = require('./paths.js');
|
|
||||||
const dashboardServer = require('../dashboard-server.js');
|
const dashboardServer = require('../dashboard-server.js');
|
||||||
|
|
||||||
const PROXY_PORT = 8443;
|
const PROXY_PORT = 8443;
|
||||||
@@ -118,7 +117,7 @@ function initStartup(holesailManager, certificateAuthority, httpsProxy, connectP
|
|||||||
// local virtual host so the HTTPS proxy can serve it over a trusted
|
// local virtual host so the HTTPS proxy can serve it over a trusted
|
||||||
// HTTPS origin (satisfying Chrome's PWA installability requirement).
|
// HTTPS origin (satisfying Chrome's PWA installability requirement).
|
||||||
try {
|
try {
|
||||||
const { port: dashPort } = await dashboardServer.start(BASE_DIR);
|
const { port: dashPort } = await dashboardServer.start();
|
||||||
holesailManager.setLocalVirtualHost(dashboardServer.getHostname(), dashPort);
|
holesailManager.setLocalVirtualHost(dashboardServer.getHostname(), dashPort);
|
||||||
log('Dashboard server ready on port', dashPort, '→ https://' + dashboardServer.getHostname() + '/');
|
log('Dashboard server ready on port', dashPort, '→ https://' + dashboardServer.getHostname() + '/');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const NATIVE_HOST_DIR = path.join(ROOT, 'native-host')
|
|||||||
const RELEASES_DIR = path.join(ROOT, 'releases')
|
const RELEASES_DIR = path.join(ROOT, 'releases')
|
||||||
const ENTRY = path.join(NATIVE_HOST_DIR, 'index.mjs')
|
const ENTRY = path.join(NATIVE_HOST_DIR, 'index.mjs')
|
||||||
const EXTENSION_DASHBOARD_DIR = path.join(ROOT, 'extension', 'dashboard')
|
const EXTENSION_DASHBOARD_DIR = path.join(ROOT, 'extension', 'dashboard')
|
||||||
|
const EXTENSION_ICONS_DIR = path.join(ROOT, 'extension', 'icons')
|
||||||
|
|
||||||
const ALL_HOSTS = [
|
const ALL_HOSTS = [
|
||||||
'darwin-arm64',
|
'darwin-arm64',
|
||||||
@@ -281,6 +282,24 @@ module.exports = EventEmitter;
|
|||||||
} else {
|
} else {
|
||||||
console.warn(' WARNING: extension/dashboard not found — dashboard assets not embedded')
|
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/<file> by dashboard-server.js.
|
||||||
|
// They live in extension/icons/ (outside the dashboard dir) so they need a
|
||||||
|
// separate embedding pass with keys like /icons/<file>.
|
||||||
|
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) {
|
async function build(hosts, doPackage) {
|
||||||
|
|||||||
Reference in New Issue
Block a user