CI / Build & Test (push) Successful in 2m54s
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)
182 lines
7.9 KiB
JavaScript
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'; }
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|