feat: decide the hosted form URL scheme from the host rather than the port, so an install that terminates TLS on a non-default port (forms.example.com:8443) keeps https in its share links, embeds and base_url instead of being downgraded to http by the port check, with a private-network host now treated as the LAN install it is, one scheme helper shared by FormsBaseURL and FormURLOn so the builder's base_url and a form's share_url can never disagree, and a table test covering every install shape

This commit is contained in:
Matthew Meszaros
2026-09-07 04:45:21 -07:00
parent d51501bef1
commit 372df39eaa
2 changed files with 70 additions and 37 deletions
+20 -21
View File
@@ -63,16 +63,26 @@ func GetInviteURL(token string) string {
// pages do not live on the API origin, so there is nothing to fall back to,
// and a share link pointing at the wrong process is worse than none.
func FormsBaseURL() string {
if host := strings.TrimSpace(os.Getenv("FORMS_DOMAIN")); host != "" {
host = strings.TrimPrefix(strings.TrimPrefix(host, "https://"), "http://")
host = strings.TrimRight(host, "/")
scheme := "https"
if strings.HasPrefix(host, "localhost") || strings.HasPrefix(host, "127.0.0.1") {
scheme = "http"
}
return scheme + "://" + host
host := NormalizeTrackingHost(os.Getenv("FORMS_DOMAIN"))
if host == "" {
return ""
}
return ""
return formsScheme(host) + "://" + host
}
// formsScheme is https except where TLS cannot be terminated: a form page on a
// loopback or private-network host is a development or LAN install. The port is
// deliberately not a signal, because an install can terminate TLS on any port
// and inferring http from one handed an https deployment http:// share links.
func formsScheme(host string) string {
name := hostWithoutPort(NormalizeTrackingHost(host))
if name == "localhost" || strings.HasSuffix(name, ".localhost") {
return "http"
}
if ip := net.ParseIP(strings.Trim(name, "[]")); ip != nil && (ip.IsLoopback() || ip.IsPrivate()) {
return "http"
}
return "https"
}
// FormsHostname is the bare host this install serves forms on. It is the
@@ -97,18 +107,7 @@ func FormURLOn(host, publicID string) string {
if host == "" {
return GetFormURL(publicID)
}
scheme := "https"
if name, port, err := net.SplitHostPort(host); err == nil {
if port != "" && port != "443" {
scheme = "http"
}
if name == "localhost" || strings.HasSuffix(name, ".localhost") {
scheme = "http"
}
} else if host == "localhost" || strings.HasSuffix(host, ".localhost") {
scheme = "http"
}
return scheme + "://" + host + "/f/" + url.PathEscape(publicID)
return formsScheme(host) + "://" + host + "/f/" + url.PathEscape(publicID)
}
// GetFormURL is the hosted page for one form; empty when no base is known.
+50 -16
View File
@@ -2,22 +2,56 @@ package config
import "testing"
// Clients dial whatever GET /v1/auth/config advertises, so every form an
// operator plausibly writes has to normalise to the Phoenix transport path.
func TestWebsocketURLNormalisation(t *testing.T) {
cases := map[string]string{
"wss://ws.example.com": "wss://ws.example.com/socket/websocket",
"wss://ws.example.com/": "wss://ws.example.com/socket/websocket",
"wss://ws.example.com/socket": "wss://ws.example.com/socket/websocket",
"wss://ws.example.com/socket/": "wss://ws.example.com/socket/websocket",
"wss://ws.example.com/socket/websocket": "wss://ws.example.com/socket/websocket",
"ws://localhost:4000/socket/websocket": "ws://localhost:4000/socket/websocket",
"": "",
// The hosted form URL has to be reachable on every install shape: the shared
// host keeps its port (a share link that drops it points at nothing), and the
// scheme follows the host rather than the port, because an install can
// terminate TLS on any port and a ported https deployment must not be handed
// http:// links (PR #368).
func TestFormURLsFollowTheInstallHost(t *testing.T) {
for _, tc := range []struct {
formsDomain string
wantBase string
wantShare string
wantCNAME string
}{
{"localhost:8090", "http://localhost:8090", "http://localhost:8090/f/abc", "localhost"},
{"127.0.0.1:8090", "http://127.0.0.1:8090", "http://127.0.0.1:8090/f/abc", "127.0.0.1"},
{"192.168.1.5:8090", "http://192.168.1.5:8090", "http://192.168.1.5:8090/f/abc", "192.168.1.5"},
{"forms.example.com", "https://forms.example.com", "https://forms.example.com/f/abc", "forms.example.com"},
{"forms.example.com:8443", "https://forms.example.com:8443", "https://forms.example.com:8443/f/abc", "forms.example.com"},
{"https://Forms.Example.com/", "https://forms.example.com", "https://forms.example.com/f/abc", "forms.example.com"},
} {
t.Run(tc.formsDomain, func(t *testing.T) {
t.Setenv("FORMS_DOMAIN", tc.formsDomain)
if got := FormsBaseURL(); got != tc.wantBase {
t.Errorf("FormsBaseURL() = %q, want %q", got, tc.wantBase)
}
if got := GetFormURL("abc"); got != tc.wantShare {
t.Errorf("GetFormURL() = %q, want %q", got, tc.wantShare)
}
// What the handler stamps on every form: the shared host, resolved
// through FormsHost, then built into a URL.
if got := FormURLOn(FormsURLHost(), "abc"); got != tc.wantShare {
t.Errorf("FormURLOn(FormsURLHost()) = %q, want %q", got, tc.wantShare)
}
// The CNAME target is a DNS name, so it never carries the port.
if got := FormsHostname(); got != tc.wantCNAME {
t.Errorf("FormsHostname() = %q, want %q", got, tc.wantCNAME)
}
})
}
for in, want := range cases {
t.Setenv("WEBSOCKET_URL", in)
if got := WebsocketURL(); got != want {
t.Errorf("WebsocketURL(%q) = %q, want %q", in, got, want)
}
}
// A verified custom forms domain replaces the shared host and is always a bare
// name, so its links stay https whatever the install runs on.
func TestFormURLOnCustomDomain(t *testing.T) {
t.Setenv("FORMS_DOMAIN", "localhost:8090")
if got := FormURLOn("forms.acme.com", "abc"); got != "https://forms.acme.com/f/abc" {
t.Errorf("custom domain URL = %q", got)
}
// No host and no configured base is an empty URL, never a relative one.
t.Setenv("FORMS_DOMAIN", "")
if got := FormURLOn("", "abc"); got != "" {
t.Errorf("unconfigured install URL = %q, want empty", got)
}
}