1 // Package svchost deals with the representations of the so-called "friendly
2 // hostnames" that we use to represent systems that provide Terraform-native
3 // remote services, such as module registry, remote operations, etc.
5 // Friendly hostnames are specified such that, as much as possible, they
6 // are consistent with how web browsers think of hostnames, so that users
7 // can bring their intuitions about how hostnames behave when they access
8 // a Terraform Enterprise instance's web UI (or indeed any other website)
9 // and have this behave in a similar way.
18 "golang.org/x/net/idna"
21 // Hostname is specialized name for string that indicates that the string
22 // has been converted to (or was already in) the storage and comparison form.
24 // Hostname values are not suitable for display in the user-interface. Use
25 // the ForDisplay method to obtain a form suitable for display in the UI.
27 // Unlike user-supplied hostnames, strings of type Hostname (assuming they
28 // were constructed by a function within this package) can be compared for
29 // equality using the standard Go == operator.
32 // acePrefix is the ASCII Compatible Encoding prefix, used to indicate that
33 // a domain name label is in "punycode" form.
34 const acePrefix = "xn--"
36 // displayProfile is a very liberal idna profile that we use to do
37 // normalization for display without imposing validation rules.
38 var displayProfile = idna.New(
40 idna.Transitional(true),
43 // ForDisplay takes a user-specified hostname and returns a normalized form of
44 // it suitable for display in the UI.
46 // If the input is so invalid that no normalization can be performed then
47 // this will return the input, assuming that the caller still wants to
48 // display _something_. This function is, however, more tolerant than the
49 // other functions in this package and will make a best effort to prepare
50 // _any_ given hostname for display.
52 // For validation, use either IsValid (for explicit validation) or
53 // ForComparison (which implicitly validates, returning an error if invalid).
54 func ForDisplay(given string) string {
55 var portPortion string
56 if colonPos := strings.Index(given, ":"); colonPos != -1 {
57 given, portPortion = given[:colonPos], given[colonPos:]
59 portPortion, _ = normalizePortPortion(portPortion)
61 ascii, err := displayProfile.ToASCII(given)
63 return given + portPortion
65 display, err := displayProfile.ToUnicode(ascii)
67 return given + portPortion
69 return display + portPortion
72 // IsValid returns true if the given user-specified hostname is a valid
75 // Validity is determined by complying with the RFC 5891 requirements for
76 // names that are valid for domain lookup (section 5), with the additional
77 // requirement that user-supplied forms must not _already_ contain
79 func IsValid(given string) bool {
80 _, err := ForComparison(given)
84 // ForComparison takes a user-specified hostname and returns a normalized
85 // form of it suitable for storage and comparison. The result is not suitable
86 // for display to end-users because it uses Punycode to represent non-ASCII
87 // characters, and this form is unreadable for non-ASCII-speaking humans.
89 // The result is typed as Hostname -- a specialized name for string -- so that
90 // other APIs can make it clear within the type system whether they expect a
91 // user-specified or display-form hostname or a value already normalized for
94 // The returned Hostname is not valid if the returned error is non-nil.
95 func ForComparison(given string) (Hostname, error) {
96 var portPortion string
97 if colonPos := strings.Index(given, ":"); colonPos != -1 {
98 given, portPortion = given[:colonPos], given[colonPos:]
102 portPortion, err = normalizePortPortion(portPortion)
104 return Hostname(""), err
108 return Hostname(""), fmt.Errorf("empty string is not a valid hostname")
111 // First we'll apply our additional constraint that Punycode must not
112 // be given directly by the user. This is not an IDN specification
113 // requirement, but we prohibit it to force users to use human-readable
114 // hostname forms within Terraform configuration.
115 labels := labelIter{orig: given}
116 for ; !labels.done(); labels.next() {
117 label := labels.label()
119 return Hostname(""), fmt.Errorf(
120 "hostname contains empty label (two consecutive periods)",
123 if strings.HasPrefix(label, acePrefix) {
124 return Hostname(""), fmt.Errorf(
125 "hostname label %q specified in punycode format; service hostnames must be given in unicode",
131 result, err := idna.Lookup.ToASCII(given)
133 return Hostname(""), err
135 return Hostname(result + portPortion), nil
138 // ForDisplay returns a version of the receiver that is appropriate for display
139 // in the UI. This includes converting any punycode labels to their
140 // corresponding Unicode characters.
142 // A round-trip through ForComparison and this ForDisplay method does not
143 // guarantee the same result as calling this package's top-level ForDisplay
144 // function, since a round-trip through the Hostname type implies stricter
145 // handling than we do when doing basic display-only processing.
146 func (h Hostname) ForDisplay() string {
148 var portPortion string
149 if colonPos := strings.Index(given, ":"); colonPos != -1 {
150 given, portPortion = given[:colonPos], given[colonPos:]
152 // We don't normalize the port portion here because we assume it's
153 // already been normalized on the way in.
155 result, err := idna.Lookup.ToUnicode(given)
157 // Should never happen, since type Hostname indicates that a string
158 // passed through our validation rules.
159 panic(fmt.Errorf("ForDisplay called on invalid Hostname: %s", err))
161 return result + portPortion
164 func (h Hostname) String() string {
168 func (h Hostname) GoString() string {
169 return fmt.Sprintf("svchost.Hostname(%q)", string(h))
172 // normalizePortPortion attempts to normalize the "port portion" of a hostname,
173 // which begins with the first colon in the hostname and should be followed
174 // by a string of decimal digits.
176 // If the port portion is valid, a normalized version of it is returned along
179 // If the port portion is invalid, the input string is returned verbatim along
180 // with a non-nil error.
182 // An empty string is a valid port portion representing the absense of a port.
183 // If non-empty, the first character must be a colon.
184 func normalizePortPortion(s string) (string, error) {
190 // should never happen, since caller tends to guarantee the presence
191 // of a colon due to how it's extracted from the string.
192 return s, errors.New("port portion is missing its initial colon")
196 num, err := strconv.Atoi(numStr)
198 return s, errors.New("port portion contains non-digit characters")
201 return "", nil // ":443" is the default
204 return s, errors.New("port number is greater than 65535")
206 return fmt.Sprintf(":%d", num), nil