mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-22 13:02:26 +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
133 lines
5.6 KiB
Go
133 lines
5.6 KiB
Go
// Copyright 2026 Beacon Contributors
|
||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||
package handlers
|
||
|
||
import (
|
||
"errors"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/MeshCore-Beacon/beacon-server/internal/api"
|
||
"github.com/go-chi/chi/v5"
|
||
"github.com/jackc/pgx/v5"
|
||
)
|
||
|
||
func routeEvidenceQuery(r *http.Request, iata, key string, now time.Time) (api.RouteEvidenceQuery, error) {
|
||
q := api.RouteEvidenceQuery{}
|
||
values := r.URL.Query()
|
||
for _, name := range []string{"pageCursor", "since", "until", "range", "limit", "hashSize", "pathBytes"} {
|
||
if len(values[name]) > 1 {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
}
|
||
if values.Has("cursor") {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
limit, err := parseLimit(r, 50)
|
||
if err != nil {
|
||
return q, err
|
||
}
|
||
q.Limit = limit
|
||
if values.Has("pageCursor") {
|
||
c, err := api.ParseRouteEvidenceCursor(values.Get("pageCursor"))
|
||
if err != nil || c.IATA != iata || c.PathKey != key {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
q.Cursor = c
|
||
q.Since = c.Since
|
||
q.Until = c.Until
|
||
q.HashSize, q.PathBytes = c.HashSize, c.PathBytes
|
||
} else {
|
||
q.Until = now.UTC().Truncate(time.Millisecond)
|
||
q.Since = q.Until.Add(-24 * time.Hour)
|
||
}
|
||
if values.Has("range") {
|
||
if values.Has("pageCursor") || values.Has("since") || values.Has("until") {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
duration, err := time.ParseDuration(values.Get("range"))
|
||
if err != nil || duration < time.Millisecond || duration > api.MaxRouteEvidenceWindow || duration%time.Millisecond != 0 {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
q.Since = q.Until.Add(-duration)
|
||
}
|
||
if values.Has("since") || values.Has("until") {
|
||
if !values.Has("since") || !values.Has("until") {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
since, e1 := strconv.ParseInt(values.Get("since"), 10, 64)
|
||
until, e2 := strconv.ParseInt(values.Get("until"), 10, 64)
|
||
if e1 != nil || e2 != nil || since < 0 || until < 0 || since > 253402300799999 || until > 253402300799999 {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
q.Since = time.UnixMilli(since).UTC()
|
||
q.Until = time.UnixMilli(until).UTC()
|
||
if q.Cursor != nil && (!q.Since.Equal(q.Cursor.Since) || !q.Until.Equal(q.Cursor.Until)) {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
}
|
||
if values.Has("hashSize") || values.Has("pathBytes") {
|
||
width, err := strconv.ParseInt(values.Get("hashSize"), 10, 16)
|
||
path := values.Get("pathBytes")
|
||
if err != nil || !api.ValidRouteEvidencePath(int16(width), path) || strconv.FormatInt(width, 10) != values.Get("hashSize") {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
if q.Cursor != nil && q.Cursor.HashSize != 0 && (q.Cursor.HashSize != int16(width) || q.Cursor.PathBytes != path) {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
q.HashSize, q.PathBytes = int16(width), path
|
||
}
|
||
if !api.ValidRouteEvidenceWindow(q.Since, q.Until) || q.Until.After(now.Add(clockSkewTolerance)) {
|
||
return q, api.ErrRouteEvidenceInput
|
||
}
|
||
return q, nil
|
||
}
|
||
|
||
// getRouteEvidence godoc
|
||
//
|
||
// @Summary Get retained reports matching a full saved route prefix sequence
|
||
// @Description Matches one complete path representation in the saved IATA. Defaults to the latest processed prefixes; hashSize/pathBytes pin a prior response. Excludes TRACE and unclassified reports; widths are never combined. Pinned prefixes must match the saved node chain or its current metadata. Byte matches do not prove hop identities or delivery. The route counter can outlive raw evidence. New cursors pin path, route, window and full timestamp precision; legacy unpinned cursors remain supported.
|
||
// @Tags Routes
|
||
// @Produce json
|
||
// @Param iata path string true "Three-character IATA code"
|
||
// @Param pathKey path string true "Stable 32-hex pathKey from a known-route response"
|
||
// @Param range query string false "Server-anchored duration (default 24h, max 720h); exclusive with since/until/pageCursor"
|
||
// @Param since query int false "Window start epoch ms; provide with until (default last 24h)"
|
||
// @Param until query int false "Exclusive window end epoch ms; maximum span 30d, end may be up to 5 minutes ahead of server time (clock skew tolerance)"
|
||
// @Param hashSize query int false "Exact hash width (1–3); provide with pathBytes to pin a response's representation" minimum(1) maximum(3)
|
||
// @Param pathBytes query string false "Complete lower-case hex path from the response; provide with hashSize"
|
||
// @Param pageCursor query string false "Opaque precise cursor from nextPageCursor; route, window and path are pinned"
|
||
// @Param limit query int false "Default 50; positive, capped at 200" minimum(1) maximum(200)
|
||
// @Success 200 {object} api.RouteEvidence
|
||
// @Failure 400 {object} handlers.APIError
|
||
// @Failure 404 {object} handlers.APIError
|
||
// @Failure 500 {object} handlers.APIError
|
||
// @Router /routes/{iata}/{pathKey}/observations [get]
|
||
func getRouteEvidence(reader api.Reader) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
iata, key := strings.ToUpper(chi.URLParam(r, "iata")), strings.ToLower(chi.URLParam(r, "pathKey"))
|
||
if !api.ValidRouteEvidenceKey(iata, key) {
|
||
respondError(w, 400, "invalid IATA or route pathKey")
|
||
return
|
||
}
|
||
query, err := routeEvidenceQuery(r, iata, key, time.Now())
|
||
if err != nil {
|
||
respondError(w, 400, "invalid route evidence window, path, limit or page cursor")
|
||
return
|
||
}
|
||
result, err := reader.GetRouteEvidence(r.Context(), iata, key, query)
|
||
switch {
|
||
case errors.Is(err, pgx.ErrNoRows), err == nil && result == nil:
|
||
respondError(w, 404, "saved route not found")
|
||
case errors.Is(err, api.ErrRouteEvidenceInput):
|
||
respondError(w, 400, "invalid route evidence request")
|
||
case err != nil:
|
||
respondError(w, 500, "internal server error")
|
||
default:
|
||
respond(w, 200, result)
|
||
}
|
||
}
|
||
}
|