Files
opentf/internal/registry/regsrc/friendly_host.go
Martin Atkins d2bef1fd47 Adopt OpenTofu's own "svchost" module
Previously we were using a third-party library, but that doesn't have any
support for passing context.Context through its API and so isn't suitable
for our goals of adding OpenTelemetry tracing for all outgoing network
requests.

We now have our own fork that is updated to use context.Context. It also
has a slightly reduced scope no longer including various details that
are tightly-coupled to our cliconfig mechanism and so better placed in the
main OpenTofu codebase so we can evolve it in future without making
lockstep library releases.

The "registry-address" library also uses svchost and uses some of its types
in its public API, so this also incorporates v2 of that library that is
updated to use our own svchost module.

Unfortunately this commit is a mix of mechanical updates to the new
libraries and some new code dealing with the functionality that is removed
in our fork of svchost. The new code is primarily in the "svcauthconfig"
package, which is similar in purpose "ociauthconfig" but for OpenTofu's
own auth mechanism instead of the OCI Distribution protocol's auth
mechanism.

This includes some additional plumbing of context.Context where it was
possible to do so without broad changes to files that would not otherwise
have been included in this commit, but there are a few leftover spots that
are context.TODO() which we'll address separately in later commits.

This removes the temporary workaround from d079da6e9e, since we are now
able to plumb the OpenTelemetry span tree all the way to the service
discovery requests.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-06-12 09:37:59 -07:00

146 lines
5.0 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package regsrc
import (
"regexp"
"strings"
"github.com/opentofu/svchost"
)
var (
// InvalidHostString is a placeholder returned when a raw host can't be
// converted by IDNA spec. It will never be returned for any host for which
// Valid() is true.
InvalidHostString = "<invalid host>"
// urlLabelEndSubRe is a sub-expression that matches any character that's
// allowed at the start or end of a URL label according to RFC1123.
urlLabelEndSubRe = "[0-9A-Za-z]"
// urlLabelEndSubRe is a sub-expression that matches any character that's
// allowed at in a non-start or end of a URL label according to RFC1123.
urlLabelMidSubRe = "[0-9A-Za-z-]"
// urlLabelUnicodeSubRe is a sub-expression that matches any non-ascii char
// in an IDN (Unicode) display URL. It's not strict - there are only ~15k
// valid Unicode points in IDN RFC (some with conditions). We are just going
// with being liberal with matching and then erroring if we fail to convert
// to punycode later (which validates chars fully). This at least ensures
// ascii chars disallowed by the RC1123 parts above don't become legal
// again.
urlLabelUnicodeSubRe = "[^[:ascii:]]"
// hostLabelSubRe is the sub-expression that matches a valid hostname label.
// It does not anchor the start or end so it can be composed into more
// complex RegExps below. Note that for sanity we don't handle disallowing
// raw punycode in this regexp (esp. since re2 doesn't support negative
// lookbehind, but we can capture it's presence here to check later).
hostLabelSubRe = "" +
// Match valid initial char, or unicode char
"(?:" + urlLabelEndSubRe + "|" + urlLabelUnicodeSubRe + ")" +
// Optionally, match 0 to 61 valid URL or Unicode chars,
// followed by one valid end char or unicode char
"(?:" +
"(?:" + urlLabelMidSubRe + "|" + urlLabelUnicodeSubRe + "){0,61}" +
"(?:" + urlLabelEndSubRe + "|" + urlLabelUnicodeSubRe + ")" +
")?"
// hostSubRe is the sub-expression that matches a valid host prefix.
// Allows custom port.
hostSubRe = hostLabelSubRe + "(?:\\." + hostLabelSubRe + ")+(?::\\d+)?"
// hostRe is a regexp that matches a valid host prefix. Additional
// validation of unicode strings is needed for matches.
hostRe = regexp.MustCompile("^" + hostSubRe + "$")
)
// FriendlyHost describes a registry instance identified in source strings by a
// simple bare hostname like registry.opentofu.org.
type FriendlyHost struct {
Raw string
}
func NewFriendlyHost(host string) *FriendlyHost {
return &FriendlyHost{Raw: host}
}
// ParseFriendlyHost attempts to parse a valid "friendly host" prefix from the
// given string. If no valid prefix is found, host will be nil and rest will
// contain the full source string. The host prefix must terminate at the end of
// the input or at the first / character. If one or more characters exist after
// the first /, they will be returned as rest (without the / delimiter).
// Hostnames containing punycode WILL be parsed successfully since they may have
// come from an internal normalized source string, however should be considered
// invalid if the string came from a user directly. This must be checked
// explicitly for user-input strings by calling Valid() on the
// returned host.
func ParseFriendlyHost(source string) (host *FriendlyHost, rest string) {
parts := strings.SplitN(source, "/", 2)
if hostRe.MatchString(parts[0]) {
host = &FriendlyHost{Raw: parts[0]}
if len(parts) == 2 {
rest = parts[1]
}
return
}
// No match, return whole string as rest along with nil host
rest = source
return
}
// Valid returns whether the host prefix is considered valid in any case.
// Example of invalid prefixes might include ones that don't conform to the host
// name specifications. Not that IDN prefixes containing punycode are not valid
// input which we expect to always be in user-input or normalised display form.
func (h *FriendlyHost) Valid() bool {
return svchost.IsValid(h.Raw)
}
// Display returns the host formatted for display to the user in CLI or web
// output.
func (h *FriendlyHost) Display() string {
return svchost.ForDisplay(h.Raw)
}
// Normalized returns the host formatted for internal reference or comparison.
func (h *FriendlyHost) Normalized() string {
host, err := svchost.ForComparison(h.Raw)
if err != nil {
return InvalidHostString
}
return string(host)
}
// String returns the host formatted as the user originally typed it assuming it
// was parsed from user input.
func (h *FriendlyHost) String() string {
return h.Raw
}
// Equal compares the FriendlyHost against another instance taking normalization
// into account. Invalid hosts cannot be compared and will always return false.
func (h *FriendlyHost) Equal(other *FriendlyHost) bool {
if other == nil {
return false
}
otherHost, err := svchost.ForComparison(other.Raw)
if err != nil {
return false
}
host, err := svchost.ForComparison(h.Raw)
if err != nil {
return false
}
return otherHost == host
}