Files
holesail-browser/extension/dashboard/pages/proxy-ca.js
T
Raven Scott 5c4f0b4aba
CI / Build & Test (push) Failing after 28s
efactor(extension): modularize dashboard, background, and docs
Split monolithic dashboard.js (2753 lines) into 18 focused modules
under dashboard/{core,data,ui,pages}/ with refresh.js and events.js
as orchestrators. Extracted ~900-line inline <style> into dashboard.css
and moved dashboard.html to dashboard/dashboard.html.

Split background.js (609 lines) into background/{logs,state,proxy,
native-messaging,tab-lifecycle,message-router}.js with a thin entry
point using importScripts().

Deleted dead files: wrong-domain.js (duplicate of inline script).
Updated manifest.json web_accessible_resources for new paths.
Updated docs/ARCHITECTURE.md to reflect the new file structure.

No functionality changed. No build step introduced.
2026-02-28 23:02:52 -05:00

153 lines
6.8 KiB
JavaScript

// 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;
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`;
}
}
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'; }
}
}
);
});
}