fix: comprehensive bug fixes, security improvements, and feature additions
CI / Build & Test (push) Successful in 3m21s
CI / Build & Test (push) Successful in 3m21s
Critical fixes:
- Fix wrong registry key (com.bridgeswarm → com.holesail.browser) in
update-native-manifest-extension-id.ps1 — script was always failing on Windows
- Create missing wrong-domain.html redirect page for .host.test URLs
- Remove options_ui pointing to non-existent options.html from manifest
High-priority bug fixes:
- ssh-manager: track and kill orphaned printf FIFO writer when key auth succeeds
- ssh-manager: fix uncancelled 2000ms fallback password timer (assign to fallbackTimer,
clear in cancelPasswordWatch); fix null-check before removeAllListeners
- ssh-manager: add 30s Promise.race timeout to holesailInst.ready()
- backup-manager: fix macOS cp -R nesting bug by removing destination before copy;
add tar -tzf integrity check after archive creation
- host.js: restoreBackup now stops running tunnels before restore and re-starts them
- holesail-manager: fix stale closure bug in virtual host and service tunnel
error/close handlers (guard with v.holesail === hs check)
- dashboard.js: remove dead setText('dashTabs', ...) call referencing non-existent element
Medium improvements:
- manifest: remove unused storage and scripting permissions; restrict
web_accessible_resources match from <all_urls> to chrome-extension://*/*
- background.js: fix self-referential browser alias (globalThis.browser ?? chrome);
add 30s per-request timeout to send(); clean up dashboardTabs on tab close
- holesail-manager: gate saveStateSync stderr log behind DEBUG flag; updateSettings
now returns requiresRestart:true when proxy port changes; add backupRetention field
- host.js: pass requiresRestart through in updateSettings response
- dashboard.js: remove dead loadSettings() function; add requiresRestart warning toast;
add chrome.runtime.lastError guards in fetchState and refreshBackups;
set dynamic version from chrome.runtime.getManifest()
- dashboard.html: remove stray </button> tag; add id="sidebarVersion" for dynamic version
- install.sh/install.ps1: fetch version from RELEASE_BASE/VERSION instead of hardcoded 1.0.0
- install.ps1: add Firefox .xpi download and Firefox registry key
- update-native-manifest-extension-id.sh: add optional Firefox manifest update
- certificate-authority.js: defer RSA key generation to setImmediate to avoid blocking
startup; expose caReady promise
- host.js: await caReady before starting HTTPS proxy
Documentation:
- REMOTE-DESKTOP.md: correct RDP WebSocket protocol field names to match rdp-manager.js
(destLeft/destTop/destRight/destBottom, mouseMove/mouseButton/keyEvent/keyUnicode)
Feature additions:
- dashboard.js: add Reconnect button for service tunnels in error/closed state
- https-proxy.js: add WebSocket upgrade handler to support ws:// over *.hole.sail
- connect-proxy.js: add 10s header-read timeout to protect against idle connections
- native-host: add bare-fs as explicit dependency
This commit is contained in:
+32
-8
@@ -164,25 +164,49 @@ The dashboard connects to `ws://127.0.0.1:<wsPort>`. All frames are binary — r
|
||||
|
||||
### RDP
|
||||
|
||||
**Native host → browser (connection established):**
|
||||
```json
|
||||
{ "type": "connected", "width": 1280, "height": 720 }
|
||||
```
|
||||
|
||||
**Native host → browser (bitmap updates):**
|
||||
```json
|
||||
{
|
||||
"type": "bitmap",
|
||||
"x": 100, "y": 50,
|
||||
"width": 200, "height": 100,
|
||||
"destLeft": 100,
|
||||
"destTop": 50,
|
||||
"destRight": 300,
|
||||
"destBottom": 150,
|
||||
"width": 200,
|
||||
"height": 100,
|
||||
"bitsPerPixel": 32,
|
||||
"isCompress": false,
|
||||
"data": "<base64-encoded bitmap>"
|
||||
}
|
||||
```
|
||||
|
||||
**Browser → native host (input):**
|
||||
**Native host → browser (session events):**
|
||||
```json
|
||||
{ "type": "mousemove", "x": 150, "y": 75 }
|
||||
{ "type": "mousedown", "button": 1 }
|
||||
{ "type": "mouseup", "button": 1 }
|
||||
{ "type": "keydown", "key": 65 }
|
||||
{ "type": "keyup", "key": 65 }
|
||||
{ "type": "close" }
|
||||
{ "type": "error", "message": "Authentication failed" }
|
||||
```
|
||||
|
||||
**Browser → native host (mouse input):**
|
||||
```json
|
||||
{ "type": "mouseMove", "x": 150, "y": 75 }
|
||||
{ "type": "mouseButton", "x": 150, "y": 75, "button": 1, "isDown": true }
|
||||
{ "type": "mouseButton", "x": 150, "y": 75, "button": 1, "isDown": false }
|
||||
```
|
||||
|
||||
**Browser → native host (keyboard input):**
|
||||
```json
|
||||
{ "type": "keyEvent", "code": 65, "isDown": true }
|
||||
{ "type": "keyEvent", "code": 65, "isDown": false }
|
||||
{ "type": "keyUnicode", "code": 65, "isDown": true }
|
||||
```
|
||||
|
||||
> **Note:** `keyEvent` sends a scancode via `sendKeyEventScancode`; `keyUnicode` sends a Unicode code point via `sendKeyEventUnicode`. Use `keyEvent` for special keys (arrows, function keys, modifiers) and `keyUnicode` for printable characters.
|
||||
|
||||
---
|
||||
|
||||
## Port allocation
|
||||
|
||||
+17
-5
@@ -39,11 +39,9 @@ function broadcastLogs() {
|
||||
// Track dashboard tabs
|
||||
const dashboardTabs = new Set();
|
||||
|
||||
const browser = typeof chrome !== 'undefined' && chrome.runtime?.connectNative
|
||||
const browser = (typeof chrome !== 'undefined' && chrome.runtime?.connectNative)
|
||||
? chrome
|
||||
: typeof browser !== 'undefined'
|
||||
? browser
|
||||
: chrome;
|
||||
: (globalThis.browser ?? chrome);
|
||||
|
||||
let port = null;
|
||||
let reconnectDelay = INITIAL_RECONNECT_DELAY;
|
||||
@@ -313,6 +311,8 @@ function scheduleReconnect() {
|
||||
}, reconnectDelay);
|
||||
}
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 30000;
|
||||
|
||||
function send(msg) {
|
||||
const id = msg.id || `req_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
msg.id = id;
|
||||
@@ -324,11 +324,22 @@ function send(msg) {
|
||||
reject(new Error('Native host not connected'));
|
||||
return;
|
||||
}
|
||||
pending.set(id, { resolve, reject });
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id);
|
||||
debugLog('send: timeout for id=', id, 'type=', msg.type);
|
||||
reject(new Error('Request timed out: ' + msg.type));
|
||||
}
|
||||
}, REQUEST_TIMEOUT_MS);
|
||||
pending.set(id, {
|
||||
resolve: (v) => { clearTimeout(timeoutId); resolve(v); },
|
||||
reject: (e) => { clearTimeout(timeoutId); reject(e); }
|
||||
});
|
||||
try {
|
||||
port.postMessage(msg);
|
||||
} catch (e) {
|
||||
pending.delete(id);
|
||||
clearTimeout(timeoutId);
|
||||
debugLog('send: postMessage threw', e.message);
|
||||
reject(e);
|
||||
}
|
||||
@@ -379,6 +390,7 @@ browser.tabs.onRemoved.addListener((tabId) => {
|
||||
tabSwarms.delete(tabId);
|
||||
}
|
||||
subscribedTabs.delete(tabId);
|
||||
dashboardTabs.delete(tabId);
|
||||
});
|
||||
|
||||
// Content script talks to background via runtime.sendMessage / onMessage
|
||||
|
||||
@@ -885,7 +885,7 @@
|
||||
<div class="sidebar-logo-icon">⛵</div>
|
||||
<div class="sidebar-logo-text">
|
||||
<div class="sidebar-logo-name">Holesail</div>
|
||||
<div class="sidebar-logo-version">v1.0.0 · hole.sail</div>
|
||||
<div class="sidebar-logo-version" id="sidebarVersion">v1.0.0 · hole.sail</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1531,8 +1531,6 @@
|
||||
</main>
|
||||
</div><!-- /layout -->
|
||||
|
||||
</button>
|
||||
|
||||
<!-- ── Toast ─────────────────────────────────────────── -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
|
||||
+50
-14
@@ -176,18 +176,6 @@ function setupNavigation() {
|
||||
|
||||
// ── Settings ────────────────────────────────────────────────────────────────
|
||||
|
||||
function loadSettings() {
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'getSettings' } },
|
||||
(response) => {
|
||||
if (response && response.ok && response.settings) {
|
||||
settings = { ...SETTINGS_DEFAULTS, ...response.settings };
|
||||
}
|
||||
updateSettingsUI();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updateSettingsUI() {
|
||||
$('toggleNotify')?.classList.toggle('active', settings.notifyOnDisconnect === true);
|
||||
$('toggleDebug')?.classList.toggle('active', settings.debug === true);
|
||||
@@ -210,9 +198,14 @@ function saveSettings() {
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'updateSettings', payload: { ...settings } } },
|
||||
(response) => {
|
||||
if (chrome.runtime.lastError) { showToast('Settings save failed: ' + chrome.runtime.lastError.message, 'error'); return; }
|
||||
if (response && response.ok) {
|
||||
if (response.settings) settings = { ...SETTINGS_DEFAULTS, ...response.settings };
|
||||
showToast('Settings saved', 'success');
|
||||
if (response.requiresRestart) {
|
||||
showToast('Settings saved — restart the native host for proxy port changes to take effect', 'warning');
|
||||
} else {
|
||||
showToast('Settings saved', 'success');
|
||||
}
|
||||
} else {
|
||||
showToast(response?.error || 'Failed to save settings', 'error');
|
||||
}
|
||||
@@ -766,6 +759,7 @@ function refreshBackups() {
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'listBackups' } },
|
||||
(response) => {
|
||||
if (chrome.runtime.lastError) return;
|
||||
if (response && response.ok) {
|
||||
updateBackupsTable(response.backups || []);
|
||||
}
|
||||
@@ -865,6 +859,11 @@ async function fetchState() {
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'getState' },
|
||||
(response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
log('fetchState error:', chrome.runtime.lastError.message);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
if (response && response.ok) resolve(response.state);
|
||||
else resolve(null);
|
||||
}
|
||||
@@ -1065,7 +1064,6 @@ function updateDashboard(state) {
|
||||
setText('dashSsh', sshConnections.length);
|
||||
setText('dashRdp', rdpConnections.length);
|
||||
setText('dashUptime', state.caInstalled ? 'Trusted' : 'Not trusted');
|
||||
setText('dashTabs', state.proxyPort != null ? state.proxyPort : '—');
|
||||
|
||||
const caIcon = $('caStatIcon');
|
||||
if (caIcon) {
|
||||
@@ -1495,6 +1493,11 @@ function updateServiceTunnelsTable(state) {
|
||||
<td>${stateTag(t.state)}</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
${(t.state === 'error' || t.state === 'closed') ? `
|
||||
<button class="btn btn-ghost btn-sm" data-reconnect-svc="${safeId}" data-hsurl="${escapeHtml(hsUrl)}" data-label="${escapeHtml(label)}" data-localport="${t.localPort != null ? t.localPort : ''}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23,4 23,10 17,10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
Reconnect
|
||||
</button>` : ''}
|
||||
<button class="btn btn-ghost btn-sm" data-edit-service-tunnel="${safeId}" title="Edit tunnel">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||
Edit
|
||||
@@ -1512,6 +1515,29 @@ function updateServiceTunnelsTable(state) {
|
||||
btn.addEventListener('click', () => copyToClipboard(btn.dataset.copy, btn));
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('[data-reconnect-svc]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tunnelId = btn.dataset.reconnectSvc;
|
||||
const hsUrl = btn.dataset.hsurl;
|
||||
const label = btn.dataset.label;
|
||||
const localPort = parseInt(btn.dataset.localport, 10);
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Reconnecting…';
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'updateServiceTunnel', payload: { tunnelId, hsUrl, label, localPort } } },
|
||||
(response) => {
|
||||
if (chrome.runtime.lastError) { showToast('Reconnect failed: ' + chrome.runtime.lastError.message, 'error'); return; }
|
||||
if (response?.ok) {
|
||||
showToast('Service tunnel reconnecting…', 'success');
|
||||
} else {
|
||||
showToast(response?.error || 'Reconnect failed', 'error');
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('[data-edit-service-tunnel]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tunnelId = btn.dataset.editServiceTunnel;
|
||||
@@ -2302,6 +2328,16 @@ function setupSshEvents() {
|
||||
|
||||
async function init() {
|
||||
log('Dashboard initializing…');
|
||||
|
||||
// Set dynamic version from manifest
|
||||
try {
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const versionEl = $('sidebarVersion');
|
||||
if (versionEl && manifest.version) {
|
||||
versionEl.textContent = 'v' + manifest.version + ' · hole.sail';
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
setupNavigation();
|
||||
setupEvents();
|
||||
setupCertValidator();
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmpSyoCuwrSnyspandk7hcQ3rTVegPWPyABcn9qX9EMleN7FIPRN81Z31IPXnbMDfXavnAnUc0vEH1ujtv/PAjew58kN4CFUTE1opU15v6dzn+ltrtG1aX+9dwYAOBsxKFU5TzfXI4SvixWzEGjJ1Irh4QEYJh4/C1A8mOdF+VvIA4y5ko7npH+7SRmCnxQ+c4Jh7gmG7qvblEHZzvM5VxT0H4gXivxD7+ABDQFnlet8ojVgyN6azND4BuPER5672vRdzjr4Emd9bL1L3N9QzQzvT7tUdFkJ2Giph9Bog9yyymnxbb8S4u0u8OlDdLBQlCWrERoYxHG9QGZse1EFWsQIDAQAB",
|
||||
"permissions": [
|
||||
"nativeMessaging",
|
||||
"storage",
|
||||
"scripting",
|
||||
"notifications",
|
||||
"tabs",
|
||||
"declarativeNetRequest",
|
||||
@@ -37,10 +35,11 @@
|
||||
"vendor/xterm.js",
|
||||
"vendor/xterm-addon-fit.js",
|
||||
"vendor/xterm.css",
|
||||
"vendor/novnc.js"
|
||||
"vendor/novnc.js",
|
||||
"wrong-domain.html"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
"chrome-extension://*/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -55,9 +54,6 @@
|
||||
"strict_min_version": "109.0"
|
||||
}
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html"
|
||||
},
|
||||
"host_permissions": [
|
||||
"*://*.host.local/*",
|
||||
"*://*.hole.sail/*"
|
||||
|
||||
+54
-24
@@ -2,46 +2,76 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Holesail Browser – Use .hs</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wrong Domain — Holesail Browser</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #1a1d27;
|
||||
--card: #21253a;
|
||||
--accent: #4f8ef7;
|
||||
--text: #e2e8f0;
|
||||
--muted: #8892a4;
|
||||
--border: #2d3352;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: #0f0f14;
|
||||
color: #e4e4e7;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 1.5rem;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: #18181b;
|
||||
border: 1px solid #27272a;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
max-width: 440px;
|
||||
padding: 2.5rem 3rem;
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; color: #fff; }
|
||||
p { font-size: 0.9rem; color: #a1a1aa; margin: 0 0 1rem; line-height: 1.5; }
|
||||
.suggestion { background: #27272a; border-radius: 8px; padding: 1rem; margin: 1.25rem 0; text-align: left; word-break: break-all; }
|
||||
.suggestion a { color: #22d3ee; text-decoration: none; }
|
||||
.suggestion a:hover { text-decoration: underline; }
|
||||
.hint { font-size: 0.85rem; color: #71717a; margin-top: 1rem; }
|
||||
.icon { font-size: 3rem; margin-bottom: 1rem; }
|
||||
h1 { font-size: 1.4rem; margin-bottom: 0.75rem; }
|
||||
p { color: var(--muted); line-height: 1.6; margin-bottom: 1rem; }
|
||||
code {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.15em 0.4em;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--accent);
|
||||
}
|
||||
.host-display {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--accent);
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Holesail Browser</h1>
|
||||
<p id="message">Virtual hosts use the <strong>.hs</strong> domain, not .host.hs.</p>
|
||||
<p id="sub" class="hint"></p>
|
||||
<div class="suggestion" id="suggestion"></div>
|
||||
<p class="hint">Add the hostname and hs:// URL in the dashboard, then open the link above.</p>
|
||||
<div class="icon">⛵</div>
|
||||
<h1>Wrong Domain</h1>
|
||||
<p>You tried to visit a <code>.host.test</code> address. Holesail Browser uses the <code>.hole.sail</code> domain instead.</p>
|
||||
<div class="host-display" id="hostDisplay"></div>
|
||||
<p>Replace <code>.host.test</code> with <code>.hole.sail</code> in the URL and try again.</p>
|
||||
</div>
|
||||
<script src="wrong-domain.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const host = params.get('host') || '';
|
||||
const suggested = host.replace(/\.host\.test$/, '.hole.sail');
|
||||
document.getElementById('hostDisplay').textContent =
|
||||
host ? host + ' → ' + suggested : '(unknown host)';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -148,6 +148,14 @@ async function createBackup() {
|
||||
|
||||
cleanupStaging(stagingDir);
|
||||
|
||||
// Verify archive integrity before reporting success
|
||||
try {
|
||||
await runCommand('tar', ['-tzf', outPath]);
|
||||
} catch (e) {
|
||||
try { fs.unlinkSync(outPath); } catch (_) {}
|
||||
return { ok: false, error: 'Backup archive failed integrity check: ' + e.message };
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
try {
|
||||
const stat = fs.statSync(outPath);
|
||||
@@ -281,12 +289,18 @@ async function restoreBackup(filename) {
|
||||
let restoredCerts = false;
|
||||
|
||||
// Restore storage/
|
||||
// Remove existing destination entries before copying to avoid macOS cp -R
|
||||
// nesting bug (when dst already exists, cp -R src dst/ creates dst/src/).
|
||||
const extractedStorage = path.join(stagingDir, 'storage');
|
||||
if (fs.existsSync(extractedStorage)) {
|
||||
try {
|
||||
const entries = fs.readdirSync(extractedStorage);
|
||||
for (const entry of entries) {
|
||||
await runCommand('cp', ['-R', path.join(extractedStorage, entry), path.join(storageDir, entry)]);
|
||||
const dest = path.join(storageDir, entry);
|
||||
if (fs.existsSync(dest)) {
|
||||
await runCommand('rm', ['-rf', dest]);
|
||||
}
|
||||
await runCommand('cp', ['-R', path.join(extractedStorage, entry), dest]);
|
||||
}
|
||||
restoredStorage = true;
|
||||
} catch (e) {
|
||||
@@ -302,7 +316,11 @@ async function restoreBackup(filename) {
|
||||
if (!fs.existsSync(certsDir)) fs.mkdirSync(certsDir, { recursive: true });
|
||||
const entries = fs.readdirSync(extractedCerts);
|
||||
for (const entry of entries) {
|
||||
await runCommand('cp', ['-R', path.join(extractedCerts, entry), path.join(certsDir, entry)]);
|
||||
const dest = path.join(certsDir, entry);
|
||||
if (fs.existsSync(dest)) {
|
||||
await runCommand('rm', ['-rf', dest]);
|
||||
}
|
||||
await runCommand('cp', ['-R', path.join(extractedCerts, entry), dest]);
|
||||
}
|
||||
restoredCerts = true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -91,27 +91,17 @@ if (!fs.existsSync(certsDir)) {
|
||||
}
|
||||
}
|
||||
|
||||
let regenerated = false;
|
||||
if (fs.existsSync(caKeyPath) && fs.existsSync(caCertPath)) {
|
||||
try {
|
||||
const caCertPem = fs.readFileSync(caCertPath, 'utf8');
|
||||
const cert = forge.pki.certificateFromPem(caCertPem);
|
||||
if (cert.validity.notAfter > new Date()) {
|
||||
logDebug('CA', 'Existing Root CA is valid.');
|
||||
} else {
|
||||
logWarn('CA', 'Existing Root CA expired, regenerating.');
|
||||
regenerated = true;
|
||||
}
|
||||
} catch (e) {
|
||||
logWarn('CA', 'Could not read existing CA: ' + e.message + '; regenerating.');
|
||||
regenerated = true;
|
||||
}
|
||||
} else {
|
||||
regenerated = true;
|
||||
}
|
||||
// Determine whether CA generation is needed without doing the expensive RSA work yet.
|
||||
let _caReadyResolve = null;
|
||||
let _caReadyReject = null;
|
||||
const caReady = new Promise((resolve, reject) => {
|
||||
_caReadyResolve = resolve;
|
||||
_caReadyReject = reject;
|
||||
});
|
||||
|
||||
if (regenerated) {
|
||||
function _generateCA() {
|
||||
try {
|
||||
logInfo('CA', 'Generating Root CA (2048-bit RSA)...');
|
||||
const keys = forge.pki.rsa.generateKeyPair(2048);
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
@@ -140,8 +130,42 @@ if (regenerated) {
|
||||
fs.writeFileSync(caKeyPath, forge.pki.privateKeyToPem(keys.privateKey));
|
||||
fs.writeFileSync(caCertPath, forge.pki.certificateToPem(cert));
|
||||
logInfo('CA', 'Root CA generated.');
|
||||
_caReadyResolve();
|
||||
} catch (e) {
|
||||
logError('CA', 'Failed to generate Root CA: ' + e.message);
|
||||
_caReadyReject(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Defer CA generation to the next event-loop tick so it does not block
|
||||
// the native host startup (RSA key generation can take several seconds).
|
||||
let _needsGeneration = false;
|
||||
if (fs.existsSync(caKeyPath) && fs.existsSync(caCertPath)) {
|
||||
try {
|
||||
const caCertPem = fs.readFileSync(caCertPath, 'utf8');
|
||||
const cert = forge.pki.certificateFromPem(caCertPem);
|
||||
if (cert.validity.notAfter > new Date()) {
|
||||
logDebug('CA', 'Existing Root CA is valid.');
|
||||
_caReadyResolve();
|
||||
} else {
|
||||
logWarn('CA', 'Existing Root CA expired, regenerating.');
|
||||
_needsGeneration = true;
|
||||
}
|
||||
} catch (e) {
|
||||
logWarn('CA', 'Could not read existing CA: ' + e.message + '; regenerating.');
|
||||
_needsGeneration = true;
|
||||
}
|
||||
} else {
|
||||
_needsGeneration = true;
|
||||
}
|
||||
|
||||
if (_needsGeneration) {
|
||||
// Use setImmediate so the event loop can process the first native message
|
||||
// before the blocking RSA generation starts.
|
||||
if (typeof setImmediate === 'function') {
|
||||
setImmediate(_generateCA);
|
||||
} else {
|
||||
setTimeout(_generateCA, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,5 +440,7 @@ module.exports = {
|
||||
isRootCAInstalled,
|
||||
getOrCreateDomainCert,
|
||||
getCaCertPath,
|
||||
getCertsDir
|
||||
getCertsDir,
|
||||
/** Resolves when the CA is ready (either already existed or was generated). */
|
||||
caReady
|
||||
};
|
||||
|
||||
@@ -50,6 +50,8 @@ function start(port, upstreamPort, callback) {
|
||||
|
||||
stderrLog('starting on 127.0.0.1:' + connectPort + ' -> ' + UPSTREAM_HOST + ':' + upstream);
|
||||
|
||||
const HEADER_READ_TIMEOUT_MS = 10000;
|
||||
|
||||
try {
|
||||
server = tcp.createServer((clientSocket) => {
|
||||
debugLog('new client connection');
|
||||
@@ -58,6 +60,14 @@ function start(port, upstreamPort, callback) {
|
||||
const pendingChunks = [];
|
||||
let upstreamSocket = null;
|
||||
|
||||
// Protect against clients that connect but never send the CONNECT header
|
||||
const headerTimeout = setTimeout(() => {
|
||||
if (!tunneled) {
|
||||
debugLog('header read timeout — destroying idle client socket');
|
||||
clientSocket.destroy();
|
||||
}
|
||||
}, HEADER_READ_TIMEOUT_MS);
|
||||
|
||||
function flushPending() {
|
||||
if (!upstreamSocket) return;
|
||||
for (const c of pendingChunks) upstreamSocket.write(c);
|
||||
@@ -74,6 +84,7 @@ function start(port, upstreamPort, callback) {
|
||||
const rest = buffer.subarray(idx + HEADER_END.length);
|
||||
buffer = null;
|
||||
tunneled = true;
|
||||
clearTimeout(headerTimeout);
|
||||
if (rest.length > 0) pendingChunks.push(rest);
|
||||
|
||||
clientSocket.write(RESPONSE_200, (err) => {
|
||||
@@ -114,8 +125,12 @@ function start(port, upstreamPort, callback) {
|
||||
tryTunnel();
|
||||
});
|
||||
clientSocket.on('error', (err) => {
|
||||
clearTimeout(headerTimeout);
|
||||
debugLog('client socket error:', err.message);
|
||||
});
|
||||
clientSocket.on('close', () => {
|
||||
clearTimeout(headerTimeout);
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
stderrLog('createServer threw:', err.message);
|
||||
|
||||
@@ -111,7 +111,6 @@ function saveStateSync() {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, data, 'utf8');
|
||||
debugLog('state saved path=', file, 'servers=', serversList.length, 'vhosts=', virtualHostsList.length);
|
||||
if (process.stderr) process.stderr.write('[holesail-manager] state saved to ' + file + '\n');
|
||||
} catch (e) {
|
||||
if (process.stderr) process.stderr.write('[holesail-manager] state save failed: ' + e.message + ' (path: ' + file + ')\n');
|
||||
}
|
||||
@@ -240,19 +239,24 @@ function getSettings() {
|
||||
}
|
||||
|
||||
function updateSettings(patch) {
|
||||
if (!patch || typeof patch !== 'object') return;
|
||||
if (!patch || typeof patch !== 'object') return { requiresRestart: false };
|
||||
let requiresRestart = false;
|
||||
if (typeof patch.proxyPort === 'number' && patch.proxyPort > 0 && patch.proxyPort < 65536) {
|
||||
if (patch.proxyPort !== currentSettings.proxyPort) requiresRestart = true;
|
||||
currentSettings.proxyPort = patch.proxyPort;
|
||||
runtimeProxyPort = patch.proxyPort;
|
||||
}
|
||||
if (typeof patch.connectProxyPort === 'number' && patch.connectProxyPort > 0 && patch.connectProxyPort < 65536) {
|
||||
if (patch.connectProxyPort !== currentSettings.connectProxyPort) requiresRestart = true;
|
||||
currentSettings.connectProxyPort = patch.connectProxyPort;
|
||||
}
|
||||
if (typeof patch.readyTimeoutMs === 'number') currentSettings.readyTimeoutMs = patch.readyTimeoutMs;
|
||||
if (typeof patch.notifyOnDisconnect === 'boolean') currentSettings.notifyOnDisconnect = patch.notifyOnDisconnect;
|
||||
if (typeof patch.debug === 'boolean') currentSettings.debug = patch.debug;
|
||||
if (typeof patch.disableOnFileUrls === 'boolean') currentSettings.disableOnFileUrls = patch.disableOnFileUrls;
|
||||
if (typeof patch.backupRetention === 'number') currentSettings.backupRetention = patch.backupRetention;
|
||||
saveStateSync();
|
||||
return { requiresRestart };
|
||||
}
|
||||
|
||||
// ── SSH Connections ───────────────────────────────────────────────────────────
|
||||
@@ -406,12 +410,13 @@ async function setVirtualHost(payload) {
|
||||
hs.on('error', (err) => {
|
||||
if (process.stderr) process.stderr.write('[holesail-manager] tunnel error ' + hostname + ': ' + (err && err.message) + '\n');
|
||||
const v = virtualHosts.get(hostname);
|
||||
if (v) v.state = 'error';
|
||||
// Guard against stale closures: only mutate the entry that owns this instance
|
||||
if (v && v.holesail === hs) v.state = 'error';
|
||||
emit('tunnelError', { hostname, error: err && err.message });
|
||||
});
|
||||
hs.on('close', () => {
|
||||
const v = virtualHosts.get(hostname);
|
||||
if (v) v.state = 'closed';
|
||||
if (v && v.holesail === hs) v.state = 'closed';
|
||||
emit('tunnelClosed', { hostname });
|
||||
});
|
||||
}
|
||||
@@ -611,12 +616,13 @@ async function startServiceTunnel(payload) {
|
||||
hs.on('error', (err) => {
|
||||
if (process.stderr) process.stderr.write('[holesail-manager] service tunnel error ' + tunnelId + ': ' + (err && err.message) + '\n');
|
||||
const t = serviceTunnels.get(tunnelId);
|
||||
if (t) t.state = 'error';
|
||||
// Guard against stale closures: only mutate the entry that owns this instance
|
||||
if (t && t.holesail === hs) t.state = 'error';
|
||||
emit('serviceTunnelError', { tunnelId, label, error: err && err.message });
|
||||
});
|
||||
hs.on('close', () => {
|
||||
const t = serviceTunnels.get(tunnelId);
|
||||
if (t) t.state = 'closed';
|
||||
if (t && t.holesail === hs) t.state = 'closed';
|
||||
emit('serviceTunnelClosed', { tunnelId, label });
|
||||
});
|
||||
}
|
||||
|
||||
+15
-4
@@ -49,6 +49,13 @@ if (typeof setImmediate === 'function') {
|
||||
const savedProxyPort = (restored.settings && restored.settings.proxyPort) || PROXY_PORT;
|
||||
const savedConnectPort = (restored.settings && restored.settings.connectProxyPort) || CONNECT_PROXY_PORT;
|
||||
|
||||
// Wait for CA to be ready before starting the HTTPS proxy (which needs the CA cert)
|
||||
try {
|
||||
await certificateAuthority.caReady;
|
||||
} catch (e) {
|
||||
log('CA generation failed:', e.message, '— HTTPS proxy may not work correctly');
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise((res, rej) => {
|
||||
httpsProxy.start(savedProxyPort, certificateAuthority, (err) => {
|
||||
@@ -223,9 +230,9 @@ async function handleMessageAsync(send, msg) {
|
||||
break;
|
||||
}
|
||||
case 'updateSettings': {
|
||||
holesailManager.updateSettings(payload);
|
||||
debugLog('updateSettings: applied', JSON.stringify(payload));
|
||||
reply({ ok: true, settings: holesailManager.getSettings() });
|
||||
const { requiresRestart } = holesailManager.updateSettings(payload);
|
||||
debugLog('updateSettings: applied', JSON.stringify(payload), 'requiresRestart=', requiresRestart);
|
||||
reply({ ok: true, settings: holesailManager.getSettings(), requiresRestart });
|
||||
break;
|
||||
}
|
||||
case 'getSshConnections': {
|
||||
@@ -386,8 +393,12 @@ async function handleMessageAsync(send, msg) {
|
||||
case 'restoreBackup': {
|
||||
const restoreResult = await backupManager.restoreBackup(payload.filename);
|
||||
if (restoreResult.ok) {
|
||||
// Stop all running tunnels so in-memory state matches the restored state.json
|
||||
await holesailManager.cleanup().catch(() => {});
|
||||
// Reload state from disk after restore
|
||||
holesailManager.restorePersistedState();
|
||||
const restored = holesailManager.restorePersistedState();
|
||||
// Re-start the tunnels described in the restored state
|
||||
tunnelsRestoredPromise = restorePersistedTunnels(restored).catch((e) => log('Post-restore tunnel start failed:', e.message));
|
||||
}
|
||||
reply(restoreResult);
|
||||
break;
|
||||
|
||||
@@ -77,6 +77,8 @@ function start(port, certsDirOrCA, callback) {
|
||||
}
|
||||
try {
|
||||
proxyServer = https.createServer(opts, onRequest);
|
||||
// Forward WebSocket upgrade requests to the backend tunnel
|
||||
proxyServer.on('upgrade', onUpgrade);
|
||||
} catch (err) {
|
||||
if (process.stderr) process.stderr.write('[https-proxy] createServer threw: ' + err.message + '\n');
|
||||
done(err);
|
||||
@@ -236,6 +238,67 @@ h1{color:#c0392b}code{background:#f4f4f4;padding:2px 6px;border-radius:3px;font-
|
||||
req.on('end', () => proxyReq.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WebSocket upgrade requests by piping the raw socket to the backend.
|
||||
* This allows ws:// / wss:// connections through *.hole.sail virtual hosts.
|
||||
*/
|
||||
function onUpgrade(req, socket, head) {
|
||||
const hostHeader = req.headers && (req.headers.host || req.headers.Host);
|
||||
const hostname = hostHeader ? hostHeader.split(':')[0].trim() : '';
|
||||
debugLog('upgrade: hostname=', hostname, 'url=', req.url);
|
||||
|
||||
if (!getBackendForHostname || !hostname) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
const backend = getBackendForHostname(hostname);
|
||||
let targetHost = '127.0.0.1';
|
||||
let targetPort = null;
|
||||
if (backend != null && typeof backend === 'object' && typeof backend.port === 'number') {
|
||||
targetHost = backend.host ?? '127.0.0.1';
|
||||
targetPort = backend.port;
|
||||
} else if (typeof backend === 'number') {
|
||||
targetPort = backend;
|
||||
}
|
||||
|
||||
if (targetPort == null) {
|
||||
debugLog('upgrade: no backend for hostname=', hostname);
|
||||
socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Open a raw TCP connection to the backend and pipe the socket
|
||||
let net = null;
|
||||
try { net = require('bare-tcp'); } catch (_) {}
|
||||
if (!net) {
|
||||
debugLog('upgrade: bare-tcp not available');
|
||||
socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const upstream = net.connect(targetPort, targetHost, () => {
|
||||
// Reconstruct the HTTP upgrade request and forward it
|
||||
const headers = Object.entries(req.headers)
|
||||
.map(([k, v]) => k + ': ' + v)
|
||||
.join('\r\n');
|
||||
const requestLine = (req.method || 'GET') + ' ' + (req.url || '/') + ' HTTP/1.1\r\n';
|
||||
upstream.write(requestLine + headers + '\r\n\r\n');
|
||||
if (head && head.length > 0) upstream.write(head);
|
||||
socket.pipe(upstream);
|
||||
upstream.pipe(socket);
|
||||
});
|
||||
|
||||
upstream.on('error', (err) => {
|
||||
debugLog('upgrade: upstream error hostname=', hostname, 'err=', err.message);
|
||||
try { socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); socket.destroy(); } catch (_) {}
|
||||
});
|
||||
socket.on('error', () => {
|
||||
try { upstream.destroy(); } catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
if (!proxyServer) {
|
||||
if (callback) callback();
|
||||
|
||||
Generated
+1
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"assert": "npm:bare-node-assert@^1.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"bare-fs": "^4.5.5",
|
||||
"bare-http1": "^4.0.0",
|
||||
"bare-https": "^2.1.2",
|
||||
"bare-module": "^6.1.3",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"assert": "npm:bare-node-assert@^1.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"bare-fs": "^4.5.5",
|
||||
"bare-http1": "^4.0.0",
|
||||
"bare-https": "^2.1.2",
|
||||
"bare-module": "^6.1.3",
|
||||
|
||||
@@ -89,13 +89,18 @@ async function startSession(payload) {
|
||||
|
||||
log('startSession id=' + sessionId + ' user=' + username + ' tunnelPort=' + tunnelPort + ' wsPort=' + wsPort);
|
||||
|
||||
// 1. Start Holesail client tunnel
|
||||
// 1. Start Holesail client tunnel (with timeout to avoid hanging indefinitely)
|
||||
let holesailInst = null;
|
||||
try {
|
||||
holesailInst = new Holesail({ client: true, key: hsUrl, host: '127.0.0.1', port: tunnelPort });
|
||||
await holesailInst.ready();
|
||||
const TUNNEL_READY_TIMEOUT_MS = 30000;
|
||||
await Promise.race([
|
||||
holesailInst.ready(),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Tunnel ready timeout after ' + TUNNEL_READY_TIMEOUT_MS + 'ms')), TUNNEL_READY_TIMEOUT_MS))
|
||||
]);
|
||||
log('tunnel ready on 127.0.0.1:' + tunnelPort);
|
||||
} catch (e) {
|
||||
if (holesailInst) try { holesailInst.close(); } catch (_) {}
|
||||
releaseTunnelPort(tunnelPort);
|
||||
releaseWsPort(wsPort);
|
||||
return { ok: false, error: 'Tunnel failed: ' + e.message };
|
||||
@@ -213,6 +218,7 @@ async function startSession(payload) {
|
||||
// invokes the askpass helper (i.e. key auth failed).
|
||||
let askpassFile = null;
|
||||
let fifoPath = null;
|
||||
let fifoWriteChild = null; // tracked so we can kill it if key auth succeeds
|
||||
const sshEnv = Object.assign({}, process.env, { TERM: 'xterm-256color' });
|
||||
|
||||
try {
|
||||
@@ -243,6 +249,12 @@ async function startSession(payload) {
|
||||
}
|
||||
|
||||
function cleanupAskpass() {
|
||||
// Kill the FIFO writer if it is still blocked (e.g. key auth succeeded
|
||||
// and the askpass helper was never invoked, so the FIFO was never read).
|
||||
if (fifoWriteChild) {
|
||||
try { fifoWriteChild.kill('SIGTERM'); } catch (_) {}
|
||||
fifoWriteChild = null;
|
||||
}
|
||||
try { if (fifoPath) require('bare-fs').unlinkSync(fifoPath); } catch (_) {}
|
||||
try { if (askpassFile) require('bare-fs').unlinkSync(askpassFile); } catch (_) {}
|
||||
}
|
||||
@@ -255,7 +267,9 @@ async function startSession(payload) {
|
||||
['-c', 'printf "%s\n" "$PW" > "' + fifoPath + '"'],
|
||||
{ env: Object.assign({}, process.env, { PW: pw }) }
|
||||
);
|
||||
fifoWriteChild = child;
|
||||
child.on('error', (e) => log('FIFO write error: ' + e.message));
|
||||
child.on('exit', () => { if (fifoWriteChild === child) fifoWriteChild = null; });
|
||||
} catch (e) {
|
||||
log('deliverPassword spawn error: ' + e.message);
|
||||
}
|
||||
@@ -276,6 +290,7 @@ async function startSession(payload) {
|
||||
}
|
||||
|
||||
pty.once('exit', cleanupAskpass);
|
||||
pty.once('error', cleanupAskpass);
|
||||
|
||||
// Update session record with the live PTY reference
|
||||
const sess = sessions.get(sessionId);
|
||||
@@ -302,12 +317,16 @@ async function startSession(payload) {
|
||||
let passwordDelivered = false;
|
||||
let collectedPw = '';
|
||||
let quietTimer = null;
|
||||
let fallbackTimer = null;
|
||||
// Only watch for the password prompt within the first 8 seconds.
|
||||
const authDeadline = Date.now() + 8000;
|
||||
|
||||
function cancelPasswordWatch() {
|
||||
passwordDelivered = true;
|
||||
clearTimeout(quietTimer);
|
||||
clearTimeout(fallbackTimer);
|
||||
quietTimer = null;
|
||||
fallbackTimer = null;
|
||||
pty.removeListener('data', onPtyDataForAuth);
|
||||
}
|
||||
|
||||
@@ -316,9 +335,12 @@ async function startSession(payload) {
|
||||
// Past the deadline — key auth must have succeeded, bail out.
|
||||
if (Date.now() > authDeadline) { cancelPasswordWatch(); return; }
|
||||
collectingPassword = true;
|
||||
if (activeWsConn) {
|
||||
try { activeWsConn.write(Buffer.from(username + '@127.0.0.1\'s password: ')); } catch (_) {}
|
||||
if (!activeWsConn) {
|
||||
// Terminal disconnected — deliver empty password to unblock the FIFO
|
||||
deliverPasswordToFifo('');
|
||||
return;
|
||||
}
|
||||
try { activeWsConn.write(Buffer.from(username + '@127.0.0.1\'s password: ')); } catch (_) {}
|
||||
activeWsConn.removeAllListeners('data');
|
||||
activeWsConn.on('data', (data) => {
|
||||
const str = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
|
||||
@@ -400,7 +422,7 @@ async function startSession(payload) {
|
||||
pty.on('data', onPtyDataForAuth);
|
||||
// If SSH produces absolutely no output within 2 s (e.g. very slow tunnel)
|
||||
// fall through to the password prompt anyway.
|
||||
quietTimer = setTimeout(startPasswordCollection, 2000);
|
||||
fallbackTimer = setTimeout(startPasswordCollection, 2000);
|
||||
}
|
||||
|
||||
// PTY output → WebSocket (live, no buffering needed — browser is already open)
|
||||
|
||||
+36
-2
@@ -11,7 +11,14 @@ $Downloads = "$env:USERPROFILE\Downloads"
|
||||
$ManifestName = "com.holesail.browser"
|
||||
|
||||
$HostZip = "holesail-browser-host-win32-x64.zip"
|
||||
$ExtZip = "Holesail-Browser-1.0.0.zip"
|
||||
# Resolve the current release version from the server so filenames stay accurate
|
||||
try {
|
||||
$ExtVersion = (Invoke-WebRequest "$ReleaseBase/VERSION" -UseBasicParsing -ErrorAction Stop).Content.Trim()
|
||||
} catch {
|
||||
$ExtVersion = "latest"
|
||||
}
|
||||
$ExtZip = "Holesail-Browser-$ExtVersion.zip"
|
||||
$ExtXpi = "Holesail-Browser-$ExtVersion.xpi"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Holesail Browser Installer" -ForegroundColor Cyan
|
||||
@@ -97,6 +104,13 @@ Get-ChildItem -Path $Downloads -Filter "Holesail-Browser-*.xpi" -ErrorAction Sil
|
||||
Write-Host "Downloading extension..."
|
||||
Invoke-WebRequest "$ReleaseBase/$ExtZip" -OutFile "$Downloads\$ExtZip"
|
||||
Write-Host " Saved: $Downloads\$ExtZip"
|
||||
# Also grab the .xpi for Firefox
|
||||
try {
|
||||
Invoke-WebRequest "$ReleaseBase/$ExtXpi" -OutFile "$Downloads\$ExtXpi" -ErrorAction Stop
|
||||
Write-Host " Saved: $Downloads\$ExtXpi"
|
||||
} catch {
|
||||
Write-Host " Note: Firefox .xpi not available for this release" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ── Native messaging manifest ──────────────────────────────────────────────────
|
||||
Write-Host "Installing native messaging manifest..."
|
||||
@@ -112,12 +126,27 @@ $Manifest = @{
|
||||
} | ConvertTo-Json -Depth 4
|
||||
Set-Content $ManifestFile -Value $Manifest -Encoding UTF8
|
||||
|
||||
foreach ($p in $RegPaths) {
|
||||
# Chrome / Chromium registry keys
|
||||
$ChromeRegPaths = @(
|
||||
"HKCU:\Software\Google\Chrome\NativeMessagingHosts\$ManifestName",
|
||||
"HKCU:\Software\Chromium\NativeMessagingHosts\$ManifestName"
|
||||
)
|
||||
foreach ($p in $ChromeRegPaths) {
|
||||
New-Item -Path $p -Force | Out-Null
|
||||
Set-ItemProperty -Path $p -Name "(Default)" -Value $ManifestFile
|
||||
Write-Host " Registry: $p"
|
||||
}
|
||||
|
||||
# Firefox registry key (native messaging on Windows)
|
||||
$FirefoxRegPath = "HKCU:\Software\Mozilla\NativeMessagingHosts\$ManifestName"
|
||||
try {
|
||||
New-Item -Path $FirefoxRegPath -Force | Out-Null
|
||||
Set-ItemProperty -Path $FirefoxRegPath -Name "(Default)" -Value $ManifestFile
|
||||
Write-Host " Registry: $FirefoxRegPath"
|
||||
} catch {
|
||||
Write-Host " Note: Could not write Firefox registry key: $_" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ── Done ───────────────────────────────────────────────────────────────────────
|
||||
Write-Host ""
|
||||
Write-Host "==========================" -ForegroundColor Green
|
||||
@@ -131,5 +160,10 @@ Write-Host " 2. Enable Developer mode"
|
||||
Write-Host " 3. Drag & drop $Downloads\$ExtZip onto the page"
|
||||
Write-Host " (or click 'Load unpacked' after extracting)"
|
||||
Write-Host ""
|
||||
Write-Host " Firefox Developer Edition / Nightly (permanent install):"
|
||||
Write-Host " 1. Open about:config -> set xpinstall.signatures.required = false"
|
||||
Write-Host " 2. Open about:addons -> gear icon -> Install Add-on From File"
|
||||
Write-Host " 3. Select $Downloads\$ExtXpi"
|
||||
Write-Host ""
|
||||
Write-Host " Then restart your browser."
|
||||
Write-Host ""
|
||||
|
||||
+4
-2
@@ -24,8 +24,10 @@ if [[ -z "${PLATFORM:-}" ]]; then
|
||||
fi
|
||||
|
||||
HOST_ZIP="holesail-browser-host-${PLATFORM}-${ARCH}.zip"
|
||||
EXT_ZIP="Holesail-Browser-1.0.0.zip"
|
||||
EXT_XPI="Holesail-Browser-1.0.0.xpi"
|
||||
# Resolve the current release version from the server so filenames stay accurate
|
||||
EXT_VERSION="$(curl -fsSL "${RELEASE_BASE}/VERSION" 2>/dev/null || echo "latest")"
|
||||
EXT_ZIP="Holesail-Browser-${EXT_VERSION}.zip"
|
||||
EXT_XPI="Holesail-Browser-${EXT_VERSION}.xpi"
|
||||
|
||||
echo ""
|
||||
echo "Holesail Browser Installer"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
param([Parameter(Mandatory=$true)] [string] $ExtensionId)
|
||||
$ExtensionId = $ExtensionId -replace '^chrome-extension://', '' -replace '/$', ''
|
||||
$Origin = "chrome-extension://$ExtensionId/"
|
||||
$manifestFile = (Get-ItemProperty -Path "HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.bridgeswarm" -ErrorAction SilentlyContinue).'(Default)'
|
||||
$manifestFile = (Get-ItemProperty -Path "HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.holesail.browser" -ErrorAction SilentlyContinue).'(Default)'
|
||||
if (-not $manifestFile -or -not (Test-Path $manifestFile)) { Write-Host "Run scripts/install.ps1 first."; exit 1 }
|
||||
$m = Get-Content $manifestFile -Raw | ConvertFrom-Json
|
||||
$m.allowed_origins = @($Origin)
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Update native host manifest with your Chrome extension ID (fix "Access forbidden").
|
||||
# Usage: ./scripts/update-native-manifest-extension-id.sh YOUR_EXTENSION_ID
|
||||
# Also accepts a Firefox extension ID as a second argument to update allowed_extensions.
|
||||
# Usage: ./scripts/update-native-manifest-extension-id.sh CHROME_EXT_ID [FIREFOX_EXT_ID]
|
||||
set -e
|
||||
[[ -z "$1" ]] && echo "Usage: $0 YOUR_CHROME_EXTENSION_ID (from chrome://extensions)" && exit 1
|
||||
[[ -z "$1" ]] && echo "Usage: $0 CHROME_EXT_ID [FIREFOX_EXT_ID]" && exit 1
|
||||
EXT_ID="${1#chrome-extension://}"; EXT_ID="${EXT_ID%/}"
|
||||
ORIGIN="chrome-extension://${EXT_ID}/"
|
||||
CHROME_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
|
||||
CHROMIUM_DIR="$HOME/.config/chromium/NativeMessagingHosts"
|
||||
[[ "$OSTYPE" == "darwin"* ]] && CHROME_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts" && CHROMIUM_DIR="$HOME/Library/Application Support/Chromium/NativeMessagingHosts"
|
||||
FIREFOX_EXT_ID="${2:-}"
|
||||
|
||||
MANIFEST_NAME="com.holesail.browser"
|
||||
|
||||
# Chrome / Chromium manifest directories
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
CHROME_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
|
||||
CHROMIUM_DIR="$HOME/Library/Application Support/Chromium/NativeMessagingHosts"
|
||||
FIREFOX_DIR="$HOME/Library/Application Support/Mozilla/NativeMessagingHosts"
|
||||
else
|
||||
CHROME_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
|
||||
CHROMIUM_DIR="$HOME/.config/chromium/NativeMessagingHosts"
|
||||
FIREFOX_DIR="$HOME/.mozilla/native-messaging-hosts"
|
||||
fi
|
||||
|
||||
updated=0
|
||||
|
||||
# Update Chrome / Chromium manifests (allowed_origins)
|
||||
for dir in "$CHROME_DIR" "$CHROMIUM_DIR"; do
|
||||
f="$dir/${MANIFEST_NAME}.json"
|
||||
[[ ! -f "$f" ]] && continue
|
||||
@@ -18,8 +32,25 @@ for dir in "$CHROME_DIR" "$CHROMIUM_DIR"; do
|
||||
else
|
||||
sed -i.bak "s|\"chrome-extension://[^\"]*/\"|\"$ORIGIN\"|g" "$f" && rm -f "${f}.bak"
|
||||
fi
|
||||
echo "Updated $f"
|
||||
echo "Updated $f (allowed_origins)"
|
||||
updated=1
|
||||
done
|
||||
|
||||
# Update Firefox manifest (allowed_extensions) if a Firefox extension ID was provided
|
||||
if [[ -n "$FIREFOX_EXT_ID" ]]; then
|
||||
f="$FIREFOX_DIR/${MANIFEST_NAME}.json"
|
||||
if [[ -f "$f" ]]; then
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
node -e "const fs=require('fs');const j=JSON.parse(fs.readFileSync('$f','utf8'));j.allowed_extensions=['$FIREFOX_EXT_ID'];fs.writeFileSync('$f',JSON.stringify(j,null,2));"
|
||||
else
|
||||
sed -i.bak "s|\"holesail-browser[^\"]*@[^\"]*\"|\"$FIREFOX_EXT_ID\"|g" "$f" && rm -f "${f}.bak"
|
||||
fi
|
||||
echo "Updated $f (allowed_extensions)"
|
||||
updated=1
|
||||
else
|
||||
echo "Note: Firefox manifest not found at $f — skipping Firefox update"
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ $updated -eq 0 ]] && echo "No manifest found. Run scripts/install.sh first." && exit 1
|
||||
echo "Done. Restart Chrome."
|
||||
echo "Done. Restart your browser."
|
||||
|
||||
Reference in New Issue
Block a user