/**
* 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 = `
| No virtual hosts to test — add one in Virtual Hosts |
`;
return;
}
tbody.innerHTML = virtualHosts.map(v => {
const hostname = v.hostname || '';
const r = validationResults.get(hostname);
return `
| ${escapeHtml(hostname)} |
${renderTlsCell(hostname, r)} |
${renderStatusCell(hostname, r)} |
${renderTimeCell(hostname, r)} |
|
`;
}).join('');
tbody.querySelectorAll('[data-validate-host]').forEach(btn => {
btn.addEventListener('click', () => runValidation(btn.dataset.validateHost));
});
}
function renderTlsCell(hostname, r) {
if (!r) return `—`;
if (r.status === 'running') return `Testing…`;
if (r.tlsOk) return `✓ Trusted`;
return `✗ Untrusted`;
}
function renderStatusCell(hostname, r) {
if (!r || r.status === 'running') return `—`;
if (!r.tlsOk) return `—`;
if (r.httpStatus == null) return `No response`;
const cls = r.httpStatus < 400 ? 'badge-green' : r.httpStatus < 500 ? 'badge-amber' : 'badge-red';
return `${r.httpStatus}`;
}
function renderTimeCell(hostname, r) {
if (!r || r.status === 'running' || !r.tlsOk || r.ms == null) return `—`;
const color = r.ms < 500 ? 'var(--green)' : r.ms < 2000 ? 'var(--amber)' : 'var(--red)';
return `${r.ms}ms`;
}
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')
? ` Testing…`
: ` 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}
*/
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}
*/
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'; }
}
}
);
});
}