diff --git a/challenge/dnspersist01/dns_persist_challenge.go b/challenge/dnspersist01/dns_persist_challenge.go index 52a9b3e29..71871c39c 100644 --- a/challenge/dnspersist01/dns_persist_challenge.go +++ b/challenge/dnspersist01/dns_persist_challenge.go @@ -59,6 +59,7 @@ type Challenge struct { userSuppliedIssuerDomainName string persistUntil *time.Time recursiveNameservers []string + authoritativeNSPort string propagationTimeout time.Duration propagationInterval time.Duration @@ -72,6 +73,7 @@ func NewChallenge(core *api.Core, validate ValidateFunc, opts ...ChallengeOption resolver: NewResolver(nil), preCheck: newPreCheck(), recursiveNameservers: DefaultNameservers(), + authoritativeNSPort: defaultAuthoritativeNSPort, propagationTimeout: DefaultPropagationTimeout, propagationInterval: DefaultPollingInterval, @@ -97,7 +99,9 @@ func WithResolver(resolver *Resolver) ChallengeOption { if resolver == nil { return errors.New("dnspersist01: resolver is nil") } + chlg.resolver = resolver + return nil } } @@ -116,7 +120,9 @@ func WithDNSTimeout(timeout time.Duration) ChallengeOption { if chlg.resolver == nil { chlg.resolver = NewResolver(nil) } + chlg.resolver.Timeout = timeout + return nil } } @@ -129,7 +135,9 @@ func WithAccountURI(accountURI string) ChallengeOption { if accountURI == "" { return errors.New("dnspersist01: ACME account URI cannot be empty") } + chlg.accountURI = accountURI + return nil } } @@ -151,6 +159,7 @@ func WithIssuerDomainName(issuerDomainName string) ChallengeOption { } chlg.userSuppliedIssuerDomainName = normalized + return nil } } @@ -165,6 +174,7 @@ func WithPersistUntil(persistUntil time.Time) ChallengeOption { ts := persistUntil.UTC().Truncate(time.Second) chlg.persistUntil = &ts + return nil } } @@ -175,7 +185,9 @@ func WithPropagationTimeout(timeout time.Duration) ChallengeOption { if timeout <= 0 { return errors.New("dnspersist01: propagation timeout must be positive") } + chlg.propagationTimeout = timeout + return nil } } @@ -186,7 +198,9 @@ func WithPropagationInterval(interval time.Duration) ChallengeOption { if interval <= 0 { return errors.New("dnspersist01: propagation interval must be positive") } + chlg.propagationInterval = interval + return nil } } @@ -194,6 +208,8 @@ func WithPropagationInterval(interval time.Duration) ChallengeOption { // Solve validates the dns-persist-01 challenge by prompting the user to create // the required TXT record (if necessary) then performing propagation checks (or // a wait-only delay) before notifying the ACME server. +// +//nolint:gocyclo // challenge flow has several required branches (reuse/manual/wait/propagation/validate). func (c *Challenge) Solve(ctx context.Context, authz acme.Authorization) error { if c.resolver == nil { return errors.New("dnspersist01: resolver is nil") @@ -215,6 +231,7 @@ func (c *Challenge) Solve(ctx context.Context, authz acme.Authorization) error { } fqdn := GetAuthorizationDomainName(domain) + result, err := c.resolver.LookupTXT(fqdn) if err != nil { return err @@ -230,15 +247,16 @@ func (c *Challenge) Solve(ctx context.Context, authz acme.Authorization) error { } if !matcher(result.Records) { - info, err := GetChallengeInfo(domain, issuerDomainName, c.accountURI, authz.Wildcard, c.persistUntil) - if err != nil { - return err + info, infoErr := GetChallengeInfo(domain, issuerDomainName, c.accountURI, authz.Wildcard, c.persistUntil) + if infoErr != nil { + return infoErr } displayRecordCreationInstructions(info.FQDN, info.Value) - err = waitForUser() - if err != nil { - return err + + waitErr := waitForUser() + if waitErr != nil { + return waitErr } } else { fmt.Printf("dnspersist01: Found existing matching TXT record for %s, no need to create a new one\n", fqdn) @@ -250,15 +268,16 @@ func (c *Challenge) Solve(ctx context.Context, authz acme.Authorization) error { log.Info("acme: Checking DNS-PERSIST-01 record propagation.", log.DomainAttr(domain), slog.String("nameservers", strings.Join(c.getRecursiveNameservers(), ",")), ) + time.Sleep(interval) err = wait.For("propagation", timeout, interval, func() (bool, error) { - ok, err := c.preCheck.call(domain, fqdn, matcher, c.checkDNSPropagation) - if !ok || err != nil { + ok, callErr := c.preCheck.call(domain, fqdn, matcher, c.checkDNSPropagation) + if !ok || callErr != nil { log.Info("acme: Waiting for DNS-PERSIST-01 record propagation.", log.DomainAttr(domain)) } - return ok, err + return ok, callErr }) if err != nil { return err @@ -289,6 +308,7 @@ func GetChallengeInfo(domain, issuerDomainName, accountURI string, wildcard bool if domain == "" { return ChallengeInfo{}, errors.New("dnspersist01: domain cannot be empty") } + if accountURI == "" { return ChallengeInfo{}, errors.New("dnspersist01: ACME account URI cannot be empty") } @@ -362,6 +382,7 @@ func (c *Challenge) selectIssuerDomainName(challIssuers []string, records []TXTR return c.userSuppliedIssuerDomainName, nil } + for _, issuerDomainName := range sortedIssuers { if c.hasMatchingRecord(records, issuerDomainName, wildcard) { return issuerDomainName, nil @@ -377,15 +398,19 @@ func (c *Challenge) hasMatchingRecord(records []TXTRecord, issuerDomainName stri if err != nil { continue } + if parsed.IssuerDomainName != issuerDomainName { continue } + if parsed.AccountURI != c.accountURI { continue } - if wildcard && strings.ToLower(parsed.Policy) != policyWildcard { + + if wildcard && !strings.EqualFold(parsed.Policy, policyWildcard) { continue } + if c.persistUntil == nil { if parsed.PersistUntil != nil { continue @@ -395,6 +420,7 @@ func (c *Challenge) hasMatchingRecord(records []TXTRecord, issuerDomainName stri continue } } + return true } diff --git a/challenge/dnspersist01/dns_persist_challenge_manual.go b/challenge/dnspersist01/dns_persist_challenge_manual.go index 74469c9dd..00591b9d8 100644 --- a/challenge/dnspersist01/dns_persist_challenge_manual.go +++ b/challenge/dnspersist01/dns_persist_challenge_manual.go @@ -43,7 +43,8 @@ func splitTXTValue(value string) []string { chunks = append(chunks, value[:maxTXTStringOctets]) value = value[maxTXTStringOctets:] } - if len(value) > 0 { + + if value != "" { chunks = append(chunks, value) } diff --git a/challenge/dnspersist01/dns_persist_challenge_test.go b/challenge/dnspersist01/dns_persist_challenge_test.go index 12f0d2395..ed53a4b51 100644 --- a/challenge/dnspersist01/dns_persist_challenge_test.go +++ b/challenge/dnspersist01/dns_persist_challenge_test.go @@ -139,6 +139,7 @@ func TestGetChallengeInfo(t *testing.T) { if test.expectErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), test.expectErr) + return } @@ -229,6 +230,7 @@ func TestWithIssuerDomainName(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { chlg := &Challenge{} + err := WithIssuerDomainName(test.input)(chlg) if test.expectErr { require.Error(t, err) diff --git a/challenge/dnspersist01/issue_values.go b/challenge/dnspersist01/issue_values.go index 05042e1fa..dd9ff0173 100644 --- a/challenge/dnspersist01/issue_values.go +++ b/challenge/dnspersist01/issue_values.go @@ -1,6 +1,7 @@ package dnspersist01 import ( + "errors" "fmt" "strconv" "strings" @@ -51,12 +52,14 @@ func trimWSP(s string) string { // ParseIssueValues parses an issue-value string. Unknown parameters are // preserved in Params. +// +//nolint:gocyclo // parsing and validating tagged parameters requires branching per field. func ParseIssueValues(value string) (IssueValue, error) { fields := strings.Split(value, ";") issuerDomainName := trimWSP(fields[0]) if issuerDomainName == "" { - return IssueValue{}, fmt.Errorf("missing issuer-domain-name") + return IssueValue{}, errors.New("missing issuer-domain-name") } parsed := IssueValue{ @@ -69,7 +72,7 @@ func ParseIssueValues(value string) (IssueValue, error) { for _, raw := range fields[1:] { part := trimWSP(raw) if part == "" { - return IssueValue{}, fmt.Errorf("empty parameter or trailing semicolon provided") + return IssueValue{}, errors.New("empty parameter or trailing semicolon provided") } tagValue := strings.SplitN(part, "=", 2) @@ -79,6 +82,7 @@ func ParseIssueValues(value string) (IssueValue, error) { tag := trimWSP(tagValue[0]) val := trimWSP(tagValue[1]) + if tag == "" { return IssueValue{}, fmt.Errorf("malformed parameter %q, empty tag", part) } @@ -87,6 +91,7 @@ func ParseIssueValues(value string) (IssueValue, error) { if seenTags[key] { return IssueValue{}, fmt.Errorf("duplicate parameter %q", tag) } + seenTags[key] = true for _, r := range val { @@ -100,19 +105,22 @@ func ParseIssueValues(value string) (IssueValue, error) { switch key { case paramAccountURI: if val == "" { - return IssueValue{}, fmt.Errorf("empty value provided for mandatory accounturi") + return IssueValue{}, errors.New("empty value provided for mandatory accounturi") } + parsed.AccountURI = val case paramPolicy: - if val != "" && strings.ToLower(val) != policyWildcard { + if val != "" && !strings.EqualFold(val, policyWildcard) { val = "" } + parsed.Policy = val case paramPersistUntil: ts, err := strconv.ParseInt(val, 10, 64) if err != nil { return IssueValue{}, fmt.Errorf("malformed persistUntil timestamp %q", val) } + persistUntil := time.Unix(ts, 0).UTC() parsed.PersistUntil = &persistUntil default: diff --git a/challenge/dnspersist01/issue_values_test.go b/challenge/dnspersist01/issue_values_test.go index 8e2f14ab6..e59df35e3 100644 --- a/challenge/dnspersist01/issue_values_test.go +++ b/challenge/dnspersist01/issue_values_test.go @@ -190,10 +190,12 @@ func TestParseIssueValues(t *testing.T) { if test.expectErrContains != "" { require.Error(t, err) assert.Contains(t, err.Error(), test.expectErrContains) + return } require.NoError(t, err) + expected := test.expected expected.PersistUntil = test.expectedPersistUTC assert.Equal(t, expected, parsed) diff --git a/challenge/dnspersist01/issuer_domain_name.go b/challenge/dnspersist01/issuer_domain_name.go index bc80f5ca1..a8aa7a9c1 100644 --- a/challenge/dnspersist01/issuer_domain_name.go +++ b/challenge/dnspersist01/issuer_domain_name.go @@ -1,12 +1,14 @@ package dnspersist01 import ( + "errors" "fmt" "strings" "golang.org/x/net/idna" ) +//nolint:gochecknoglobals // test seam for injecting IDNA conversion failures/variants. var issuerDomainNameToASCII = idna.Lookup.ToASCII // validateIssuerDomainName validates a single issuer-domain-name according to @@ -19,26 +21,31 @@ var issuerDomainNameToASCII = idna.Lookup.ToASCII // - A-label (Punycode, RFC5890) func validateIssuerDomainName(name string) error { if name == "" { - return fmt.Errorf("issuer-domain-name cannot be empty") + return errors.New("issuer-domain-name cannot be empty") } + if strings.ToLower(name) != name { - return fmt.Errorf("issuer-domain-name must be lowercase") + return errors.New("issuer-domain-name must be lowercase") } + if strings.HasSuffix(name, ".") { - return fmt.Errorf("issuer-domain-name must not have a trailing dot") + return errors.New("issuer-domain-name must not have a trailing dot") } + if len(name) > 253 { - return fmt.Errorf("issuer-domain-name exceeds maximum length of 253 octets") + return errors.New("issuer-domain-name exceeds maximum length of 253 octets") } labels := strings.SplitSeq(name, ".") for label := range labels { if label == "" { - return fmt.Errorf("issuer-domain-name contains an empty label") + return errors.New("issuer-domain-name contains an empty label") } + if len(label) > 63 { - return fmt.Errorf("issuer-domain-name label exceeds maximum length of 63 octets") + return errors.New("issuer-domain-name label exceeds maximum length of 63 octets") } + if !isLDHLabel(label) { return fmt.Errorf("issuer-domain-name label %q must be a lowercase LDH label", label) } @@ -48,26 +55,32 @@ func validateIssuerDomainName(name string) error { if err != nil { return fmt.Errorf("issuer-domain-name must be represented in A-label format: %w", err) } + if ascii != name { - return fmt.Errorf("issuer-domain-name must be represented in A-label format") + return errors.New("issuer-domain-name must be represented in A-label format") } + return nil } func isLDHLabel(label string) bool { - if len(label) == 0 { + if label == "" { return false } + if !isLowerAlphaNum(label[0]) || !isLowerAlphaNum(label[len(label)-1]) { return false } - for i := 0; i < len(label); i++ { + + for i := range len(label) { c := label[i] if isLowerAlphaNum(c) || c == '-' { continue } + return false } + return true } @@ -86,5 +99,6 @@ func normalizeUserSuppliedIssuerDomainName(name string) (string, error) { if err != nil { return "", fmt.Errorf("normalizing supplied issuer-domain-name %q: %w", n, err) } + return ascii, nil } diff --git a/challenge/dnspersist01/issuer_domain_name_test.go b/challenge/dnspersist01/issuer_domain_name_test.go index 1bd0e7220..69c46c820 100644 --- a/challenge/dnspersist01/issuer_domain_name_test.go +++ b/challenge/dnspersist01/issuer_domain_name_test.go @@ -48,6 +48,7 @@ func TestValidateIssuerDomainName_Errors(t *testing.T) { func TestValidateIssuerDomainName_ErrorNonCanonicalALabel(t *testing.T) { originalToASCII := issuerDomainNameToASCII + t.Cleanup(func() { issuerDomainNameToASCII = originalToASCII }) @@ -63,6 +64,7 @@ func TestValidateIssuerDomainName_ErrorNonCanonicalALabel(t *testing.T) { func TestValidateIssuerDomainName_Valid(t *testing.T) { originalToASCII := issuerDomainNameToASCII + t.Cleanup(func() { issuerDomainNameToASCII = originalToASCII }) @@ -76,12 +78,14 @@ func TestValidateIssuerDomainName_Valid(t *testing.T) { } func TestValidateIssuerDomainName_ErrorWrap(t *testing.T) { + sentinelErr := errors.New("sentinel idna failure") + originalToASCII := issuerDomainNameToASCII + t.Cleanup(func() { issuerDomainNameToASCII = originalToASCII }) - sentinelErr := errors.New("sentinel idna failure") issuerDomainNameToASCII = func(string) (string, error) { return "", sentinelErr } diff --git a/challenge/dnspersist01/mock_test.go b/challenge/dnspersist01/mock_test.go index 3a13bb39a..96809ae7a 100644 --- a/challenge/dnspersist01/mock_test.go +++ b/challenge/dnspersist01/mock_test.go @@ -33,19 +33,14 @@ func fakeTXT(name, value string, ttl uint32) *dns.TXT { // mockResolver modifies the default DNS resolver to use a custom network address during the test execution. // IMPORTANT: it modifies global variables. -func mockResolver(t *testing.T, addr net.Addr) { +func mockResolver(t *testing.T, addr net.Addr) string { t.Helper() _, port, err := net.SplitHostPort(addr.String()) require.NoError(t, err) - originalDefaultNameserverPort := defaultNameserverPort - t.Cleanup(func() { - defaultNameserverPort = originalDefaultNameserverPort - }) - defaultNameserverPort = port - originalResolver := net.DefaultResolver + t.Cleanup(func() { net.DefaultResolver = originalResolver }) @@ -58,4 +53,6 @@ func mockResolver(t *testing.T, addr net.Addr) { return d.DialContext(ctx, network, addr.String()) }, } + + return port } diff --git a/challenge/dnspersist01/precheck.go b/challenge/dnspersist01/precheck.go index ff7205dfc..8a25ab68b 100644 --- a/challenge/dnspersist01/precheck.go +++ b/challenge/dnspersist01/precheck.go @@ -9,8 +9,7 @@ import ( "github.com/miekg/dns" ) -// defaultNameserverPort used by authoritative NS. -var defaultNameserverPort = "53" +const defaultAuthoritativeNSPort = "53" // RecordMatcher returns true when the expected record is present. type RecordMatcher func(records []TXTRecord) bool @@ -133,7 +132,7 @@ func (c *Challenge) checkDNSPropagation(fqdn string, matcher RecordMatcher) (boo func (c *Challenge) checkNameserversPropagation(fqdn string, nameservers []string, addPort, recursive bool, matcher RecordMatcher) (bool, error) { for _, ns := range nameservers { if addPort { - ns = net.JoinHostPort(ns, defaultNameserverPort) + ns = net.JoinHostPort(ns, c.getAuthoritativeNSPort()) } result, err := c.resolver.lookupTXT(fqdn, []string{ns}, recursive) @@ -149,6 +148,14 @@ func (c *Challenge) checkNameserversPropagation(fqdn string, nameservers []strin return true, nil } +func (c *Challenge) getAuthoritativeNSPort() string { + if c == nil || c.authoritativeNSPort == "" { + return defaultAuthoritativeNSPort + } + + return c.authoritativeNSPort +} + func txtValues(records []TXTRecord) []string { values := make([]string, 0, len(records)) for _, record := range records { diff --git a/challenge/dnspersist01/precheck_test.go b/challenge/dnspersist01/precheck_test.go index 13d664057..86f294c78 100644 --- a/challenge/dnspersist01/precheck_test.go +++ b/challenge/dnspersist01/precheck_test.go @@ -44,13 +44,14 @@ func Test_preCheck_checkDNSPropagation(t *testing.T) { ). Build(t) - mockResolver(t, addr) + port := mockResolver(t, addr) resolver := NewResolver([]string{addr.String()}) chlg := &Challenge{ resolver: resolver, preCheck: newPreCheck(), recursiveNameservers: ParseNameservers([]string{addr.String()}), + authoritativeNSPort: port, } testCases := []struct { diff --git a/challenge/dnspersist01/resolver.go b/challenge/dnspersist01/resolver.go index 5f09ccd9a..52e6f7ce6 100644 --- a/challenge/dnspersist01/resolver.go +++ b/challenge/dnspersist01/resolver.go @@ -5,7 +5,6 @@ import ( "fmt" "net" "os" - "slices" "strconv" "strings" "time" @@ -15,11 +14,6 @@ import ( const defaultResolvConf = "/etc/resolv.conf" -var defaultNameservers = []string{ - "google-public-dns-a.google.com:53", - "google-public-dns-b.google.com:53", -} - // Resolver performs DNS lookups using the configured nameservers and timeout. type Resolver struct { Nameservers []string @@ -55,12 +49,19 @@ func NewResolver(nameservers []string) *Resolver { func DefaultNameservers() []string { config, err := dns.ClientConfigFromFile(defaultResolvConf) if err != nil || len(config.Servers) == 0 { - return slices.Clone(defaultNameservers) + return defaultFallbackNameservers() } return ParseNameservers(config.Servers) } +func defaultFallbackNameservers() []string { + return []string{ + "google-public-dns-a.google.com:53", + "google-public-dns-b.google.com:53", + } +} + // ParseNameservers ensures all servers have a port number. func ParseNameservers(servers []string) []string { var resolvers []string @@ -109,6 +110,7 @@ func (r *Resolver) lookupTXT(fqdn string, nameservers []string, recursive bool) if _, ok := seen[name]; ok { return result, fmt.Errorf("CNAME loop detected for %s", name) } + seen[name] = struct{}{} msg, err := dnsQueryWithTimeout(name, dns.TypeTXT, nameservers, recursive, timeout) @@ -188,9 +190,11 @@ func dnsQueryWithTimeout(fqdn string, rtype uint16, nameservers []string, recurs return nil, &DNSError{Message: "empty list of nameservers"} } - var msg *dns.Msg - var err error - var errAll error + var ( + msg *dns.Msg + err error + errAll error + ) for _, ns := range nameservers { msg, err = sendDNSQuery(m, ns, timeout) @@ -267,6 +271,7 @@ func (d *DNSError) Error() string { for _, question := range questions { parts = append(parts, strings.ReplaceAll(strings.TrimPrefix(question.String(), ";"), "\t", " ")) } + return strings.Join(parts, ";") } diff --git a/challenge/resolver/solver_manager.go b/challenge/resolver/solver_manager.go index 78eb8ed2a..87b6271d1 100644 --- a/challenge/resolver/solver_manager.go +++ b/challenge/resolver/solver_manager.go @@ -30,9 +30,11 @@ func (a byType) Less(i, j int) bool { if a[i].Type == string(challenge.DNS01) && a[j].Type == string(challenge.DNSPersist01) { return true } + if a[i].Type == string(challenge.DNSPersist01) && a[j].Type == string(challenge.DNS01) { return false } + return a[i].Type > a[j].Type } diff --git a/cmd/setup_challenges.go b/cmd/setup_challenges.go index d837fc768..12fb4450e 100644 --- a/cmd/setup_challenges.go +++ b/cmd/setup_challenges.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "log/slog" "net" @@ -22,6 +23,7 @@ import ( "github.com/urfave/cli/v3" ) +//nolint:gocyclo // challenge setup dispatch is expected to branch by enabled challenge type. func setupChallenges(cmd *cli.Command, client *lego.Client, account registration.User) { if !cmd.Bool(flgHTTP) && !cmd.Bool(flgTLS) && !cmd.IsSet(flgDNS) && !cmd.Bool(flgDNSPersist) { log.Fatal(fmt.Sprintf("No challenge selected. You must specify at least one challenge: `--%s`, `--%s`, `--%s`, `--%s`.", flgHTTP, flgTLS, flgDNS, flgDNSPersist)) @@ -205,9 +207,10 @@ func setupDNS(cmd *cli.Command, client *lego.Client) error { return err } +//nolint:gocyclo // option assembly mirrors CLI flags and challenge configuration branches. func setupDNSPersist(cmd *cli.Command, client *lego.Client, account registration.User) error { if account == nil || account.GetRegistration() == nil || account.GetRegistration().URI == "" { - return fmt.Errorf("dns-persist-01 requires a registered account with an account URI") + return errors.New("dns-persist-01 requires a registered account with an account URI") } err := validateDNSPersistPropagationExclusiveOptions(cmd) @@ -233,8 +236,10 @@ func setupDNSPersist(cmd *cli.Command, client *lego.Client, account registration if cmd.IsSet(flgDNSPersistResolvers) { resolvers := cmd.StringSlice(flgDNSPersistResolvers) if len(resolvers) > 0 { - opts = append(opts, dnspersist01.WithNameservers(resolvers)) - opts = append(opts, dnspersist01.AddRecursiveNameservers(resolvers)) + opts = append(opts, + dnspersist01.WithNameservers(resolvers), + dnspersist01.AddRecursiveNameservers(resolvers), + ) } } diff --git a/e2e/dnschallenge/dns_persist_challenges_test.go b/e2e/dnschallenge/dns_persist_challenges_test.go index 361a83f7b..65329c865 100644 --- a/e2e/dnschallenge/dns_persist_challenges_test.go +++ b/e2e/dnschallenge/dns_persist_challenges_test.go @@ -57,6 +57,7 @@ func setTXTRecordRaw(host, value string) error { if err != nil { return err } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -76,6 +77,7 @@ func clearTXTRecord(t *testing.T, host string) { resp, err := http.Post("http://localhost:8055/clear-txt", "application/json", bytes.NewReader(body)) require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() require.Equal(t, http.StatusOK, resp.StatusCode) @@ -157,10 +159,12 @@ func waitForCLIAccountURI(ctx context.Context, email string) (string, error) { if os.IsNotExist(err) { continue } + return "", err } var account accountFile + err = json.Unmarshal(content, &account) if err != nil { continue @@ -234,6 +238,7 @@ func TestChallengeDNSPersist_Run(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "../fixtures/certs/pebble.minica.pem") require.NoError(t, err) + defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() accountURI := createCLIAccountState(t, testPersistCLIEmail) @@ -266,28 +271,32 @@ func TestChallengeDNSPersist_Run_NewAccount(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "../fixtures/certs/pebble.minica.pem") require.NoError(t, err) + defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() txtHost := fmt.Sprintf("_validation-persist.%s", testPersistCLIDomain) defer clearTXTRecord(t, txtHost) stdinReader, stdinWriter := io.Pipe() + defer func() { _ = stdinReader.Close() }() errChan := make(chan error, 1) + go func() { defer func() { _ = stdinWriter.Close() }() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - accountURI, err := waitForCLIAccountURI(ctx, testPersistCLIFreshEmail) - if err != nil { - errChan <- fmt.Errorf("wait for account URI: %w", err) + accountURI, waitErr := waitForCLIAccountURI(ctx, testPersistCLIFreshEmail) + if waitErr != nil { + errChan <- fmt.Errorf("wait for account URI: %w", waitErr) return } txtValue := dnspersist01.BuildIssueValues(testPersistIssuer, accountURI, true, nil) + err = setTXTRecordRaw(txtHost, txtValue) if err != nil { errChan <- fmt.Errorf("set TXT record: %w", err) @@ -299,6 +308,7 @@ func TestChallengeDNSPersist_Run_NewAccount(t *testing.T) { errChan <- fmt.Errorf("send enter to lego: %w", err) return } + errChan <- nil }() @@ -325,6 +335,7 @@ func TestChallengeDNSPersist_Renew(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "../fixtures/certs/pebble.minica.pem") require.NoError(t, err) + defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() accountURI := createCLIAccountState(t, testPersistCLIRenewEmail)