api.go raw

   1  package api
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"crypto"
   7  	"encoding/json"
   8  	"errors"
   9  	"fmt"
  10  	"net/http"
  11  	"time"
  12  
  13  	"github.com/cenkalti/backoff/v5"
  14  	"github.com/go-acme/lego/v4/acme"
  15  	"github.com/go-acme/lego/v4/acme/api/internal/nonces"
  16  	"github.com/go-acme/lego/v4/acme/api/internal/secure"
  17  	"github.com/go-acme/lego/v4/acme/api/internal/sender"
  18  	"github.com/go-acme/lego/v4/log"
  19  )
  20  
  21  // Core ACME/LE core API.
  22  type Core struct {
  23  	doer         *sender.Doer
  24  	nonceManager *nonces.Manager
  25  	jws          *secure.JWS
  26  	directory    acme.Directory
  27  	HTTPClient   *http.Client
  28  
  29  	common         service // Reuse a single struct instead of allocating one for each service on the heap.
  30  	Accounts       *AccountService
  31  	Authorizations *AuthorizationService
  32  	Certificates   *CertificateService
  33  	Challenges     *ChallengeService
  34  	Orders         *OrderService
  35  }
  36  
  37  // New Creates a new Core.
  38  func New(httpClient *http.Client, userAgent, caDirURL, kid string, privateKey crypto.PrivateKey) (*Core, error) {
  39  	doer := sender.NewDoer(httpClient, userAgent)
  40  
  41  	dir, err := getDirectory(doer, caDirURL)
  42  	if err != nil {
  43  		return nil, err
  44  	}
  45  
  46  	nonceManager := nonces.NewManager(doer, dir.NewNonceURL)
  47  
  48  	jws := secure.NewJWS(privateKey, kid, nonceManager)
  49  
  50  	c := &Core{doer: doer, nonceManager: nonceManager, jws: jws, directory: dir, HTTPClient: httpClient}
  51  
  52  	c.common.core = c
  53  	c.Accounts = (*AccountService)(&c.common)
  54  	c.Authorizations = (*AuthorizationService)(&c.common)
  55  	c.Certificates = (*CertificateService)(&c.common)
  56  	c.Challenges = (*ChallengeService)(&c.common)
  57  	c.Orders = (*OrderService)(&c.common)
  58  
  59  	return c, nil
  60  }
  61  
  62  // post performs an HTTP POST request and parses the response body as JSON,
  63  // into the provided respBody object.
  64  func (a *Core) post(uri string, reqBody, response any) (*http.Response, error) {
  65  	content, err := json.Marshal(reqBody)
  66  	if err != nil {
  67  		return nil, errors.New("failed to marshal message")
  68  	}
  69  
  70  	return a.retrievablePost(uri, content, response)
  71  }
  72  
  73  // postAsGet performs an HTTP POST ("POST-as-GET") request.
  74  // https://www.rfc-editor.org/rfc/rfc8555.html#section-6.3
  75  func (a *Core) postAsGet(uri string, response any) (*http.Response, error) {
  76  	return a.retrievablePost(uri, []byte{}, response)
  77  }
  78  
  79  func (a *Core) retrievablePost(uri string, content []byte, response any) (*http.Response, error) {
  80  	ctx := context.Background()
  81  
  82  	// during tests, allow to support ~90% of bad nonce with a minimum of attempts.
  83  	bo := backoff.NewExponentialBackOff()
  84  	bo.InitialInterval = 200 * time.Millisecond
  85  	bo.MaxInterval = 5 * time.Second
  86  
  87  	operation := func() (*http.Response, error) {
  88  		resp, err := a.signedPost(uri, content, response)
  89  		if err != nil {
  90  			// Retry if the nonce was invalidated
  91  			var e *acme.NonceError
  92  			if errors.As(err, &e) {
  93  				return resp, err
  94  			}
  95  
  96  			return resp, backoff.Permanent(err)
  97  		}
  98  
  99  		return resp, nil
 100  	}
 101  
 102  	notify := func(err error, duration time.Duration) {
 103  		log.Infof("retry due to: %v", err)
 104  	}
 105  
 106  	return backoff.Retry(ctx, operation,
 107  		backoff.WithBackOff(bo),
 108  		backoff.WithMaxElapsedTime(20*time.Second),
 109  		backoff.WithNotify(notify))
 110  }
 111  
 112  func (a *Core) signedPost(uri string, content []byte, response any) (*http.Response, error) {
 113  	signedContent, err := a.jws.SignContent(uri, content)
 114  	if err != nil {
 115  		return nil, fmt.Errorf("failed to post JWS message: failed to sign content: %w", err)
 116  	}
 117  
 118  	signedBody := bytes.NewBufferString(signedContent.FullSerialize())
 119  
 120  	resp, err := a.doer.Post(uri, signedBody, "application/jose+json", response)
 121  
 122  	// nonceErr is ignored to keep the root error.
 123  	nonce, nonceErr := nonces.GetFromResponse(resp)
 124  	if nonceErr == nil {
 125  		a.nonceManager.Push(nonce)
 126  	}
 127  
 128  	return resp, err
 129  }
 130  
 131  func (a *Core) signEABContent(newAccountURL, kid string, hmac []byte) ([]byte, error) {
 132  	eabJWS, err := a.jws.SignEABContent(newAccountURL, kid, hmac)
 133  	if err != nil {
 134  		return nil, err
 135  	}
 136  
 137  	return []byte(eabJWS.FullSerialize()), nil
 138  }
 139  
 140  // GetKeyAuthorization Gets the key authorization.
 141  func (a *Core) GetKeyAuthorization(token string) (string, error) {
 142  	return a.jws.GetKeyAuthorization(token)
 143  }
 144  
 145  func (a *Core) GetDirectory() acme.Directory {
 146  	return a.directory
 147  }
 148  
 149  func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) {
 150  	var dir acme.Directory
 151  	if _, err := do.Get(caDirURL, &dir); err != nil {
 152  		return dir, fmt.Errorf("get directory at '%s': %w", caDirURL, err)
 153  	}
 154  
 155  	if dir.NewAccountURL == "" {
 156  		return dir, errors.New("directory missing new registration URL")
 157  	}
 158  
 159  	if dir.NewOrderURL == "" {
 160  		return dir, errors.New("directory missing new order URL")
 161  	}
 162  
 163  	return dir, nil
 164  }
 165