// 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(); 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`; } } 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); } async function runAllValidations(virtualHosts) { if (!virtualHosts || virtualHosts.length === 0) return; for (const v of virtualHosts) { await runValidation(v.hostname || ''); } } 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'; } } } ); }); }