package beach import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "sort" "strings" "time" ) type TunnelSite struct { Name string `json:"name"` Online bool `json:"online"` Local bool `json:"local"` Version string `json:"version,omitempty"` LastSeen time.Time `json:"lastSeen"` } type TunnelPort struct { Name string `json:"name"` Proto string `json:"proto"` Hostname string `json:"hostname,omitempty"` Port int `json:"port"` Enabled bool `json:"enabled"` Up bool `json:"up"` // the edge reports it listening without error Known bool `json:"known"` Healthy int `json:"healthy"` Targets int `json:"targets"` } type Tunnels struct { Sites []TunnelSite `json:"sites"` Ports []TunnelPort `json:"ports"` Error string `json:"error,omitempty"` } // FetchTunnels reads GET /api/orgs/{org}/state from Hammerhead with an API key. func FetchTunnels(ctx context.Context, base, key, org string) Tunnels { u := strings.TrimRight(base, "/") + "/api/orgs/" + url.PathEscape(org) + "/state" req, err := http.NewRequestWithContext(ctx, "GET", u, nil) if err != nil { return Tunnels{Error: err.Error()} } req.Header.Set("Authorization", "Bearer "+key) resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req) if err != nil { return Tunnels{Error: err.Error()} } defer resp.Body.Close() b, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<20)) if resp.StatusCode != 200 { return Tunnels{Error: fmt.Sprintf("hammerhead: %s", resp.Status)} } t, err := parseTunnels(b) if err != nil { return Tunnels{Error: err.Error()} } return t } func parseTunnels(b []byte) (Tunnels, error) { var st struct { Sites []struct { ID string `json:"id"` Name string `json:"name"` Local bool `json:"local"` Version string `json:"version"` LastSeen time.Time `json:"lastSeen"` } `json:"sites"` Online map[string]bool `json:"online"` Mappings []struct { Name string `json:"name"` Proto string `json:"proto"` Hostname string `json:"hostname"` Port int `json:"port"` Enabled bool `json:"enabled"` Targets []any `json:"targets"` Status struct { Error string `json:"error"` Targets []struct { Healthy bool `json:"healthy"` } `json:"targets"` } `json:"status"` Known bool `json:"statusKnown"` } `json:"mappings"` } if err := json.Unmarshal(b, &st); err != nil { return Tunnels{}, fmt.Errorf("hammerhead: unexpected response: %w", err) } t := Tunnels{Sites: []TunnelSite{}, Ports: []TunnelPort{}} for _, s := range st.Sites { t.Sites = append(t.Sites, TunnelSite{Name: s.Name, Online: st.Online[s.ID], Local: s.Local, Version: s.Version, LastSeen: s.LastSeen}) } for _, m := range st.Mappings { p := TunnelPort{Name: m.Name, Proto: m.Proto, Hostname: m.Hostname, Port: m.Port, Enabled: m.Enabled, Known: m.Known, Targets: len(m.Targets)} p.Up = m.Enabled && m.Known && m.Status.Error == "" for _, th := range m.Status.Targets { if th.Healthy { p.Healthy++ } } t.Ports = append(t.Ports, p) } sort.Slice(t.Sites, func(i, j int) bool { return t.Sites[i].Name < t.Sites[j].Name }) sort.Slice(t.Ports, func(i, j int) bool { return t.Ports[i].Name < t.Ports[j].Name }) return t, nil }