Files
meshcore-analyzer/cmd/server/rf_params_route.go
T
n30nex c085ca8cba fix(rf-params): respect node visibility in responses (#2276)
The RF endpoint now applies the current blacklist and hidden-name policy
to both exposed node metadata and deviations, including after cache
warm-up. Unhiding a node restores its data without waiting for cache
expiry.

Fixes #2268.

The shared RF snapshot and peer calculation remain unchanged; filtering
copies only the response data. This adds a linear node pass and bounded
deviation copies, with no additional database calls or cache
invalidation. The feature remains opt-in.

Validation: RED reproduces both response leaks. The full server and
standalone frontend suites pass, as do focused race tests and the RF
browser suite. Regressions cover warm/cold caches, policy updates,
unhide, visible controls and default-off behavior. Common ingestor
validation is covered by the separate #2272 test-stability change.
2026-10-25 12:10:47 +01:00

70 lines
2.0 KiB
Go

package main
import (
"net/http"
"strings"
"time"
)
// handleRfParams — GET /api/rf-params (issue #1268).
//
// Returns the radio settings the MeshCore map holds for the visible nodes this
// instance knows, limited to adverts within maxAgeDays and not dated in the
// future (rfAdvertFresh), plus the named presets and when the map was last
// fetched. Served straight from the atomic snapshot; never waits on the
// upstream fetch.
func (s *Server) handleRfParams(w http.ResponseWriter, r *http.Request) {
resp := RfParamsResponse{Presets: []RfPreset{}, Nodes: map[string]RfParams{}, Deviations: map[string]RfDeviation{}}
if s.rfParams == nil {
writeJSON(w, resp)
return
}
resp.Enabled = true
snap := s.rfParams.load()
if snap == nil {
w.Header().Set("Cache-Control", "public, max-age=30")
writeJSON(w, resp)
return
}
fetched := snap.FetchedAt
resp.FetchedAt = &fetched
resp.Source = snap.Source
if snap.Presets != nil {
resp.Presets = snap.Presets
}
now := time.Now().UTC()
maxAge := s.rfParams.cfg.maxAge()
nodes, _ := s.store.getCachedNodesAndPM()
for _, n := range nodes {
pk := strings.ToLower(n.PublicKey)
p, ok := snap.Nodes[pk]
if !ok {
continue
}
if at, err := time.Parse(time.RFC3339, p.LastAdvert); err != nil || !rfAdvertFresh(at, now, maxAge) {
continue
}
resp.Nodes[pk] = p
}
deviations := s.rfParams.deviations.get(snap, time.Now(), func() map[string]RfDeviation {
return computeRfDeviations(resp.Nodes, nodes, s.rfParams.cfg.compareRadiusKm(), rfMinPeers)
})
// Keep the shared computation independent of visibility. Filter only the
// response maps so hiding or unhiding a node takes effect even on a cache hit.
for _, n := range nodes {
pk := strings.ToLower(n.PublicKey)
if _, fresh := resp.Nodes[pk]; !fresh {
continue
}
if s.cfg.IsBlacklisted(pk) || s.cfg.IsNameHidden(n.Name) {
delete(resp.Nodes, pk)
continue
}
if d, ok := deviations[pk]; ok {
resp.Deviations[pk] = d
}
}
w.Header().Set("Cache-Control", "public, max-age=300")
writeJSON(w, resp)
}