github.com/chanzuckerberg/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  }