aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/hashicorp/terraform/svchost/svchost.go
blob: 4eded142c7f0e2fdff776037c07ffbb3c7ebc30e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Package svchost deals with the representations of the so-called "friendly
// hostnames" that we use to represent systems that provide Terraform-native
// remote services, such as module registry, remote operations, etc.
//
// Friendly hostnames are specified such that, as much as possible, they
// are consistent with how web browsers think of hostnames, so that users
// can bring their intuitions about how hostnames behave when they access
// a Terraform Enterprise instance's web UI (or indeed any other website)
// and have this behave in a similar way.
package svchost

import (
	"errors"
	"fmt"
	"strconv"
	"strings"

	"golang.org/x/net/idna"
)

// Hostname is specialized name for string that indicates that the string
// has been converted to (or was already in) the storage and comparison form.
//
// Hostname values are not suitable for display in the user-interface. Use
// the ForDisplay method to obtain a form suitable for display in the UI.
//
// Unlike user-supplied hostnames, strings of type Hostname (assuming they
// were constructed by a function within this package) can be compared for
// equality using the standard Go == operator.
type Hostname string

// acePrefix is the ASCII Compatible Encoding prefix, used to indicate that
// a domain name label is in "punycode" form.
const acePrefix = "xn--"

// displayProfile is a very liberal idna profile that we use to do
// normalization for display without imposing validation rules.
var displayProfile = idna.New(
	idna.MapForLookup(),
	idna.Transitional(true),
)

// ForDisplay takes a user-specified hostname and returns a normalized form of
// it suitable for display in the UI.
//
// If the input is so invalid that no normalization can be performed then
// this will return the input, assuming that the caller still wants to
// display _something_. This function is, however, more tolerant than the
// other functions in this package and will make a best effort to prepare
// _any_ given hostname for display.
//
// For validation, use either IsValid (for explicit validation) or
// ForComparison (which implicitly validates, returning an error if invalid).
func ForDisplay(given string) string {
	var portPortion string
	if colonPos := strings.Index(given, ":"); colonPos != -1 {
		given, portPortion = given[:colonPos], given[colonPos:]
	}
	portPortion, _ = normalizePortPortion(portPortion)

	ascii, err := displayProfile.ToASCII(given)
	if err != nil {
		return given + portPortion
	}
	display, err := displayProfile.ToUnicode(ascii)
	if err != nil {
		return given + portPortion
	}
	return display + portPortion
}

// IsValid returns true if the given user-specified hostname is a valid
// service hostname.
//
// Validity is determined by complying with the RFC 5891 requirements for
// names that are valid for domain lookup (section 5), with the additional
// requirement that user-supplied forms must not _already_ contain
// Punycode segments.
func IsValid(given string) bool {
	_, err := ForComparison(given)
	return err == nil
}

// ForComparison takes a user-specified hostname and returns a normalized
// form of it suitable for storage and comparison. The result is not suitable
// for display to end-users because it uses Punycode to represent non-ASCII
// characters, and this form is unreadable for non-ASCII-speaking humans.
//
// The result is typed as Hostname -- a specialized name for string -- so that
// other APIs can make it clear within the type system whether they expect a
// user-specified or display-form hostname or a value already normalized for
// comparison.
//
// The returned Hostname is not valid if the returned error is non-nil.
func ForComparison(given string) (Hostname, error) {
	var portPortion string
	if colonPos := strings.Index(given, ":"); colonPos != -1 {
		given, portPortion = given[:colonPos], given[colonPos:]
	}

	var err error
	portPortion, err = normalizePortPortion(portPortion)
	if err != nil {
		return Hostname(""), err
	}

	if given == "" {
		return Hostname(""), fmt.Errorf("empty string is not a valid hostname")
	}

	// First we'll apply our additional constraint that Punycode must not
	// be given directly by the user. This is not an IDN specification
	// requirement, but we prohibit it to force users to use human-readable
	// hostname forms within Terraform configuration.
	labels := labelIter{orig: given}
	for ; !labels.done(); labels.next() {
		label := labels.label()
		if label == "" {
			return Hostname(""), fmt.Errorf(
				"hostname contains empty label (two consecutive periods)",
			)
		}
		if strings.HasPrefix(label, acePrefix) {
			return Hostname(""), fmt.Errorf(
				"hostname label %q specified in punycode format; service hostnames must be given in unicode",
				label,
			)
		}
	}

	result, err := idna.Lookup.ToASCII(given)
	if err != nil {
		return Hostname(""), err
	}
	return Hostname(result + portPortion), nil
}

// ForDisplay returns a version of the receiver that is appropriate for display
// in the UI. This includes converting any punycode labels to their
// corresponding Unicode characters.
//
// A round-trip through ForComparison and this ForDisplay method does not
// guarantee the same result as calling this package's top-level ForDisplay
// function, since a round-trip through the Hostname type implies stricter
// handling than we do when doing basic display-only processing.
func (h Hostname) ForDisplay() string {
	given := string(h)
	var portPortion string
	if colonPos := strings.Index(given, ":"); colonPos != -1 {
		given, portPortion = given[:colonPos], given[colonPos:]
	}
	// We don't normalize the port portion here because we assume it's
	// already been normalized on the way in.

	result, err := idna.Lookup.ToUnicode(given)
	if err != nil {
		// Should never happen, since type Hostname indicates that a string
		// passed through our validation rules.
		panic(fmt.Errorf("ForDisplay called on invalid Hostname: %s", err))
	}
	return result + portPortion
}

func (h Hostname) String() string {
	return string(h)
}

func (h Hostname) GoString() string {
	return fmt.Sprintf("svchost.Hostname(%q)", string(h))
}

// normalizePortPortion attempts to normalize the "port portion" of a hostname,
// which begins with the first colon in the hostname and should be followed
// by a string of decimal digits.
//
// If the port portion is valid, a normalized version of it is returned along
// with a nil error.
//
// If the port portion is invalid, the input string is returned verbatim along
// with a non-nil error.
//
// An empty string is a valid port portion representing the absense of a port.
// If non-empty, the first character must be a colon.
func normalizePortPortion(s string) (string, error) {
	if s == "" {
		return s, nil
	}

	if s[0] != ':' {
		// should never happen, since caller tends to guarantee the presence
		// of a colon due to how it's extracted from the string.
		return s, errors.New("port portion is missing its initial colon")
	}

	numStr := s[1:]
	num, err := strconv.Atoi(numStr)
	if err != nil {
		return s, errors.New("port portion contains non-digit characters")
	}
	if num == 443 {
		return "", nil // ":443" is the default
	}
	if num > 65535 {
		return s, errors.New("port number is greater than 65535")
	}
	return fmt.Sprintf(":%d", num), nil
}