src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

uri.go (3848B)


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