Sandbar channels raise and resolve alerts through its API, with the API key sealed at rest and never returned to the browser. Resources and sites can be tagged and filtered by tag; sites can be renamed. The secretbox package now comes from Reef v0.1.9 instead of a private copy. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
122 lines
3.6 KiB
Go
122 lines
3.6 KiB
Go
package hammerhead
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.veillax.com/Veillax/reef/model"
|
|
"git.veillax.com/Veillax/reef/secretbox"
|
|
)
|
|
|
|
func edgeConfigOf(t *testing.T, s *Server, token string) model.EdgeConfig {
|
|
t.Helper()
|
|
ts := httptest.NewServer(s.Handler())
|
|
defer ts.Close()
|
|
req, _ := http.NewRequest("GET", ts.URL+"/edge/config?v=0", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
var cfg model.EdgeConfig
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("edge config: %s", resp.Status)
|
|
}
|
|
json.NewDecoder(resp.Body).Decode(&cfg)
|
|
return cfg
|
|
}
|
|
|
|
func tokenFile(t *testing.T, dir string) string {
|
|
b, err := os.ReadFile(filepath.Join(dir, "self-edge.env"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return strings.TrimSpace(strings.SplitN(strings.SplitN(string(b), "BLACKTIP_TOKEN=", 2)[1], "\n", 2)[0])
|
|
}
|
|
|
|
var key = secretbox.GenerateKey()
|
|
|
|
func TestSelfEdgeServesTheDashboardWithoutAProxy(t *testing.T) {
|
|
dir := t.TempDir()
|
|
opts := Options{MasterKey: key, SelfDomain: "Hammerhead.Example.com", SelfTarget: "127.0.0.1:8080"}
|
|
s, err := New(dir, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
token := tokenFile(t, dir)
|
|
cfg := edgeConfigOf(t, s, token)
|
|
if len(cfg.Mappings) != 1 || len(cfg.Local) != 1 {
|
|
t.Fatalf("mappings=%d local=%d", len(cfg.Mappings), len(cfg.Local))
|
|
}
|
|
m := cfg.Mappings[0]
|
|
if m.Proto != model.ProtoHTTPS || m.Hostname != "hammerhead.example.com" || m.Port != 443 || !m.RedirectHTTP || !m.Enabled ||
|
|
len(m.Targets) != 1 || m.Targets[0].Host != "127.0.0.1" || m.Targets[0].Port != 8080 || m.Targets[0].SiteID != cfg.Local[0] {
|
|
t.Fatalf("unexpected dashboard route: %+v", m)
|
|
}
|
|
if m.Auth.Protected() {
|
|
t.Fatal("the dashboard has its own login and must not sit behind resource auth")
|
|
}
|
|
edgeID := s.st.Settings.SelfEdgeID
|
|
if info, _ := os.Stat(filepath.Join(dir, "self-edge.env")); info.Mode().Perm()&0o077 != 0 && os.PathSeparator == '/' {
|
|
t.Fatalf("token file is readable by others: %v", info.Mode())
|
|
}
|
|
s.Close()
|
|
|
|
// a restart keeps the same edge and token instead of making a new one
|
|
s2, err := New(dir, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s2.Close()
|
|
if s2.st.Settings.SelfEdgeID != edgeID || tokenFile(t, dir) != token || len(s2.st.Edges) != 1 {
|
|
t.Fatal("restart changed the self edge")
|
|
}
|
|
if len(edgeConfigOf(t, s2, token).Mappings) != 1 {
|
|
t.Fatal("route missing after restart")
|
|
}
|
|
}
|
|
|
|
func TestNoSelfEdgeUnlessAsked(t *testing.T) {
|
|
dir := t.TempDir()
|
|
s, err := New(dir, Options{MasterKey: key})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
if len(s.st.Edges) != 0 {
|
|
t.Fatal("an edge appeared without HAMMERHEAD_SELF_DOMAIN")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "self-edge.env")); err == nil {
|
|
t.Fatal("token file written without HAMMERHEAD_SELF_DOMAIN")
|
|
}
|
|
}
|
|
|
|
func TestSavingSettingsKeepsTheSelfEdge(t *testing.T) {
|
|
dir := t.TempDir()
|
|
opts := Options{AdminEmail: "admin@test.local", AdminPassword: "password123", MasterKey: key, SelfDomain: "hh.example.com", SelfTarget: "127.0.0.1:8080"}
|
|
s, err := New(dir, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
token := tokenFile(t, dir)
|
|
do := apiClient(t, s)
|
|
if code, b := do("PUT", "/api/admin/settings", map[string]any{"dashboardUrl": "https://hh.example.com", "acmeEmail": "me@example.com"}); code != 200 {
|
|
t.Fatalf("settings: %d %s", code, b)
|
|
}
|
|
s.Close()
|
|
s2, err := New(dir, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s2.Close()
|
|
if len(s2.st.Edges) != 1 || tokenFile(t, dir) != token {
|
|
t.Fatalf("saving settings lost the self edge: %d edges", len(s2.st.Edges))
|
|
}
|
|
}
|