183 lines
5.5 KiB
Go
183 lines
5.5 KiB
Go
// Package client is a client for a Wingbits station (such as the MGW310). It
|
|
// exposes the readsb JSON files (aircraft, receiver, stats, outline) and the
|
|
// Wingbits-specific endpoints (network diagnostics, Prometheus metrics and
|
|
// Tailscale status) as typed Go values, with both one-shot queries and
|
|
// channel-based streaming.
|
|
//
|
|
// Decoded values come from the type-only packages pkg/types/readsb and
|
|
// pkg/types/wingbits; this package owns the transport and request logic.
|
|
//
|
|
// A station serves two HTTP roots:
|
|
//
|
|
// - The Wingbits root (default :8088) serves the Wingbits endpoints directly
|
|
// and the readsb files under readsb/.
|
|
// - The tar1090 root (default :8504) serves the readsb files under data/,
|
|
// plus data/outline.json. When configured it overrides the Wingbits root
|
|
// for all readsb files.
|
|
package client
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Default ports exposed by an MGW310 station.
|
|
const (
|
|
DefaultWingbitsPort = 8088
|
|
DefaultTar1090Port = 8504
|
|
// defaultDataPath is the tar1090 path under which readsb files are served.
|
|
defaultDataPath = "data"
|
|
// wingbitsReadsbPath is the path under which the Wingbits root serves readsb files.
|
|
wingbitsReadsbPath = "readsb"
|
|
)
|
|
|
|
// Endpoint describes one HTTP root on a station.
|
|
type Endpoint struct {
|
|
// Scheme is "http" (default) or "https".
|
|
Scheme string
|
|
// Host is the hostname or IP of the station.
|
|
Host string
|
|
// Port is the TCP port; 0 selects the role's default.
|
|
Port int
|
|
// DataPath is the URL path prefix under which readsb files are served. For
|
|
// the tar1090 root this defaults to "data"; for the Wingbits root the
|
|
// readsb files live under "readsb".
|
|
DataPath string
|
|
// TLS configures the transport when Scheme is "https". It is only applied
|
|
// when the client builds its own http.Client (i.e. no WithHTTPClient).
|
|
TLS *tls.Config
|
|
}
|
|
|
|
func (e Endpoint) scheme() string {
|
|
if e.Scheme != "" {
|
|
return e.Scheme
|
|
}
|
|
return "http"
|
|
}
|
|
|
|
func (e Endpoint) hostPort(defPort int) string {
|
|
port := e.Port
|
|
if port == 0 {
|
|
port = defPort
|
|
}
|
|
return fmt.Sprintf("%s:%d", e.Host, port)
|
|
}
|
|
|
|
// url builds an absolute URL for path elements joined under the endpoint,
|
|
// optionally prefixed by the endpoint's data path.
|
|
func (e Endpoint) url(defPort int, withData bool, elem ...string) string {
|
|
parts := elem
|
|
if withData && e.DataPath != "" {
|
|
parts = append([]string{e.DataPath}, elem...)
|
|
}
|
|
u := url.URL{
|
|
Scheme: e.scheme(),
|
|
Host: e.hostPort(defPort),
|
|
Path: "/" + strings.Join(parts, "/"),
|
|
}
|
|
return u.String()
|
|
}
|
|
|
|
// Client talks to a Wingbits station. Construct it with New or NewMGW310Client.
|
|
// It is safe for concurrent use.
|
|
type Client struct {
|
|
wingbits Endpoint
|
|
tar1090 *Endpoint
|
|
http *http.Client
|
|
userAgent string
|
|
}
|
|
|
|
// Option customizes a Client.
|
|
type Option func(*Client)
|
|
|
|
// WithWingbitsEndpoint sets (and overrides) the Wingbits HTTP root.
|
|
func WithWingbitsEndpoint(e Endpoint) Option {
|
|
return func(c *Client) { c.wingbits = e }
|
|
}
|
|
|
|
// WithTar1090Endpoint enables a tar1090 HTTP root. When set it overrides the
|
|
// Wingbits root for all readsb files (aircraft, receiver, stats) and is the
|
|
// only source for outline.json.
|
|
func WithTar1090Endpoint(e Endpoint) Option {
|
|
return func(c *Client) {
|
|
if e.DataPath == "" {
|
|
e.DataPath = defaultDataPath
|
|
}
|
|
c.tar1090 = &e
|
|
}
|
|
}
|
|
|
|
// WithHTTPClient supplies a custom http.Client. When provided, per-endpoint TLS
|
|
// config is ignored (configure it on the supplied client's transport instead).
|
|
func WithHTTPClient(h *http.Client) Option {
|
|
return func(c *Client) { c.http = h }
|
|
}
|
|
|
|
// WithUserAgent overrides the User-Agent header sent with requests.
|
|
func WithUserAgent(ua string) Option {
|
|
return func(c *Client) { c.userAgent = ua }
|
|
}
|
|
|
|
// New constructs a Client from options. A Wingbits endpoint with a non-empty
|
|
// Host must be supplied via WithWingbitsEndpoint.
|
|
func New(opts ...Option) (*Client, error) {
|
|
c := &Client{
|
|
userAgent: "wingbits-go/1.0",
|
|
}
|
|
for _, opt := range opts {
|
|
opt(c)
|
|
}
|
|
if c.wingbits.DataPath == "" {
|
|
c.wingbits.DataPath = wingbitsReadsbPath
|
|
}
|
|
if c.wingbits.Host == "" {
|
|
return nil, fmt.Errorf("wingbits: a wingbits endpoint host is required")
|
|
}
|
|
if c.http == nil {
|
|
c.http = buildHTTPClient(c.wingbits, c.tar1090)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// NewMGW310Client is a convenience constructor for a standard MGW310 reachable
|
|
// at host. It wires the Wingbits root to :8088 and a tar1090 root to :8504, then
|
|
// applies any options (which may override either endpoint, TLS, the HTTP client,
|
|
// and so on).
|
|
func NewMGW310Client(host string, opts ...Option) (*Client, error) {
|
|
base := []Option{
|
|
WithWingbitsEndpoint(Endpoint{Host: host, Port: DefaultWingbitsPort, DataPath: wingbitsReadsbPath}),
|
|
WithTar1090Endpoint(Endpoint{Host: host, Port: DefaultTar1090Port, DataPath: defaultDataPath}),
|
|
}
|
|
return New(append(base, opts...)...)
|
|
}
|
|
|
|
// readsbEndpoint returns the endpoint and default port serving readsb files,
|
|
// preferring a configured tar1090 root.
|
|
func (c *Client) readsbEndpoint() (Endpoint, int) {
|
|
if c.tar1090 != nil {
|
|
return *c.tar1090, DefaultTar1090Port
|
|
}
|
|
return c.wingbits, DefaultWingbitsPort
|
|
}
|
|
|
|
// buildHTTPClient creates a default client, attaching TLS config from whichever
|
|
// endpoint supplies one (the tar1090 root takes precedence when both do).
|
|
func buildHTTPClient(wb Endpoint, tar *Endpoint) *http.Client {
|
|
var tlsCfg *tls.Config
|
|
if wb.TLS != nil {
|
|
tlsCfg = wb.TLS
|
|
}
|
|
if tar != nil && tar.TLS != nil {
|
|
tlsCfg = tar.TLS
|
|
}
|
|
tr := &http.Transport{
|
|
TLSClientConfig: tlsCfg,
|
|
MaxIdleConnsPerHost: 2,
|
|
}
|
|
return &http.Client{Timeout: 15 * time.Second, Transport: tr}
|
|
}
|