From f541e5b326389e5b423ccbe916cc018d7c5c4afd Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:19:23 -0500 Subject: [PATCH 01/11] create NormalizeAddr which accepts addresses and returns a formatted copy of that address conformant with RFC5952 --- configutil/normalize.go | 94 +++++++ configutil/normalize_test.go | 468 +++++++++++++++++++++++++++++++++++ 2 files changed, 562 insertions(+) create mode 100644 configutil/normalize.go create mode 100644 configutil/normalize_test.go diff --git a/configutil/normalize.go b/configutil/normalize.go new file mode 100644 index 0000000..f83d853 --- /dev/null +++ b/configutil/normalize.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configutil + +import ( + "fmt" + "net" + "net/url" + "strings" +) + +// general delimiters as defined in RFC 3986 +// See: https://www.rfc-editor.org/rfc/rfc3986#section-2.2 +const genDelims = ":/?#[]@" + +func normalizeHostPort(host string, port string, url bool) (string, error) { + // fmt.Println("host:", host, "port:", port) + if host == "" { + return "", nil + } + if ip := net.ParseIP(host); ip != nil { + if url && ip.To4() == nil && port == "" { + // this is a unique case, host is ipv6 and requires brackets due to + // being part of a url, but they won't be added by net.JoinHostPort + // as there is no port + return "[" + ip.String() + "]", nil + } + host = ip.String() + } else if strings.Contains(host, ":") { + // host is an invalid ipv6 literal. + // hosts cannot contain certain reserved characters, including ":" + // See: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2, + // https://www.rfc-editor.org/rfc/rfc3986#section-2.2 + return "", fmt.Errorf("host contains an invalid IPv6 literal") + } + if port == "" { + return host, nil + } + return net.JoinHostPort(host, port), nil +} + +// NormalizeAddr takes an address as a string and returns a normalized copy. +// If the addr is a URL, IP Address, or host:port address that includes an IPv6 +// address, the normalized copy will be conformant with RFC-5942 §4 +// +// Valid formats include: +// - host +// - host:port +// - scheme://user@host/path?query#frag +// +// Note: URLs and URIs must conform with https://www.rfc-editor.org/rfc/rfc3986#section-3 +// or else the returned address may have been parsed and formatted incorrectly +// +// See: https://rfc-editor.org/rfc/rfc5952.html +func NormalizeAddr(address string) (string, error) { + if address == "" { + return "", fmt.Errorf("empty or invalid hostname") + } + + if ip := net.ParseIP(address); ip != nil { + return ip.String(), nil + } + + if strings.HasPrefix(address, "[") && strings.HasSuffix(address, "]") { + return NormalizeAddr(address[1 : len(address)-1]) + } + + if host, port, err := net.SplitHostPort(address); err == nil { + return normalizeHostPort(host, port, false) + } + + if u, err := url.ParseRequestURI(address); err == nil { + if u.Host, err = normalizeHostPort(u.Hostname(), u.Port(), true); err != nil { + return "", err + } + return u.String(), nil + } + // if the provided address does not have a scheme provided, attempt to + // provide one and re-parse the result. this is done by looking for the + // first general delimiter and checking if it exists or if it's not a colon + // See: https://www.rfc-editor.org/rfc/rfc3986#section-3 + if idx := strings.IndexAny(address, genDelims); idx < 0 || address[idx] != ':' { + const scheme = "https://" + if u, err := url.ParseRequestURI(scheme + address); err == nil { + if u.Host, err = normalizeHostPort(u.Hostname(), u.Port(), true); err != nil { + return "", err + } + return strings.TrimPrefix(u.String(), scheme), nil + } + } + + return "", fmt.Errorf("unable to normalize given address") +} diff --git a/configutil/normalize_test.go b/configutil/normalize_test.go new file mode 100644 index 0000000..e30b796 --- /dev/null +++ b/configutil/normalize_test.go @@ -0,0 +1,468 @@ +package configutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_NormalizeAddr(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + address string + expected string + err string + }{ + { + name: "valid ipv4 address", + address: "127.0.0.1", + expected: "127.0.0.1", + }, + { + name: "valid ipv4 address with port", + address: "127.0.0.1:80", + expected: "127.0.0.1:80", + }, + { + name: "valid ipv4 address with port and path", + address: "127.0.0.1:80/test/path", + expected: "127.0.0.1:80/test/path", + }, + { + name: "valid ipv4 address with path", + address: "127.0.0.1/test/path", + expected: "127.0.0.1/test/path", + }, + { + name: "valid ipv4 uri with path", + address: "http://127.0.0.1/test/path", + expected: "http://127.0.0.1/test/path", + }, + { + name: "valid ipv4 uri with port and path", + address: "http://127.0.0.1:80/test/path", + expected: "http://127.0.0.1:80/test/path", + }, + { + name: "valid double colon address", + address: "::", + expected: "::", + }, + { + name: "valid ipv6 localhost address", + address: "::1", + expected: "::1", + }, + { + name: "valid ipv6 literal", + address: "2001:BEEF:0:0:0:1:0:0001", + expected: "2001:beef::1:0:1", + }, + { + name: "valid ipv6 literal with brackets", + address: "[2001:BEEF:0:0:0:1:0:0001]", + expected: "2001:beef::1:0:1", + }, + { + name: "valid ipv6 host:port", + address: "[2001:BEEF:0:0:0:1:0:0001]:80", + expected: "[2001:beef::1:0:1]:80", + }, + { + name: "valid ipv6 uri", + address: "https://[2001:BEEF:0:0:0:1:0:0001]", + expected: "https://[2001:beef::1:0:1]", + }, + { + name: "valid ipv6 uri with path", + address: "https://[2001:BEEF:0:0:0:1:0:0001]/test/path", + expected: "https://[2001:beef::1:0:1]/test/path", + }, + { + name: "valid ipv6 uri with port", + address: "https://[2001:BEEF:0:0:0:1:0:0001]:80", + expected: "https://[2001:beef::1:0:1]:80", + }, + { + name: "invalid ipv6 uri missing closing bracket", + address: "https://[2001:BEEF:0:0:0:1:0:0001", + err: "unable to normalize given address", + }, + { + name: "invalid ipv6 uri missing brackets", + address: "https://2001:BEEF:0:0:0:1:0:0001", + err: "host contains an invalid IPv6 literal", + }, + { + name: "invalid ipv6 literal", + address: ":0:", + err: "unable to normalize given address", + }, + { + name: "invalid ipv6 literal", + address: "::0:", + err: "unable to normalize given address", + }, + { + name: "invalid ipv6, not enough segments", + address: "2001:BEEF:0:0:1:0:0001", + err: "unable to normalize given address", + }, + { + name: "invalid ipv6 host:port, not enough segments", + address: "[2001:BEEF:0:0:1:0:0001]:80", + err: "host contains an invalid IPv6 literal", + }, + { + name: "invalid ipv6 literal with brackets, not enough segments", + address: "[2001:BEEF:0:0:1:0:0001]", + err: "unable to normalize given address", + }, + { + name: "invalid ipv6 uri, not enough segments", + address: "https://[2001:BEEF:0:0:1:0:0001]:80", + err: "host contains an invalid IPv6 literal", + }, + { + name: "invalid ipv6 uri withut port, not enough segments", + address: "https://[2001:BEEF:0:0:1:0:0001]", + err: "host contains an invalid IPv6 literal", + }, + { + name: "invalid ipv6, it's just brackets", + address: "[]", + err: "empty or invalid hostname", + }, + { + name: "valid url with domain", + address: "https://www.google.com", + expected: "https://www.google.com", + }, + { + name: "valid url with domain and port", + address: "https://www.google.com:443", + expected: "https://www.google.com:443", + }, + { + name: "valid host with only sub domain", + address: "www.google.com", + expected: "www.google.com", + }, + { + name: "valid host:port with sub domain and port", + address: "www.google.com:443", + expected: "www.google.com:443", + }, + { + name: "valid host with only domain", + address: "google.com", + expected: "google.com", + }, + { + name: "valid host:port with domain and port", + address: "google.com:443", + expected: "google.com:443", + }, + { + name: "valid host with only dns name", + address: "hashicorp", + expected: "hashicorp", + }, + { + name: "valid host:port with dns name and port", + address: "hashicorp:443", + expected: "hashicorp:443", + }, + { + name: "invalid host with only dns name", + address: "hashi corp", + err: "unable to normalize given address", + }, + { + name: "valid url with path, schema, and subdomain", + address: "https://www.google.com/search?client=firefox-b-1-d&q=hey#section-1.2.3", + expected: "https://www.google.com/search?client=firefox-b-1-d&q=hey#section-1.2.3", + }, + { + name: "valid url with path but without schema or subdomain", + address: "google.com/search?client=firefox-b-1-d&q=hey#section-1.2.3", + expected: "google.com/search?client=firefox-b-1-d&q=hey#section-1.2.3", + }, + { + name: "valid uri with hostname and path", + address: "hashicorp/test/path?query=some&extra=data#section-1.2.3", + expected: "hashicorp/test/path?query=some&extra=data#section-1.2.3", + }, + { + name: "valid uri with crazy chars in query", + address: "hashicorp/test/path?I think actually anything can be past here !@#$%^&*()[:]{;}", + expected: "hashicorp/test/path?I think actually anything can be past here !@#$%^&*()[:]{;}", + }, + { + name: "valid uri with crazy chars in path", + // note the last of % as that would need to be encoded already + address: "hashicorp/test/path/ !@$^&*()[:]{;}", + expected: "hashicorp/test/path/%20%21@$%5E&%2A%28%29%5B:%5D%7B;%7D", + }, + { + name: "valid uri without schema, with ipv6", + address: "[2001:BEEF:0:0:0:1:0:0001]/test/path", + expected: "[2001:beef::1:0:1]/test/path", + }, + { + name: "valid host with user", + address: "dani@localhost", + expected: "dani@localhost", + }, + + // imported from vault + { + name: "hostname", + address: "vaultproject.io", + expected: "vaultproject.io", + }, + { + name: "hostname port", + address: "vaultproject.io:8200", + expected: "vaultproject.io:8200", + }, + { + name: "hostname URL", + address: "https://vaultproject.io", + expected: "https://vaultproject.io", + }, + { + name: "hostname port URL", + address: "https://vaultproject.io:8200", + expected: "https://vaultproject.io:8200", + }, + { + name: "hostname destination address", + address: "user@vaultproject.io", + expected: "user@vaultproject.io", + }, + { + name: "hostname destination address URL", + address: "http://user@vaultproject.io", + expected: "http://user@vaultproject.io", + }, + { + name: "hostname destination address URL port", + address: "http://user@vaultproject.io:8200", + expected: "http://user@vaultproject.io:8200", + }, + { + name: "ipv4", + address: "10.10.1.10", + expected: "10.10.1.10", + }, + { + name: "ipv4 invalid bracketed", + address: "[10.10.1.10]", + expected: "10.10.1.10", + }, + { + name: "ipv4 IP:Port addr", + address: "10.10.1.10:8500", + expected: "10.10.1.10:8500", + }, + { + name: "ipv4 invalid IP:Port addr", + address: "[10.10.1.10]:8500", + expected: "10.10.1.10:8500", + }, + { + name: "ipv4 URL", + address: "https://10.10.1.10:8200", + expected: "https://10.10.1.10:8200", + }, + { + name: "ipv4 invalid URL", + address: "https://[10.10.1.10]:8200", + expected: "https://10.10.1.10:8200", + }, + { + name: "ipv4 destination address", + address: "username@10.10.1.10", + expected: "username@10.10.1.10", + }, + { + name: "ipv4 invalid destination address", + address: "username@10.10.1.10", + expected: "username@10.10.1.10", + }, + { + name: "ipv4 destination address port", + address: "username@10.10.1.10:8200", + expected: "username@10.10.1.10:8200", + }, + { + name: "ipv4 invalid destination address port", + address: "username@[10.10.1.10]:8200", + expected: "username@10.10.1.10:8200", + }, + { + name: "ipv4 destination address URL", + address: "https://username@10.10.1.10", + expected: "https://username@10.10.1.10", + }, + { + name: "ipv4 destination address URL port", + address: "https://username@10.10.1.10:8200", + expected: "https://username@10.10.1.10:8200", + }, + { + name: "ipv6 invalid address", + address: "[2001:0db8::0001]", + expected: "2001:db8::1", + }, + { + name: "ipv6 IP:Port RFC-5952 4.1 conformance leading zeroes", + address: "[2001:0db8::0001]:8500", + expected: "[2001:db8::1]:8500", + }, + { + name: "ipv6 RFC-5952 4.1 conformance leading zeroes", + address: "2001:0db8::0001", + expected: "2001:db8::1", + }, + { + name: "ipv6 URL RFC-5952 4.1 conformance leading zeroes", + address: "https://[2001:0db8::0001]:8200", + expected: "https://[2001:db8::1]:8200", + }, + { + name: "ipv6 bracketed destination address with port RFC-5952 4.1 conformance leading zeroes", + address: "username@[2001:0db8::0001]:8200", + expected: "username@[2001:db8::1]:8200", + }, + { + name: "ipv6 RFC-5952 4.2.2 conformance one 16-bit 0 field", + address: "2001:db8:0:1:1:1:1:1", + expected: "2001:db8:0:1:1:1:1:1", + }, + { + name: "ipv6 URL RFC-5952 4.2.2 conformance one 16-bit 0 field", + address: "https://[2001:db8:0:1:1:1:1:1]:8200", + expected: "https://[2001:db8:0:1:1:1:1:1]:8200", + }, + { + name: "ipv6 destination address with port RFC-5952 4.2.2 conformance one 16-bit 0 field", + address: "username@[2001:db8:0:1:1:1:1:1]:8200", + expected: "username@[2001:db8:0:1:1:1:1:1]:8200", + }, + { + name: "ipv6 RFC-5952 4.2.3 conformance longest run of 0 bits shortened", + address: "2001:0:0:1:0:0:0:1", + expected: "2001:0:0:1::1", + }, + { + name: "ipv6 URL RFC-5952 4.2.3 conformance longest run of 0 bits shortened", + address: "https://[2001:0:0:1:0:0:0:1]:8200", + expected: "https://[2001:0:0:1::1]:8200", + }, + { + name: "ipv6 destination address with port RFC-5952 4.2.3 conformance longest run of 0 bits shortened", + address: "username@[2001:0:0:1:0:0:0:1]:8200", + expected: "username@[2001:0:0:1::1]:8200", + }, + { + name: "ipv6 RFC-5952 4.2.3 conformance equal runs of 0 bits shortened", + address: "2001:db8:0:0:1:0:0:1", + expected: "2001:db8::1:0:0:1", + }, + { + name: "ipv6 URL no port RFC-5952 4.2.3 conformance equal runs of 0 bits shortened", + address: "https://[2001:db8:0:0:1:0:0:1]", + expected: "https://[2001:db8::1:0:0:1]", + }, + { + name: "ipv6 URL with port RFC-5952 4.2.3 conformance equal runs of 0 bits shortened", + address: "https://[2001:db8:0:0:1:0:0:1]:8200", + expected: "https://[2001:db8::1:0:0:1]:8200", + }, + + { + name: "ipv6 destination address with port RFC-5952 4.2.3 conformance equal runs of 0 bits shortened", + address: "username@[2001:db8:0:0:1:0:0:1]:8200", + expected: "username@[2001:db8::1:0:0:1]:8200", + }, + { + name: "ipv6 RFC-5952 4.3 conformance downcase hex letters", + address: "2001:DB8:AC3:FE4::1", + expected: "2001:db8:ac3:fe4::1", + }, + { + name: "ipv6 URL RFC-5952 4.3 conformance downcase hex letters", + address: "https://[2001:DB8:AC3:FE4::1]:8200", + expected: "https://[2001:db8:ac3:fe4::1]:8200", + }, + { + name: "ipv6 destination address with port RFC-5952 4.3 conformance downcase hex letters", + address: "username@[2001:DB8:AC3:FE4::1]:8200", + expected: "username@[2001:db8:ac3:fe4::1]:8200", + }, + // NOTE: these tests are NOT conformant with the URI spec https://www.rfc-editor.org/rfc/rfc3986#section-3 + // and therefore have been omitted. according to RFC3986, ipv6 literals must always be encapsulated within + // square brackets, even when part of a destination address. + // See: https://www.rfc-editor.org/rfc/rfc3986#section-3 + // https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 + // { + // name: "ipv6 invalid ambiguous destination address with port", + // address: "username@2001:0db8::0001:8200", + // // Since the address and port are ambiguous the value appears to be + // // only an address and as such is normalized as an address only + // expected: "username@2001:db8::1:8200", + // }, + // { + // name: "ipv6 invalid leading zeroes ambiguous destination address with port", + // address: "username@2001:db8:0:1:1:1:1:1:8200", + // // Since the address and port are ambiguous the value is treated as + // // a string because it has too many colons to be a valid IPv6 address. + // expected: "username@2001:db8:0:1:1:1:1:1:8200", + // }, + // { + // name: "ipv6 destination address no port RFC-5952 4.1 conformance leading zeroes", + // address: "username@2001:0db8::0001", + // expected: "username@2001:db8::1", + // }, + // { + // name: "ipv6 destination address no port RFC-5952 4.2.2 conformance one 16-bit 0 field", + // address: "username@2001:db8:0:1:1:1:1:1", + // expected: "username@2001:db8:0:1:1:1:1:1", + // }, + // { + // name: "ipv6 destination address no port RFC-5952 4.2.3 conformance longest run of 0 bits shortened", + // address: "username@2001:0:0:1:0:0:0:1", + // expected: "username@2001:0:0:1::1", + // }, + // { + // name: "ipv6 destination address no port RFC-5952 4.2.3 conformance equal runs of 0 bits shortened", + // address: "username@2001:db8:0:0:1:0:0:1", + // expected: "username@2001:db8::1:0:0:1", + // }, + // { + // name: "ipv6 destination address no port RFC-5952 4.3 conformance downcase hex letters", + // address: "username@2001:DB8:AC3:FE4::1", + // expected: "username@2001:db8:ac3:fe4::1", + // }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + actual, err := NormalizeAddr(tt.address) + assert.Equal(tt.expected, actual) + if tt.err != "" { + require.Error(t, err) + assert.ErrorContains(err, tt.err) + } else { + assert.Nil(err) + } + }) + } + +} From d8d2dfef6e67eaa0c9482b84c8c74c46bd96da20 Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:06:12 -0500 Subject: [PATCH 02/11] add more tests and switch back to using url.Parse for better edge case handling --- configutil/normalize.go | 36 ++++++++++--------- configutil/normalize_test.go | 69 +++++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/configutil/normalize.go b/configutil/normalize.go index f83d853..78231e9 100644 --- a/configutil/normalize.go +++ b/configutil/normalize.go @@ -10,14 +10,13 @@ import ( "strings" ) -// general delimiters as defined in RFC 3986 +// general delimiters as defined in RFC-3986 §2.2 // See: https://www.rfc-editor.org/rfc/rfc3986#section-2.2 const genDelims = ":/?#[]@" func normalizeHostPort(host string, port string, url bool) (string, error) { - // fmt.Println("host:", host, "port:", port) if host == "" { - return "", nil + return "", fmt.Errorf("empty or invalid hostname") } if ip := net.ParseIP(host); ip != nil { if url && ip.To4() == nil && port == "" { @@ -41,21 +40,22 @@ func normalizeHostPort(host string, port string, url bool) (string, error) { } // NormalizeAddr takes an address as a string and returns a normalized copy. -// If the addr is a URL, IP Address, or host:port address that includes an IPv6 -// address, the normalized copy will be conformant with RFC-5942 §4 +// If the address is a URL, IP Address, or host:port address that includes an +// IPv6 address, the normalized copy will be conformant with RFC-5952 §4. If +// the address cannot be parsed, an error will be returned. // // Valid formats include: // - host // - host:port // - scheme://user@host/path?query#frag // -// Note: URLs and URIs must conform with https://www.rfc-editor.org/rfc/rfc3986#section-3 -// or else the returned address may have been parsed and formatted incorrectly +// Note: URLs and URIs must conform with RFC-3986 §3 or else the returned address +// may be parsed and formatted incorrectly // -// See: https://rfc-editor.org/rfc/rfc5952.html +// See: https://www.rfc-editor.org/rfc/rfc5952#section-4, https://www.rfc-editor.org/rfc/rfc3986#section-3 func NormalizeAddr(address string) (string, error) { if address == "" { - return "", fmt.Errorf("empty or invalid hostname") + return "", fmt.Errorf("empty or invalid address") } if ip := net.ParseIP(address); ip != nil { @@ -70,24 +70,28 @@ func NormalizeAddr(address string) (string, error) { return normalizeHostPort(host, port, false) } - if u, err := url.ParseRequestURI(address); err == nil { - if u.Host, err = normalizeHostPort(u.Hostname(), u.Port(), true); err != nil { - return "", err - } - return u.String(), nil - } // if the provided address does not have a scheme provided, attempt to // provide one and re-parse the result. this is done by looking for the // first general delimiter and checking if it exists or if it's not a colon // See: https://www.rfc-editor.org/rfc/rfc3986#section-3 if idx := strings.IndexAny(address, genDelims); idx < 0 || address[idx] != ':' { const scheme = "https://" - if u, err := url.ParseRequestURI(scheme + address); err == nil { + // attempt to parse it as a url, we only want to try this func when we + // know for sure it has a scheme, since it can parse ANYTHING, but then + // just puts it into u.Path without the scheme + if u, err := url.Parse(scheme + address); err == nil { if u.Host, err = normalizeHostPort(u.Hostname(), u.Port(), true); err != nil { return "", err } return strings.TrimPrefix(u.String(), scheme), nil } + } else { + if u, err := url.Parse(address); err == nil { + if u.Host, err = normalizeHostPort(u.Hostname(), u.Port(), true); err != nil { + return "", err + } + return u.String(), nil + } } return "", fmt.Errorf("unable to normalize given address") diff --git a/configutil/normalize_test.go b/configutil/normalize_test.go index e30b796..dd20476 100644 --- a/configutil/normalize_test.go +++ b/configutil/normalize_test.go @@ -134,7 +134,7 @@ func Test_NormalizeAddr(t *testing.T) { { name: "invalid ipv6, it's just brackets", address: "[]", - err: "empty or invalid hostname", + err: "empty or invalid address", }, { name: "valid url with domain", @@ -198,8 +198,13 @@ func Test_NormalizeAddr(t *testing.T) { }, { name: "valid uri with crazy chars in query", - address: "hashicorp/test/path?I think actually anything can be past here !@#$%^&*()[:]{;}", - expected: "hashicorp/test/path?I think actually anything can be past here !@#$%^&*()[:]{;}", + address: "hashicorp/test/path?I think actually anything can be past here !@#$^&*()[:]{;}", + expected: "hashicorp/test/path?I think actually anything can be past here !@#$%5E&*()%5B:%5D%7B;%7D", + }, + { + name: "valid uri with pre-encoded components", + address: "hashicorp/test/path?!@#$%5E&*()%5B:%5D%7B;%7D", + expected: "hashicorp/test/path?!@#$%5E&*()%5B:%5D%7B;%7D", }, { name: "valid uri with crazy chars in path", @@ -207,6 +212,18 @@ func Test_NormalizeAddr(t *testing.T) { address: "hashicorp/test/path/ !@$^&*()[:]{;}", expected: "hashicorp/test/path/%20%21@$%5E&%2A%28%29%5B:%5D%7B;%7D", }, + { + name: "invalid uri with invalid percent encoding", + address: "hashicorp/test/path?I think actually anything can be past here !@#$%^&*()[:]{;}", + err: "unable to normalize given address", + }, + { + name: "invalid uri with invalid percent encoding", + address: "hashicorp/test/path?%^&", + // since there is nothing that needs to be encoded in this url, + // url.Parse does not detect that `%^&` in an invalid url encoding + expected: "hashicorp/test/path?%^&", // sad + }, { name: "valid uri without schema, with ipv6", address: "[2001:BEEF:0:0:0:1:0:0001]/test/path", @@ -217,6 +234,51 @@ func Test_NormalizeAddr(t *testing.T) { address: "dani@localhost", expected: "dani@localhost", }, + { + name: "valid uri no closing slash with frag", + address: "[2001:BEEF:0:0:0:1:0:0001]#test", + expected: "[2001:beef::1:0:1]#test", + }, + { + name: "valid uri with scheme, no closing slash, with frag", + address: "https://[2001:BEEF:0:0:0:1:0:0001]#test", + expected: "https://[2001:beef::1:0:1]#test", + }, + { + name: "valid ldap url with a bunch of data", + address: "ldap://ds.example.com:389/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)#extra", + expected: "ldap://ds.example.com:389/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)#extra", + }, + { + name: "valid ldap url with IPv6 address, port, and data", + address: "ldap://[2001:BEEF:0:0:0:1:0:0001]:389/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)#extra", + expected: "ldap://[2001:beef::1:0:1]:389/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)#extra", + }, + { + name: "valid ldap url with IPv6 address and data", + address: "ldap://[2001:BEEF:0:0:0:1:0:0001]/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)#extra", + expected: "ldap://[2001:beef::1:0:1]/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)#extra", + }, + { + name: "valid ldap url with IPv4 address, port, and data", + address: "ldap://127.0.0.1:389/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)#extra", + expected: "ldap://127.0.0.1:389/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)#extra", + }, + { + name: "valid ldap url with IPv4 address and data", + address: "ldap://127.0.0.1/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)#extra", + expected: "ldap://127.0.0.1/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)#extra", + }, + { + name: "valid ldap url with IPv6 address, no slash, and query data", + address: "ldap://[2001:BEEF:0:0:0:1:0:0001]:389?givenName,sn,cn?sub?(uid=john.doe)#extra", + expected: "ldap://[2001:beef::1:0:1]:389?givenName,sn,cn?sub?(uid=john.doe)#extra", + }, + { + name: "valid ldap url with IPv6 address, no slash, and frag data", + address: "ldap://[2001:BEEF:0:0:0:1:0:0001]:389#extra", + expected: "ldap://[2001:beef::1:0:1]:389#extra", + }, // imported from vault { @@ -384,7 +446,6 @@ func Test_NormalizeAddr(t *testing.T) { address: "https://[2001:db8:0:0:1:0:0:1]:8200", expected: "https://[2001:db8::1:0:0:1]:8200", }, - { name: "ipv6 destination address with port RFC-5952 4.2.3 conformance equal runs of 0 bits shortened", address: "username@[2001:db8:0:0:1:0:0:1]:8200", From 7217bade5eebdbe1a24147fb407de90f79e01570 Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:23:59 -0500 Subject: [PATCH 03/11] update empty or invalid errors to just empty --- configutil/normalize.go | 4 ++-- configutil/normalize_test.go | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/configutil/normalize.go b/configutil/normalize.go index 78231e9..37ba491 100644 --- a/configutil/normalize.go +++ b/configutil/normalize.go @@ -16,7 +16,7 @@ const genDelims = ":/?#[]@" func normalizeHostPort(host string, port string, url bool) (string, error) { if host == "" { - return "", fmt.Errorf("empty or invalid hostname") + return "", fmt.Errorf("empty hostname") } if ip := net.ParseIP(host); ip != nil { if url && ip.To4() == nil && port == "" { @@ -55,7 +55,7 @@ func normalizeHostPort(host string, port string, url bool) (string, error) { // See: https://www.rfc-editor.org/rfc/rfc5952#section-4, https://www.rfc-editor.org/rfc/rfc3986#section-3 func NormalizeAddr(address string) (string, error) { if address == "" { - return "", fmt.Errorf("empty or invalid address") + return "", fmt.Errorf("empty address") } if ip := net.ParseIP(address); ip != nil { diff --git a/configutil/normalize_test.go b/configutil/normalize_test.go index dd20476..ea3198d 100644 --- a/configutil/normalize_test.go +++ b/configutil/normalize_test.go @@ -134,7 +134,12 @@ func Test_NormalizeAddr(t *testing.T) { { name: "invalid ipv6, it's just brackets", address: "[]", - err: "empty or invalid address", + err: "empty address", + }, + { + name: "invalid address, empty", + address: "", + err: "empty address", }, { name: "valid url with domain", From 7466b9667faee160be850a772da2b5ba029ec3e6 Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:03:21 -0500 Subject: [PATCH 04/11] update NormalizeAddr func comment --- configutil/normalize.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/configutil/normalize.go b/configutil/normalize.go index 37ba491..d676581 100644 --- a/configutil/normalize.go +++ b/configutil/normalize.go @@ -44,15 +44,23 @@ func normalizeHostPort(host string, port string, url bool) (string, error) { // IPv6 address, the normalized copy will be conformant with RFC-5952 §4. If // the address cannot be parsed, an error will be returned. // -// Valid formats include: -// - host -// - host:port -// - scheme://user@host/path?query#frag +// There are two valid formats: // -// Note: URLs and URIs must conform with RFC-3986 §3 or else the returned address -// may be parsed and formatted incorrectly +// - hosts: "host" +// - may be any of: IPv6 literal, IPv4 literal, dns name, or [sub]domain name +// - IPv6 literals are not required to be encapsulated within square brackets +// in this format // -// See: https://www.rfc-editor.org/rfc/rfc5952#section-4, https://www.rfc-editor.org/rfc/rfc3986#section-3 +// - URIs: "[scheme://] [user@] host [:port] [/path] [?query] [#frag]" +// - format should conform with RFC-3986 §3 or else the returned address may +// be parsed and formatted incorrectly +// - hosts containing IPv6 literals MUST be encapsulated within square brackets, +// as defined in RFC-3986 §3.2.2 and RFC-5952 §6 +// - all non-host components are optional +// +// See: +// - https://www.rfc-editor.org/rfc/rfc5952#section-4 +// - https://www.rfc-editor.org/rfc/rfc3986#section-3 func NormalizeAddr(address string) (string, error) { if address == "" { return "", fmt.Errorf("empty address") From 24aa702506b030c5ad3bc8550c3d1a8ae897a7bf Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:07:39 -0500 Subject: [PATCH 05/11] update file licenses --- configutil/normalize.go | 2 +- configutil/normalize_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/configutil/normalize.go b/configutil/normalize.go index d676581..7fbf05e 100644 --- a/configutil/normalize.go +++ b/configutil/normalize.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MPL-2.0 package configutil diff --git a/configutil/normalize_test.go b/configutil/normalize_test.go index ea3198d..8730991 100644 --- a/configutil/normalize_test.go +++ b/configutil/normalize_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package configutil import ( From ba4215ffc460bea5a395f26fa96a3f11d1b1efd0 Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:13:00 -0500 Subject: [PATCH 06/11] comment typo --- configutil/normalize_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configutil/normalize_test.go b/configutil/normalize_test.go index 8730991..d035c7f 100644 --- a/configutil/normalize_test.go +++ b/configutil/normalize_test.go @@ -216,7 +216,7 @@ func Test_NormalizeAddr(t *testing.T) { }, { name: "valid uri with crazy chars in path", - // note the last of % as that would need to be encoded already + // note the lack of % as that would need to be encoded already address: "hashicorp/test/path/ !@$^&*()[:]{;}", expected: "hashicorp/test/path/%20%21@$%5E&%2A%28%29%5B:%5D%7B;%7D", }, From ebe38e0aa2d144cf7b888393fdc3c192bd2abb58 Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:07:15 -0500 Subject: [PATCH 07/11] add additional rfc reference in comment, use loop instead of recursion, some other typo and wording fixes in comments --- configutil/normalize.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/configutil/normalize.go b/configutil/normalize.go index 7fbf05e..d902677 100644 --- a/configutil/normalize.go +++ b/configutil/normalize.go @@ -19,18 +19,19 @@ func normalizeHostPort(host string, port string, url bool) (string, error) { return "", fmt.Errorf("empty hostname") } if ip := net.ParseIP(host); ip != nil { - if url && ip.To4() == nil && port == "" { + if url && ip.To4() == nil && ip.To16() != nil && port == "" { // this is a unique case, host is ipv6 and requires brackets due to // being part of a url, but they won't be added by net.JoinHostPort // as there is no port + // See: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 return "[" + ip.String() + "]", nil } host = ip.String() } else if strings.Contains(host, ":") { // host is an invalid ipv6 literal. // hosts cannot contain certain reserved characters, including ":" - // See: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2, - // https://www.rfc-editor.org/rfc/rfc3986#section-2.2 + // See: https://www.rfc-editor.org/rfc/rfc3986#section-2.2, + // https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 return "", fmt.Errorf("host contains an invalid IPv6 literal") } if port == "" { @@ -59,9 +60,16 @@ func normalizeHostPort(host string, port string, url bool) (string, error) { // - all non-host components are optional // // See: -// - https://www.rfc-editor.org/rfc/rfc5952#section-4 -// - https://www.rfc-editor.org/rfc/rfc3986#section-3 +// - https://www.rfc-editor.org/rfc/rfc5952 +// - https://www.rfc-editor.org/rfc/rfc3986 func NormalizeAddr(address string) (string, error) { + for { + if !strings.HasPrefix(address, "[") || !strings.HasSuffix(address, "]") { + break + } + address = address[1 : len(address)-1] + } + if address == "" { return "", fmt.Errorf("empty address") } @@ -70,10 +78,6 @@ func NormalizeAddr(address string) (string, error) { return ip.String(), nil } - if strings.HasPrefix(address, "[") && strings.HasSuffix(address, "]") { - return NormalizeAddr(address[1 : len(address)-1]) - } - if host, port, err := net.SplitHostPort(address); err == nil { return normalizeHostPort(host, port, false) } @@ -84,9 +88,9 @@ func NormalizeAddr(address string) (string, error) { // See: https://www.rfc-editor.org/rfc/rfc3986#section-3 if idx := strings.IndexAny(address, genDelims); idx < 0 || address[idx] != ':' { const scheme = "https://" - // attempt to parse it as a url, we only want to try this func when we - // know for sure it has a scheme, since it can parse ANYTHING, but then - // just puts it into u.Path without the scheme + // attempt to parse it as a url. we only want to try this func when we + // know for sure it has a scheme, since it will parse ANYTHING, but + // just put it into u.Path when called without the scheme if u, err := url.Parse(scheme + address); err == nil { if u.Host, err = normalizeHostPort(u.Hostname(), u.Port(), true); err != nil { return "", err From f985fe190f6e9523fe0bb3035d6368973e7994bf Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:21:33 -0500 Subject: [PATCH 08/11] remove the commented vault tests --- configutil/normalize_test.go | 44 ------------------------------------ 1 file changed, 44 deletions(-) diff --git a/configutil/normalize_test.go b/configutil/normalize_test.go index d035c7f..c3e6639 100644 --- a/configutil/normalize_test.go +++ b/configutil/normalize_test.go @@ -474,50 +474,6 @@ func Test_NormalizeAddr(t *testing.T) { address: "username@[2001:DB8:AC3:FE4::1]:8200", expected: "username@[2001:db8:ac3:fe4::1]:8200", }, - // NOTE: these tests are NOT conformant with the URI spec https://www.rfc-editor.org/rfc/rfc3986#section-3 - // and therefore have been omitted. according to RFC3986, ipv6 literals must always be encapsulated within - // square brackets, even when part of a destination address. - // See: https://www.rfc-editor.org/rfc/rfc3986#section-3 - // https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 - // { - // name: "ipv6 invalid ambiguous destination address with port", - // address: "username@2001:0db8::0001:8200", - // // Since the address and port are ambiguous the value appears to be - // // only an address and as such is normalized as an address only - // expected: "username@2001:db8::1:8200", - // }, - // { - // name: "ipv6 invalid leading zeroes ambiguous destination address with port", - // address: "username@2001:db8:0:1:1:1:1:1:8200", - // // Since the address and port are ambiguous the value is treated as - // // a string because it has too many colons to be a valid IPv6 address. - // expected: "username@2001:db8:0:1:1:1:1:1:8200", - // }, - // { - // name: "ipv6 destination address no port RFC-5952 4.1 conformance leading zeroes", - // address: "username@2001:0db8::0001", - // expected: "username@2001:db8::1", - // }, - // { - // name: "ipv6 destination address no port RFC-5952 4.2.2 conformance one 16-bit 0 field", - // address: "username@2001:db8:0:1:1:1:1:1", - // expected: "username@2001:db8:0:1:1:1:1:1", - // }, - // { - // name: "ipv6 destination address no port RFC-5952 4.2.3 conformance longest run of 0 bits shortened", - // address: "username@2001:0:0:1:0:0:0:1", - // expected: "username@2001:0:0:1::1", - // }, - // { - // name: "ipv6 destination address no port RFC-5952 4.2.3 conformance equal runs of 0 bits shortened", - // address: "username@2001:db8:0:0:1:0:0:1", - // expected: "username@2001:db8::1:0:0:1", - // }, - // { - // name: "ipv6 destination address no port RFC-5952 4.3 conformance downcase hex letters", - // address: "username@2001:DB8:AC3:FE4::1", - // expected: "username@2001:db8:ac3:fe4::1", - // }, } for _, tt := range tests { tt := tt From 1eb90a97b56c37a2a68616fdf0ef3af0b466be3c Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Fri, 28 Feb 2025 17:07:07 -0500 Subject: [PATCH 09/11] remove net.SplitHostPort because it will parse anything into port as long as it does not contain [, ], or : --- configutil/normalize.go | 33 +++++++++++++++++------------- configutil/normalize_test.go | 39 ++++++++++++++++++++---------------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/configutil/normalize.go b/configutil/normalize.go index d902677..e1ee07c 100644 --- a/configutil/normalize.go +++ b/configutil/normalize.go @@ -49,8 +49,7 @@ func normalizeHostPort(host string, port string, url bool) (string, error) { // // - hosts: "host" // - may be any of: IPv6 literal, IPv4 literal, dns name, or [sub]domain name -// - IPv6 literals are not required to be encapsulated within square brackets -// in this format +// - IPv6 literals cannot be encapsulated within square brackets in this format // // - URIs: "[scheme://] [user@] host [:port] [/path] [?query] [#frag]" // - format should conform with RFC-3986 §3 or else the returned address may @@ -63,11 +62,8 @@ func normalizeHostPort(host string, port string, url bool) (string, error) { // - https://www.rfc-editor.org/rfc/rfc5952 // - https://www.rfc-editor.org/rfc/rfc3986 func NormalizeAddr(address string) (string, error) { - for { - if !strings.HasPrefix(address, "[") || !strings.HasSuffix(address, "]") { - break - } - address = address[1 : len(address)-1] + if strings.HasPrefix(address, "[") && strings.HasSuffix(address, "]") { + return "", fmt.Errorf("address cannot be encapsulated by brackets") } if address == "" { @@ -78,16 +74,25 @@ func NormalizeAddr(address string) (string, error) { return ip.String(), nil } - if host, port, err := net.SplitHostPort(address); err == nil { - return normalizeHostPort(host, port, false) - } - // if the provided address does not have a scheme provided, attempt to // provide one and re-parse the result. this is done by looking for the // first general delimiter and checking if it exists or if it's not a colon + // or by subsequently checking if the first character of the address is a + // letter or a colon or if the colon is part of "://" // See: https://www.rfc-editor.org/rfc/rfc3986#section-3 - if idx := strings.IndexAny(address, genDelims); idx < 0 || address[idx] != ':' { - const scheme = "https://" + // + // though the first character being a colon is not mentioned in the scheme + // spec, we check for it as url.Parse will read certain invalid ipv6 + // addresses as valid urls, and we want to avoid that + idx := strings.IndexAny(address, genDelims) + switch { + case idx < 0: + fallthrough + case address[idx] != ':': + fallthrough + // by this point we already know that idx > 0 and that address[idx] == ':' + case idx > 1 && !strings.HasPrefix(address[idx:], "://"): + const scheme = "default://" // attempt to parse it as a url. we only want to try this func when we // know for sure it has a scheme, since it will parse ANYTHING, but // just put it into u.Path when called without the scheme @@ -97,7 +102,7 @@ func NormalizeAddr(address string) (string, error) { } return strings.TrimPrefix(u.String(), scheme), nil } - } else { + default: if u, err := url.Parse(address); err == nil { if u.Host, err = normalizeHostPort(u.Hostname(), u.Port(), true); err != nil { return "", err diff --git a/configutil/normalize_test.go b/configutil/normalize_test.go index c3e6639..4d7680c 100644 --- a/configutil/normalize_test.go +++ b/configutil/normalize_test.go @@ -65,9 +65,9 @@ func Test_NormalizeAddr(t *testing.T) { expected: "2001:beef::1:0:1", }, { - name: "valid ipv6 literal with brackets", - address: "[2001:BEEF:0:0:0:1:0:0001]", - expected: "2001:beef::1:0:1", + name: "valid ipv6 literal with brackets", + address: "[2001:BEEF:0:0:0:1:0:0001]", + err: "address cannot be encapsulated by brackets", }, { name: "valid ipv6 host:port", @@ -112,7 +112,7 @@ func Test_NormalizeAddr(t *testing.T) { { name: "invalid ipv6, not enough segments", address: "2001:BEEF:0:0:1:0:0001", - err: "unable to normalize given address", + err: "host contains an invalid IPv6 literal", }, { name: "invalid ipv6 host:port, not enough segments", @@ -122,7 +122,7 @@ func Test_NormalizeAddr(t *testing.T) { { name: "invalid ipv6 literal with brackets, not enough segments", address: "[2001:BEEF:0:0:1:0:0001]", - err: "unable to normalize given address", + err: "address cannot be encapsulated by brackets", }, { name: "invalid ipv6 uri, not enough segments", @@ -137,7 +137,7 @@ func Test_NormalizeAddr(t *testing.T) { { name: "invalid ipv6, it's just brackets", address: "[]", - err: "empty address", + err: "address cannot be encapsulated by brackets", }, { name: "invalid address, empty", @@ -222,7 +222,7 @@ func Test_NormalizeAddr(t *testing.T) { }, { name: "invalid uri with invalid percent encoding", - address: "hashicorp/test/path?I think actually anything can be past here !@#$%^&*()[:]{;}", + address: "hashicorp/test/path?!@#$%^&*()[:]{;}", err: "unable to normalize given address", }, { @@ -287,6 +287,21 @@ func Test_NormalizeAddr(t *testing.T) { address: "ldap://[2001:BEEF:0:0:0:1:0:0001]:389#extra", expected: "ldap://[2001:beef::1:0:1]:389#extra", }, + { + name: "valid url with no scheme, ipv4 host, port address, and colon after port", + address: "127.0.0.1:80/test/path:123", + expected: "127.0.0.1:80/test/path:123", + }, + { + name: "valid url with no scheme, ipv6 host, port address, and colon after port", + address: "[2001:BEEF:0:0:0:1:0:0001]:80/test/path:123", + expected: "[2001:beef::1:0:1]:80/test/path:123", + }, + { + name: "anything other than numbers in port", + address: "abc:gh", + err: "unable to normalize given address", + }, // imported from vault { @@ -329,11 +344,6 @@ func Test_NormalizeAddr(t *testing.T) { address: "10.10.1.10", expected: "10.10.1.10", }, - { - name: "ipv4 invalid bracketed", - address: "[10.10.1.10]", - expected: "10.10.1.10", - }, { name: "ipv4 IP:Port addr", address: "10.10.1.10:8500", @@ -384,11 +394,6 @@ func Test_NormalizeAddr(t *testing.T) { address: "https://username@10.10.1.10:8200", expected: "https://username@10.10.1.10:8200", }, - { - name: "ipv6 invalid address", - address: "[2001:0db8::0001]", - expected: "2001:db8::1", - }, { name: "ipv6 IP:Port RFC-5952 4.1 conformance leading zeroes", address: "[2001:0db8::0001]:8500", From b9faf193ae6388b5ed05be2d1bdadafbf6fc901a Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Mon, 3 Mar 2025 13:02:07 -0500 Subject: [PATCH 10/11] add edge case for missing port values --- configutil/normalize.go | 44 ++++++++++++++++++++---------------- configutil/normalize_test.go | 22 +++++++++++++----- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/configutil/normalize.go b/configutil/normalize.go index e1ee07c..693f552 100644 --- a/configutil/normalize.go +++ b/configutil/normalize.go @@ -14,12 +14,12 @@ import ( // See: https://www.rfc-editor.org/rfc/rfc3986#section-2.2 const genDelims = ":/?#[]@" -func normalizeHostPort(host string, port string, url bool) (string, error) { +func normalizeHostPort(host string, port string) (string, error) { if host == "" { return "", fmt.Errorf("empty hostname") } if ip := net.ParseIP(host); ip != nil { - if url && ip.To4() == nil && ip.To16() != nil && port == "" { + if ip.To4() == nil && ip.To16() != nil && port == "" { // this is a unique case, host is ipv6 and requires brackets due to // being part of a url, but they won't be added by net.JoinHostPort // as there is no port @@ -40,6 +40,19 @@ func normalizeHostPort(host string, port string, url bool) (string, error) { return net.JoinHostPort(host, port), nil } +func parseUrl(addr string) (string, error) { + if u, err := url.Parse(addr); err == nil { + if strings.HasSuffix(u.Host, ":") { + return "", fmt.Errorf("url has malformed host: missing port value after colon") + } + if u.Host, err = normalizeHostPort(u.Hostname(), u.Port()); err != nil { + return "", err + } + return u.String(), nil + } + return "", fmt.Errorf("failed to parse address") +} + // NormalizeAddr takes an address as a string and returns a normalized copy. // If the address is a URL, IP Address, or host:port address that includes an // IPv6 address, the normalized copy will be conformant with RFC-5952 §4. If @@ -62,14 +75,14 @@ func normalizeHostPort(host string, port string, url bool) (string, error) { // - https://www.rfc-editor.org/rfc/rfc5952 // - https://www.rfc-editor.org/rfc/rfc3986 func NormalizeAddr(address string) (string, error) { - if strings.HasPrefix(address, "[") && strings.HasSuffix(address, "]") { - return "", fmt.Errorf("address cannot be encapsulated by brackets") - } - if address == "" { return "", fmt.Errorf("empty address") } + if strings.HasPrefix(address, "[") && strings.HasSuffix(address, "]") { + return "", fmt.Errorf("address cannot be encapsulated by brackets") + } + if ip := net.ParseIP(address); ip != nil { return ip.String(), nil } @@ -96,20 +109,13 @@ func NormalizeAddr(address string) (string, error) { // attempt to parse it as a url. we only want to try this func when we // know for sure it has a scheme, since it will parse ANYTHING, but // just put it into u.Path when called without the scheme - if u, err := url.Parse(scheme + address); err == nil { - if u.Host, err = normalizeHostPort(u.Hostname(), u.Port(), true); err != nil { - return "", err - } - return strings.TrimPrefix(u.String(), scheme), nil + u, err := parseUrl(scheme + address) + if err != nil { + return "", err } + return strings.TrimPrefix(u, scheme), nil + default: - if u, err := url.Parse(address); err == nil { - if u.Host, err = normalizeHostPort(u.Hostname(), u.Port(), true); err != nil { - return "", err - } - return u.String(), nil - } + return parseUrl(address) } - - return "", fmt.Errorf("unable to normalize given address") } diff --git a/configutil/normalize_test.go b/configutil/normalize_test.go index 4d7680c..4d49739 100644 --- a/configutil/normalize_test.go +++ b/configutil/normalize_test.go @@ -92,7 +92,7 @@ func Test_NormalizeAddr(t *testing.T) { { name: "invalid ipv6 uri missing closing bracket", address: "https://[2001:BEEF:0:0:0:1:0:0001", - err: "unable to normalize given address", + err: "failed to parse address", }, { name: "invalid ipv6 uri missing brackets", @@ -102,12 +102,12 @@ func Test_NormalizeAddr(t *testing.T) { { name: "invalid ipv6 literal", address: ":0:", - err: "unable to normalize given address", + err: "failed to parse address", }, { name: "invalid ipv6 literal", address: "::0:", - err: "unable to normalize given address", + err: "failed to parse address", }, { name: "invalid ipv6, not enough segments", @@ -187,7 +187,7 @@ func Test_NormalizeAddr(t *testing.T) { { name: "invalid host with only dns name", address: "hashi corp", - err: "unable to normalize given address", + err: "failed to parse address", }, { name: "valid url with path, schema, and subdomain", @@ -223,7 +223,7 @@ func Test_NormalizeAddr(t *testing.T) { { name: "invalid uri with invalid percent encoding", address: "hashicorp/test/path?!@#$%^&*()[:]{;}", - err: "unable to normalize given address", + err: "failed to parse address", }, { name: "invalid uri with invalid percent encoding", @@ -300,7 +300,17 @@ func Test_NormalizeAddr(t *testing.T) { { name: "anything other than numbers in port", address: "abc:gh", - err: "unable to normalize given address", + err: "failed to parse address", + }, + { + name: "invalid ipv4 host:port, host contains colon but no port", + address: "127.0.0.1:", + err: "url has malformed host: missing port value after colon", + }, + { + name: "invalid ipv6 host:port, host contains colon but no port", + address: "[2001:4860:4860::8888]:", + err: "url has malformed host: missing port value after colon", }, // imported from vault From 25cf3e706c94a3a14b3d8bf97c5c0c0e69657b86 Mon Sep 17 00:00:00 2001 From: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:30:56 -0500 Subject: [PATCH 11/11] move NormalizeAddr from configutil to parseutil --- {configutil => parseutil}/normalize.go | 2 +- {configutil => parseutil}/normalize_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename {configutil => parseutil}/normalize.go (99%) rename {configutil => parseutil}/normalize_test.go (99%) diff --git a/configutil/normalize.go b/parseutil/normalize.go similarity index 99% rename from configutil/normalize.go rename to parseutil/normalize.go index 693f552..879bcfc 100644 --- a/configutil/normalize.go +++ b/parseutil/normalize.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package configutil +package parseutil import ( "fmt" diff --git a/configutil/normalize_test.go b/parseutil/normalize_test.go similarity index 99% rename from configutil/normalize_test.go rename to parseutil/normalize_test.go index 4d49739..b8a45a1 100644 --- a/configutil/normalize_test.go +++ b/parseutil/normalize_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package configutil +package parseutil import ( "testing"