Files
beacon-server/internal/meshmapper/client.go
T
MrAlders0n 041d9c1f45 release: v2.0.2
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.
2026-10-05 18:35:41 -04:00

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.
}