Skip to content

Commit 8565209

Browse files
authored
Creates the NormalizeAddr Func to Format IPv6 Addresses (#157)
Create NormalizeAddr which accepts addresses and returns a formatted copy of that address conformant with RFC5952 --------- Co-authored-by: Danielle Miu <29378233+DanielleMiu@users.noreply.github.com>
1 parent 20f08ea commit 8565209

File tree

2 files changed

+629
-0
lines changed

2 files changed

+629
-0
lines changed

parseutil/normalize.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package parseutil
5+
6+
import (
7+
"fmt"
8+
"net"
9+
"net/url"
10+
"strings"
11+
)
12+
13+
// general delimiters as defined in RFC-3986 §2.2
14+
// See: https://www.rfc-editor.org/rfc/rfc3986#section-2.2
15+
const genDelims = ":/?#[]@"
16+
17+
func normalizeHostPort(host string, port string) (string, error) {
18+
if host == "" {
19+
return "", fmt.Errorf("empty hostname")
20+
}
21+
if ip := net.ParseIP(host); ip != nil {
22+
if ip.To4() == nil && ip.To16() != nil && port == "" {
23+
// this is a unique case, host is ipv6 and requires brackets due to
24+
// being part of a url, but they won't be added by net.JoinHostPort
25+
// as there is no port
26+
// See: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2
27+
return "[" + ip.String() + "]", nil
28+
}
29+
host = ip.String()
30+
} else if strings.Contains(host, ":") {
31+
// host is an invalid ipv6 literal.
32+
// hosts cannot contain certain reserved characters, including ":"
33+
// See: https://www.rfc-editor.org/rfc/rfc3986#section-2.2,
34+
// https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2
35+
return "", fmt.Errorf("host contains an invalid IPv6 literal")
36+
}
37+
if port == "" {
38+
return host, nil
39+
}
40+
return net.JoinHostPort(host, port), nil
41+
}
42+
43+
func parseUrl(addr string) (string, error) {
44+
if u, err := url.Parse(addr); err == nil {
45+
if strings.HasSuffix(u.Host, ":") {
46+
return "", fmt.Errorf("url has malformed host: missing port value after colon")
47+
}
48+
if u.Host, err = normalizeHostPort(u.Hostname(), u.Port()); err != nil {
49+
return "", err
50+
}
51+
return u.String(), nil
52+
}
53+
return "", fmt.Errorf("failed to parse address")
54+
}
55+
56+
// NormalizeAddr takes an address as a string and returns a normalized copy.
57+
// If the address is a URL, IP Address, or host:port address that includes an
58+
// IPv6 address, the normalized copy will be conformant with RFC-5952 §4. If
59+
// the address cannot be parsed, an error will be returned.
60+
//
61+
// There are two valid formats:
62+
//
63+
// - hosts: "host"
64+
// - may be any of: IPv6 literal, IPv4 literal, dns name, or [sub]domain name
65+
// - IPv6 literals cannot be encapsulated within square brackets in this format
66+
//
67+
// - URIs: "[scheme://] [user@] host [:port] [/path] [?query] [#frag]"
68+
// - format should conform with RFC-3986 §3 or else the returned address may
69+
// be parsed and formatted incorrectly
70+
// - hosts containing IPv6 literals MUST be encapsulated within square brackets,
71+
// as defined in RFC-3986 §3.2.2 and RFC-5952 §6
72+
// - all non-host components are optional
73+
//
74+
// See:
75+
// - https://www.rfc-editor.org/rfc/rfc5952
76+
// - https://www.rfc-editor.org/rfc/rfc3986
77+
func NormalizeAddr(address string) (string, error) {
78+
if address == "" {
79+
return "", fmt.Errorf("empty address")
80+
}
81+
82+
if strings.HasPrefix(address, "[") && strings.HasSuffix(address, "]") {
83+
return "", fmt.Errorf("address cannot be encapsulated by brackets")
84+
}
85+
86+
if ip := net.ParseIP(address); ip != nil {
87+
return ip.String(), nil
88+
}
89+
90+
// if the provided address does not have a scheme provided, attempt to
91+
// provide one and re-parse the result. this is done by looking for the
92+
// first general delimiter and checking if it exists or if it's not a colon
93+
// or by subsequently checking if the first character of the address is a
94+
// letter or a colon or if the colon is part of "://"
95+
// See: https://www.rfc-editor.org/rfc/rfc3986#section-3
96+
//
97+
// though the first character being a colon is not mentioned in the scheme
98+
// spec, we check for it as url.Parse will read certain invalid ipv6
99+
// addresses as valid urls, and we want to avoid that
100+
idx := strings.IndexAny(address, genDelims)
101+
switch {
102+
case idx < 0:
103+
fallthrough
104+
case address[idx] != ':':
105+
fallthrough
106+
// by this point we already know that idx > 0 and that address[idx] == ':'
107+
case idx > 1 && !strings.HasPrefix(address[idx:], "://"):
108+
const scheme = "default://"
109+
// attempt to parse it as a url. we only want to try this func when we
110+
// know for sure it has a scheme, since it will parse ANYTHING, but
111+
// just put it into u.Path when called without the scheme
112+
u, err := parseUrl(scheme + address)
113+
if err != nil {
114+
return "", err
115+
}
116+
return strings.TrimPrefix(u, scheme), nil
117+
118+
default:
119+
return parseUrl(address)
120+
}
121+
}

0 commit comments

Comments
 (0)