diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 603317a9..b4298323 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -11,7 +11,7 @@ jobs: name: Build and deploy documentation runs-on: ubuntu-latest env: - GO_VERSION: '1.20' + GO_VERSION: stable HUGO_VERSION: '0.117.0' CGO_ENABLED: 0 diff --git a/.github/workflows/go-cross.yml b/.github/workflows/go-cross.yml index 1839a0e7..fdc93031 100644 --- a/.github/workflows/go-cross.yml +++ b/.github/workflows/go-cross.yml @@ -16,37 +16,19 @@ jobs: strategy: matrix: - go-version: [ '1.19', '1.20', 1.x ] + go-version: [ stable, oldstable ] os: [ubuntu-latest, macos-latest, windows-latest] steps: - # https://github.com/marketplace/actions/setup-go-environment - - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@v3 - with: - go-version: ${{ matrix.go-version }} - # https://github.com/marketplace/actions/checkout - name: Checkout code uses: actions/checkout@v3 - # https://github.com/marketplace/actions/cache - - name: Cache Go modules - uses: actions/cache@v3 + # https://github.com/marketplace/actions/setup-go-environment + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@v4 with: - # In order: - # * Module download cache - # * Build cache (Linux) - # * Build cache (Mac) - # * Build cache (Windows) - path: | - ~/go/pkg/mod - ~/.cache/go-build - ~/Library/Caches/go-build - %LocalAppData%\go-build - key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.go-version }}-go- + go-version: ${{ matrix.go-version }} - name: Test run: go test -v -cover ./... diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5226f343..3ad804d0 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -12,8 +12,8 @@ jobs: name: Main Process runs-on: ubuntu-latest env: - GO_VERSION: '1.20' - GOLANGCI_LINT_VERSION: v1.53.1 + GO_VERSION: stable + GOLANGCI_LINT_VERSION: v1.54.1 HUGO_VERSION: '0.117.0' CGO_ENABLED: 0 LEGO_E2E_TESTS: CI @@ -21,26 +21,17 @@ jobs: steps: - # https://github.com/marketplace/actions/setup-go-environment - - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v3 - with: - go-version: ${{ env.GO_VERSION }} - # https://github.com/marketplace/actions/checkout - name: Check out code uses: actions/checkout@v3 with: fetch-depth: 0 - # https://github.com/marketplace/actions/cache - - name: Cache Go modules - uses: actions/cache@v3 + # https://github.com/marketplace/actions/setup-go-environment + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v4 with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + go-version: ${{ env.GO_VERSION }} - name: Check and get dependencies run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e0ec6c6..1b4f2b39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: name: Release version runs-on: ubuntu-latest env: - GO_VERSION: '1.20' + GO_VERSION: stable CGO_ENABLED: 0 steps: diff --git a/certificate/authorization.go b/certificate/authorization.go index 452db0d9..de9988ad 100644 --- a/certificate/authorization.go +++ b/certificate/authorization.go @@ -35,13 +35,14 @@ func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authoriz } var responses []acme.Authorization - failures := make(obtainError) + + failures := newObtainError() for i := 0; i < len(order.Authorizations); i++ { select { case res := <-resc: responses = append(responses, res) case err := <-errc: - failures[err.Domain] = err.Error + failures.Add(err.Domain, err.Error) } } @@ -52,12 +53,7 @@ func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authoriz close(resc) close(errc) - // be careful to not return an empty failures map; - // even if empty, they become non-nil error values - if len(failures) > 0 { - return responses, failures - } - return responses, nil + return responses, failures.Join() } func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder, force bool) { diff --git a/certificate/certificates.go b/certificate/certificates.go index b09e66bd..2d0fa8d2 100644 --- a/certificate/certificates.go +++ b/certificate/certificates.go @@ -149,11 +149,11 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) { log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - failures := make(obtainError) + failures := newObtainError() cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple, request.PreferredChain) if err != nil { for _, auth := range authz { - failures[challenge.GetTargetedDomain(auth)] = err + failures.Add(challenge.GetTargetedDomain(auth), err) } } @@ -161,12 +161,7 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) { c.deactivateAuthorizations(order, true) } - // Do not return an empty failures map, because - // it would still be a non-nil error value - if len(failures) > 0 { - return cert, failures - } - return cert, nil + return cert, failures.Join() } // ObtainForCSR tries to obtain a certificate matching the CSR passed into it. @@ -219,11 +214,11 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error) log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - failures := make(obtainError) + failures := newObtainError() cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, nil, request.PreferredChain) if err != nil { for _, auth := range authz { - failures[challenge.GetTargetedDomain(auth)] = err + failures.Add(challenge.GetTargetedDomain(auth), err) } } @@ -236,12 +231,7 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error) cert.CSR = certcrypto.PEMEncode(request.CSR) } - // Do not return an empty failures map, - // because it would still be a non-nil error value - if len(failures) > 0 { - return cert, failures - } - return cert, nil + return cert, failures.Join() } func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool, preferredChain string) (*Resource, error) { diff --git a/certificate/errors.go b/certificate/errors.go index e0f283ba..f305524d 100644 --- a/certificate/errors.go +++ b/certificate/errors.go @@ -1,27 +1,37 @@ package certificate import ( - "bytes" + "errors" "fmt" - "sort" ) -// obtainError is returned when there are specific errors available per domain. -type obtainError map[string]error +type obtainError struct { + data map[string]error +} -func (e obtainError) Error() string { - buffer := bytes.NewBufferString("error: one or more domains had a problem:\n") +func newObtainError() *obtainError { + return &obtainError{data: make(map[string]error)} +} - var domains []string - for domain := range e { - domains = append(domains, domain) +func (e *obtainError) Add(domain string, err error) { + e.data[domain] = err +} + +func (e *obtainError) Join() error { + if e == nil { + return nil } - sort.Strings(domains) - for _, domain := range domains { - _, _ = fmt.Fprintf(buffer, "[%s] %s\n", domain, e[domain]) + if len(e.data) == 0 { + return nil } - return buffer.String() + + var err error + for d, e := range e.data { + err = errors.Join(err, fmt.Errorf("%s: %w", d, e)) + } + + return fmt.Errorf("error: one or more domains had a problem:\n%w", err) } type domainError struct { diff --git a/certificate/errors_test.go b/certificate/errors_test.go new file mode 100644 index 00000000..b4866462 --- /dev/null +++ b/certificate/errors_test.go @@ -0,0 +1,70 @@ +package certificate + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type TomatoError struct{} + +func (t TomatoError) Error() string { + return "tomato" +} + +type CarrotError struct{} + +func (t CarrotError) Error() string { + return "carrot" +} + +func Test_obtainError_Join(t *testing.T) { + failures := newObtainError() + + failures.Add("example.com", &TomatoError{}) + + err := failures.Join() + + to := &TomatoError{} + assert.ErrorAs(t, err, &to) +} + +func Test_obtainError_Join_multiple_domains(t *testing.T) { + failures := newObtainError() + + failures.Add("example.com", &TomatoError{}) + failures.Add("example.org", &CarrotError{}) + + err := failures.Join() + + to := &TomatoError{} + assert.ErrorAs(t, err, &to) + + ca := &CarrotError{} + assert.ErrorAs(t, err, &ca) +} + +func Test_obtainError_Join_no_error(t *testing.T) { + failures := newObtainError() + + assert.NoError(t, failures.Join()) +} + +func Test_obtainError_Join_same_domain(t *testing.T) { + failures := newObtainError() + + failures.Add("example.com", &TomatoError{}) + failures.Add("example.com", &CarrotError{}) + + err := failures.Join() + + to := &TomatoError{} + if errors.As(err, &to) { + require.Fail(t, "TomatoError should be overridden by CarrotError") + } + + ca := &CarrotError{} + assert.ErrorAs(t, err, &ca) +} diff --git a/go.mod b/go.mod index eb8e7ac3..7eb0ba1a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/go-acme/lego/v4 -go 1.19 +go 1.20 require ( cloud.google.com/go/compute/metadata v0.2.3 diff --git a/providers/dns/allinkl/internal/types_api.go b/providers/dns/allinkl/internal/types_api.go index 9207dc1a..145163cd 100644 --- a/providers/dns/allinkl/internal/types_api.go +++ b/providers/dns/allinkl/internal/types_api.go @@ -54,7 +54,7 @@ type DNSRequest struct { // --- type GetDNSSettingsAPIResponse struct { - Response GetDNSSettingsResponse `json:"Response" mapstructure:"Response"` + Response GetDNSSettingsResponse `json:"Response" mapstructure:"Response"` } type GetDNSSettingsResponse struct { diff --git a/providers/dns/ipv64/internal/client.go b/providers/dns/ipv64/internal/client.go index e1fdbd19..fbb871aa 100644 --- a/providers/dns/ipv64/internal/client.go +++ b/providers/dns/ipv64/internal/client.go @@ -11,26 +11,26 @@ import ( "time" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "golang.org/x/oauth2" ) const defaultBaseURL = "https://ipv64.net" -const authorizationHeader = "Authorization" - type Client struct { - apiKey string - baseURL *url.URL HTTPClient *http.Client } -func NewClient(apiKey string) *Client { +func NewClient(hc *http.Client) *Client { baseURL, _ := url.Parse(defaultBaseURL) + if hc == nil { + hc = &http.Client{Timeout: 15 * time.Second} + } + return &Client{ - apiKey: apiKey, baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 15 * time.Second}, + HTTPClient: hc, } } @@ -91,8 +91,6 @@ func (c Client) DeleteRecord(ctx context.Context, domain, prefix, recordType, co } func (c Client) do(req *http.Request, result any) error { - req.Header.Set(authorizationHeader, fmt.Sprintf("Bearer %s", c.apiKey)) - if req.Method != http.MethodGet { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } @@ -140,3 +138,16 @@ func parseError(req *http.Request, resp *http.Response) error { return errAPI } + +func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client { + if client == nil { + client = &http.Client{Timeout: 15 * time.Second} + } + + client.Transport = &oauth2.Transport{ + Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}), + Base: client.Transport, + } + + return client +} diff --git a/providers/dns/ipv64/internal/client_test.go b/providers/dns/ipv64/internal/client_test.go index fd2f8991..1966f9f6 100644 --- a/providers/dns/ipv64/internal/client_test.go +++ b/providers/dns/ipv64/internal/client_test.go @@ -15,12 +15,14 @@ import ( "github.com/stretchr/testify/require" ) +const testAPIKey = "secret" + func setupTest(t *testing.T, handler http.HandlerFunc) *Client { t.Helper() server := httptest.NewServer(handler) - client := NewClient("secret") + client := NewClient(OAuthStaticAccessToken(server.Client(), testAPIKey)) client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) @@ -34,8 +36,8 @@ func testHandler(method, filename string, statusCode int) http.HandlerFunc { return } - auth := req.Header.Get(authorizationHeader) - if auth != "Bearer secret" { + auth := req.Header.Get("Authorization") + if auth != "Bearer "+testAPIKey { http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) return } diff --git a/providers/dns/ipv64/ipv64.go b/providers/dns/ipv64/ipv64.go index b943f311..97032886 100644 --- a/providers/dns/ipv64/ipv64.go +++ b/providers/dns/ipv64/ipv64.go @@ -78,7 +78,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("ipv64: credentials missing") } - client := internal.NewClient(config.APIKey) + client := internal.NewClient(internal.OAuthStaticAccessToken(config.HTTPClient, config.APIKey)) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient