Domain redirects: send an address to another, over HTTP and HTTPS
CI / test (push) Successful in 4m36s
Release / release (push) Successful in 5m10s

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:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 04:15:40 -06:00
1 parent 227c890409
commit 39659f0144
6 files changed
+211 -12

No files matched your search

+45 -2
View File
@@ -6,6 +6,8 @@ import (
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@@ -97,7 +99,16 @@ func (s *Server) validateMapping(c *orgCtx, m *model.Mapping) error {
if edge == nil || !s.edgeAllowedFor(edge, c.Org.ID) {
return errors.New("choose an edge")
}
if len(m.Targets) == 0 {
redirect := strings.TrimSpace(m.RedirectTo) != ""
if redirect {
if !model.Web(m.Proto) {
return errors.New("only websites (http or https) can redirect")
}
m.Targets = []model.Target{} // a redirect has nothing behind it
} else {
m.RedirectTo, m.RedirectPermanent, m.RedirectKeepPath = "", false, false
}
if len(m.Targets) == 0 && !redirect {
return errors.New("add at least one target")
}
if m.Proto == model.ProtoUDP && len(m.Targets) > 1 {
@@ -173,9 +184,19 @@ func (s *Server) validateMapping(c *orgCtx, m *model.Mapping) error {
return errors.New("PROXY protocol must be v1, v2 or off")
}
// web-only options
if !model.Web(m.Proto) {
if redirect {
to, err := cleanRedirect(m)
if err != nil {
return err
}
m.RedirectTo = to
}
if !model.Web(m.Proto) || redirect {
m.Auth, m.Rules, m.RewriteHost = model.Auth{}, nil, ""
m.RedirectHTTP, m.BackendHTTPS, m.HealthPath = false, false, ""
if redirect {
m.HealthSeconds = -1 // nothing to check
}
} else {
a := &m.Auth
a.AllowedRoles, a.AllowedUsers, a.AllowedMails = listFromCSV(a.AllowedRoles), listFromCSV(a.AllowedUsers), listFromCSV(a.AllowedMails)
@@ -262,6 +283,28 @@ func (s *Server) validateMapping(c *orgCtx, m *model.Mapping) error {
return nil
}
// cleanRedirect checks where a redirect sends visitors and refuses a destination that would
// send them straight back to the same address.
func cleanRedirect(m *model.Mapping) (string, error) {
to := strings.TrimSpace(m.RedirectTo)
u, err := url.Parse(to)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" || u.User != nil {
return "", errors.New("Redirect to a full address such as https://www.example.com.")
}
if len(to) > 2048 || strings.ContainsAny(to, " \t\r\n$") {
return "", errors.New("The redirect address is too long or contains spaces or a dollar sign.")
}
port := u.Port()
if port == "" {
port = map[string]string{"http": "80", "https": "443"}[u.Scheme]
}
same := u.Scheme == m.Proto && strings.EqualFold(u.Hostname(), m.Hostname) && port == strconv.Itoa(m.Port)
if same && (m.RedirectKeepPath || u.Path == "" || u.Path == "/") {
return "", errors.New("That redirect would send visitors back to the same address.")
}
return to, nil
}
// portLabel is "25565" or, for a range, "2456-2458".
func portLabel(m model.Mapping) string {
if m.LastPort() > m.Port {