model: redirect mappings (RedirectTo, RedirectPermanent, RedirectKeepPath)
CI / test (push) Successful in 1m14s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 04:06:42 -06:00
1 parent 6753422f7a
commit 6663b268c8
2 files changed
+51 -2

No files matched your search

+26 -2
View File
@@ -1,7 +1,10 @@
// Package model holds the types shared by hammerhead, blacktip and bamboo. // Package model holds the types shared by hammerhead, blacktip and bamboo.
package model package model
import "time" import (
"strings"
"time"
)
const ( const (
ProtoTCP = "tcp" // raw TCP on ip:port ProtoTCP = "tcp" // raw TCP on ip:port
@@ -19,6 +22,21 @@ func (m Mapping) LastPort() int {
return m.Port 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. // HostBased reports whether mappings of this protocol are routed by hostname.
func HostBased(proto string) bool { func HostBased(proto string) bool {
return proto == ProtoHTTP || proto == ProtoHTTPS || proto == ProtoTLS 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 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 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 // 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. // target with a PROXY protocol header carrying the real client address.
ProxyProtocol string `json:"proxyProtocol,omitempty"` 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 // web (http/https) options
Auth Auth `json:"auth"` Auth Auth `json:"auth"`
AuthEpoch int64 `json:"authEpoch"` // bumped to sign every visitor out; set by hammerhead only AuthEpoch int64 `json:"authEpoch"` // bumped to sign every visitor out; set by hammerhead only
+25
View File
@@ -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")
}
}