diff --git a/model/model.go b/model/model.go index af730d4..e5f5143 100644 --- a/model/model.go +++ b/model/model.go @@ -1,7 +1,10 @@ // Package model holds the types shared by hammerhead, blacktip and bamboo. package model -import "time" +import ( + "strings" + "time" +) const ( ProtoTCP = "tcp" // raw TCP on ip:port @@ -19,6 +22,21 @@ func (m Mapping) LastPort() int { return m.Port } +// IsRedirect reports whether the mapping answers with a redirect instead of forwarding to a target. +func (m Mapping) IsRedirect() bool { return m.RedirectTo != "" } + +// RedirectLocation is where a request for requestURI (path and query) is sent. With KeepPath the +// request's path and query follow the destination, so example.com/a?b=1 goes to to/a?b=1. +func (m Mapping) RedirectLocation(requestURI string) string { + if !m.RedirectKeepPath { + return m.RedirectTo + } + if requestURI == "" || requestURI[0] != '/' { + requestURI = "/" + requestURI + } + return strings.TrimRight(m.RedirectTo, "/") + requestURI +} + // HostBased reports whether mappings of this protocol are routed by hostname. func HostBased(proto string) bool { return proto == ProtoHTTP || proto == ProtoHTTPS || proto == ProtoTLS @@ -63,7 +81,7 @@ type Site struct { Tags []string `json:"tags,omitempty"` // labels for grouping and filtering in the dashboard - TunnelIP string `json:"tunnelIp,omitempty"` // the site's address inside the tunnel network + TunnelIP string `json:"tunnelIp,omitempty"` // the site's address inside the tunnel network PublicKey string `json:"publicKey,omitempty"` // WireGuard public key, reported by the agent // Version is the release the program reports (e.g. v0.1.5). AutoUpdate says whether it may update @@ -132,6 +150,12 @@ type Mapping struct { // target with a PROXY protocol header carrying the real client address. ProxyProtocol string `json:"proxyProtocol,omitempty"` + // A web mapping with RedirectTo set sends visitors there instead of to a target (so it has no + // targets, login or health checks). It answers 301 when RedirectPermanent, otherwise 302. + RedirectTo string `json:"redirectTo,omitempty"` + RedirectPermanent bool `json:"redirectPermanent,omitempty"` + RedirectKeepPath bool `json:"redirectKeepPath,omitempty"` + // web (http/https) options Auth Auth `json:"auth"` AuthEpoch int64 `json:"authEpoch"` // bumped to sign every visitor out; set by hammerhead only diff --git a/model/model_test.go b/model/model_test.go new file mode 100644 index 0000000..bdb18fc --- /dev/null +++ b/model/model_test.go @@ -0,0 +1,25 @@ +package model + +import "testing" + +func TestRedirectLocation(t *testing.T) { + cases := []struct { + m Mapping + uri string + want string + }{ + {Mapping{RedirectTo: "https://new.example.com"}, "/a?b=1", "https://new.example.com"}, + {Mapping{RedirectTo: "https://new.example.com", RedirectKeepPath: true}, "/a?b=1", "https://new.example.com/a?b=1"}, + {Mapping{RedirectTo: "https://new.example.com/", RedirectKeepPath: true}, "/", "https://new.example.com/"}, + {Mapping{RedirectTo: "https://new.example.com/base", RedirectKeepPath: true}, "/x/y", "https://new.example.com/base/x/y"}, + {Mapping{RedirectTo: "https://new.example.com", RedirectKeepPath: true}, "", "https://new.example.com/"}, + } + for _, c := range cases { + if got := c.m.RedirectLocation(c.uri); got != c.want { + t.Errorf("%+v %q: got %q, want %q", c.m, c.uri, got, c.want) + } + } + if (Mapping{}).IsRedirect() || !(Mapping{RedirectTo: "https://x"}).IsRedirect() { + t.Error("IsRedirect is wrong") + } +}