Domain redirects: send an address to another, over HTTP and HTTPS
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>
This commit is contained in:
1 parent
227c890409
commit
39659f0144
6 files changed
+211
-12
No files matched your search
@@ -0,0 +1,109 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user