mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-16 11:39:53 +00:00
Release the changes on `dev` as v2.0.2: authenticated MeshMapper imports and the traffic-sorted observer directory, including rolling seven-day counts from available history. Bump the API version and regenerate Swagger. MeshMapper integration requires an API key. Regional admins’ existing per-region or grouped-region coverage keys also work for these APIs. Deployments should set `MESHMAPPER_API_KEY` and restart Beacon. Validation: `go build ./...`, `gofmt -l .` (empty), `go vet ./...`, `go test ./...`, `govulncheck ./...`, and race tests for MeshMapper and scopestore passed in a clean checkout. PostgreSQL integration tests require CI because Docker is unavailable locally.
68 lines
2.0 KiB
Go
68 lines
2.0 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package meshmapper
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
errMissingKey = errors.New("unconfigured: MeshMapper API key is missing")
|
|
errInvalidKey = errors.New("unconfigured: MeshMapper API key contains invalid characters")
|
|
errUntrustedEndpoint = errors.New("untrusted MeshMapper endpoint")
|
|
)
|
|
|
|
func newClient(apiKey string) *http.Client {
|
|
return &http.Client{
|
|
Timeout: requestTimeout,
|
|
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
|
|
Transport: &authenticatedTransport{key: apiKey, base: http.DefaultTransport},
|
|
}
|
|
}
|
|
|
|
type authenticatedTransport struct {
|
|
key string
|
|
base http.RoundTripper
|
|
}
|
|
|
|
func (t *authenticatedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
if t.key == "" {
|
|
return nil, errMissingKey
|
|
}
|
|
if strings.ContainsFunc(t.key, func(r rune) bool { return r <= ' ' || r >= 127 }) {
|
|
return nil, errInvalidKey
|
|
}
|
|
if !trustedEndpoint(req.URL) || (req.Host != "" && req.Host != req.URL.Host) {
|
|
return nil, errUntrustedEndpoint
|
|
}
|
|
// Clone so credentials never enter a caller's request or redirect headers.
|
|
request := req.Clone(req.Context())
|
|
request.Header.Set("X-API-Key", t.key)
|
|
return t.base.RoundTrip(request)
|
|
}
|
|
|
|
func trustedEndpoint(u *url.URL) bool {
|
|
if u.Scheme != "https" || u.User != nil || u.Fragment != "" || u.Opaque != "" ||
|
|
(u.Host != "meshmapper.net" && !zoneSite.MatchString(u.Host)) || u.EscapedPath() != u.Path {
|
|
return false
|
|
}
|
|
switch u.Path {
|
|
case "/get_zones.php", "/get_geojson.php", "/get_scopes.php", "/get_channels.php", "/get_repeaters.php":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func requestProblem(err error) string {
|
|
for _, problem := range []error{errMissingKey, errInvalidKey, errUntrustedEndpoint} {
|
|
if errors.Is(err, problem) {
|
|
return problem.Error()
|
|
}
|
|
}
|
|
return "request failed" // Transport errors may contain URLs or credentials.
|
|
}
|