From 7e1f4948ecd085a0bf7837590e0a380d31dfdb1a Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 9 Jan 2019 08:29:17 +0100 Subject: [PATCH] Add configurable timeout when obtaining certificates. (#747) --- README.md | 3 +- certificate/certificates.go | 74 ++++++++++++++++---------------- certificate/certificates_test.go | 6 +-- cmd/flags.go | 5 +++ cmd/setup.go | 8 ++-- lego/client.go | 3 +- lego/client_config.go | 20 ++++++--- 7 files changed, 68 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 35ee4251..cc157bb6 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ GLOBAL OPTIONS: --http-timeout value Set the HTTP timeout value to a specific value in seconds. (default: 0) --dns-timeout value Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries. (default: 10) --pem Generate a .pem file by concatenating the .key and .crt files together. + --cert.timeout value Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30) --help, -h show help --version, -v print the version ``` @@ -234,7 +235,7 @@ func main() { // This CA URL is configured for a local dev instance of Boulder running in Docker in a VM. config.CADirURL = "http://192.168.99.100:4000/directory" - config.KeyType = certcrypto.RSA2048 + config.Certificate.KeyType = certcrypto.RSA2048 // A client facilitates communication with the CA server. client, err := lego.NewClient(config) diff --git a/certificate/certificates.go b/certificate/certificates.go index e9c04197..10003ac0 100644 --- a/certificate/certificates.go +++ b/certificate/certificates.go @@ -17,6 +17,7 @@ import ( "github.com/xenolf/lego/certcrypto" "github.com/xenolf/lego/challenge" "github.com/xenolf/lego/log" + "github.com/xenolf/lego/platform/wait" "golang.org/x/crypto/ocsp" "golang.org/x/net/idna" ) @@ -60,17 +61,24 @@ type resolver interface { Solve(authorizations []acme.Authorization) error } -type Certifier struct { - core *api.Core - keyType certcrypto.KeyType - resolver resolver +type CertifierOptions struct { + KeyType certcrypto.KeyType + Timeout time.Duration } -func NewCertifier(core *api.Core, keyType certcrypto.KeyType, resolver resolver) *Certifier { +// Certifier A service to obtain/renew/revoke certificates. +type Certifier struct { + core *api.Core + resolver resolver + options CertifierOptions +} + +// NewCertifier creates a Certifier. +func NewCertifier(core *api.Core, resolver resolver, options CertifierOptions) *Certifier { return &Certifier{ core: core, - keyType: keyType, resolver: resolver, + options: options, } } @@ -191,7 +199,7 @@ func (c *Certifier) ObtainForCSR(csr x509.CertificateRequest, bundle bool) (*Res func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool) (*Resource, error) { if privateKey == nil { var err error - privateKey, err = certcrypto.GeneratePrivateKey(c.keyType) + privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType) if err != nil { return nil, err } @@ -237,9 +245,9 @@ func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle if respOrder.Status == acme.StatusValid { // if the certificate is available right away, short cut! - ok, err := c.checkResponse(respOrder, certRes, bundle) - if err != nil { - return nil, err + ok, errR := c.checkResponse(respOrder, certRes, bundle) + if errR != nil { + return nil, errR } if ok { @@ -247,34 +255,26 @@ func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle } } - return c.waitForCertificate(certRes, order.Location, bundle) -} - -func (c *Certifier) waitForCertificate(certRes *Resource, orderURL string, bundle bool) (*Resource, error) { - stopTimer := time.NewTimer(30 * time.Second) - defer stopTimer.Stop() - retryTick := time.NewTicker(500 * time.Millisecond) - defer retryTick.Stop() - - for { - select { - case <-stopTimer.C: - return nil, errors.New("certificate polling timed out") - case <-retryTick.C: - order, err := c.core.Orders.Get(orderURL) - if err != nil { - return nil, err - } - - done, err := c.checkResponse(order, certRes, bundle) - if err != nil { - return nil, err - } - if done { - return certRes, nil - } - } + timeout := c.options.Timeout + if c.options.Timeout <= 0 { + timeout = 30 * time.Second } + + err = wait.For("certificate", timeout, timeout/60, func() (bool, error) { + ord, errW := c.core.Orders.Get(order.Location) + if errW != nil { + return false, errW + } + + done, errW := c.checkResponse(ord, certRes, bundle) + if errW != nil { + return false, errW + } + + return done, nil + }) + + return certRes, err } // checkResponse checks to see if the certificate is ready and a link is contained in the response. diff --git a/certificate/certificates_test.go b/certificate/certificates_test.go index 0c01a02b..6991600d 100644 --- a/certificate/certificates_test.go +++ b/certificate/certificates_test.go @@ -93,7 +93,7 @@ func Test_checkResponse(t *testing.T) { core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) - certifier := NewCertifier(core, certcrypto.RSA2048, &resolverMock{}) + certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) order := acme.Order{ Status: acme.StatusValid, @@ -141,7 +141,7 @@ func Test_checkResponse_issuerRelUp(t *testing.T) { core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) - certifier := NewCertifier(core, certcrypto.RSA2048, &resolverMock{}) + certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) order := acme.Order{ Status: acme.StatusValid, @@ -180,7 +180,7 @@ func Test_checkResponse_embeddedIssuer(t *testing.T) { core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) - certifier := NewCertifier(core, certcrypto.RSA2048, &resolverMock{}) + certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) order := acme.Order{ Status: acme.StatusValid, diff --git a/cmd/flags.go b/cmd/flags.go index dbca98ab..432e8c76 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -105,5 +105,10 @@ func CreateFlags(defaultPath string) []cli.Flag { Name: "pem", Usage: "Generate a .pem file by concatenating the .key and .crt files together.", }, + cli.IntFlag{ + Name: "cert.timeout", + Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.", + Value: 30, + }, } } diff --git a/cmd/setup.go b/cmd/setup.go index 84e76a82..efcc28b3 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -34,11 +34,13 @@ func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego. } func newClient(ctx *cli.Context, acc registration.User) *lego.Client { - keyType := getKeyType(ctx) - config := lego.NewConfig(acc) config.CADirURL = ctx.GlobalString("server") - config.KeyType = keyType + + config.Certificate = lego.CertificateConfig{ + KeyType: getKeyType(ctx), + Timeout: time.Duration(ctx.GlobalInt("cert.timeout")) * time.Second, + } config.UserAgent = fmt.Sprintf("lego-cli/%s", ctx.App.Version) if ctx.GlobalIsSet("http-timeout") { diff --git a/lego/client.go b/lego/client.go index 5235b1de..1f7b5c8d 100644 --- a/lego/client.go +++ b/lego/client.go @@ -53,9 +53,10 @@ func NewClient(config *Config) (*Client, error) { solversManager := resolver.NewSolversManager(core) prober := resolver.NewProber(solversManager) + certifier := certificate.NewCertifier(core, prober, certificate.CertifierOptions{KeyType: config.Certificate.KeyType, Timeout: config.Certificate.Timeout}) return &Client{ - Certificate: certificate.NewCertifier(core, config.KeyType, prober), + Certificate: certifier, Challenge: solversManager, Registration: registration.NewRegistrar(core, config.User), core: core, diff --git a/lego/client_config.go b/lego/client_config.go index 738be86a..4807fcfe 100644 --- a/lego/client_config.go +++ b/lego/client_config.go @@ -35,22 +35,30 @@ const ( ) type Config struct { - CADirURL string - User registration.User - KeyType certcrypto.KeyType - UserAgent string - HTTPClient *http.Client + CADirURL string + User registration.User + UserAgent string + HTTPClient *http.Client + Certificate CertificateConfig } func NewConfig(user registration.User) *Config { return &Config{ CADirURL: LEDirectoryProduction, User: user, - KeyType: certcrypto.RSA2048, HTTPClient: createDefaultHTTPClient(), + Certificate: CertificateConfig{ + KeyType: certcrypto.RSA2048, + Timeout: 30 * time.Second, + }, } } +type CertificateConfig struct { + KeyType certcrypto.KeyType + Timeout time.Duration +} + // createDefaultHTTPClient Creates an HTTP client with a reasonable timeout value // and potentially a custom *x509.CertPool // based on the caCertificatesEnvVar environment variable (see the `initCertPool` function)