mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-20 08:42:01 +00:00
Port the qualified server preview at84ac3c8781onto org devc7209b7043. Preserve upstream changes and branch policies; keep the independent private Collector service separate. Preview-Version: 2.2.0-n30nex.1 Preview-Revision:84ac3c8781
129 lines
5.0 KiB
Go
129 lines
5.0 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
package db
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"math"
|
|
"time"
|
|
|
|
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
|
|
"github.com/MeshCore-Beacon/beacon-server/internal/api"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
func savedRoutePath(prefixes [][]byte, count int32) (int16, []byte, bool) {
|
|
if count < 2 || count > 63 || len(prefixes) != int(count) {
|
|
return 0, nil, false
|
|
}
|
|
width := len(prefixes[0])
|
|
if width < 1 || width > 3 {
|
|
return 0, nil, false
|
|
}
|
|
for _, p := range prefixes {
|
|
if len(p) != width {
|
|
return 0, nil, false
|
|
}
|
|
}
|
|
return int16(width), bytes.Join(prefixes, nil), true
|
|
}
|
|
|
|
func (s *Store) GetRouteEvidence(ctx context.Context, iata, key string, query api.RouteEvidenceQuery) (*api.RouteEvidence, error) {
|
|
if !api.ValidRouteEvidenceKey(iata, key) || !api.ValidRouteEvidenceWindow(query.Since, query.Until) || query.Limit < 1 || query.Limit > 200 {
|
|
return nil, api.ErrRouteEvidenceInput
|
|
}
|
|
if c := query.Cursor; c != nil {
|
|
if c.IATA != iata || c.PathKey != key || !c.Since.Equal(query.Since) || !c.Until.Equal(query.Until) || c.ID <= 0 || c.HeardAt.Before(query.Since) || !c.HeardAt.Before(query.Until) {
|
|
return nil, api.ErrRouteEvidenceInput
|
|
}
|
|
if c.HashSize != 0 || c.PathBytes != "" {
|
|
if (query.HashSize != 0 || query.PathBytes != "") && (query.HashSize != c.HashSize || query.PathBytes != c.PathBytes) {
|
|
return nil, api.ErrRouteEvidenceInput
|
|
}
|
|
query.HashSize, query.PathBytes = c.HashSize, c.PathBytes
|
|
}
|
|
}
|
|
pinned := query.HashSize != 0 || query.PathBytes != ""
|
|
if pinned && !api.ValidRouteEvidencePath(query.HashSize, query.PathBytes) {
|
|
return nil, api.ErrRouteEvidenceInput
|
|
}
|
|
keyBytes, _ := hex.DecodeString(key)
|
|
row, err := s.q.GetRouteEvidenceRoute(ctx, sqlc.GetRouteEvidenceRouteParams{Iata: iata, PathKey: keyBytes})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodes, err := s.GetNodesByIDs(ctx, row.NodeIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
width, path, valid := savedRoutePath(row.HashPrefix, row.HopCount)
|
|
if pinned {
|
|
selected, _ := hex.DecodeString(query.PathBytes)
|
|
hops := len(selected) / int(query.HashSize)
|
|
if hops != len(row.NodeIds) || hops != int(row.HopCount) {
|
|
return nil, api.ErrRouteEvidenceInput
|
|
}
|
|
prefixes := make([][]byte, hops)
|
|
for i, id := range row.NodeIds {
|
|
prefixes[i] = selected[i*int(query.HashSize) : (i+1)*int(query.HashSize)]
|
|
// A changed representation must still belong to this node chain.
|
|
// Reuse the already-loaded keys; never widen the evidence query.
|
|
if !valid || width != query.HashSize || !bytes.Equal(path, selected) {
|
|
node := nodes[id]
|
|
if node == nil {
|
|
return nil, api.ErrRouteEvidenceInput
|
|
}
|
|
pubkey, err := hex.DecodeString(node.PublicKey)
|
|
if err != nil || !bytes.HasPrefix(pubkey, prefixes[i]) {
|
|
return nil, api.ErrRouteEvidenceInput
|
|
}
|
|
}
|
|
}
|
|
row.HashPrefix = prefixes
|
|
width, path, valid = query.HashSize, selected, true
|
|
}
|
|
route := toKnownRoutes([]knownRouteRow{{ID: row.ID, NodeIds: row.NodeIds, HashPrefix: row.HashPrefix, Iata: row.Iata, HopCount: row.HopCount, FirstSeen: row.FirstSeen, LastSeen: row.LastSeen, ObservationCount: row.ObservationCount}}, nodes)[0]
|
|
route.PathKey = hex.EncodeToString(row.PathKey)
|
|
out := &api.RouteEvidence{Items: []api.RouteObservation{}, Route: route, WindowStart: query.Since.UnixMilli(), WindowEnd: query.Until.UnixMilli(), GeneratedAt: time.Now().UnixMilli(), MatchType: "saved_path_prefixes"}
|
|
if !valid {
|
|
return out, nil
|
|
}
|
|
out.MatchAvailable = true
|
|
out.HashSize = width
|
|
out.PathBytes = hex.EncodeToString(path)
|
|
beforeAt, beforeID := query.Until, int64(math.MaxInt64)
|
|
if query.Cursor != nil {
|
|
beforeAt, beforeID = query.Cursor.HeardAt, query.Cursor.ID
|
|
}
|
|
ts := func(t time.Time) pgtype.Timestamptz { return pgtype.Timestamptz{Time: t, Valid: true} }
|
|
digest := md5.Sum(path)
|
|
rows, err := s.q.ListRouteEvidence(ctx, sqlc.ListRouteEvidenceParams{Iata: iata, HashSize: width, HopCount: int16(row.HopCount), PathDigest: digest[:], PathBytes: path, Since: ts(query.Since), Until: ts(query.Until), BeforeAt: ts(beforeAt), BeforeID: beforeID, PageLimit: query.Limit + 1})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out.HasMore = len(rows) > int(query.Limit)
|
|
if out.HasMore {
|
|
rows = rows[:query.Limit]
|
|
}
|
|
for _, r := range rows {
|
|
payload := int16(-1)
|
|
if r.PayloadType != nil {
|
|
payload = *r.PayloadType
|
|
}
|
|
snr := r.Snr
|
|
if snr != nil && (math.IsNaN(float64(*snr)) || math.IsInf(float64(*snr), 0)) {
|
|
snr = nil
|
|
}
|
|
out.Items = append(out.Items, api.RouteObservation{ID: r.ID, PacketHash: hex.EncodeToString(r.PacketHash), ObserverID: r.ObserverID, ObserverName: r.ObserverName, HeardAt: r.HeardAt.Time.UnixMilli(), PayloadType: payload, PayloadTypeName: api.PayloadTypeName(payload), RSSI: r.Rssi, SNR: snr})
|
|
}
|
|
if out.HasMore {
|
|
last := rows[len(rows)-1]
|
|
cursor := (api.RouteEvidenceCursor{IATA: iata, PathKey: key, Since: query.Since, Until: query.Until, HeardAt: last.HeardAt.Time, ID: last.ID, HashSize: width, PathBytes: out.PathBytes}).String()
|
|
out.NextPageCursor = &cursor
|
|
}
|
|
return out, nil
|
|
}
|