Files
holesail-browser/extension/dashboard/pages/proxy-ca.js
T
Raven Scott f1e98a7edd
CI / Build & Test (push) Successful in 2m54s
docs: add CONTRIBUTING.md, CHANGELOG.md, and JSDoc to entire codebase
Add docs/CONTRIBUTING.md covering the build system, dev workflow, all
npm scripts, how to add new native host message types, code style, and
debugging guidance.

Add CHANGELOG.md at the project root documenting all features and fixes
across the 1.0.0 release.

Add JSDoc (@param, @returns) to all previously undocumented exported
functions across 35 JS files:
- native-host/holesail-manager/ (index, virtual-hosts, service-tunnels,
  servers, port-allocator)
- native-host top-level managers (startup, connect-proxy, https-proxy,
  certificate-authority, ssh-manager, rdp-manager)
- extension/background/ (logs, native-messaging, proxy, message-router)
- extension/dashboard/core/ (utils, navigation, init)
- extension/dashboard/ui/ (modal, toast, state-tag)
- extension/dashboard/pages/ (all 10 page files)
- extension/dashboard/refresh.js, events.js
- extension/dashboard/data/hostname-validator.js
- scripts/ (build-host, run-install)
2026-03-01 00:40:53 -05:00

182 lines
7.9 KiB
JavaScript

/**
* Proxy & CA page — displays proxy port/CA status, renders a per-virtual-host
* certificate validator table, and wires up CA install/uninstall events.
* Depends on: core/utils.js ($, escapeHtml), core/state.js (currentState),
* ui/toast.js (showToast), ui/modal.js (openModal, closeModal)
*/
// Per-host test results: hostname -> { status, tlsOk, httpStatus, ms, error }
const validationResults = new Map();
/**
* Update the proxy port and CA installation status display.
* @param {object} state - Full extension state from `fetchState()`.
*/
function updateTabsTable(state) {
const portEl = $('proxyInfoPort');
const caEl = $('proxyInfoCA');
if (portEl) portEl.textContent = state.proxyPort != null ? state.proxyPort : '—';
if (caEl) {
caEl.textContent = state.caInstalled ? 'Installed' : 'Not installed';
caEl.style.color = state.caInstalled ? 'var(--green)' : 'var(--red)';
}
const validatorCard = $('certValidatorCard');
if (validatorCard) validatorCard.style.display = state.caInstalled ? '' : 'none';
if (state.caInstalled) {
renderValidatorTable(state.virtualHosts || []);
}
}
function renderValidatorTable(virtualHosts) {
const tbody = $('certValidatorBody');
if (!tbody) return;
// Prune stale entries for removed virtual hosts
const currentHostnames = new Set(virtualHosts.map(v => v.hostname || ''));
for (const key of validationResults.keys()) {
if (!currentHostnames.has(key)) validationResults.delete(key);
}
if (virtualHosts.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" class="empty">No virtual hosts to test — add one in Virtual Hosts</td></tr>`;
return;
}
tbody.innerHTML = virtualHosts.map(v => {
const hostname = v.hostname || '';
const r = validationResults.get(hostname);
return `<tr id="vrow-${CSS.escape(hostname)}">
<td><span class="mono-chip">${escapeHtml(hostname)}</span></td>
<td>${renderTlsCell(hostname, r)}</td>
<td>${renderStatusCell(hostname, r)}</td>
<td>${renderTimeCell(hostname, r)}</td>
<td>
<button class="btn btn-secondary btn-sm" data-validate-host="${escapeHtml(hostname)}" id="vbtn-${CSS.escape(hostname)}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:12px;height:12px;"><polygon points="5,3 19,12 5,21"/></svg>
Test
</button>
</td>
</tr>`;
}).join('');
tbody.querySelectorAll('[data-validate-host]').forEach(btn => {
btn.addEventListener('click', () => runValidation(btn.dataset.validateHost));
});
}
function renderTlsCell(hostname, r) {
if (!r) return `<span class="badge badge-neutral">—</span>`;
if (r.status === 'running') return `<span class="badge badge-amber">Testing…</span>`;
if (r.tlsOk) return `<span class="badge badge-green">✓ Trusted</span>`;
return `<span class="badge badge-red" title="${escapeHtml(r.error || '')}">✗ Untrusted</span>`;
}
function renderStatusCell(hostname, r) {
if (!r || r.status === 'running') return `<span style="color:var(--text4);">—</span>`;
if (!r.tlsOk) return `<span style="color:var(--text4);">—</span>`;
if (r.httpStatus == null) return `<span class="badge badge-red">No response</span>`;
const cls = r.httpStatus < 400 ? 'badge-green' : r.httpStatus < 500 ? 'badge-amber' : 'badge-red';
return `<span class="badge ${cls}">${r.httpStatus}</span>`;
}
function renderTimeCell(hostname, r) {
if (!r || r.status === 'running' || !r.tlsOk || r.ms == null) return `<span style="color:var(--text4);">—</span>`;
const color = r.ms < 500 ? 'var(--green)' : r.ms < 2000 ? 'var(--amber)' : 'var(--red)';
return `<span style="font-family:'JetBrains Mono',monospace;font-size:12px;color:${color};">${r.ms}ms</span>`;
}
function updateValidatorRow(hostname) {
const r = validationResults.get(hostname);
const row = document.getElementById('vrow-' + CSS.escape(hostname));
if (!row) return;
const cells = row.querySelectorAll('td');
if (cells[1]) cells[1].innerHTML = renderTlsCell(hostname, r);
if (cells[2]) cells[2].innerHTML = renderStatusCell(hostname, r);
if (cells[3]) cells[3].innerHTML = renderTimeCell(hostname, r);
const btn = document.getElementById('vbtn-' + CSS.escape(hostname));
if (btn) {
btn.disabled = r && r.status === 'running';
btn.innerHTML = (r && r.status === 'running')
? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:12px;height:12px;animation:spin 1s linear infinite;"><polyline points="23,4 23,10 17,10"/><path d="M20.49 15a9 9 0 1 1-.07-8.13"/></svg> Testing…`
: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:12px;height:12px;"><polygon points="5,3 19,12 5,21"/></svg> Test`;
}
}
/**
* Run a TLS + HTTP validation check for a single virtual hostname.
* Updates `validationResults` and re-renders the row on completion.
* @param {string} hostname - The virtual hostname to validate (e.g. `myapp.hs`).
* @returns {Promise<void>}
*/
async function runValidation(hostname) {
validationResults.set(hostname, { status: 'running' });
updateValidatorRow(hostname);
const url = `https://${hostname}`;
const start = Date.now();
try {
// fetch() goes through the PAC proxy → HTTPS proxy → Holesail tunnel.
// If the CA is not trusted by the browser, this throws a TypeError (net::ERR_CERT_AUTHORITY_INVALID).
// mode: 'no-cors' avoids CORS errors from opaque responses — we only care about TLS + reachability.
const resp = await fetch(url, { mode: 'no-cors', cache: 'no-store', signal: AbortSignal.timeout(15000) });
const ms = Date.now() - start;
const httpStatus = resp.type === 'opaque' ? null : resp.status;
validationResults.set(hostname, { status: 'done', tlsOk: true, httpStatus, ms });
} catch (err) {
const ms = Date.now() - start;
const msg = err.message || String(err);
const isTlsError = msg.includes('ERR_CERT') || msg.includes('certificate') || msg.includes('SSL') || msg.includes('CERT');
validationResults.set(hostname, { status: 'done', tlsOk: false, ms, error: msg, isTlsError });
}
updateValidatorRow(hostname);
}
/**
* Run validation checks for all virtual hosts sequentially.
* @param {Array<{hostname: string}>} virtualHosts
* @returns {Promise<void>}
*/
async function runAllValidations(virtualHosts) {
if (!virtualHosts || virtualHosts.length === 0) return;
for (const v of virtualHosts) {
await runValidation(v.hostname || '');
}
}
/**
* Attach all event listeners for the Proxy & CA page.
* Called once during dashboard initialisation.
*/
function setupCertValidator() {
$('runAllValidationsBtn')?.addEventListener('click', () => {
if (currentState) runAllValidations(currentState.virtualHosts || []);
});
$('installCaBtn')?.addEventListener('click', () => openModal('modal-installCA'));
$('installCaSubmit')?.addEventListener('click', () => {
const btn = $('installCaSubmit');
const errEl = $('installCaError');
const successEl = $('installCaSuccess');
if (btn) { btn.disabled = true; btn.textContent = 'Installing…'; }
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
if (successEl) { successEl.style.display = 'none'; }
chrome.runtime.sendMessage(
{ target: 'holesail-native', action: 'send', payload: { type: 'installRootCA', payload: {} } },
(response) => {
if (btn) { btn.disabled = false; btn.textContent = 'Install CA'; }
if (response?.ok) {
if (successEl) { successEl.textContent = '✓ Root CA installed. Fully quit and reopen Chrome (Cmd+Q) to apply trust.'; successEl.style.display = 'block'; }
showToast('Root CA installed', 'success');
setTimeout(() => closeModal('modal-installCA'), 2000);
refresh();
} else {
if (errEl) { errEl.textContent = response?.error || 'Installation failed'; errEl.style.display = 'block'; }
}
}
);
});
}