From 12f8b450157792f7caa9eade3eeeec36392eb7c9 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Fri, 17 Jul 2026 18:35:36 -0400 Subject: [PATCH] v0.0.42 - Fix segfault on onion/fips/nostr URL redirect in new tab: defer webkit_web_view_load_uri to idle callback to avoid re-entrant navigation inside decide-policy handler --- VERSION | 2 +- plans/fips-management-page.md | 465 ++++++++++++++++++++++ plans/fips-network-view.md | 255 ++++++++++++ src/fips_control.c | 43 ++ src/fips_control.h | 9 +- src/main.c | 15 + src/nostr_bridge.c | 724 +++++++++++++++++++++++++++++++++- src/tab_manager.c | 83 +++- src/version.h | 4 +- www/fips.css | 193 +++++++++ www/fips.html | 160 ++++++++ www/fips.js | 564 ++++++++++++++++++++++++++ www/settings.html | 7 + 13 files changed, 2509 insertions(+), 15 deletions(-) create mode 100644 plans/fips-management-page.md create mode 100644 plans/fips-network-view.md create mode 100644 www/fips.css create mode 100644 www/fips.html create mode 100644 www/fips.js diff --git a/VERSION b/VERSION index 89a67e8..54a0022 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.41 +0.0.42 diff --git a/plans/fips-management-page.md b/plans/fips-management-page.md new file mode 100644 index 0000000..09438e9 --- /dev/null +++ b/plans/fips-management-page.md @@ -0,0 +1,465 @@ +# FIPS Management Page — `sovereign://fips` + +## Goal + +A dedicated `sovereign://fips` page for managing the FIPS mesh daemon from +inside the browser. This is **not** part of the settings page — it's a +separate status + management page, like a network control panel. + +The page lets the user: + +1. **See FIPS status** — daemon state, node npub, TUN interface, peer count, + tree state, ownership (managed vs attached), error messages. +2. **See connected peers** — list of peers with their npub, address, + transport, and connection state. +3. **Connect to a peer** — add a new peer by npub + address + transport. +4. **Disconnect a peer** — remove a peer connection. +5. **Restart the FIPS service** — stop + start the managed daemon, or + re-attach. +6. **See Tor status** (bonus) — since Tor and FIPS are the two network + services, showing both on one "Network" page makes sense. Tor is + read-only status (bootstrap progress, SOCKS endpoint); FIPS is + interactive management. + +This makes `.fips` addresses actually usable: the user can add peers from +the browser UI instead of dropping to `fipsctl` on the command line. + +--- + +## Current State + +### FIPS control API (`src/fips_control.c` / `src/fips_control.h`) + +The FIPS daemon exposes a JSON-line control protocol over a Unix socket. +The browser already has a client: + +- [`fips_control_connect()`](src/fips_control.c:30) — open a control + connection to the socket. +- [`fips_control_show_status()`](src/fips_control.c:142) — sends + `{"command":"show_status"}`, parses response into `fips_status_t` + (npub, peer_count, tun_state, tun_name, daemon state, tree_state, + tun_active). +- [`fips_control_show_peers()`](src/fips_control.c:171) — sends + `{"command":"show_peers"}`, returns the `data` field as a JSON string + (caller frees). **This is not currently called anywhere** — it exists + but is unused. +- [`fips_control_connect_peer()`](src/fips_control.c:185) — sends + `{"command":"connect","params":{"npub":...,"address":...,"transport":...}}`. + **Also unused.** +- [`fips_control_close()`](src/fips_control.c:205) — close the fd. + +**Missing from the client:** a `disconnect` peer command. The FIPS daemon +likely supports `{"command":"disconnect","params":{"npub":...}}` (this +needs verification against the FIPS daemon's control protocol — see +`fipsctl` source or `docs/control-api.md` in the FIPS repo). We'll add a +`fips_control_disconnect_peer()` function. + +### Net services layer (`src/net_services.c` / `src/net_services.h`) + +- [`net_service_get_status(NET_SERVICE_FIPS)`](src/net_services.c:493) + returns a pointer to the global `g_services[NET_SERVICE_FIPS]` struct. + The `status.fips` union member has: `peer_count`, `node_npub[80]`, + `tree_state[32]`, `tun_active`, `tun_name[16]`, `daemon_state[32]`, + `control_socket[512]`. +- [`fips_poll_cb()`](src/net_services.c:213) polls `show_status` every 2s + (managed) or 5s (attached) and updates the struct. It does **not** poll + `show_peers` — only the peer *count* is tracked. +- The service `state` field (`service_state_t`) tracks the lifecycle: + `SERVICE_DISABLED`, `DISCOVERING`, `ATTACHING`, `STARTING`, + `BOOTSTRAPPING`, `READY`, `STOPPING`, `EXITED`, `FAILED`. +- `ownership` is `OWNERSHIP_NONE`, `ATTACHED`, or `MANAGED`. +- `error_msg` holds the failure reason when `state == SERVICE_FAILED`. +- [`net_service_restart(NET_SERVICE_FIPS)`](src/net_services.c) exists — + it disables then re-enables the service. + +### sovereign:// page pattern (`src/nostr_bridge.c`) + +The existing pages (settings, profile, bookmarks, agents) follow this +pattern: + +1. **HTML page** served from an embedded `www/` file via + [`serve_embedded_file()`](src/nostr_bridge.c:132). +2. **CSS** served from `www/.css`, linked via + ``. +3. **JS** served from `www/.js`, loaded via + ` + + diff --git a/www/fips.js b/www/fips.js new file mode 100644 index 0000000..34e646d --- /dev/null +++ b/www/fips.js @@ -0,0 +1,564 @@ +/* FIPS management page — sovereign://fips + * + * Tabs: Status | Peers | Network | Tree + * + * - Status: FIPS + Tor status cards (sovereign://fips/status) + * - Peers: direct peers table + add-peer form (sovereign://fips/peers) + * - Network: mesh summary + known-nodes table (sovereign://fips/network) + * - Tree: spanning tree view (sovereign://fips/tree) + * + * Uses XMLHttpRequest via sovereignGet() because WebKit's custom scheme + * handler does not reliably support fetch() for sovereign:// URLs. + */ + +function sovereignGet(url) { + return new Promise(function(resolve, reject) { + var x = new XMLHttpRequest(); + x.open('GET', url, true); + x.onreadystatechange = function() { + if (x.readyState !== 4) return; + try { resolve(JSON.parse(x.responseText)); } + catch (e) { reject(e); } + }; + x.onerror = function() { reject(new Error('XHR error')); }; + x.send(); + }); +} + +function esc(s) { + var d = document.createElement('div'); + d.textContent = s == null ? '' : String(s); + return d.innerHTML; +} + +function showMsg(text, isErr) { + var el = document.getElementById('status-msg'); + el.textContent = text || ''; + el.className = 'status show ' + (isErr ? 'err' : 'ok'); + if (!text) el.className = 'status'; +} + +function stateDotClass(state) { + return 'state-dot ' + (state || ''); +} + +/* Global: our own FIPS node npub (from show_status), used to pin our + * node in the Directory tab. */ +var gOurNpub = ''; + +/* ── Tab switching ──────────────────────────────────────────────── */ + +function switchTab(tabName) { + document.querySelectorAll('.tab-btn').forEach(function(btn) { + btn.classList.toggle('active', btn.dataset.tab === tabName); + }); + document.querySelectorAll('.tab-panel').forEach(function(panel) { + panel.classList.toggle('active', panel.id === 'tab-' + tabName); + }); + /* Widen the page for the directory tab (long npub list needs width). */ + document.body.classList.toggle('wide', tabName === 'directory'); + /* Load tab data on demand. */ + if (tabName === 'network') fetchNetwork(); + if (tabName === 'tree') fetchTree(); + if (tabName === 'directory') fetchDirectory(); +} + +document.querySelectorAll('.tab-btn').forEach(function(btn) { + btn.addEventListener('click', function() { switchTab(btn.dataset.tab); }); +}); + +/* ── Status tab ─────────────────────────────────────────────────── */ + +function renderFipsStatus(data) { + var f = data.fips || {}; + var state = f.state || 'disabled'; + /* Store our npub for the Directory tab pinning. */ + gOurNpub = f.node_npub || ''; + document.getElementById('fips-state').innerHTML = + esc(state) + ''; + document.getElementById('fips-node').textContent = f.node_npub || '—'; + var tun = '—'; + if (f.tun_name && f.tun_name.length) { + tun = f.tun_name + (f.tun_active ? ' (active)' : ' (inactive)'); + } + document.getElementById('fips-tun').textContent = tun; + document.getElementById('fips-peers').textContent = + (f.peer_count != null ? f.peer_count : '—') + ' connected'; + document.getElementById('fips-tree').textContent = f.tree_state || '—'; + document.getElementById('fips-ownership').textContent = + f.ownership || '—'; + document.getElementById('fips-control').textContent = + f.control_socket || '—'; + + var errRow = document.getElementById('fips-error-row'); + var errEl = document.getElementById('fips-error'); + if (f.error && f.error.length) { + errEl.textContent = f.error; + errRow.style.display = ''; + } else { + errRow.style.display = 'none'; + } +} + +function renderTorStatus(data) { + var t = data.tor || {}; + var state = t.state || 'disabled'; + document.getElementById('tor-state').innerHTML = + esc(state) + ''; + document.getElementById('tor-bootstrap').textContent = + (t.bootstrap_progress != null ? t.bootstrap_progress : '—') + '%'; + document.getElementById('tor-socks').textContent = + t.socks_endpoint || '—'; + document.getElementById('tor-ownership').textContent = + t.ownership || '—'; + + var errRow = document.getElementById('tor-error-row'); + var errEl = document.getElementById('tor-error'); + if (t.error && t.error.length) { + errEl.textContent = t.error; + errRow.style.display = ''; + } else { + errRow.style.display = 'none'; + } +} + +function fetchStatus() { + return sovereignGet('sovereign://fips/status?_=' + Date.now()) + .then(function(data) { + renderFipsStatus(data); + renderTorStatus(data); + }) + .catch(function(e) { + showMsg('Failed to load status: ' + e.message, true); + }); +} + +/* ── Peers tab ──────────────────────────────────────────────────── */ + +function renderPeers(data) { + var body = document.getElementById('peers-body'); + var table = document.getElementById('peers-table'); + var empty = document.getElementById('peers-empty'); + + var peers = data.peers || []; + if (!peers.length) { + table.style.display = 'none'; + empty.style.display = ''; + empty.textContent = data.error + ? 'No peers (' + data.error + ')' + : 'No peers connected.'; + return; + } + + empty.style.display = 'none'; + table.style.display = ''; + var html = ''; + peers.forEach(function(p) { + var npub = p.npub || p.id || ''; + var addr = p.transport_addr || p.address || p.addr || ''; + var transport = p.transport_type || p.transport || ''; + var state = p.connectivity || p.state || p.status || ''; + var label = p.display_name || ''; + html += ''; + html += '' + esc(npub) + + (label ? '
' + esc(label) + '' : '') + + ''; + html += '' + esc(addr) + ''; + html += '' + esc(transport) + ''; + html += '' + esc(state) + ''; + html += ''; + html += ''; + }); + body.innerHTML = html; +} + +function fetchPeers() { + return sovereignGet('sovereign://fips/peers?_=' + Date.now()) + .then(function(data) { renderPeers(data); }) + .catch(function(e) { + document.getElementById('peers-empty').textContent = + 'Failed to load peers: ' + e.message; + }); +} + +function connectPeer() { + var npub = document.getElementById('peer-npub').value.trim(); + var address = document.getElementById('peer-address').value.trim(); + var transport = document.getElementById('peer-transport').value; + if (!npub || !address) { + showMsg('npub and address are required', true); + return; + } + var url = 'sovereign://fips/connect?npub=' + encodeURIComponent(npub) + + '&address=' + encodeURIComponent(address) + + '&transport=' + encodeURIComponent(transport); + sovereignGet(url) + .then(function(d) { + if (d.error) { + showMsg('Connect failed: ' + d.error, true); + } else { + showMsg('Peer added'); + document.getElementById('peer-npub').value = ''; + document.getElementById('peer-address').value = ''; + fetchPeers(); + } + }) + .catch(function(e) { showMsg('Connect failed: ' + e.message, true); }); +} + +function disconnectPeer(npub) { + if (!npub) return; + if (!confirm('Disconnect peer ' + npub + '?')) return; + var url = 'sovereign://fips/disconnect?npub=' + encodeURIComponent(npub); + sovereignGet(url) + .then(function(d) { + if (d.error) { + showMsg('Disconnect failed: ' + d.error, true); + } else { + showMsg('Peer disconnected'); + fetchPeers(); + } + }) + .catch(function(e) { showMsg('Disconnect failed: ' + e.message, true); }); +} + +function restartService() { + if (!confirm('Restart the FIPS service? This will briefly drop all peer connections.')) return; + showMsg('Restarting FIPS service…'); + sovereignGet('sovereign://fips/restart') + .then(function(d) { + if (d.error) showMsg('Restart failed: ' + d.error, true); + else showMsg('Restart initiated'); + }) + .catch(function(e) { showMsg('Restart failed: ' + e.message, true); }); +} + +/* ── Network tab ────────────────────────────────────────────────── */ + +function formatAge(ageMs) { + if (ageMs == null) return '—'; + var s = Math.floor(ageMs / 1000); + if (s < 60) return s + 's ago'; + var m = Math.floor(s / 60); + if (m < 60) return m + 'm ago'; + var h = Math.floor(m / 60); + if (h < 24) return h + 'h ago'; + return Math.floor(h / 24) + 'd ago'; +} + +function ageClass(ageMs) { + if (ageMs == null) return ''; + if (ageMs < 10000) return 'last-seen-fresh'; + if (ageMs < 60000) return 'last-seen-stale'; + return 'last-seen-old'; +} + +function renderNetwork(data) { + var s = data.summary || {}; + var nodes = data.nodes || []; + + /* Mesh summary. */ + document.getElementById('mesh-known').textContent = + (s.known_nodes != null ? s.known_nodes : '—') + + (s.max_cache_entries != null ? ' / ' + s.max_cache_entries : ''); + document.getElementById('mesh-direct').textContent = + (data.peer_count != null ? data.peer_count : '—'); + document.getElementById('mesh-depth').textContent = + (s.tree_depth != null ? s.tree_depth : '—'); + document.getElementById('mesh-root').textContent = + s.root_npub ? esc(s.root_npub) : '—'; + document.getElementById('mesh-parent').textContent = + s.parent_display_name ? esc(s.parent_display_name) : + (s.is_root ? '(this node is root)' : '—'); + + var errRow = document.getElementById('mesh-error-row'); + var errEl = document.getElementById('mesh-error'); + if (data.error) { + errEl.textContent = data.error; + errRow.style.display = ''; + } else { + errRow.style.display = 'none'; + } + + /* Known nodes table. */ + var body = document.getElementById('nodes-body'); + var table = document.getElementById('nodes-table'); + var empty = document.getElementById('nodes-empty'); + var countLabel = document.getElementById('nodes-count-label'); + + countLabel.textContent = '(' + nodes.length + ' nodes)'; + + if (!nodes.length) { + table.style.display = 'none'; + empty.style.display = ''; + empty.textContent = data.error + ? 'Network unavailable: ' + data.error + : 'No known nodes (FIPS may not be ready).'; + return; + } + + /* Sort by last_seen descending (most recent first). */ + nodes.sort(function(a, b) { + var aT = a.last_seen_ms || 0; + var bT = b.last_seen_ms || 0; + return bT - aT; + }); + + empty.style.display = 'none'; + table.style.display = ''; + var html = ''; + nodes.forEach(function(n) { + var npub = n.npub || ''; + var name = n.display_name || '—'; + var ipv6 = n.ipv6_addr || ''; + var age = n.age_ms != null ? n.age_ms : null; + var direct = n.is_direct_peer; + var badge = ''; + html += ''; + html += '' + badge + ''; + html += '' + esc(npub) + ''; + html += '' + esc(name) + ''; + html += '' + esc(ipv6) + ''; + html += '' + formatAge(age) + ''; + html += ''; + }); + body.innerHTML = html; +} + +function fetchNetwork() { + return sovereignGet('sovereign://fips/network?_=' + Date.now()) + .then(function(data) { renderNetwork(data); }) + .catch(function(e) { + document.getElementById('nodes-empty').textContent = + 'Failed to load network: ' + e.message; + }); +} + +/* ── Tree tab ───────────────────────────────────────────────────── */ + +function renderTree(data) { + var t = data.tree || {}; + var errRow = document.getElementById('tree-error-row'); + var errEl = document.getElementById('tree-error'); + + if (data.error) { + errEl.textContent = data.error; + errRow.style.display = ''; + document.getElementById('tree-peers-empty').textContent = + 'Tree unavailable: ' + data.error; + document.getElementById('tree-peers-table').style.display = 'none'; + /* Clear summary. */ + ['tree-root','tree-is-root','tree-depth','tree-parent','tree-node-addr'] + .forEach(function(id) { document.getElementById(id).textContent = '—'; }); + return; + } + errRow.style.display = 'none'; + + document.getElementById('tree-root').textContent = + t.root_npub || t.root || '—'; + document.getElementById('tree-is-root').textContent = + t.is_root ? 'yes' : 'no'; + document.getElementById('tree-depth').textContent = + (t.depth != null ? t.depth : '—'); + document.getElementById('tree-parent').textContent = + t.parent_display_name || t.parent || '—'; + document.getElementById('tree-node-addr').textContent = + t.my_node_addr || '—'; + + /* Tree peers (1-hop neighbors). */ + var body = document.getElementById('tree-peers-body'); + var table = document.getElementById('tree-peers-table'); + var empty = document.getElementById('tree-peers-empty'); + var peers = t.peers || []; + + if (!peers.length) { + table.style.display = 'none'; + empty.style.display = ''; + empty.textContent = 'No tree peers.'; + return; + } + + empty.style.display = 'none'; + table.style.display = ''; + var myAddr = t.my_node_addr || ''; + var parentAddr = t.parent || ''; + var html = ''; + peers.forEach(function(p) { + var addr = p.node_addr || ''; + var name = p.display_name || '—'; + var depth = p.depth != null ? p.depth : '—'; + var dist = p.distance_to_us != null ? p.distance_to_us : '—'; + var role = ''; + if (addr === parentAddr && !t.is_root) { + role = 'parent'; + } else if (addr === t.root) { + role = 'root'; + } else { + role = 'peer'; + } + html += ''; + html += '' + esc(addr) + ''; + html += '' + esc(name) + ''; + html += '' + esc(depth) + ''; + html += '' + esc(dist) + ''; + html += '' + role + ''; + html += ''; + }); + body.innerHTML = html; +} + +function fetchTree() { + return sovereignGet('sovereign://fips/tree?_=' + Date.now()) + .then(function(data) { renderTree(data); }) + .catch(function(e) { + document.getElementById('tree-peers-empty').textContent = + 'Failed to load tree: ' + e.message; + }); +} + +/* ── Directory tab ───────────────────────────────────────────────── */ + +function renderDirectory(data) { + var body = document.getElementById('dir-body'); + var table = document.getElementById('dir-table'); + var empty = document.getElementById('dir-empty'); + var countLabel = document.getElementById('dir-count-label'); + var actions = document.getElementById('dir-actions'); + var nodes = data.nodes || []; + + countLabel.textContent = '(' + nodes.length + ' nodes)'; + + if (!nodes.length) { + table.style.display = 'none'; + actions.style.display = 'none'; + empty.style.display = ''; + empty.textContent = data.error + ? 'Directory unavailable: ' + data.error + : 'No advertised nodes found on the advert relays.'; + return; + } + + /* Sort: our own node first (pinned), then by created_at descending. */ + nodes.sort(function(a, b) { + var aOurs = (a.npub && a.npub === gOurNpub) ? 1 : 0; + var bOurs = (b.npub && b.npub === gOurNpub) ? 1 : 0; + if (aOurs !== bOurs) return bOurs - aOurs; + var aT = a.created_at || 0; + var bT = b.created_at || 0; + return bT - aT; + }); + + empty.style.display = 'none'; + table.style.display = ''; + actions.style.display = ''; + var html = ''; + nodes.forEach(function(n) { + var npub = n.npub || ''; + var direct = n.is_direct_peer; + var isOurs = (npub && npub === gOurNpub); + var badge = ''; + + /* Compact endpoints: "udp 1.2.3.4:2121" (first one only, +N if more). */ + var endpointsStr = '—'; + if (n.endpoints && n.endpoints.length) { + var ep = n.endpoints[0]; + endpointsStr = esc(ep.transport || '?') + ' ' + esc(ep.addr || ''); + if (n.endpoints.length > 1) endpointsStr += ' +' + (n.endpoints.length - 1); + } + + /* Connect: clickable text, not a button. */ + var firstEp = (n.endpoints && n.endpoints[0]) || {}; + var addr = firstEp.addr || ''; + var transport = firstEp.transport || 'udp'; + var connectLink = ''; + if (isOurs) { + connectLink = 'this is you'; + } else if (addr && !direct) { + connectLink = 'connect'; + } else if (direct) { + connectLink = 'connected'; + } + + var rowClass = isOurs ? ' class="dir-ours-row"' : ''; + html += ''; + html += '' + badge + ''; + html += '' + esc(npub) + ''; + html += '' + endpointsStr + ''; + html += '' + connectLink + ''; + html += ''; + }); + body.innerHTML = html; +} + +function fetchDirectory() { + var empty = document.getElementById('dir-empty'); + empty.style.display = ''; + empty.textContent = 'Querying Nostr advert relays (this takes a few seconds)…'; + document.getElementById('dir-table').style.display = 'none'; + document.getElementById('dir-actions').style.display = 'none'; + return sovereignGet('sovereign://fips/directory?_=' + Date.now()) + .then(function(data) { renderDirectory(data); }) + .catch(function(e) { + empty.textContent = 'Failed to load directory: ' + e.message; + }); +} + +function connectFromDirectory(npub, address, transport) { + /* Switch to the Peers tab, pre-fill the add-peer form, and connect. */ + switchTab('peers'); + document.getElementById('peer-npub').value = npub; + document.getElementById('peer-address').value = address; + var sel = document.getElementById('peer-transport'); + for (var i = 0; i < sel.options.length; i++) { + if (sel.options[i].value === transport) { + sel.selectedIndex = i; + break; + } + } + connectPeer(); +} + +/* ── Refresh + auto-refresh ─────────────────────────────────────── */ + +function refresh() { + showMsg(''); + var activeTab = document.querySelector('.tab-btn.active'); + var tabName = activeTab ? activeTab.dataset.tab : 'status'; + var promises = [fetchStatus(), fetchPeers()]; + if (tabName === 'network') promises.push(fetchNetwork()); + if (tabName === 'tree') promises.push(fetchTree()); + Promise.all(promises).then(function() {}); +} + +document.getElementById('refresh-btn').addEventListener('click', refresh); +document.getElementById('connect-btn').addEventListener('click', connectPeer); +document.getElementById('restart-btn').addEventListener('click', restartService); +var dirRefreshBtn = document.getElementById('dir-refresh-btn'); +if (dirRefreshBtn) dirRefreshBtn.addEventListener('click', fetchDirectory); + +/* Initial load. */ +refresh(); + +/* Auto-refresh status + peers every 5s. Network/tree refresh on + * tab activation and every 10s when active. */ +setInterval(function() { + fetchStatus(); + fetchPeers(); +}, 5000); + +setInterval(function() { + var activeTab = document.querySelector('.tab-btn.active'); + if (!activeTab) return; + var tabName = activeTab.dataset.tab; + if (tabName === 'network') fetchNetwork(); + if (tabName === 'tree') fetchTree(); +}, 10000); diff --git a/www/settings.html b/www/settings.html index 56635ae..e05979f 100644 --- a/www/settings.html +++ b/www/settings.html @@ -118,6 +118,13 @@ +

Network

+
+
FIPS Mesh Network
+
Manage the FIPS mesh daemon: status, peers, and service control.
+ +
+

Security