mirror of
https://github.com/go-acme/lego
synced 2026-03-14 14:35:48 +01:00
refactor: extract challenge options
This commit is contained in:
parent
6891aa83b7
commit
8412635bd8
4 changed files with 260 additions and 246 deletions
|
|
@ -31,9 +31,6 @@ const (
|
|||
// ValidateFunc validates a challenge with the ACME server.
|
||||
type ValidateFunc func(ctx context.Context, core *api.Core, domain string, chlng acme.Challenge) error
|
||||
|
||||
// ChallengeOption configures the dns-persist-01 challenge.
|
||||
type ChallengeOption func(*Challenge) error
|
||||
|
||||
// ChallengeInfo contains the information used to create a dns-persist-01 TXT
|
||||
// record.
|
||||
type ChallengeInfo struct {
|
||||
|
|
@ -94,143 +91,6 @@ func NewChallenge(core *api.Core, validate ValidateFunc, opts ...ChallengeOption
|
|||
return chlg, nil
|
||||
}
|
||||
|
||||
// CondOptions Conditional challenge options.
|
||||
func CondOptions(condition bool, opt ...ChallengeOption) ChallengeOption {
|
||||
if !condition {
|
||||
// NoOp options
|
||||
return func(*Challenge) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return func(chlg *Challenge) error {
|
||||
for _, opt := range opt {
|
||||
err := opt(chlg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithResolver overrides the resolver used for DNS lookups.
|
||||
func WithResolver(resolver *Resolver) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if resolver == nil {
|
||||
return errors.New("resolver is nil")
|
||||
}
|
||||
|
||||
chlg.resolver = resolver
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNameservers overrides resolver nameservers using the default timeout.
|
||||
func WithNameservers(nameservers []string) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
chlg.resolver = NewResolver(nameservers)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDNSTimeout overrides the default DNS resolver timeout.
|
||||
func WithDNSTimeout(timeout time.Duration) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if chlg.resolver == nil {
|
||||
chlg.resolver = NewResolver(nil)
|
||||
}
|
||||
|
||||
chlg.resolver.Timeout = timeout
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAccountURI sets the ACME account URI bound to dns-persist-01 records. It
|
||||
// is required both to construct the `accounturi=` parameter and to match
|
||||
// already-provisioned TXT records that should be updated.
|
||||
func WithAccountURI(accountURI string) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if accountURI == "" {
|
||||
return errors.New("ACME account URI cannot be empty")
|
||||
}
|
||||
|
||||
chlg.accountURI = accountURI
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithIssuerDomainName forces the issuer-domain-name used for dns-persist-01.
|
||||
// When set, it overrides automatic issuer selection and must match one of the
|
||||
// issuer-domain-names offered in the ACME challenge. User input is normalized
|
||||
// and validated at configuration time.
|
||||
func WithIssuerDomainName(issuerDomainName string) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if issuerDomainName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
normalized, err := normalizeUserSuppliedIssuerDomainName(issuerDomainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = validateIssuerDomainName(normalized)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chlg.userSuppliedIssuerDomainName = normalized
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPersistUntil sets the optional persistUntil value used when constructing
|
||||
// dns-persist-01 TXT records.
|
||||
func WithPersistUntil(persistUntil time.Time) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if persistUntil.IsZero() {
|
||||
return errors.New("persistUntil cannot be zero")
|
||||
}
|
||||
|
||||
chlg.persistUntil = persistUntil.UTC().Truncate(time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPropagationTimeout overrides the propagation timeout duration.
|
||||
func WithPropagationTimeout(timeout time.Duration) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if timeout <= 0 {
|
||||
return errors.New("propagation timeout must be positive")
|
||||
}
|
||||
|
||||
chlg.propagationTimeout = timeout
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPropagationInterval overrides the propagation polling interval.
|
||||
func WithPropagationInterval(interval time.Duration) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if interval <= 0 {
|
||||
return errors.New("propagation interval must be positive")
|
||||
}
|
||||
|
||||
chlg.propagationInterval = interval
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
@ -320,69 +180,6 @@ func (c *Challenge) getRecursiveNameservers() []string {
|
|||
return slices.Clone(c.recursiveNameservers)
|
||||
}
|
||||
|
||||
// GetAuthorizationDomainName returns the fully-qualified DNS label used by the
|
||||
// dns-persist-01 challenge for the given domain.
|
||||
func GetAuthorizationDomainName(domain string) string {
|
||||
return dns.Fqdn(validationLabel + "." + domain)
|
||||
}
|
||||
|
||||
// GetChallengeInfo returns information used to create a DNS TXT record which
|
||||
// can fulfill the `dns-persist-01` challenge. Domain, issuerDomainName, and
|
||||
// accountURI parameters are required. Wildcard and persistUntil parameters are
|
||||
// optional.
|
||||
func GetChallengeInfo(domain, issuerDomainName, accountURI string, wildcard bool, persistUntil time.Time) (ChallengeInfo, error) {
|
||||
if domain == "" {
|
||||
return ChallengeInfo{}, errors.New("dnspersist01: domain cannot be empty")
|
||||
}
|
||||
|
||||
value, err := BuildIssueValue(issuerDomainName, accountURI, wildcard, persistUntil)
|
||||
if err != nil {
|
||||
return ChallengeInfo{}, err
|
||||
}
|
||||
|
||||
return ChallengeInfo{
|
||||
FQDN: GetAuthorizationDomainName(domain),
|
||||
Value: value,
|
||||
IssuerDomainName: issuerDomainName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validateIssuerDomainNames validates the ACME challenge "issuer-domain-names"
|
||||
// array for dns-persist-01.
|
||||
//
|
||||
// Rules enforced:
|
||||
// - The array is required and must contain at least 1 entry.
|
||||
// - The array must not contain more than 10 entries; larger arrays are
|
||||
// treated as malformed challenges and rejected.
|
||||
//
|
||||
// Each issuer-domain-name must be a normalized domain name:
|
||||
// - represented in A-label (Punycode, RFC5890) form
|
||||
// - all lowercase
|
||||
// - no trailing dot
|
||||
// - maximum total length of 253 octets
|
||||
//
|
||||
// The returned list is intended for issuer selection when constructing or
|
||||
// matching dns-persist-01 TXT records. The challenge can be satisfied by using
|
||||
// any one valid issuer-domain-name from this list.
|
||||
func validateIssuerDomainNames(chlng acme.Challenge) error {
|
||||
if len(chlng.IssuerDomainNames) == 0 {
|
||||
return errors.New("issuer-domain-names missing from the challenge")
|
||||
}
|
||||
|
||||
if len(chlng.IssuerDomainNames) > 10 {
|
||||
return errors.New(" issuer-domain-names exceeds maximum length of 10")
|
||||
}
|
||||
|
||||
for _, issuerDomainName := range chlng.IssuerDomainNames {
|
||||
err := validateIssuerDomainName(issuerDomainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectIssuerDomainName selects the issuer-domain-name to use for a
|
||||
// dns-persist-01 challenge. If the user has supplied an issuer-domain-name, it
|
||||
// is used after verifying that it is offered by the ACME challenge. Otherwise,
|
||||
|
|
@ -446,3 +243,66 @@ func (c *Challenge) hasMatchingRecord(records []TXTRecord, issuerDomainName stri
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetAuthorizationDomainName returns the fully-qualified DNS label used by the
|
||||
// dns-persist-01 challenge for the given domain.
|
||||
func GetAuthorizationDomainName(domain string) string {
|
||||
return dns.Fqdn(validationLabel + "." + domain)
|
||||
}
|
||||
|
||||
// GetChallengeInfo returns information used to create a DNS TXT record which
|
||||
// can fulfill the `dns-persist-01` challenge. Domain, issuerDomainName, and
|
||||
// accountURI parameters are required. Wildcard and persistUntil parameters are
|
||||
// optional.
|
||||
func GetChallengeInfo(domain, issuerDomainName, accountURI string, wildcard bool, persistUntil time.Time) (ChallengeInfo, error) {
|
||||
if domain == "" {
|
||||
return ChallengeInfo{}, errors.New("dnspersist01: domain cannot be empty")
|
||||
}
|
||||
|
||||
value, err := BuildIssueValue(issuerDomainName, accountURI, wildcard, persistUntil)
|
||||
if err != nil {
|
||||
return ChallengeInfo{}, err
|
||||
}
|
||||
|
||||
return ChallengeInfo{
|
||||
FQDN: GetAuthorizationDomainName(domain),
|
||||
Value: value,
|
||||
IssuerDomainName: issuerDomainName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validateIssuerDomainNames validates the ACME challenge "issuer-domain-names"
|
||||
// array for dns-persist-01.
|
||||
//
|
||||
// Rules enforced:
|
||||
// - The array is required and must contain at least 1 entry.
|
||||
// - The array must not contain more than 10 entries; larger arrays are
|
||||
// treated as malformed challenges and rejected.
|
||||
//
|
||||
// Each issuer-domain-name must be a normalized domain name:
|
||||
// - represented in A-label (Punycode, RFC5890) form
|
||||
// - all lowercase
|
||||
// - no trailing dot
|
||||
// - maximum total length of 253 octets
|
||||
//
|
||||
// The returned list is intended for issuer selection when constructing or
|
||||
// matching dns-persist-01 TXT records. The challenge can be satisfied by using
|
||||
// any one valid issuer-domain-name from this list.
|
||||
func validateIssuerDomainNames(chlng acme.Challenge) error {
|
||||
if len(chlng.IssuerDomainNames) == 0 {
|
||||
return errors.New("issuer-domain-names missing from the challenge")
|
||||
}
|
||||
|
||||
if len(chlng.IssuerDomainNames) > 10 {
|
||||
return errors.New(" issuer-domain-names exceeds maximum length of 10")
|
||||
}
|
||||
|
||||
for _, issuerDomainName := range chlng.IssuerDomainNames {
|
||||
err := validateIssuerDomainName(issuerDomainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
146
challenge/dnspersist01/dns_persist_challenge_options.go
Normal file
146
challenge/dnspersist01/dns_persist_challenge_options.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
package dnspersist01
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ChallengeOption configures the dns-persist-01 challenge.
|
||||
type ChallengeOption func(*Challenge) error
|
||||
|
||||
// CondOptions Conditional challenge options.
|
||||
func CondOptions(condition bool, opt ...ChallengeOption) ChallengeOption {
|
||||
if !condition {
|
||||
// NoOp options
|
||||
return func(*Challenge) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return func(chlg *Challenge) error {
|
||||
for _, opt := range opt {
|
||||
err := opt(chlg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithResolver overrides the resolver used for DNS lookups.
|
||||
func WithResolver(resolver *Resolver) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if resolver == nil {
|
||||
return errors.New("resolver is nil")
|
||||
}
|
||||
|
||||
chlg.resolver = resolver
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNameservers overrides resolver nameservers using the default timeout.
|
||||
func WithNameservers(nameservers []string) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
chlg.resolver = NewResolver(nameservers)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDNSTimeout overrides the default DNS resolver timeout.
|
||||
func WithDNSTimeout(timeout time.Duration) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if chlg.resolver == nil {
|
||||
chlg.resolver = NewResolver(nil)
|
||||
}
|
||||
|
||||
chlg.resolver.Timeout = timeout
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAccountURI sets the ACME account URI bound to dns-persist-01 records. It
|
||||
// is required both to construct the `accounturi=` parameter and to match
|
||||
// already-provisioned TXT records that should be updated.
|
||||
func WithAccountURI(accountURI string) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if accountURI == "" {
|
||||
return errors.New("ACME account URI cannot be empty")
|
||||
}
|
||||
|
||||
chlg.accountURI = accountURI
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithIssuerDomainName forces the issuer-domain-name used for dns-persist-01.
|
||||
// When set, it overrides automatic issuer selection and must match one of the
|
||||
// issuer-domain-names offered in the ACME challenge. User input is normalized
|
||||
// and validated at configuration time.
|
||||
func WithIssuerDomainName(issuerDomainName string) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if issuerDomainName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
normalized, err := normalizeUserSuppliedIssuerDomainName(issuerDomainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = validateIssuerDomainName(normalized)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chlg.userSuppliedIssuerDomainName = normalized
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPersistUntil sets the optional persistUntil value used when constructing
|
||||
// dns-persist-01 TXT records.
|
||||
func WithPersistUntil(persistUntil time.Time) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if persistUntil.IsZero() {
|
||||
return errors.New("persistUntil cannot be zero")
|
||||
}
|
||||
|
||||
chlg.persistUntil = persistUntil.UTC().Truncate(time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPropagationTimeout overrides the propagation timeout duration.
|
||||
func WithPropagationTimeout(timeout time.Duration) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if timeout <= 0 {
|
||||
return errors.New("propagation timeout must be positive")
|
||||
}
|
||||
|
||||
chlg.propagationTimeout = timeout
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPropagationInterval overrides the propagation polling interval.
|
||||
func WithPropagationInterval(interval time.Duration) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
if interval <= 0 {
|
||||
return errors.New("propagation interval must be positive")
|
||||
}
|
||||
|
||||
chlg.propagationInterval = interval
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
50
challenge/dnspersist01/dns_persist_challenge_options_test.go
Normal file
50
challenge/dnspersist01/dns_persist_challenge_options_test.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package dnspersist01
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWithIssuerDomainName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
input string
|
||||
expected string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
desc: "normalizes uppercase and trailing dot",
|
||||
input: "CA.EXAMPLE.",
|
||||
expected: "ca.example",
|
||||
},
|
||||
{
|
||||
desc: "normalizes idna issuer",
|
||||
input: "BÜCHER.example",
|
||||
expected: "xn--bcher-kva.example",
|
||||
},
|
||||
{
|
||||
desc: "rejects invalid issuer",
|
||||
input: "ca_.example",
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
chlg := &Challenge{}
|
||||
|
||||
err := WithIssuerDomainName(test.input)(chlg)
|
||||
if test.expectErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, chlg.userSuppliedIssuerDomainName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -152,7 +152,7 @@ func TestGetChallengeInfo(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidateIssuerDomainNames(t *testing.T) {
|
||||
func Test_validateIssuerDomainNames(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
issuers []string
|
||||
|
|
@ -204,48 +204,6 @@ func TestValidateIssuerDomainNames(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWithIssuerDomainName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
input string
|
||||
expected string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
desc: "normalizes uppercase and trailing dot",
|
||||
input: "CA.EXAMPLE.",
|
||||
expected: "ca.example",
|
||||
},
|
||||
{
|
||||
desc: "normalizes idna issuer",
|
||||
input: "BÜCHER.example",
|
||||
expected: "xn--bcher-kva.example",
|
||||
},
|
||||
{
|
||||
desc: "rejects invalid issuer",
|
||||
input: "ca_.example",
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
chlg := &Challenge{}
|
||||
|
||||
err := WithIssuerDomainName(test.input)(chlg)
|
||||
if test.expectErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, chlg.userSuppliedIssuerDomainName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChallenge_selectIssuerDomainName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue