github.com/KevinKlinger/open_terraform@v0.11.12-beta1/svchost/svchost.go (about) 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. 4 // 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. 10 package svchost 11 12 import ( 13 "errors" 14 "fmt" 15 "strconv" 16 "strings" 17 18 "golang.org/x/net/idna" 19 ) 20 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. 23 // 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. 26 // 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. 30 type Hostname string 31 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--" 35 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( 39 idna.MapForLookup(), 40 idna.Transitional(true), 41 ) 42 43 // ForDisplay takes a user-specified hostname and returns a normalized form of 44 // it suitable for display in the UI. 45 // 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. 51 // 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:] 58 } 59 portPortion, _ = normalizePortPortion(portPortion) 60 61 ascii, err := displayProfile.ToASCII(given) 62 if err != nil { 63 return given + portPortion 64 } 65 display, err := displayProfile.ToUnicode(ascii) 66 if err != nil { 67 return given + portPortion 68 } 69 return display + portPortion 70 } 71 72 // IsValid returns true if the given user-specified hostname is a valid 73 // service hostname. 74 // 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 78 // Punycode segments. 79 func IsValid(given string) bool { 80 _, err := ForComparison(given) 81 return err == nil 82 } 83 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. 88 // 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 92 // comparison. 93 // 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:] 99 } 100 101 var err error 102 portPortion, err = normalizePortPortion(portPortion) 103 if err != nil { 104 return Hostname(""), err 105 } 106 107 if given == "" { 108 return Hostname(""), fmt.Errorf("empty string is not a valid hostname") 109 } 110 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() 118 if label == "" { 119 return Hostname(""), fmt.Errorf( 120 "hostname contains empty label (two consecutive periods)", 121 ) 122 } 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", 126 label, 127 ) 128 } 129 } 130 131 result, err := idna.Lookup.ToASCII(given) 132 if err != nil { 133 return Hostname(""), err 134 } 135 return Hostname(result + portPortion), nil 136 } 137 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. 141 // 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 { 147 given := string(h) 148 var portPortion string 149 if colonPos := strings.Index(given, ":"); colonPos != -1 { 150 given, portPortion = given[:colonPos], given[colonPos:] 151 } 152 // We don't normalize the port portion here because we assume it's 153 // already been normalized on the way in. 154 155 result, err := idna.Lookup.ToUnicode(given) 156 if err != nil { 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)) 160 } 161 return result + portPortion 162 } 163 164 func (h Hostname) String() string { 165 return string(h) 166 } 167 168 func (h Hostname) GoString() string { 169 return fmt.Sprintf("svchost.Hostname(%q)", string(h)) 170 } 171 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. 175 // 176 // If the port portion is valid, a normalized version of it is returned along 177 // with a nil error. 178 // 179 // If the port portion is invalid, the input string is returned verbatim along 180 // with a non-nil error. 181 // 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) { 185 if s == "" { 186 return s, nil 187 } 188 189 if s[0] != ':' { 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") 193 } 194 195 numStr := s[1:] 196 num, err := strconv.Atoi(numStr) 197 if err != nil { 198 return s, errors.New("port portion contains non-digit characters") 199 } 200 if num == 443 { 201 return "", nil // ":443" is the default 202 } 203 if num > 65535 { 204 return s, errors.New("port number is greater than 65535") 205 } 206 return fmt.Sprintf(":%d", num), nil 207 }