]> Cypherpunks repositories - gostls13.git/commitdiff
net/url: permit colons in the host of postgresql:// URLs
authorDamien Neil <dneil@google.com>
Wed, 19 Nov 2025 23:32:04 +0000 (15:32 -0800)
committerDamien Neil <dneil@google.com>
Thu, 20 Nov 2025 22:25:21 +0000 (14:25 -0800)
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>
src/net/url/url.go
src/net/url/url_test.go

index ca5ff9e3d70f27fd91c06f4baf054554073218fb..6dc57eab85dd5cb740d4d828d76de2d1278db365 100644 (file)
@@ -489,7 +489,7 @@ func parse(rawURL string, viaRequest bool) (*URL, error) {
                if i := strings.Index(authority, "/"); i >= 0 {
                        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 {
                        return nil, err
                }
@@ -509,12 +509,12 @@ func parse(rawURL string, viaRequest bool) (*URL, error) {
        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, "@")
        if i < 0 {
-               host, err = parseHost(authority)
+               host, err = parseHost(scheme, authority)
        } else {
-               host, err = parseHost(authority[i+1:])
+               host, err = parseHost(scheme, authority[i+1:])
        }
        if err != nil {
                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
 // 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 {
                // Parse an IP-Literal in RFC 3986 and RFC 6874.
                // E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80".
@@ -603,9 +603,22 @@ func parseHost(host string) (string, error) {
                }
                return "[" + unescapedHostname + "]" + unescapedColonPort, nil
        } else if i := strings.Index(host, ":"); i != -1 {
-               if j := strings.LastIndex(host, ":"); urlstrictcolons.Value() == "0" && j != i {
-                       urlstrictcolons.IncNonDefault()
-                       i = j
+               lastColon := strings.LastIndex(host, ":")
+               if lastColon != i {
+                       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:]
                if !validOptionalPort(colonPort) {
index b601849ce5581d29da2b5f4bcdda10469187a22e..a70b94b424567d8201e2398ba2969b45afd09fd7 100644 (file)
@@ -606,6 +606,26 @@ var urltests = []URLTest{
                },
                "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