...

Source file src/github.com/aws/smithy-go/endpoints/private/rulesfn/uri.go

Documentation: github.com/aws/smithy-go/endpoints/private/rulesfn

     1  package rulesfn
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"net/url"
     7  	"strings"
     8  
     9  	smithyhttp "github.com/aws/smithy-go/transport/http"
    10  )
    11  
    12  // IsValidHostLabel returns if the input is a single valid [RFC 1123] host
    13  // label. If allowSubDomains is true, will allow validation to include nested
    14  // host labels. Returns false if the input is not a valid host label. If errors
    15  // occur they will be added to the provided [ErrorCollector].
    16  //
    17  // [RFC 1123]: https://www.ietf.org/rfc/rfc1123.txt
    18  func IsValidHostLabel(input string, allowSubDomains bool) bool {
    19  	var labels []string
    20  	if allowSubDomains {
    21  		labels = strings.Split(input, ".")
    22  	} else {
    23  		labels = []string{input}
    24  	}
    25  
    26  	for _, label := range labels {
    27  		if !smithyhttp.ValidHostLabel(label) {
    28  			return false
    29  		}
    30  	}
    31  
    32  	return true
    33  }
    34  
    35  // ParseURL returns a [URL] if the provided string could be parsed. Returns nil
    36  // if the string could not be parsed. Any parsing error will be added to the
    37  // [ErrorCollector].
    38  //
    39  // If the input URL string contains an IP6 address with a zone index. The
    40  // returned [builtin.URL.Authority] value will contain the percent escaped (%)
    41  // zone index separator.
    42  func ParseURL(input string) *URL {
    43  	u, err := url.Parse(input)
    44  	if err != nil {
    45  		return nil
    46  	}
    47  
    48  	if u.RawQuery != "" {
    49  		return nil
    50  	}
    51  
    52  	if u.Scheme != "http" && u.Scheme != "https" {
    53  		return nil
    54  	}
    55  
    56  	normalizedPath := u.Path
    57  	if !strings.HasPrefix(normalizedPath, "/") {
    58  		normalizedPath = "/" + normalizedPath
    59  	}
    60  	if !strings.HasSuffix(normalizedPath, "/") {
    61  		normalizedPath = normalizedPath + "/"
    62  	}
    63  
    64  	// IP6 hosts may have zone indexes that need to be escaped to be valid in a
    65  	// URI. The Go URL parser will unescape the `%25` into `%`. This needs to
    66  	// be reverted since the returned URL will be used in string builders.
    67  	authority := strings.ReplaceAll(u.Host, "%", "%25")
    68  
    69  	return &URL{
    70  		Scheme:         u.Scheme,
    71  		Authority:      authority,
    72  		Path:           u.Path,
    73  		NormalizedPath: normalizedPath,
    74  		IsIp:           net.ParseIP(hostnameWithoutZone(u)) != nil,
    75  	}
    76  }
    77  
    78  // URL provides the structure describing the parts of a parsed URL returned by
    79  // [ParseURL].
    80  type URL struct {
    81  	Scheme         string // https://www.rfc-editor.org/rfc/rfc3986#section-3.1
    82  	Authority      string // https://www.rfc-editor.org/rfc/rfc3986#section-3.2
    83  	Path           string // https://www.rfc-editor.org/rfc/rfc3986#section-3.3
    84  	NormalizedPath string // https://www.rfc-editor.org/rfc/rfc3986#section-6.2.3
    85  	IsIp           bool
    86  }
    87  
    88  // URIEncode returns an percent-encoded [RFC3986 section 2.1] version of the
    89  // input string.
    90  //
    91  // [RFC3986 section 2.1]: https://www.rfc-editor.org/rfc/rfc3986#section-2.1
    92  func URIEncode(input string) string {
    93  	var output strings.Builder
    94  	for _, c := range []byte(input) {
    95  		if validPercentEncodedChar(c) {
    96  			output.WriteByte(c)
    97  			continue
    98  		}
    99  
   100  		fmt.Fprintf(&output, "%%%X", c)
   101  	}
   102  
   103  	return output.String()
   104  }
   105  
   106  func validPercentEncodedChar(c byte) bool {
   107  	return (c >= 'a' && c <= 'z') ||
   108  		(c >= 'A' && c <= 'Z') ||
   109  		(c >= '0' && c <= '9') ||
   110  		c == '-' || c == '_' || c == '.' || c == '~'
   111  }
   112  
   113  // hostname implements u.Hostname() but strips the ipv6 zone ID (if present)
   114  // such that net.ParseIP can still recognize IPv6 addresses with zone IDs.
   115  //
   116  // FUTURE(10/2023): netip.ParseAddr handles this natively but we can't take
   117  // that package as a dependency yet due to our min go version (1.15, netip
   118  // starts in 1.18).  When we align with go runtime deprecation policy in
   119  // 10/2023, we can remove this.
   120  func hostnameWithoutZone(u *url.URL) string {
   121  	full := u.Hostname()
   122  
   123  	// this more or less mimics the internals of net/ (see unexported
   124  	// splitHostZone in that source) but throws the zone away because we don't
   125  	// need it
   126  	if i := strings.LastIndex(full, "%"); i > -1 {
   127  		return full[:i]
   128  	}
   129  	return full
   130  }
   131  

View as plain text