A website resource can now redirect to a full address (301 or 302), optionally keeping the path and query, with no target or login. The Add resource wizard has a Domain redirect choice that opens the HTTPS and HTTP sides together. Deleting a site no longer removes redirects. Uses Reef v0.1.10 and Blacktip v0.1.9. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
110 lines
5.2 KiB
Go
110 lines
5.2 KiB
Go
package hammerhead
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// a redirect is a website with a destination instead of a target
|
|
func TestDomainRedirects(t *testing.T) {
|
|
s, err := New(t.TempDir(), Options{AdminEmail: "admin@test.local", AdminPassword: "password123", MasterKey: key})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
do := apiClient(t, s)
|
|
var me struct{ Orgs []struct{ ID string } }
|
|
_, b := do("GET", "/api/me", nil)
|
|
json.Unmarshal(b, &me)
|
|
org := me.Orgs[0].ID
|
|
var edge, site struct{ ID string }
|
|
_, b = do("POST", "/api/edges", map[string]any{"name": "e", "tunnelAddr": "203.0.113.5:51820"})
|
|
json.Unmarshal(b, &edge)
|
|
_, b = do("POST", "/api/orgs/"+org+"/sites", map[string]any{"name": "home"})
|
|
json.Unmarshal(b, &site)
|
|
if code, b := do("POST", "/api/orgs/"+org+"/domains", map[string]any{"name": "example.test", "edgeId": edge.ID, "ip": "203.0.113.10"}); code != 200 {
|
|
t.Fatalf("domain: %d %s", code, b)
|
|
}
|
|
base := "/api/orgs/" + org + "/mappings"
|
|
open := func(m map[string]any) (int, []byte) {
|
|
m["edgeId"], m["enabled"], m["bindIp"] = edge.ID, true, "0.0.0.0"
|
|
return do("POST", base, m)
|
|
}
|
|
|
|
// the redirect needs no target, and anything that only makes sense with one is dropped
|
|
code, raw := open(map[string]any{"proto": "https", "port": 443, "hostname": "old.example.test", "redirectTo": " https://new.example.test/home ", "redirectPermanent": true, "redirectKeepPath": true,
|
|
"auth": map[string]any{"password": true}, "rewriteHost": "x", "healthPath": "/h"})
|
|
if code != 200 {
|
|
t.Fatalf("create redirect: %d %s", code, raw)
|
|
}
|
|
var m struct {
|
|
ID, RedirectTo, RewriteHost, HealthPath string
|
|
RedirectPermanent, RedirectKeepPath bool
|
|
Targets []any
|
|
HealthSeconds int
|
|
Auth struct{ Password bool }
|
|
}
|
|
json.Unmarshal(raw, &m)
|
|
if m.RedirectTo != "https://new.example.test/home" || !m.RedirectPermanent || !m.RedirectKeepPath || len(m.Targets) != 0 ||
|
|
m.Auth.Password || m.RewriteHost != "" || m.HealthPath != "" || m.HealthSeconds != -1 {
|
|
t.Fatalf("redirect was not normalised: %s", raw)
|
|
}
|
|
|
|
// it shares the port with other websites by hostname, like any website
|
|
if code, raw := open(map[string]any{"proto": "https", "port": 443, "hostname": "app.example.test", "targets": []any{map[string]any{"siteId": site.ID, "host": "127.0.0.1", "port": 8080}}}); code != 200 {
|
|
t.Fatalf("a website beside the redirect: %d %s", code, raw)
|
|
}
|
|
// and its plain-HTTP twin on port 80 is its own resource
|
|
if code, raw := open(map[string]any{"proto": "http", "port": 80, "hostname": "old.example.test", "redirectTo": "https://new.example.test/home"}); code != 200 {
|
|
t.Fatalf("http side of the redirect: %d %s", code, raw)
|
|
}
|
|
|
|
for name, in := range map[string]map[string]any{
|
|
"not a web port": {"proto": "tcp", "port": 9000, "redirectTo": "https://new.example.test"},
|
|
"no scheme": {"proto": "https", "port": 443, "hostname": "a.example.test", "redirectTo": "new.example.test"},
|
|
"other scheme": {"proto": "https", "port": 443, "hostname": "a.example.test", "redirectTo": "ftp://new.example.test"},
|
|
"credentials": {"proto": "https", "port": 443, "hostname": "a.example.test", "redirectTo": "https://u:p@new.example.test"},
|
|
"a dollar sign": {"proto": "https", "port": 443, "hostname": "a.example.test", "redirectTo": "https://new.example.test/$1"},
|
|
"back to itself": {"proto": "https", "port": 443, "hostname": "a.example.test", "redirectTo": "https://a.example.test/"},
|
|
"itself with a path": {"proto": "https", "port": 443, "hostname": "a.example.test", "redirectTo": "https://a.example.test/x", "redirectKeepPath": true},
|
|
"host not in domains": {"proto": "https", "port": 443, "hostname": "a.elsewhere.test", "redirectTo": "https://new.example.test"},
|
|
} {
|
|
if code, raw := open(in); code != 400 {
|
|
t.Errorf("%s: expected 400, got %d %s", name, code, raw)
|
|
}
|
|
}
|
|
// http to https on the same name is the normal use, and is allowed
|
|
if code, raw := open(map[string]any{"proto": "http", "port": 8080, "hostname": "a.example.test", "redirectTo": "https://a.example.test/"}); code != 200 {
|
|
t.Errorf("an http to https redirect: %d %s", code, raw)
|
|
}
|
|
|
|
// turning a redirect into a normal website needs a target again
|
|
code, raw = do("PUT", base+"/"+m.ID, map[string]any{"proto": "https", "edgeId": edge.ID, "bindIp": "0.0.0.0", "port": 443, "hostname": "old.example.test", "enabled": true})
|
|
if code != 400 || !strings.Contains(string(raw), "target") {
|
|
t.Errorf("a website with no target and no redirect: %d %s", code, raw)
|
|
}
|
|
|
|
// deleting a site must not sweep away redirects, which have no targets
|
|
if code, raw := do("DELETE", "/api/orgs/"+org+"/sites/"+site.ID, nil); code != 200 {
|
|
t.Fatalf("delete site: %d %s", code, raw)
|
|
}
|
|
_, raw = do("GET", "/api/orgs/"+org+"/state", nil)
|
|
var st struct {
|
|
Mappings []struct{ Hostname, RedirectTo string }
|
|
}
|
|
json.Unmarshal(raw, &st)
|
|
var redirects, apps int
|
|
for _, x := range st.Mappings {
|
|
if x.RedirectTo != "" {
|
|
redirects++
|
|
}
|
|
if x.Hostname == "app.example.test" {
|
|
apps++
|
|
}
|
|
}
|
|
if redirects != 3 || apps != 0 {
|
|
t.Errorf("after deleting the site: %d redirects (want 3), %d apps (want 0): %s", redirects, apps, raw)
|
|
}
|
|
}
|