net/url: permit colons in the host of postgresql:// URLs

PostgreSQL's postgresql:// URL scheme permits a comma-separated list of
host:ports to appear in the host subcomponent:
https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS

While this is not compliant with RFC 3986, it's something we've accepted
for a long time. Continue to accept colons in the host when the URL
scheme is "postgresql".

Fixes #75859

Change-Id: Iaa2e82b0be11d8e034e10dd7f4d6070039acfa2c
Reviewed-on: https://go-review.googlesource.com/c/go/+/722300
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Sean Liao <sean@liao.dev>
This commit is contained in:
Damien Neil 2025-11-19 15:32:04 -08:00
parent a662badab9
commit ff654ea100
2 changed files with 41 additions and 8 deletions

View file

@ -489,7 +489,7 @@ func parse(rawURL string, viaRequest bool) (*URL, error) {
if i := strings.Index(authority, "/"); i >= 0 { if i := strings.Index(authority, "/"); i >= 0 {
authority, rest = authority[:i], authority[i:] authority, rest = authority[:i], authority[i:]
} }
url.User, url.Host, err = parseAuthority(authority) url.User, url.Host, err = parseAuthority(url.Scheme, authority)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -509,12 +509,12 @@ func parse(rawURL string, viaRequest bool) (*URL, error) {
return url, nil return url, nil
} }
func parseAuthority(authority string) (user *Userinfo, host string, err error) { func parseAuthority(scheme, authority string) (user *Userinfo, host string, err error) {
i := strings.LastIndex(authority, "@") i := strings.LastIndex(authority, "@")
if i < 0 { if i < 0 {
host, err = parseHost(authority) host, err = parseHost(scheme, authority)
} else { } else {
host, err = parseHost(authority[i+1:]) host, err = parseHost(scheme, authority[i+1:])
} }
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@ -546,7 +546,7 @@ func parseAuthority(authority string) (user *Userinfo, host string, err error) {
// parseHost parses host as an authority without user // parseHost parses host as an authority without user
// information. That is, as host[:port]. // information. That is, as host[:port].
func parseHost(host string) (string, error) { func parseHost(scheme, host string) (string, error) {
if openBracketIdx := strings.LastIndex(host, "["); openBracketIdx != -1 { if openBracketIdx := strings.LastIndex(host, "["); openBracketIdx != -1 {
// Parse an IP-Literal in RFC 3986 and RFC 6874. // Parse an IP-Literal in RFC 3986 and RFC 6874.
// E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80". // E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80".
@ -603,9 +603,22 @@ func parseHost(host string) (string, error) {
} }
return "[" + unescapedHostname + "]" + unescapedColonPort, nil return "[" + unescapedHostname + "]" + unescapedColonPort, nil
} else if i := strings.Index(host, ":"); i != -1 { } else if i := strings.Index(host, ":"); i != -1 {
if j := strings.LastIndex(host, ":"); urlstrictcolons.Value() == "0" && j != i { lastColon := strings.LastIndex(host, ":")
urlstrictcolons.IncNonDefault() if lastColon != i {
i = j if scheme == "postgresql" || scheme == "postgres" {
// PostgreSQL relies on non-RFC-3986 parsing to accept
// a comma-separated list of hosts (with optional ports)
// in the host subcomponent:
// https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS
//
// Since we historically permitted colons to appear in the host,
// continue to permit it for postgres:// URLs only.
// https://go.dev/issue/75223
i = lastColon
} else if urlstrictcolons.Value() == "0" {
urlstrictcolons.IncNonDefault()
i = lastColon
}
} }
colonPort := host[i:] colonPort := host[i:]
if !validOptionalPort(colonPort) { if !validOptionalPort(colonPort) {

View file

@ -606,6 +606,26 @@ var urltests = []URLTest{
}, },
"mailto:?subject=hi", "mailto:?subject=hi",
}, },
// PostgreSQL URLs can include a comma-separated list of host:post hosts.
// https://go.dev/issue/75859
{
"postgres://host1:1,host2:2,host3:3",
&URL{
Scheme: "postgres",
Host: "host1:1,host2:2,host3:3",
Path: "",
},
"postgres://host1:1,host2:2,host3:3",
},
{
"postgresql://host1:1,host2:2,host3:3",
&URL{
Scheme: "postgresql",
Host: "host1:1,host2:2,host3:3",
Path: "",
},
"postgresql://host1:1,host2:2,host3:3",
},
} }
// more useful string for debugging than fmt's struct printer // more useful string for debugging than fmt's struct printer