From e7fd871a9ce973f903214a0a9d10d31b72d8154a Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 30 May 2018 19:53:04 +0200 Subject: [PATCH] ACME V2 support (#555) --- .gitignore | 4 +- README.md | 8 +- account.go | 45 +- acme/challenges.go | 3 - acme/client.go | 687 +++++++++--------- acme/client_test.go | 115 ++- acme/crypto.go | 27 +- acme/crypto_test.go | 2 +- acme/dns_challenge.go | 12 +- acme/dns_challenge_manual.go | 12 +- acme/dns_challenge_test.go | 4 +- acme/error.go | 31 +- acme/http.go | 2 +- acme/http_challenge.go | 7 +- acme/http_challenge_server.go | 6 +- acme/http_challenge_test.go | 4 +- acme/jws.go | 94 ++- acme/messages.go | 120 ++- acme/pop_challenge.go | 1 - acme/tls_sni_challenge.go | 67 -- acme/tls_sni_challenge_server.go | 62 -- acme/tls_sni_challenge_test.go | 65 -- cli.go | 33 +- cli_handlers.go | 213 +++--- crypto.go | 2 +- log/logger.go | 59 ++ providers/dns/auroradns/auroradns.go | 16 +- providers/dns/auroradns/auroradns_test.go | 6 +- providers/dns/azure/azure.go | 70 +- providers/dns/bluecat/bluecat.go | 119 ++- providers/dns/bluecat/bluecat_test.go | 14 +- providers/dns/cloudflare/cloudflare.go | 15 +- providers/dns/cloudxns/cloudxns.go | 424 ++++++----- providers/dns/cloudxns/cloudxns_test.go | 160 ++-- providers/dns/digitalocean/digitalocean.go | 47 +- .../dns/digitalocean/digitalocean_test.go | 2 +- providers/dns/dns_providers.go | 6 +- providers/dns/dnsimple/dnsimple.go | 15 +- providers/dns/dnsimple/dnsimple_test.go | 16 +- providers/dns/dnsmadeeasy/dnsmadeeasy.go | 8 +- providers/dns/dnsmadeeasy/dnsmadeeasy_test.go | 1 + providers/dns/duckdns/duckdns.go | 163 +++-- providers/dns/duckdns/duckdns_test.go | 130 ++-- providers/dns/dyn/dyn.go | 27 +- providers/dns/exoscale/exoscale.go | 14 +- providers/dns/gandi/gandi.go | 41 +- providers/dns/gandi/gandi_test.go | 81 --- providers/dns/gandiv5/gandiv5.go | 19 +- providers/dns/gandiv5/gandiv5_test.go | 81 --- providers/dns/glesys/glesys.go | 60 +- providers/dns/godaddy/godaddy.go | 10 +- providers/dns/googlecloud/googlecloud.go | 3 +- providers/dns/googlecloud/googlecloud_test.go | 1 + providers/dns/linode/linode.go | 10 +- providers/dns/namecheap/namecheap.go | 4 +- providers/dns/otc/mock.go | 12 +- providers/dns/otc/otc.go | 30 +- providers/dns/ovh/ovh.go | 2 +- providers/dns/pdns/pdns.go | 15 +- providers/dns/rackspace/rackspace.go | 49 +- providers/dns/rackspace/rackspace_test.go | 9 +- providers/dns/rfc2136/rfc2136.go | 30 +- providers/dns/route53/route53.go | 14 +- .../dns/route53/route53_integration_test.go | 14 +- providers/dns/route53/route53_test.go | 2 +- providers/http/memcached/memcached.go | 20 +- providers/http/memcached/memcached_test.go | 2 +- providers/http/webroot/webroot.go | 9 +- 68 files changed, 1637 insertions(+), 1819 deletions(-) delete mode 100644 acme/pop_challenge.go delete mode 100644 acme/tls_sni_challenge.go delete mode 100644 acme/tls_sni_challenge_server.go delete mode 100644 acme/tls_sni_challenge_test.go create mode 100644 log/logger.go diff --git a/.gitignore b/.gitignore index 61a095ef..1d610d90 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ lego.exe lego .lego +.gitcookies .idea +.vscode/ dist/ builds/ -.gitcookies +vendor/ diff --git a/README.md b/README.md index fde4b531..6cfce7a3 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,6 @@ yaourt -S lego-git - Revoke certificates - Robust implementation of all ACME challenges - HTTP (http-01) - - TLS with Server Name Indication (tls-sni-01) - DNS (dns-01) - SAN certificate support - Comes with multiple optional [DNS providers](https://github.com/xenolf/lego/tree/master/providers/dns) @@ -107,15 +106,14 @@ GLOBAL OPTIONS: --accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service. --key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default: "rsa2048") --path value Directory to use for storing the data (default: "/.lego") - --exclude value, -x value Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01". + --exclude value, -x value Explicitly disallow solvers by name from being used. Solvers: "http-01", "dns-01",. --webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge --memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts. --http value Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port - --tls value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port --dns value Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage. --http-timeout value Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds. (default: 0) --dns-timeout value Set the DNS timeout value to a specific value in seconds. The default is 10 seconds. (default: 0) - --dns-resolvers value Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use Google's DNS resolvers. + --dns-resolvers value Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined. --pem Generate a .pem file by concatanating the .key and .crt files together. --help, -h show help --version, -v print the version @@ -152,7 +150,7 @@ Obtain a certificate using the DNS challenge and AWS Route 53: $ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" run ``` -Note that `--dns=foo` implies `--exclude=http-01` and `--exclude=tls-sni-01`. lego will not attempt other challenges if you've told it to use DNS instead. +Note that `--dns=foo` implies `--exclude=http-01`. lego will not attempt other challenges if you've told it to use DNS instead. Obtain a certificate given a certificate signing request (CSR) generated by something else: diff --git a/account.go b/account.go index 34856e16..938f886f 100644 --- a/account.go +++ b/account.go @@ -8,6 +8,7 @@ import ( "path" "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/log" ) // Account represents a users local saved credentials @@ -25,23 +26,23 @@ func NewAccount(email string, conf *Configuration) *Account { // TODO: move to function in configuration? accKeyPath := accKeysPath + string(os.PathSeparator) + email + ".key" if err := checkFolder(accKeysPath); err != nil { - logger().Fatalf("Could not check/create directory for account %s: %v", email, err) + log.Fatalf("Could not check/create directory for account %s: %v", email, err) } var privKey crypto.PrivateKey if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { - logger().Printf("No key found for account %s. Generating a curve P384 EC key.", email) + log.Printf("No key found for account %s. Generating a curve P384 EC key.", email) privKey, err = generatePrivateKey(accKeyPath) if err != nil { - logger().Fatalf("Could not generate RSA private account key for account %s: %v", email, err) + log.Fatalf("Could not generate RSA private account key for account %s: %v", email, err) } - logger().Printf("Saved key to %s", accKeyPath) + log.Printf("Saved key to %s", accKeyPath) } else { privKey, err = loadPrivateKey(accKeyPath) if err != nil { - logger().Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err) + log.Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err) } } @@ -52,29 +53,53 @@ func NewAccount(email string, conf *Configuration) *Account { fileBytes, err := ioutil.ReadFile(accountFile) if err != nil { - logger().Fatalf("Could not load file for account %s -> %v", email, err) + log.Fatalf("Could not load file for account %s -> %v", email, err) } var acc Account err = json.Unmarshal(fileBytes, &acc) if err != nil { - logger().Fatalf("Could not parse file for account %s -> %v", email, err) + log.Fatalf("Could not parse file for account %s -> %v", email, err) } acc.key = privKey acc.conf = conf - if acc.Registration == nil { - logger().Fatalf("Could not load account for %s. Registration is nil.", email) + if acc.Registration == nil || acc.Registration.Body.Status == "" { + reg, err := tryRecoverAccount(privKey, conf) + if err != nil { + log.Fatalf("Could not load account for %s. Registration is nil -> %#v", email, err) + } + + acc.Registration = reg + err = acc.Save() + if err != nil { + log.Fatalf("Could not save account for %s. Registration is nil -> %#v", email, err) + } } if acc.conf == nil { - logger().Fatalf("Could not load account for %s. Configuration is nil.", email) + log.Fatalf("Could not load account for %s. Configuration is nil.", email) } return &acc } +func tryRecoverAccount(privKey crypto.PrivateKey, conf *Configuration) (*acme.RegistrationResource, error) { + // couldn't load account but got a key. Try to look the account up. + serverURL := conf.context.GlobalString("server") + client, err := acme.NewClient(serverURL, &Account{key: privKey, conf: conf}, acme.RSA2048) + if err != nil { + return nil, err + } + + reg, err := client.ResolveAccountByKey() + if err != nil { + return nil, err + } + return reg, nil +} + /** Implementation of the acme.User interface **/ // GetEmail returns the email address for the account diff --git a/acme/challenges.go b/acme/challenges.go index 85790050..cf7bd7f7 100644 --- a/acme/challenges.go +++ b/acme/challenges.go @@ -7,9 +7,6 @@ const ( // HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http // Note: HTTP01ChallengePath returns the URL path to fulfill this challenge HTTP01 = Challenge("http-01") - // TLSSNI01 is the "tls-sni-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#tls-with-server-name-indication-tls-sni - // Note: TLSSNI01ChallengeCert returns a certificate to fulfill this challenge - TLSSNI01 = Challenge("tls-sni-01") // DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns // Note: DNS01Record returns a DNS record which will fulfill this challenge DNS01 = Challenge("dns-01") diff --git a/acme/client.go b/acme/client.go index bcb84437..786e5b2d 100644 --- a/acme/client.go +++ b/acme/client.go @@ -5,22 +5,16 @@ import ( "crypto" "crypto/x509" "encoding/base64" - "encoding/json" "errors" "fmt" "io/ioutil" - "log" "net" - "net/http" "regexp" "strconv" "strings" "time" -) -var ( - // Logger is an optional custom logger. - Logger *log.Logger + "github.com/xenolf/lego/log" ) const ( @@ -33,16 +27,6 @@ const ( overallRequestLimit = 18 ) -// logf writes a log entry. It uses Logger if not -// nil, otherwise it uses the default log.Logger. -func logf(format string, args ...interface{}) { - if Logger != nil { - Logger.Printf(format, args...) - } else { - log.Printf(format, args...) - } -} - // User interface is to be implemented by users of this library. // It is used by the client type to get user specific information. type User interface { @@ -82,27 +66,23 @@ func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err) } - if dir.NewRegURL == "" { + if dir.NewAccountURL == "" { return nil, errors.New("directory missing new registration URL") } - if dir.NewAuthzURL == "" { - return nil, errors.New("directory missing new authz URL") - } - if dir.NewCertURL == "" { - return nil, errors.New("directory missing new certificate URL") - } - if dir.RevokeCertURL == "" { - return nil, errors.New("directory missing revoke certificate URL") + if dir.NewOrderURL == "" { + return nil, errors.New("directory missing new order URL") } - jws := &jws{privKey: privKey, directoryURL: caDirURL} + jws := &jws{privKey: privKey, getNonceURL: dir.NewNonceURL} + if reg := user.GetRegistration(); reg != nil { + jws.kid = reg.URI + } // REVIEW: best possibility? // Add all available solvers with the right index as per ACME // spec to this map. Otherwise they won`t be found. solvers := make(map[Challenge]solver) solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}} - solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}} return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil } @@ -112,8 +92,6 @@ func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) switch challenge { case HTTP01: c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p} - case TLSSNI01: - c.solvers[challenge] = &tlsSNIChallenge{jws: c.jws, validate: validate, provider: p} case DNS01: c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p} default: @@ -141,24 +119,6 @@ func (c *Client) SetHTTPAddress(iface string) error { return nil } -// SetTLSAddress specifies a custom interface:port to be used for TLS based challenges. -// If this option is not used, the default port 443 and all interfaces will be used. -// To only specify a port and no interface use the ":port" notation. -// -// NOTE: This REPLACES any custom TLS-SNI provider previously set by calling -// c.SetChallengeProvider with the default TLS-SNI challenge provider. -func (c *Client) SetTLSAddress(iface string) error { - host, port, err := net.SplitHostPort(iface) - if err != nil { - return err - } - - if chlng, ok := c.solvers[TLSSNI01]; ok { - chlng.(*tlsSNIChallenge).provider = NewTLSProviderServer(host, port) - } - return nil -} - // ExcludeChallenges explicitly removes challenges from the pool for solving. func (c *Client) ExcludeChallenges(challenges []Challenge) { // Loop through all challenges and delete the requested one if found. @@ -167,80 +127,138 @@ func (c *Client) ExcludeChallenges(challenges []Challenge) { } } +// GetToSURL returns the current ToS URL from the Directory +func (c *Client) GetToSURL() string { + return c.directory.Meta.TermsOfService +} + +// GetExternalAccountRequired returns the External Account Binding requirement of the Directory +func (c *Client) GetExternalAccountRequired() bool { + return c.directory.Meta.ExternalAccountRequired +} + // Register the current account to the ACME server. -func (c *Client) Register() (*RegistrationResource, error) { +func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) { if c == nil || c.user == nil { return nil, errors.New("acme: cannot register a nil client or user") } - logf("[INFO] acme: Registering account for %s", c.user.GetEmail()) + log.Printf("[INFO] acme: Registering account for %s", c.user.GetEmail()) - regMsg := registrationMessage{ - Resource: "new-reg", - } + accMsg := accountMessage{} if c.user.GetEmail() != "" { - regMsg.Contact = []string{"mailto:" + c.user.GetEmail()} + accMsg.Contact = []string{"mailto:" + c.user.GetEmail()} } else { - regMsg.Contact = []string{} + accMsg.Contact = []string{} } + accMsg.TermsOfServiceAgreed = tosAgreed - var serverReg Registration - var regURI string - hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &serverReg) + var serverReg accountMessage + hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg) if err != nil { remoteErr, ok := err.(RemoteError) if ok && remoteErr.StatusCode == 409 { - regURI = hdr.Get("Location") - regMsg = registrationMessage{ - Resource: "reg", - } - if hdr, err = postJSON(c.jws, regURI, regMsg, &serverReg); err != nil { - return nil, err - } } else { return nil, err } } - reg := &RegistrationResource{Body: serverReg} - - links := parseLinks(hdr["Link"]) - - if regURI == "" { - regURI = hdr.Get("Location") - } - reg.URI = regURI - if links["terms-of-service"] != "" { - reg.TosURL = links["terms-of-service"] - } - - if links["next"] != "" { - reg.NewAuthzURL = links["next"] - } else { - return nil, errors.New("acme: The server did not return 'next' link to proceed") + reg := &RegistrationResource{ + URI: hdr.Get("Location"), + Body: serverReg, } + c.jws.kid = reg.URI return reg, nil } +// Register the current account to the ACME server. +func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string, hmacEncoded string) (*RegistrationResource, error) { + if c == nil || c.user == nil { + return nil, errors.New("acme: cannot register a nil client or user") + } + log.Printf("[INFO] acme: Registering account (EAB) for %s", c.user.GetEmail()) + + accMsg := accountMessage{} + if c.user.GetEmail() != "" { + accMsg.Contact = []string{"mailto:" + c.user.GetEmail()} + } else { + accMsg.Contact = []string{} + } + accMsg.TermsOfServiceAgreed = tosAgreed + + hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded) + if err != nil { + return nil, fmt.Errorf("acme: could not decode hmac key: %s", err.Error()) + } + + eabJWS, err := c.jws.signEABContent(c.directory.NewAccountURL, kid, hmac) + if err != nil { + return nil, fmt.Errorf("acme: error signing eab content: %s", err.Error()) + } + + eabPayload := eabJWS.FullSerialize() + + accMsg.ExternalAccountBinding = []byte(eabPayload) + + var serverReg accountMessage + hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg) + if err != nil { + remoteErr, ok := err.(RemoteError) + if ok && remoteErr.StatusCode == 409 { + } else { + return nil, err + } + } + + reg := &RegistrationResource{ + URI: hdr.Get("Location"), + Body: serverReg, + } + c.jws.kid = reg.URI + + return reg, nil +} + +// ResolveAccountByKey will attempt to look up an account using the given account key +// and return its registration resource. +func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) { + log.Printf("[INFO] acme: Trying to resolve account by key") + + acc := accountMessage{OnlyReturnExisting: true} + hdr, err := postJSON(c.jws, c.directory.NewAccountURL, acc, nil) + if err != nil { + return nil, err + } + + accountLink := hdr.Get("Location") + if accountLink == "" { + return nil, errors.New("Server did not return the account link") + } + + var retAccount accountMessage + c.jws.kid = accountLink + _, err = postJSON(c.jws, accountLink, accountMessage{}, &retAccount) + if err != nil { + return nil, err + } + + return &RegistrationResource{URI: accountLink, Body: retAccount}, nil +} + // DeleteRegistration deletes the client's user registration from the ACME // server. func (c *Client) DeleteRegistration() error { if c == nil || c.user == nil { return errors.New("acme: cannot unregister a nil client or user") } - logf("[INFO] acme: Deleting account for %s", c.user.GetEmail()) + log.Printf("[INFO] acme: Deleting account for %s", c.user.GetEmail()) - regMsg := registrationMessage{ - Resource: "reg", - Delete: true, + accMsg := accountMessage{ + Status: "deactivated", } - _, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, nil) - if err != nil { - return err - } - - return nil + _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, nil) + return err } // QueryRegistration runs a POST request on the client's registration and @@ -253,48 +271,25 @@ func (c *Client) QueryRegistration() (*RegistrationResource, error) { return nil, errors.New("acme: cannot query the registration of a nil client or user") } // Log the URL here instead of the email as the email may not be set - logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI) + log.Printf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI) - regMsg := registrationMessage{ - Resource: "reg", - } + accMsg := accountMessage{} - var serverReg Registration - hdr, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, &serverReg) + var serverReg accountMessage + _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, &serverReg) if err != nil { return nil, err } reg := &RegistrationResource{Body: serverReg} - links := parseLinks(hdr["Link"]) // Location: header is not returned so this needs to be populated off of // existing URI reg.URI = c.user.GetRegistration().URI - if links["terms-of-service"] != "" { - reg.TosURL = links["terms-of-service"] - } - - if links["next"] != "" { - reg.NewAuthzURL = links["next"] - } else { - return nil, errors.New("acme: No new-authz link in response to registration query") - } return reg, nil } -// AgreeToTOS updates the Client registration and sends the agreement to -// the server. -func (c *Client) AgreeToTOS() error { - reg := c.user.GetRegistration() - - reg.Body.Agreement = c.user.GetRegistration().TosURL - reg.Body.Resource = "reg" - _, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil) - return err -} - // ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it. // The domains are inferred from the CommonName and SubjectAltNames, if any. The private key // for this CSR is not required. @@ -302,7 +297,7 @@ func (c *Client) AgreeToTOS() error { // your issued certificate as a bundle. // This function will never return a partial certificate. If one domain in the list fails, // the whole certificate will fail. -func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) { +func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (*CertificateResource, error) { // figure out what domains it concerns // start with the common name domains := []string{csr.Subject.CommonName} @@ -322,40 +317,49 @@ DNSNames: } if bundle { - logf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", ")) + log.Printf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", ")) } else { - logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", ")) + log.Printf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", ")) } - challenges, failures := c.getChallenges(domains) - // If any challenge fails - return. Do not generate partial SAN certificates. - if len(failures) > 0 { - for _, auth := range challenges { - c.disableAuthz(auth) - } - - return CertificateResource{}, failures - } - - errs := c.solveChallenges(challenges) - // If any challenge fails - return. Do not generate partial SAN certificates. - if len(errs) > 0 { - return CertificateResource{}, errs - } - - logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - - cert, err := c.requestCertificateForCsr(challenges, bundle, csr.Raw, nil) + order, err := c.createOrderForIdentifiers(domains) if err != nil { - for _, chln := range challenges { - failures[chln.Domain] = err + return nil, err + } + authz, err := c.getAuthzForOrder(order) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + /*for _, auth := range authz { + c.disableAuthz(auth) + }*/ + return nil, err + } + + err = c.solveChallengeForAuthz(authz) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + return nil, err + } + + log.Printf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) + + failures := make(ObtainError) + cert, err := c.requestCertificateForCsr(order, bundle, csr.Raw, nil) + if err != nil { + for _, chln := range authz { + failures[chln.Identifier.Value] = err } } // Add the CSR to the certificate so that it can be used for renewals. cert.CSR = pemEncode(&csr) - return cert, failures + // 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 } // ObtainCertificate tries to obtain a single certificate using all domains passed into it. @@ -367,39 +371,52 @@ DNSNames: // your issued certificate as a bundle. // This function will never return a partial certificate. If one domain in the list fails, // the whole certificate will fail. -func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, map[string]error) { +func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { + if len(domains) == 0 { + return nil, errors.New("No domains to obtain a certificate for") + } + if bundle { - logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) + log.Printf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) } else { - logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) + log.Printf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) } - challenges, failures := c.getChallenges(domains) - // If any challenge fails - return. Do not generate partial SAN certificates. - if len(failures) > 0 { - for _, auth := range challenges { - c.disableAuthz(auth) - } - - return CertificateResource{}, failures - } - - errs := c.solveChallenges(challenges) - // If any challenge fails - return. Do not generate partial SAN certificates. - if len(errs) > 0 { - return CertificateResource{}, errs - } - - logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - - cert, err := c.requestCertificate(challenges, bundle, privKey, mustStaple) + order, err := c.createOrderForIdentifiers(domains) if err != nil { - for _, chln := range challenges { - failures[chln.Domain] = err + return nil, err + } + authz, err := c.getAuthzForOrder(order) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + /*for _, auth := range authz { + c.disableAuthz(auth) + }*/ + return nil, err + } + + err = c.solveChallengeForAuthz(authz) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + return nil, err + } + + log.Printf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) + + failures := make(ObtainError) + cert, err := c.requestCertificateForOrder(order, bundle, privKey, mustStaple) + if err != nil { + for _, auth := range authz { + failures[auth.Identifier.Value] = err } } - return cert, failures + // 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 } // RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA. @@ -416,7 +433,7 @@ func (c *Client) RevokeCertificate(certificate []byte) error { encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw) - _, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert}, nil) + _, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Certificate: encodedCert}, nil) return err } @@ -428,22 +445,22 @@ func (c *Client) RevokeCertificate(certificate []byte) error { // If bundle is true, the []byte contains both the issuer certificate and // your issued certificate as a bundle. // For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil. -func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (CertificateResource, error) { +func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (*CertificateResource, error) { // Input certificate is PEM encoded. Decode it here as we may need the decoded // cert later on in the renewal process. The input may be a bundle or a single certificate. certificates, err := parsePEMBundle(cert.Certificate) if err != nil { - return CertificateResource{}, err + return nil, err } x509Cert := certificates[0] if x509Cert.IsCA { - return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain) + return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain) } // This is just meant to be informal for the user. timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) - logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours())) + log.Printf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours())) // We always need to request a new certificate to renew. // Start by checking to see if the certificate was based off a CSR, and @@ -451,22 +468,21 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b if len(cert.CSR) > 0 { csr, err := pemDecodeTox509CSR(cert.CSR) if err != nil { - return CertificateResource{}, err + return nil, err } newCert, failures := c.ObtainCertificateForCSR(*csr, bundle) - return newCert, failures[cert.Domain] + return newCert, failures } var privKey crypto.PrivateKey if cert.PrivateKey != nil { privKey, err = parsePEMPrivateKey(cert.PrivateKey) if err != nil { - return CertificateResource{}, err + return nil, err } } var domains []string - var failures map[string]error // check for SAN certificate if len(x509Cert.DNSNames) > 1 { domains = append(domains, x509Cert.Subject.CommonName) @@ -480,272 +496,276 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b domains = append(domains, x509Cert.Subject.CommonName) } - newCert, failures := c.ObtainCertificate(domains, bundle, privKey, mustStaple) - return newCert, failures[cert.Domain] + newCert, err := c.ObtainCertificate(domains, bundle, privKey, mustStaple) + return newCert, err +} + +func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) { + + var identifiers []identifier + for _, domain := range domains { + identifiers = append(identifiers, identifier{Type: "dns", Value: domain}) + } + + order := orderMessage{ + Identifiers: identifiers, + } + + var response orderMessage + hdr, err := postJSON(c.jws, c.directory.NewOrderURL, order, &response) + if err != nil { + return orderResource{}, err + } + + orderRes := orderResource{ + URL: hdr.Get("Location"), + Domains: domains, + orderMessage: response, + } + return orderRes, nil } // Looks through the challenge combinations to find a solvable match. // Then solves the challenges in series and returns. -func (c *Client) solveChallenges(challenges []authorizationResource) map[string]error { +func (c *Client) solveChallengeForAuthz(authorizations []authorization) error { + failures := make(ObtainError) + // loop through the resources, basically through the domains. - failures := make(map[string]error) - for _, authz := range challenges { - if authz.Body.Status == "valid" { + for _, authz := range authorizations { + if authz.Status == "valid" { // Boulder might recycle recent validated authz (see issue #267) - logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Domain) + log.Printf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value) continue } + // no solvers - no solving - if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil { - for i, solver := range solvers { - // TODO: do not immediately fail if one domain fails to validate. - err := solver.Solve(authz.Body.Challenges[i], authz.Domain) - if err != nil { - c.disableAuthz(authz) - failures[authz.Domain] = err - } + if i, solver := c.chooseSolver(authz, authz.Identifier.Value); solver != nil { + err := solver.Solve(authz.Challenges[i], authz.Identifier.Value) + if err != nil { + //c.disableAuthz(authz.Identifier) + failures[authz.Identifier.Value] = err } } else { - c.disableAuthz(authz) - failures[authz.Domain] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Domain) + //c.disableAuthz(authz) + failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value) } } - return failures -} - -// Checks all combinations from the server and returns an array of -// solvers which should get executed in series. -func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver { - for _, combination := range auth.Combinations { - solvers := make(map[int]solver) - for _, idx := range combination { - if solver, ok := c.solvers[auth.Challenges[idx].Type]; ok { - solvers[idx] = solver - } else { - logf("[INFO][%s] acme: Could not find solver for: %s", domain, auth.Challenges[idx].Type) - } - } - - // If we can solve the whole combination, return the solvers - if len(solvers) == len(combination) { - return solvers - } + // be careful not to return an empty failures map, for + // even an empty ObtainError is a non-nil error value + if len(failures) > 0 { + return failures } return nil } +// Checks all challenges from the server in order and returns the first matching solver. +func (c *Client) chooseSolver(auth authorization, domain string) (int, solver) { + for i, challenge := range auth.Challenges { + if solver, ok := c.solvers[Challenge(challenge.Type)]; ok { + return i, solver + } + log.Printf("[INFO][%s] acme: Could not find solver for: %s", domain, challenge.Type) + } + return 0, nil +} + // Get the challenges needed to proof our identifier to the ACME server. -func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) { - resc, errc := make(chan authorizationResource), make(chan domainError) +func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error) { + resc, errc := make(chan authorization), make(chan domainError) delay := time.Second / overallRequestLimit - for _, domain := range domains { + for _, authzURL := range order.Authorizations { time.Sleep(delay) - go func(domain string) { - authMsg := authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}} + go func(authzURL string) { var authz authorization - hdr, err := postJSON(c.jws, c.user.GetRegistration().NewAuthzURL, authMsg, &authz) + _, err := getJSON(authzURL, &authz) if err != nil { - errc <- domainError{Domain: domain, Error: err} + errc <- domainError{Domain: authz.Identifier.Value, Error: err} return } - links := parseLinks(hdr["Link"]) - if links["next"] == "" { - logf("[ERROR][%s] acme: Server did not provide next link to proceed", domain) - errc <- domainError{Domain: domain, Error: errors.New("Server did not provide next link to proceed")} - return - } - - resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: hdr.Get("Location"), Domain: domain} - }(domain) + resc <- authz + }(authzURL) } - responses := make(map[string]authorizationResource) - failures := make(map[string]error) - for i := 0; i < len(domains); i++ { + var responses []authorization + failures := make(ObtainError) + for i := 0; i < len(order.Authorizations); i++ { select { case res := <-resc: - responses[res.Domain] = res + responses = append(responses, res) case err := <-errc: failures[err.Domain] = err.Error } } - challenges := make([]authorizationResource, 0, len(responses)) - for _, domain := range domains { - if challenge, ok := responses[domain]; ok { - challenges = append(challenges, challenge) - } - } - - logAuthz(challenges) + logAuthz(order) close(resc) close(errc) - return challenges, failures + // 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 } -func logAuthz(authz []authorizationResource) { - for _, auth := range authz { - logf("[INFO][%s] AuthURL: %s", auth.Domain, auth.AuthURL) +func logAuthz(order orderResource) { + for i, auth := range order.Authorizations { + log.Printf("[INFO][%s] AuthURL: %s", order.Identifiers[i].Value, auth) } } // cleanAuthz loops through the passed in slice and disables any auths which are not "valid" -func (c *Client) disableAuthz(auth authorizationResource) error { +func (c *Client) disableAuthz(authURL string) error { var disabledAuth authorization - _, err := postJSON(c.jws, auth.AuthURL, deactivateAuthMessage{Resource: "authz", Status: "deactivated"}, &disabledAuth) + _, err := postJSON(c.jws, authURL, deactivateAuthMessage{Status: "deactivated"}, &disabledAuth) return err } -func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, error) { - if len(authz) == 0 { - return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!") - } +func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { var err error if privKey == nil { privKey, err = generatePrivateKey(c.keyType) if err != nil { - return CertificateResource{}, err + return nil, err } } // determine certificate name(s) based on the authorization resources - commonName := authz[0] + commonName := order.Domains[0] var san []string - for _, auth := range authz[1:] { - san = append(san, auth.Domain) + for _, auth := range order.Identifiers { + san = append(san, auth.Value) } // TODO: should the CSR be customizable? - csr, err := generateCsr(privKey, commonName.Domain, san, mustStaple) + csr, err := generateCsr(privKey, commonName, san, mustStaple) if err != nil { - return CertificateResource{}, err + return nil, err } - return c.requestCertificateForCsr(authz, bundle, csr, pemEncode(privKey)) + return c.requestCertificateForCsr(order, bundle, csr, pemEncode(privKey)) } -func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) { - commonName := authz[0] +func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr []byte, privateKeyPem []byte) (*CertificateResource, error) { + commonName := order.Domains[0] - var authURLs []string - for _, auth := range authz[1:] { - authURLs = append(authURLs, auth.AuthURL) + csrString := base64.RawURLEncoding.EncodeToString(csr) + var retOrder orderMessage + _, error := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder) + if error != nil { + return nil, error } - csrString := base64.URLEncoding.EncodeToString(csr) - jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs}) - if err != nil { - return CertificateResource{}, err - } - - resp, err := c.jws.post(commonName.NewCertURL, jsonBytes) - if err != nil { - return CertificateResource{}, err + if retOrder.Status == "invalid" { + return nil, error } certRes := CertificateResource{ - Domain: commonName.Domain, - CertURL: resp.Header.Get("Location"), + Domain: commonName, + CertURL: retOrder.Certificate, PrivateKey: privateKeyPem, } + if retOrder.Status == "valid" { + // if the certificate is available right away, short cut! + ok, err := c.checkCertResponse(retOrder, &certRes, bundle) + if err != nil { + return nil, err + } + + if ok { + return &certRes, nil + } + } + maxChecks := 1000 for i := 0; i < maxChecks; i++ { - done, err := c.checkCertResponse(resp, &certRes, bundle) - resp.Body.Close() + _, err := getJSON(order.URL, &retOrder) if err != nil { - return CertificateResource{}, err + return nil, err + } + done, err := c.checkCertResponse(retOrder, &certRes, bundle) + if err != nil { + return nil, err } if done { break } if i == maxChecks-1 { - return CertificateResource{}, fmt.Errorf("polled for certificate %d times; giving up", i) - } - resp, err = httpGet(certRes.CertURL) - if err != nil { - return CertificateResource{}, err + return nil, fmt.Errorf("polled for certificate %d times; giving up", i) } } - return certRes, nil + return &certRes, nil } -// checkCertResponse checks resp to see if a certificate is contained in the -// response, and if so, loads it into certRes and returns true. If the cert -// is not yet ready, it returns false. This function honors the waiting period -// required by the Retry-After header of the response, if specified. This -// function may read from resp.Body but does NOT close it. The certRes input +// checkCertResponse checks to see if the certificate is ready and a link is contained in the +// response. if so, loads it into certRes and returns true. If the cert +// is not yet ready, it returns false. The certRes input // should already have the Domain (common name) field populated. If bundle is // true, the certificate will be bundled with the issuer's cert. -func (c *Client) checkCertResponse(resp *http.Response, certRes *CertificateResource, bundle bool) (bool, error) { - switch resp.StatusCode { - case 201, 202: +func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) { + + switch order.Status { + case "valid": + resp, err := httpGet(order.Certificate) + if err != nil { + return false, err + } + cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize)) if err != nil { return false, err } - // The server returns a body with a length of zero if the - // certificate was not ready at the time this request completed. - // Otherwise the body is the certificate. - if len(cert) > 0 { - certRes.CertStableURL = resp.Header.Get("Content-Location") - certRes.AccountRef = c.user.GetRegistration().URI + // The issuer certificate link is always supplied via an "up" link + // in the response headers of a new certificate. + links := parseLinks(resp.Header["Link"]) + if link, ok := links["up"]; ok { + issuerCert, err := c.getIssuerCertificate(link) - issuedCert := pemEncode(derCertificateBytes(cert)) - - // The issuer certificate link is always supplied via an "up" link - // in the response headers of a new certificate. - links := parseLinks(resp.Header["Link"]) - issuerCert, err := c.getIssuerCertificate(links["up"]) if err != nil { // If we fail to acquire the issuer cert, return the issued certificate - do not fail. - logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err) + log.Printf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err) } else { issuerCert = pemEncode(derCertificateBytes(issuerCert)) // If bundle is true, we want to return a certificate bundle. // To do this, we append the issuer cert to the issued cert. if bundle { - issuedCert = append(issuedCert, issuerCert...) + cert = append(cert, issuerCert...) } + + certRes.IssuerCertificate = issuerCert } - - certRes.Certificate = issuedCert - certRes.IssuerCertificate = issuerCert - logf("[INFO][%s] Server responded with a certificate.", certRes.Domain) - return true, nil } - // The certificate was granted but is not yet issued. - // Check retry-after and loop. - ra := resp.Header.Get("Retry-After") - retryAfter, err := strconv.Atoi(ra) - if err != nil { - return false, err - } - - logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", certRes.Domain, retryAfter) - time.Sleep(time.Duration(retryAfter) * time.Second) + certRes.Certificate = cert + certRes.CertURL = order.Certificate + certRes.CertStableURL = order.Certificate + log.Printf("[INFO][%s] Server responded with a certificate.", certRes.Domain) + return true, nil + case "processing": return false, nil - default: - return false, handleHTTPError(resp) + case "invalid": + return false, errors.New("Order has invalid state: invalid") } + + return false, nil } // getIssuerCertificate requests the issuer certificate func (c *Client) getIssuerCertificate(url string) ([]byte, error) { - logf("[INFO] acme: Requesting issuer cert from %s", url) + log.Printf("[INFO] acme: Requesting issuer cert from %s", url) resp, err := httpGet(url) if err != nil { return nil, err @@ -786,10 +806,10 @@ func parseLinks(links []string) map[string]string { // validate makes the ACME server start validating a // challenge response, only returning once it is done. -func validate(j *jws, domain, uri string, chlng challenge) error { - var challengeResponse challenge +func validate(j *jws, domain, uri string, c challenge) error { + var chlng challenge - hdr, err := postJSON(j, uri, chlng, &challengeResponse) + hdr, err := postJSON(j, uri, c, &chlng) if err != nil { return err } @@ -797,27 +817,26 @@ func validate(j *jws, domain, uri string, chlng challenge) error { // After the path is sent, the ACME server will access our server. // Repeatedly check the server for an updated status on our request. for { - switch challengeResponse.Status { + switch chlng.Status { case "valid": - logf("[INFO][%s] The server validated our request", domain) + log.Printf("[INFO][%s] The server validated our request", domain) return nil case "pending": - break case "invalid": - return handleChallengeError(challengeResponse) + return handleChallengeError(chlng) default: - return errors.New("The server returned an unexpected state.") + return errors.New("the server returned an unexpected state") } ra, err := strconv.Atoi(hdr.Get("Retry-After")) if err != nil { // The ACME server MUST return a Retry-After. // If it doesn't, we'll just poll hard. - ra = 1 + ra = 5 } time.Sleep(time.Duration(ra) * time.Second) - hdr, err = getJSON(uri, &challengeResponse) + hdr, err = getJSON(uri, &chlng) if err != nil { return err } diff --git a/acme/client_test.go b/acme/client_test.go index b18334c8..1e51b9a6 100644 --- a/acme/client_test.go +++ b/acme/client_test.go @@ -27,7 +27,13 @@ func TestNewClient(t *testing.T) { } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"}) + data, _ := json.Marshal(directory{ + NewNonceURL: "http://test", + NewAccountURL: "http://test", + NewOrderURL: "http://test", + RevokeCertURL: "http://test", + KeyChangeURL: "http://test", + }) w.Write(data) })) @@ -47,7 +53,7 @@ func TestNewClient(t *testing.T) { t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType) } - if expected, actual := 2, len(client.solvers); actual != expected { + if expected, actual := 1, len(client.solvers); actual != expected { t.Fatalf("Expected %d solver(s), got %d", expected, actual) } } @@ -65,7 +71,13 @@ func TestClientOptPort(t *testing.T) { } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"}) + data, _ := json.Marshal(directory{ + NewNonceURL: "http://test", + NewAccountURL: "http://test", + NewOrderURL: "http://test", + RevokeCertURL: "http://test", + KeyChangeURL: "http://test", + }) w.Write(data) })) @@ -76,7 +88,6 @@ func TestClientOptPort(t *testing.T) { t.Fatalf("Could not create client: %v", err) } client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) - client.SetTLSAddress(net.JoinHostPort(optHost, optPort)) httpSolver, ok := client.solvers[HTTP01].(*httpChallenge) if !ok { @@ -92,31 +103,13 @@ func TestClientOptPort(t *testing.T) { t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got) } - httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge) - if !ok { - t.Fatal("Expected tls-sni-01 solver to be httpChallenge type") - } - if httpsSolver.jws != client.jws { - t.Error("Expected tls-sni-01 to have same jws as client") - } - if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort { - t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got) - } - if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost { - t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got) - } - // test setting different host optHost = "127.0.0.1" client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) - client.SetTLSAddress(net.JoinHostPort(optHost, optPort)) if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost { t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got) } - if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort { - t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got) - } } func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { @@ -124,12 +117,12 @@ func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { time.Sleep(250 * time.Millisecond) w.Header().Add("Replay-Nonce", "12345") w.Header().Add("Retry-After", "0") - writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URI: "http://example.com/", Token: "token"}) + writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"}) })) defer ts.Close() privKey, _ := rsa.GenerateKey(rand.Reader, 512) - j := &jws{privKey: privKey, directoryURL: ts.URL} + j := &jws{privKey: privKey, getNonceURL: ts.URL} ch := make(chan bool) resultCh := make(chan bool) go func() { @@ -163,12 +156,12 @@ func TestValidate(t *testing.T) { case "POST": st := statuses[0] statuses = statuses[1:] - writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"}) + writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}) case "GET": st := statuses[0] statuses = statuses[1:] - writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"}) + writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}) default: http.Error(w, r.Method, http.StatusMethodNotAllowed) @@ -177,7 +170,7 @@ func TestValidate(t *testing.T) { defer ts.Close() privKey, _ := rsa.GenerateKey(rand.Reader, 512) - j := &jws{privKey: privKey, directoryURL: ts.URL} + j := &jws{privKey: privKey, getNonceURL: ts.URL} tsts := []struct { name string @@ -186,10 +179,10 @@ func TestValidate(t *testing.T) { }{ {"POST-unexpected", []string{"weird"}, "unexpected"}, {"POST-valid", []string{"valid"}, ""}, - {"POST-invalid", []string{"invalid"}, "Error Detail"}, + {"POST-invalid", []string{"invalid"}, "Error"}, {"GET-unexpected", []string{"pending", "weird"}, "unexpected"}, {"GET-valid", []string{"pending", "valid"}, ""}, - {"GET-invalid", []string{"pending", "invalid"}, "Error Detail"}, + {"GET-invalid", []string{"pending", "invalid"}, "Error"}, } for _, tst := range tsts { @@ -209,9 +202,15 @@ func TestGetChallenges(t *testing.T) { case "GET", "HEAD": w.Header().Add("Replay-Nonce", "12345") w.Header().Add("Retry-After", "0") - writeJSONResponse(w, directory{NewAuthzURL: ts.URL, NewCertURL: ts.URL, NewRegURL: ts.URL, RevokeCertURL: ts.URL}) + writeJSONResponse(w, directory{ + NewNonceURL: ts.URL, + NewAccountURL: ts.URL, + NewOrderURL: ts.URL, + RevokeCertURL: ts.URL, + KeyChangeURL: ts.URL, + }) case "POST": - writeJSONResponse(w, authorization{}) + writeJSONResponse(w, orderMessage{}) } })) defer ts.Close() @@ -224,7 +223,7 @@ func TestGetChallenges(t *testing.T) { } user := mockUser{ email: "test@test.com", - regres: &RegistrationResource{NewAuthzURL: ts.URL}, + regres: &RegistrationResource{URI: ts.URL}, privatekey: key, } @@ -233,12 +232,60 @@ func TestGetChallenges(t *testing.T) { t.Fatalf("Could not create client: %v", err) } - _, failures := client.getChallenges([]string{"example.com"}) - if failures["example.com"] == nil { + _, err = client.createOrderForIdentifiers([]string{"example.com"}) + if err != nil { t.Fatal("Expecting \"Server did not provide next link to proceed\" error, got nil") } } +func TestResolveAccountByKey(t *testing.T) { + keyBits := 512 + keyType := RSA2048 + key, err := rsa.GenerateKey(rand.Reader, keyBits) + if err != nil { + t.Fatal("Could not generate test key:", err) + } + user := mockUser{ + email: "test@test.com", + regres: new(RegistrationResource), + privatekey: key, + } + + var ts *httptest.Server + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/directory": + writeJSONResponse(w, directory{ + NewNonceURL: ts.URL + "/nonce", + NewAccountURL: ts.URL + "/account", + NewOrderURL: ts.URL + "/newOrder", + RevokeCertURL: ts.URL + "/revokeCert", + KeyChangeURL: ts.URL + "/keyChange", + }) + case "/nonce": + w.Header().Add("Replay-Nonce", "12345") + w.Header().Add("Retry-After", "0") + case "/account": + w.Header().Set("Location", ts.URL+"/account_recovery") + case "/account_recovery": + writeJSONResponse(w, accountMessage{ + Status: "valid", + }) + } + })) + + client, err := NewClient(ts.URL+"/directory", user, keyType) + if err != nil { + t.Fatalf("Could not create client: %v", err) + } + + if res, err := client.ResolveAccountByKey(); err != nil { + t.Fatalf("Unexpected error resolving account by key: %v", err) + } else if res.Body.Status != "valid" { + t.Errorf("Unexpected account status: %v", res.Body.Status) + } +} + // writeJSONResponse marshals the body as JSON and writes it to the response. func writeJSONResponse(w http.ResponseWriter, body interface{}) { bs, err := json.Marshal(body) diff --git a/acme/crypto.go b/acme/crypto.go index fa868a90..7d4f4425 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -9,6 +9,7 @@ import ( "crypto/rsa" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "encoding/base64" "encoding/pem" "errors" @@ -17,12 +18,10 @@ import ( "io/ioutil" "math/big" "net/http" - "strings" "time" - "encoding/asn1" - "golang.org/x/crypto/ocsp" + jose "gopkg.in/square/go-jose.v2" ) // KeyType represents the key algo as well as the key size or curve to use. @@ -118,6 +117,10 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { defer req.Body.Close() ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024)) + if err != nil { + return nil, nil, err + } + ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert) if err != nil { return nil, nil, err @@ -136,9 +139,9 @@ func getKeyAuthorization(token string, key interface{}) (string, error) { } // Generate the Key Authorization for the challenge - jwk := keyAsJWK(publicKey) + jwk := &jose.JSONWebKey{Key: publicKey} if jwk == nil { - return "", errors.New("Could not generate JWK from key.") + return "", errors.New("could not generate JWK from key") } thumbBytes, err := jwk.Thumbprint(crypto.SHA256) if err != nil { @@ -146,11 +149,7 @@ func getKeyAuthorization(token string, key interface{}) (string, error) { } // unpad the base64URL - keyThumb := base64.URLEncoding.EncodeToString(thumbBytes) - index := strings.Index(keyThumb, "=") - if index != -1 { - keyThumb = keyThumb[:index] - } + keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes) return token + "." + keyThumb, nil } @@ -177,7 +176,7 @@ func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { } if len(certificates) == 0 { - return nil, errors.New("No certificates were found while parsing the bundle.") + return nil, errors.New("no certificates were found while parsing the bundle") } return certificates, nil @@ -192,7 +191,7 @@ func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { case "EC PRIVATE KEY": return x509.ParseECPrivateKey(keyBlock.Bytes) default: - return nil, errors.New("Unknown PEM header value") + return nil, errors.New("unknown PEM header value") } } @@ -211,7 +210,7 @@ func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { return rsa.GenerateKey(rand.Reader, 8192) } - return nil, fmt.Errorf("Invalid KeyType: %s", keyType) + return nil, fmt.Errorf("invalid KeyType: %s", keyType) } func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { @@ -243,10 +242,8 @@ func pemEncode(data interface{}) []byte { pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} case *rsa.PrivateKey: pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} - break case *x509.CertificateRequest: pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw} - break case derCertificateBytes: pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))} } diff --git a/acme/crypto_test.go b/acme/crypto_test.go index 6f43835f..3ddf5d01 100644 --- a/acme/crypto_test.go +++ b/acme/crypto_test.go @@ -28,7 +28,7 @@ func TestGenerateCSR(t *testing.T) { if err != nil { t.Error("Error generating CSR:", err) } - if csr == nil || len(csr) == 0 { + if len(csr) == 0 { t.Error("Expected CSR with data, but it was nil or length 0") } } diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index d6844dcd..d494501c 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -5,12 +5,12 @@ import ( "encoding/base64" "errors" "fmt" - "log" "net" "strings" "time" "github.com/miekg/dns" + "github.com/xenolf/lego/log" ) type preCheckDNSFunc func(fqdn, value string) (bool, error) @@ -29,6 +29,7 @@ var defaultNameservers = []string{ "google-public-dns-b.google.com:53", } +// RecursiveNameservers are used to pre-check DNS propagations var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers) // DNSTimeout is used to override the default DNS timeout of 10 seconds. @@ -57,8 +58,7 @@ func getNameservers(path string, defaults []string) []string { func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) { keyAuthShaBytes := sha256.Sum256([]byte(keyAuth)) // base64URL encoding without padding - keyAuthSha := base64.URLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) - value = strings.TrimRight(keyAuthSha, "=") + value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) ttl = 120 fqdn = fmt.Sprintf("_acme-challenge.%s.", domain) return @@ -72,7 +72,7 @@ type dnsChallenge struct { } func (s *dnsChallenge) Solve(chlng challenge, domain string) error { - logf("[INFO][%s] acme: Trying to solve DNS-01", domain) + log.Printf("[INFO][%s] acme: Trying to solve DNS-01", domain) if s.provider == nil { return errors.New("No DNS Provider configured") @@ -97,7 +97,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { fqdn, value, _ := DNS01Record(domain, keyAuth) - logf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers) + log.Printf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers) var timeout, interval time.Duration switch provider := s.provider.(type) { @@ -114,7 +114,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { return err } - return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) + return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) } // checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. diff --git a/acme/dns_challenge_manual.go b/acme/dns_challenge_manual.go index 240384e6..cd4c3c8a 100644 --- a/acme/dns_challenge_manual.go +++ b/acme/dns_challenge_manual.go @@ -4,6 +4,8 @@ import ( "bufio" "fmt" "os" + + "github.com/xenolf/lego/log" ) const ( @@ -28,9 +30,9 @@ func (*DNSProviderManual) Present(domain, token, keyAuth string) error { return err } - logf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone) - logf("[INFO] acme: %s", dnsRecord) - logf("[INFO] acme: Press 'Enter' when you are done") + log.Printf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone) + log.Printf("[INFO] acme: %s", dnsRecord) + log.Printf("[INFO] acme: Press 'Enter' when you are done") reader := bufio.NewReader(os.Stdin) _, _ = reader.ReadString('\n') @@ -47,7 +49,7 @@ func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error { return err } - logf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone) - logf("[INFO] acme: %s", dnsRecord) + log.Printf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone) + log.Printf("[INFO] acme: %s", dnsRecord) return nil } diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index 117ac303..e561739b 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -100,9 +100,9 @@ func TestDNSValidServerResponse(t *testing.T) { })) manualProvider, _ := NewDNSProviderManual() - jws := &jws{privKey: privKey, directoryURL: ts.URL} + jws := &jws{privKey: privKey, getNonceURL: ts.URL} solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider} - clientChallenge := challenge{Type: "dns01", Status: "pending", URI: ts.URL, Token: "http8"} + clientChallenge := challenge{Type: "dns01", Status: "pending", URL: ts.URL, Token: "http8"} go func() { time.Sleep(time.Second * 2) diff --git a/acme/error.go b/acme/error.go index e4bc934c..78694deb 100644 --- a/acme/error.go +++ b/acme/error.go @@ -1,6 +1,7 @@ package acme import ( + "bytes" "encoding/json" "fmt" "io/ioutil" @@ -9,8 +10,8 @@ import ( ) const ( - tosAgreementError = "Must agree to subscriber agreement before any further actions" - invalidNonceError = "JWS has invalid anti-replay nonce" + tosAgreementError = "Terms of service have changed" + invalidNonceError = "urn:ietf:params:acme:error:badNonce" ) // RemoteError is the base type for all errors specific to the ACME protocol. @@ -42,27 +43,23 @@ type domainError struct { Error error } -type challengeError struct { - RemoteError - records []validationRecord -} +// ObtainError is returned when there are specific errors available +// per domain. For example in ObtainCertificate +type ObtainError map[string]error -func (c challengeError) Error() string { - - var errStr string - for _, validation := range c.records { - errStr = errStr + fmt.Sprintf("\tValidation for %s:%s\n\tResolved to:\n\t\t%s\n\tUsed: %s\n\n", - validation.Hostname, validation.Port, strings.Join(validation.ResolvedAddresses, "\n\t\t"), validation.UsedAddress) +func (e ObtainError) Error() string { + buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n") + for dom, err := range e { + buffer.WriteString(fmt.Sprintf("[%s] %s\n", dom, err)) } - - return fmt.Sprintf("%s\nError Detail:\n%s", c.RemoteError.Error(), errStr) + return buffer.String() } func handleHTTPError(resp *http.Response) error { var errorDetail RemoteError contentType := resp.Header.Get("Content-Type") - if contentType == "application/json" || contentType == "application/problem+json" { + if contentType == "application/json" || strings.HasPrefix(contentType, "application/problem+json") { err := json.NewDecoder(resp.Body).Decode(&errorDetail) if err != nil { return err @@ -82,7 +79,7 @@ func handleHTTPError(resp *http.Response) error { return TOSError{errorDetail} } - if errorDetail.StatusCode == http.StatusBadRequest && strings.HasPrefix(errorDetail.Detail, invalidNonceError) { + if errorDetail.StatusCode == http.StatusBadRequest && errorDetail.Type == invalidNonceError { return NonceError{errorDetail} } @@ -90,5 +87,5 @@ func handleHTTPError(resp *http.Response) error { } func handleChallengeError(chlng challenge) error { - return challengeError{chlng.Error, chlng.ValidationRecords} + return chlng.Error } diff --git a/acme/http.go b/acme/http.go index e469e0de..b93e5344 100644 --- a/acme/http.go +++ b/acme/http.go @@ -102,7 +102,7 @@ func getJSON(uri string, respBody interface{}) (http.Header, error) { func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) { jsonBytes, err := json.Marshal(reqBody) if err != nil { - return nil, errors.New("Failed to marshal network message...") + return nil, errors.New("Failed to marshal network message") } resp, err := j.post(uri, jsonBytes) diff --git a/acme/http_challenge.go b/acme/http_challenge.go index 95cb1fd8..7659bfc5 100644 --- a/acme/http_challenge.go +++ b/acme/http_challenge.go @@ -2,7 +2,8 @@ package acme import ( "fmt" - "log" + + "github.com/xenolf/lego/log" ) type httpChallenge struct { @@ -18,7 +19,7 @@ func HTTP01ChallengePath(token string) string { func (s *httpChallenge) Solve(chlng challenge, domain string) error { - logf("[INFO][%s] acme: Trying to solve HTTP-01", domain) + log.Printf("[INFO][%s] acme: Trying to solve HTTP-01", domain) // Generate the Key Authorization for the challenge keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) @@ -37,5 +38,5 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error { } }() - return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) + return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) } diff --git a/acme/http_challenge_server.go b/acme/http_challenge_server.go index 64c6a828..214a278f 100644 --- a/acme/http_challenge_server.go +++ b/acme/http_challenge_server.go @@ -5,6 +5,8 @@ import ( "net" "net/http" "strings" + + "github.com/xenolf/lego/log" ) // HTTPProviderServer implements ChallengeProvider for `http-01` challenge @@ -61,9 +63,9 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) { if strings.HasPrefix(r.Host, domain) && r.Method == "GET" { w.Header().Add("Content-Type", "text/plain") w.Write([]byte(keyAuth)) - logf("[INFO][%s] Served key authentication", domain) + log.Printf("[INFO][%s] Served key authentication", domain) } else { - logf("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method) + log.Printf("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method) w.Write([]byte("TEST")) } }) diff --git a/acme/http_challenge_test.go b/acme/http_challenge_test.go index 7400f56d..10f92028 100644 --- a/acme/http_challenge_test.go +++ b/acme/http_challenge_test.go @@ -11,7 +11,7 @@ import ( func TestHTTPChallenge(t *testing.T) { privKey, _ := rsa.GenerateKey(rand.Reader, 512) j := &jws{privKey: privKey} - clientChallenge := challenge{Type: HTTP01, Token: "http1"} + clientChallenge := challenge{Type: string(HTTP01), Token: "http1"} mockValidate := func(_ *jws, _, _ string, chlng challenge) error { uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token resp, err := httpGet(uri) @@ -46,7 +46,7 @@ func TestHTTPChallenge(t *testing.T) { func TestHTTPChallengeInvalidPort(t *testing.T) { privKey, _ := rsa.GenerateKey(rand.Reader, 128) j := &jws{privKey: privKey} - clientChallenge := challenge{Type: HTTP01, Token: "http2"} + clientChallenge := challenge{Type: string(HTTP01), Token: "http2"} solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}} if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil { diff --git a/acme/jws.go b/acme/jws.go index a3943434..bea76210 100644 --- a/acme/jws.go +++ b/acme/jws.go @@ -10,39 +10,29 @@ import ( "net/http" "sync" - "gopkg.in/square/go-jose.v1" + "gopkg.in/square/go-jose.v2" ) type jws struct { - directoryURL string - privKey crypto.PrivateKey - nonces nonceManager -} - -func keyAsJWK(key interface{}) *jose.JsonWebKey { - switch k := key.(type) { - case *ecdsa.PublicKey: - return &jose.JsonWebKey{Key: k, Algorithm: "EC"} - case *rsa.PublicKey: - return &jose.JsonWebKey{Key: k, Algorithm: "RSA"} - - default: - return nil - } + getNonceURL string + privKey crypto.PrivateKey + kid string + nonces nonceManager } // Posts a JWS signed message to the specified URL. // It does NOT close the response body, so the caller must // do that if no error was returned. func (j *jws) post(url string, content []byte) (*http.Response, error) { - signedContent, err := j.signContent(content) + signedContent, err := j.signContent(url, content) if err != nil { - return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) + return nil, fmt.Errorf("failed to sign content -> %s", err.Error()) } - resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize()))) + data := bytes.NewBuffer([]byte(signedContent.FullSerialize())) + resp, err := httpPost(url, "application/jose+json", data) if err != nil { - return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error()) + return nil, fmt.Errorf("failed to HTTP POST to %s -> %s", url, err.Error()) } nonce, nonceErr := getNonceFromResponse(resp) @@ -53,7 +43,7 @@ func (j *jws) post(url string, content []byte) (*http.Response, error) { return resp, nil } -func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) { +func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, error) { var alg jose.SignatureAlgorithm switch k := j.privKey.(type) { @@ -67,25 +57,71 @@ func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) { } } - signer, err := jose.NewSigner(alg, j.privKey) - if err != nil { - return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error()) + jsonKey := jose.JSONWebKey{ + Key: j.privKey, + KeyID: j.kid, + } + + signKey := jose.SigningKey{ + Algorithm: alg, + Key: jsonKey, + } + options := jose.SignerOptions{ + NonceSource: j, + ExtraHeaders: make(map[jose.HeaderKey]interface{}), + } + options.ExtraHeaders["url"] = url + if j.kid == "" { + options.EmbedJWK = true + } + + signer, err := jose.NewSigner(signKey, &options) + if err != nil { + return nil, fmt.Errorf("failed to create jose signer -> %s", err.Error()) } - signer.SetNonceSource(j) signed, err := signer.Sign(content) if err != nil { - return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) + return nil, fmt.Errorf("failed to sign content -> %s", err.Error()) } return signed, nil } +func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { + jwk := jose.JSONWebKey{Key: j.privKey} + jwkJSON, err := jwk.Public().MarshalJSON() + if err != nil { + return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error()) + } + + signer, err := jose.NewSigner( + jose.SigningKey{Algorithm: jose.HS256, Key: hmac}, + &jose.SignerOptions{ + EmbedJWK: false, + ExtraHeaders: map[jose.HeaderKey]interface{}{ + "kid": kid, + "url": url, + }, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to create External Account Binding jose signer -> %s", err.Error()) + } + + signed, err := signer.Sign(jwkJSON) + if err != nil { + return nil, fmt.Errorf("failed to External Account Binding sign content -> %s", err.Error()) + } + + return signed, nil +} + func (j *jws) Nonce() (string, error) { if nonce, ok := j.nonces.Pop(); ok { return nonce, nil } - return getNonce(j.directoryURL) + return getNonce(j.getNonceURL) } type nonceManager struct { @@ -115,7 +151,7 @@ func (n *nonceManager) Push(nonce string) { func getNonce(url string) (string, error) { resp, err := httpHead(url) if err != nil { - return "", fmt.Errorf("Failed to get nonce from HTTP HEAD -> %s", err.Error()) + return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %s", err.Error()) } return getNonceFromResponse(resp) @@ -124,7 +160,7 @@ func getNonce(url string) (string, error) { func getNonceFromResponse(resp *http.Response) (string, error) { nonce := resp.Header.Get("Replay-Nonce") if nonce == "" { - return "", fmt.Errorf("Server did not respond with a proper nonce header.") + return "", fmt.Errorf("server did not respond with a proper nonce header") } return nonce, nil diff --git a/acme/messages.go b/acme/messages.go index 79ccf154..6946cc15 100644 --- a/acme/messages.go +++ b/acme/messages.go @@ -1,59 +1,62 @@ package acme import ( + "encoding/json" "time" - - "gopkg.in/square/go-jose.v1" ) -type directory struct { - NewAuthzURL string `json:"new-authz"` - NewCertURL string `json:"new-cert"` - NewRegURL string `json:"new-reg"` - RevokeCertURL string `json:"revoke-cert"` -} - -type registrationMessage struct { - Resource string `json:"resource"` - Contact []string `json:"contact"` - Delete bool `json:"delete,omitempty"` -} - -// Registration is returned by the ACME server after the registration -// The client implementation should save this registration somewhere. -type Registration struct { - Resource string `json:"resource,omitempty"` - ID int `json:"id"` - Key jose.JsonWebKey `json:"key"` - Contact []string `json:"contact"` - Agreement string `json:"agreement,omitempty"` - Authorizations string `json:"authorizations,omitempty"` - Certificates string `json:"certificates,omitempty"` -} - // RegistrationResource represents all important informations about a registration // of which the client needs to keep track itself. type RegistrationResource struct { - Body Registration `json:"body,omitempty"` - URI string `json:"uri,omitempty"` - NewAuthzURL string `json:"new_authzr_uri,omitempty"` - TosURL string `json:"terms_of_service,omitempty"` + Body accountMessage `json:"body,omitempty"` + URI string `json:"uri,omitempty"` } -type authorizationResource struct { - Body authorization - Domain string - NewCertURL string - AuthURL string +type directory struct { + NewNonceURL string `json:"newNonce"` + NewAccountURL string `json:"newAccount"` + NewOrderURL string `json:"newOrder"` + RevokeCertURL string `json:"revokeCert"` + KeyChangeURL string `json:"keyChange"` + Meta struct { + TermsOfService string `json:"termsOfService"` + Website string `json:"website"` + CaaIdentities []string `json:"caaIdentities"` + ExternalAccountRequired bool `json:"externalAccountRequired"` + } `json:"meta"` +} + +type accountMessage struct { + Status string `json:"status,omitempty"` + Contact []string `json:"contact,omitempty"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` + Orders string `json:"orders,omitempty"` + OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` + ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` +} + +type orderResource struct { + URL string `json:"url,omitempty"` + Domains []string `json:"domains,omitempty"` + orderMessage `json:"body,omitempty"` +} + +type orderMessage struct { + Status string `json:"status,omitempty"` + Expires string `json:"expires,omitempty"` + Identifiers []identifier `json:"identifiers"` + NotBefore string `json:"notBefore,omitempty"` + NotAfter string `json:"notAfter,omitempty"` + Authorizations []string `json:"authorizations,omitempty"` + Finalize string `json:"finalize,omitempty"` + Certificate string `json:"certificate,omitempty"` } type authorization struct { - Resource string `json:"resource,omitempty"` - Identifier identifier `json:"identifier"` - Status string `json:"status,omitempty"` - Expires time.Time `json:"expires,omitempty"` - Challenges []challenge `json:"challenges,omitempty"` - Combinations [][]int `json:"combinations,omitempty"` + Status string `json:"status"` + Expires time.Time `json:"expires"` + Identifier identifier `json:"identifier"` + Challenges []challenge `json:"challenges"` } type identifier struct { @@ -61,41 +64,26 @@ type identifier struct { Value string `json:"value"` } -type validationRecord struct { - URI string `json:"url,omitempty"` - Hostname string `json:"hostname,omitempty"` - Port string `json:"port,omitempty"` - ResolvedAddresses []string `json:"addressesResolved,omitempty"` - UsedAddress string `json:"addressUsed,omitempty"` -} - type challenge struct { - Resource string `json:"resource,omitempty"` - Type Challenge `json:"type,omitempty"` - Status string `json:"status,omitempty"` - URI string `json:"uri,omitempty"` - Token string `json:"token,omitempty"` - KeyAuthorization string `json:"keyAuthorization,omitempty"` - TLS bool `json:"tls,omitempty"` - Iterations int `json:"n,omitempty"` - Error RemoteError `json:"error,omitempty"` - ValidationRecords []validationRecord `json:"validationRecord,omitempty"` + URL string `json:"url"` + Type string `json:"type"` + Status string `json:"status"` + Token string `json:"token"` + Validated time.Time `json:"validated"` + KeyAuthorization string `json:"keyAuthorization"` + Error RemoteError `json:"error"` } type csrMessage struct { - Resource string `json:"resource,omitempty"` - Csr string `json:"csr"` - Authorizations []string `json:"authorizations"` + Csr string `json:"csr"` } type revokeCertMessage struct { - Resource string `json:"resource"` Certificate string `json:"certificate"` } type deactivateAuthMessage struct { - Resource string `json:"resource,omitempty"` - Status string `jsom:"status"` + Status string `jsom:"status"` } // CertificateResource represents a CA issued certificate. diff --git a/acme/pop_challenge.go b/acme/pop_challenge.go deleted file mode 100644 index 8d2a213b..00000000 --- a/acme/pop_challenge.go +++ /dev/null @@ -1 +0,0 @@ -package acme diff --git a/acme/tls_sni_challenge.go b/acme/tls_sni_challenge.go deleted file mode 100644 index 34383cbf..00000000 --- a/acme/tls_sni_challenge.go +++ /dev/null @@ -1,67 +0,0 @@ -package acme - -import ( - "crypto/rsa" - "crypto/sha256" - "crypto/tls" - "encoding/hex" - "fmt" - "log" -) - -type tlsSNIChallenge struct { - jws *jws - validate validateFunc - provider ChallengeProvider -} - -func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error { - // FIXME: https://github.com/ietf-wg-acme/acme/pull/22 - // Currently we implement this challenge to track boulder, not the current spec! - - logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain) - - // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey) - if err != nil { - return err - } - - err = t.provider.Present(domain, chlng.Token, keyAuth) - if err != nil { - return fmt.Errorf("[%s] error presenting token: %v", domain, err) - } - defer func() { - err := t.provider.CleanUp(domain, chlng.Token, keyAuth) - if err != nil { - log.Printf("[%s] error cleaning up: %v", domain, err) - } - }() - return t.validate(t.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) -} - -// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge -func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, string, error) { - // generate a new RSA key for the certificates - tempPrivKey, err := generatePrivateKey(RSA2048) - if err != nil { - return tls.Certificate{}, "", err - } - rsaPrivKey := tempPrivKey.(*rsa.PrivateKey) - rsaPrivPEM := pemEncode(rsaPrivKey) - - zBytes := sha256.Sum256([]byte(keyAuth)) - z := hex.EncodeToString(zBytes[:sha256.Size]) - domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:]) - tempCertPEM, err := generatePemCert(rsaPrivKey, domain) - if err != nil { - return tls.Certificate{}, "", err - } - - certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM) - if err != nil { - return tls.Certificate{}, "", err - } - - return certificate, domain, nil -} diff --git a/acme/tls_sni_challenge_server.go b/acme/tls_sni_challenge_server.go deleted file mode 100644 index df00fbb5..00000000 --- a/acme/tls_sni_challenge_server.go +++ /dev/null @@ -1,62 +0,0 @@ -package acme - -import ( - "crypto/tls" - "fmt" - "net" - "net/http" -) - -// TLSProviderServer implements ChallengeProvider for `TLS-SNI-01` challenge -// It may be instantiated without using the NewTLSProviderServer function if -// you want only to use the default values. -type TLSProviderServer struct { - iface string - port string - done chan bool - listener net.Listener -} - -// NewTLSProviderServer creates a new TLSProviderServer on the selected interface and port. -// Setting iface and / or port to an empty string will make the server fall back to -// the "any" interface and port 443 respectively. -func NewTLSProviderServer(iface, port string) *TLSProviderServer { - return &TLSProviderServer{iface: iface, port: port} -} - -// Present makes the keyAuth available as a cert -func (s *TLSProviderServer) Present(domain, token, keyAuth string) error { - if s.port == "" { - s.port = "443" - } - - cert, _, err := TLSSNI01ChallengeCert(keyAuth) - if err != nil { - return err - } - - tlsConf := new(tls.Config) - tlsConf.Certificates = []tls.Certificate{cert} - - s.listener, err = tls.Listen("tcp", net.JoinHostPort(s.iface, s.port), tlsConf) - if err != nil { - return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err) - } - - s.done = make(chan bool) - go func() { - http.Serve(s.listener, nil) - s.done <- true - }() - return nil -} - -// CleanUp closes the HTTP server. -func (s *TLSProviderServer) CleanUp(domain, token, keyAuth string) error { - if s.listener == nil { - return nil - } - s.listener.Close() - <-s.done - return nil -} diff --git a/acme/tls_sni_challenge_test.go b/acme/tls_sni_challenge_test.go deleted file mode 100644 index 83b2833a..00000000 --- a/acme/tls_sni_challenge_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package acme - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/tls" - "encoding/hex" - "fmt" - "strings" - "testing" -) - -func TestTLSSNIChallenge(t *testing.T) { - privKey, _ := rsa.GenerateKey(rand.Reader, 512) - j := &jws{privKey: privKey} - clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni1"} - mockValidate := func(_ *jws, _, _ string, chlng challenge) error { - conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{ - InsecureSkipVerify: true, - }) - if err != nil { - t.Errorf("Expected to connect to challenge server without an error. %s", err.Error()) - } - - // Expect the server to only return one certificate - connState := conn.ConnectionState() - if count := len(connState.PeerCertificates); count != 1 { - t.Errorf("Expected the challenge server to return exactly one certificate but got %d", count) - } - - remoteCert := connState.PeerCertificates[0] - if count := len(remoteCert.DNSNames); count != 1 { - t.Errorf("Expected the challenge certificate to have exactly one DNSNames entry but had %d", count) - } - - zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization)) - z := hex.EncodeToString(zBytes[:sha256.Size]) - domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:]) - - if remoteCert.DNSNames[0] != domain { - t.Errorf("Expected the challenge certificate DNSName to match %s but was %s", domain, remoteCert.DNSNames[0]) - } - - return nil - } - solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &TLSProviderServer{port: "23457"}} - - if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil { - t.Errorf("Solve error: got %v, want nil", err) - } -} - -func TestTLSSNIChallengeInvalidPort(t *testing.T) { - privKey, _ := rsa.GenerateKey(rand.Reader, 128) - j := &jws{privKey: privKey} - clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"} - solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &TLSProviderServer{port: "123456"}} - - if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil { - t.Errorf("Solve error: got %v, want error", err) - } else if want, want18 := "invalid port 123456", "123456: invalid port"; !strings.HasSuffix(err.Error(), want) && !strings.HasSuffix(err.Error(), want18) { - t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want) - } -} diff --git a/cli.go b/cli.go index 1fce0c56..ba51c1bc 100644 --- a/cli.go +++ b/cli.go @@ -4,26 +4,15 @@ package main import ( "fmt" - "log" "os" "path" "text/tabwriter" "github.com/urfave/cli" "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/log" ) -// Logger is used to log errors; if nil, the default log.Logger is used. -var Logger *log.Logger - -// logger is an helper function to retrieve the available logger -func logger() *log.Logger { - if Logger == nil { - Logger = log.New(os.Stderr, "", log.LstdFlags) - } - return Logger -} - var ( version = "dev" ) @@ -45,7 +34,7 @@ func main() { app.Before = func(c *cli.Context) error { if c.GlobalString("path") == "" { - logger().Fatal("Could not determine current working directory. Please pass --path.") + log.Fatal("Could not determine current working directory. Please pass --path.") } return nil } @@ -124,6 +113,18 @@ func main() { Name: "accept-tos, a", Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.", }, + cli.BoolFlag{ + Name: "eab", + Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.", + }, + cli.StringFlag{ + Name: "kid", + Usage: "Key identifier from External CA. Used for External Account Binding.", + }, + cli.StringFlag{ + Name: "hmac", + Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.", + }, cli.StringFlag{ Name: "key-type, k", Value: "rsa2048", @@ -136,7 +137,7 @@ func main() { }, cli.StringSliceFlag{ Name: "exclude, x", - Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"tls-sni-01\", \"dns-01\",.", + Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"dns-01\".", }, cli.StringFlag{ Name: "webroot", @@ -150,10 +151,6 @@ func main() { Name: "http", Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port", }, - cli.StringFlag{ - Name: "tls", - Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port", - }, cli.StringFlag{ Name: "dns", Usage: "Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.", diff --git a/cli_handlers.go b/cli_handlers.go index b8790c4b..c5aaafe8 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/json" "encoding/pem" + "fmt" "io/ioutil" "net/http" "os" @@ -15,6 +16,7 @@ import ( "github.com/urfave/cli" "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/log" "github.com/xenolf/lego/providers/dns" "github.com/xenolf/lego/providers/http/memcached" "github.com/xenolf/lego/providers/http/webroot" @@ -50,12 +52,12 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { err := checkFolder(c.GlobalString("path")) if err != nil { - logger().Fatalf("Could not check/create path: %s", err.Error()) + log.Fatalf("Could not check/create path: %v", err) } conf := NewConfiguration(c) if len(c.GlobalString("email")) == 0 { - logger().Fatal("You have to pass an account (email address) to the program using --email or -m") + log.Fatal("You have to pass an account (email address) to the program using --email or -m") } //TODO: move to account struct? Currently MUST pass email. @@ -63,12 +65,14 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { keyType, err := conf.KeyType() if err != nil { - logger().Fatal(err.Error()) + log.Fatal(err) } + acme.UserAgent = fmt.Sprintf("le-go/cli %s", c.App.Version) + client, err := acme.NewClient(c.GlobalString("server"), acc, keyType) if err != nil { - logger().Fatalf("Could not create client: %s", err.Error()) + log.Fatalf("Could not create client: %v", err) } if len(c.GlobalStringSlice("exclude")) > 0 { @@ -78,75 +82,88 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { if c.GlobalIsSet("webroot") { provider, err := webroot.NewHTTPProvider(c.GlobalString("webroot")) if err != nil { - logger().Fatal(err) + log.Fatal(err) } - client.SetChallengeProvider(acme.HTTP01, provider) + err = client.SetChallengeProvider(acme.HTTP01, provider) + if err != nil { + log.Fatal(err) + } // --webroot=foo indicates that the user specifically want to do a HTTP challenge // infer that the user also wants to exclude all other challenges - client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) + client.ExcludeChallenges([]acme.Challenge{acme.DNS01}) } if c.GlobalIsSet("memcached-host") { provider, err := memcached.NewMemcachedProvider(c.GlobalStringSlice("memcached-host")) if err != nil { - logger().Fatal(err) + log.Fatal(err) } - client.SetChallengeProvider(acme.HTTP01, provider) + err = client.SetChallengeProvider(acme.HTTP01, provider) + if err != nil { + log.Fatal(err) + } // --memcached-host=foo:11211 indicates that the user specifically want to do a HTTP challenge // infer that the user also wants to exclude all other challenges - client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) + client.ExcludeChallenges([]acme.Challenge{acme.DNS01}) } if c.GlobalIsSet("http") { - if strings.Index(c.GlobalString("http"), ":") == -1 { - logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.") + if !strings.Contains(c.GlobalString("http"), ":") { + log.Fatalf("The --http switch only accepts interface:port or :port for its argument.") } - client.SetHTTPAddress(c.GlobalString("http")) - } - if c.GlobalIsSet("tls") { - if strings.Index(c.GlobalString("tls"), ":") == -1 { - logger().Fatalf("The --tls switch only accepts interface:port or :port for its argument.") + err = client.SetHTTPAddress(c.GlobalString("http")) + if err != nil { + log.Fatal(err) } - client.SetTLSAddress(c.GlobalString("tls")) } if c.GlobalIsSet("dns") { provider, err := dns.NewDNSChallengeProviderByName(c.GlobalString("dns")) if err != nil { - logger().Fatal(err) + log.Fatal(err) } - client.SetChallengeProvider(acme.DNS01, provider) + err = client.SetChallengeProvider(acme.DNS01, provider) + if err != nil { + log.Fatal(err) + } // --dns=foo indicates that the user specifically want to do a DNS challenge // infer that the user also wants to exclude all other challenges - client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) + client.ExcludeChallenges([]acme.Challenge{acme.HTTP01}) + } + + if client.GetExternalAccountRequired() && !c.GlobalIsSet("eab") { + log.Fatal("Server requires External Account Binding. Use --eab with --kid and --hmac.") } return conf, acc, client } -func saveCertRes(certRes acme.CertificateResource, conf *Configuration) { +func saveCertRes(certRes *acme.CertificateResource, conf *Configuration) { + // make sure no funny chars are in the cert names (like wildcards ;)) + domainName := strings.Replace(certRes.Domain, "*", "_", -1) + // We store the certificate, private key and metadata in different files // as web servers would not be able to work with a combined file. - certOut := path.Join(conf.CertPath(), certRes.Domain+".crt") - privOut := path.Join(conf.CertPath(), certRes.Domain+".key") - pemOut := path.Join(conf.CertPath(), certRes.Domain+".pem") - metaOut := path.Join(conf.CertPath(), certRes.Domain+".json") - issuerOut := path.Join(conf.CertPath(), certRes.Domain+".issuer.crt") + certOut := path.Join(conf.CertPath(), domainName+".crt") + privOut := path.Join(conf.CertPath(), domainName+".key") + pemOut := path.Join(conf.CertPath(), domainName+".pem") + metaOut := path.Join(conf.CertPath(), domainName+".json") + issuerOut := path.Join(conf.CertPath(), domainName+".issuer.crt") err := ioutil.WriteFile(certOut, certRes.Certificate, 0600) if err != nil { - logger().Fatalf("Unable to save Certificate for domain %s\n\t%s", certRes.Domain, err.Error()) + log.Fatalf("Unable to save Certificate for domain %s\n\t%v", certRes.Domain, err) } if certRes.IssuerCertificate != nil { err = ioutil.WriteFile(issuerOut, certRes.IssuerCertificate, 0600) if err != nil { - logger().Fatalf("Unable to save IssuerCertificate for domain %s\n\t%s", certRes.Domain, err.Error()) + log.Fatalf("Unable to save IssuerCertificate for domain %s\n\t%v", certRes.Domain, err) } } @@ -154,70 +171,59 @@ func saveCertRes(certRes acme.CertificateResource, conf *Configuration) { // if we were given a CSR, we don't know the private key err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600) if err != nil { - logger().Fatalf("Unable to save PrivateKey for domain %s\n\t%s", certRes.Domain, err.Error()) + log.Fatalf("Unable to save PrivateKey for domain %s\n\t%v", certRes.Domain, err) } if conf.context.GlobalBool("pem") { err = ioutil.WriteFile(pemOut, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil), 0600) if err != nil { - logger().Fatalf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%s", certRes.Domain, err.Error()) + log.Fatalf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%v", certRes.Domain, err) } } } else if conf.context.GlobalBool("pem") { // we don't have the private key; can't write the .pem file - logger().Fatalf("Unable to save pem without private key for domain %s\n\t%s; are you using a CSR?", certRes.Domain, err.Error()) + log.Fatalf("Unable to save pem without private key for domain %s\n\t%v; are you using a CSR?", certRes.Domain, err) } jsonBytes, err := json.MarshalIndent(certRes, "", "\t") if err != nil { - logger().Fatalf("Unable to marshal CertResource for domain %s\n\t%s", certRes.Domain, err.Error()) + log.Fatalf("Unable to marshal CertResource for domain %s\n\t%v", certRes.Domain, err) } err = ioutil.WriteFile(metaOut, jsonBytes, 0600) if err != nil { - logger().Fatalf("Unable to save CertResource for domain %s\n\t%s", certRes.Domain, err.Error()) + log.Fatalf("Unable to save CertResource for domain %s\n\t%v", certRes.Domain, err) } } -func handleTOS(c *cli.Context, client *acme.Client, acc *Account) { +func handleTOS(c *cli.Context, client *acme.Client) bool { // Check for a global accept override if c.GlobalBool("accept-tos") { - err := client.AgreeToTOS() - if err != nil { - logger().Fatalf("Could not agree to TOS: %s", err.Error()) - } - - acc.Save() - return + return true } reader := bufio.NewReader(os.Stdin) - logger().Printf("Please review the TOS at %s", acc.Registration.TosURL) + log.Printf("Please review the TOS at %s", client.GetToSURL()) for { - logger().Println("Do you accept the TOS? Y/n") + log.Println("Do you accept the TOS? Y/n") text, err := reader.ReadString('\n') if err != nil { - logger().Fatalf("Could not read from console: %s", err.Error()) + log.Fatalf("Could not read from console: %v", err) } text = strings.Trim(text, "\r\n") if text == "n" { - logger().Fatal("You did not accept the TOS. Unable to proceed.") + log.Fatal("You did not accept the TOS. Unable to proceed.") } if text == "Y" || text == "y" || text == "" { - err = client.AgreeToTOS() - if err != nil { - logger().Fatalf("Could not agree to TOS: %s", err.Error()) - } - acc.Save() - break + return true } - logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.") + log.Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.") } } @@ -253,18 +259,43 @@ func readCSRFile(filename string) (*x509.CertificateRequest, error) { } func run(c *cli.Context) error { + var err error + conf, acc, client := setup(c) if acc.Registration == nil { - reg, err := client.Register() + accepted := handleTOS(c, client) + if !accepted { + log.Fatal("You did not accept the TOS. Unable to proceed.") + } + + var reg *acme.RegistrationResource + + if c.GlobalBool("eab") { + kid := c.GlobalString("kid") + hmacEncoded := c.GlobalString("hmac") + + if kid == "" || hmacEncoded == "" { + log.Fatalf("Requires arguments --kid and --hmac.") + } + + reg, err = client.RegisterWithExternalAccountBinding( + accepted, + kid, + hmacEncoded, + ) + } else { + reg, err = client.Register(accepted) + } + if err != nil { - logger().Fatalf("Could not complete registration\n\t%s", err.Error()) + log.Fatalf("Could not complete registration\n\t%v", err) } acc.Registration = reg acc.Save() - logger().Print("!!!! HEADS UP !!!!") - logger().Printf(` + log.Print("!!!! HEADS UP !!!!") + log.Printf(` Your account credentials have been saved in your Let's Encrypt configuration directory at "%s". You should make a secure backup of this folder now. This @@ -274,43 +305,32 @@ func run(c *cli.Context) error { } - // If the agreement URL is empty, the account still needs to accept the LE TOS. - if acc.Registration.Body.Agreement == "" { - handleTOS(c, client, acc) - } - // we require either domains or csr, but not both hasDomains := len(c.GlobalStringSlice("domains")) > 0 hasCsr := len(c.GlobalString("csr")) > 0 if hasDomains && hasCsr { - logger().Fatal("Please specify either --domains/-d or --csr/-c, but not both") + log.Fatal("Please specify either --domains/-d or --csr/-c, but not both") } if !hasDomains && !hasCsr { - logger().Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)") + log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)") } - var cert acme.CertificateResource - var failures map[string]error + var cert *acme.CertificateResource if hasDomains { // obtain a certificate, generating a new private key - cert, failures = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil, c.Bool("must-staple")) + cert, err = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil, c.Bool("must-staple")) } else { // read the CSR csr, err := readCSRFile(c.GlobalString("csr")) - if err != nil { - // we couldn't read the CSR - failures = map[string]error{"csr": err} - } else { + if err == nil { // obtain a certificate for this CSR - cert, failures = client.ObtainCertificateForCSR(*csr, !c.Bool("no-bundle")) + cert, err = client.ObtainCertificateForCSR(*csr, !c.Bool("no-bundle")) } } - if len(failures) > 0 { - for k, v := range failures { - logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error()) - } + if err != nil { + log.Printf("Could not obtain certificates\n\t%v", err) // Make sure to return a non-zero exit code if ObtainSANCertificate // returned at least one error. Due to us not returning partial @@ -318,9 +338,8 @@ func run(c *cli.Context) error { os.Exit(1) } - err := checkFolder(conf.CertPath()) - if err != nil { - logger().Fatalf("Could not check/create path: %s", err.Error()) + if err = checkFolder(conf.CertPath()); err != nil { + log.Fatalf("Could not check/create path: %v", err) } saveCertRes(cert, conf) @@ -331,25 +350,27 @@ func run(c *cli.Context) error { func revoke(c *cli.Context) error { conf, acc, client := setup(c) if acc.Registration == nil { - logger().Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email) + log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email) } - err := checkFolder(conf.CertPath()) - if err != nil { - logger().Fatalf("Could not check/create path: %s", err.Error()) + if err := checkFolder(conf.CertPath()); err != nil { + log.Fatalf("Could not check/create path: %v", err) } for _, domain := range c.GlobalStringSlice("domains") { - logger().Printf("Trying to revoke certificate for domain %s", domain) + log.Printf("Trying to revoke certificate for domain %s", domain) certPath := path.Join(conf.CertPath(), domain+".crt") certBytes, err := ioutil.ReadFile(certPath) + if err != nil { + log.Println(err) + } err = client.RevokeCertificate(certBytes) if err != nil { - logger().Fatalf("Error while revoking the certificate for domain %s\n\t%s", domain, err.Error()) + log.Fatalf("Error while revoking the certificate for domain %s\n\t%v", domain, err) } else { - logger().Print("Certificate was revoked.") + log.Println("Certificate was revoked.") } } @@ -359,14 +380,15 @@ func revoke(c *cli.Context) error { func renew(c *cli.Context) error { conf, acc, client := setup(c) if acc.Registration == nil { - logger().Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email) + log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email) } if len(c.GlobalStringSlice("domains")) <= 0 { - logger().Fatal("Please specify at least one domain.") + log.Fatal("Please specify at least one domain.") } domain := c.GlobalStringSlice("domains")[0] + domain = strings.Replace(domain, "*", "_", -1) // load the cert resource from files. // We store the certificate, private key and metadata in different files @@ -377,13 +399,13 @@ func renew(c *cli.Context) error { certBytes, err := ioutil.ReadFile(certPath) if err != nil { - logger().Fatalf("Error while loading the certificate for domain %s\n\t%s", domain, err.Error()) + log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err) } if c.IsSet("days") { expTime, err := acme.GetPEMCertExpiration(certBytes) if err != nil { - logger().Printf("Could not get Certification expiration for domain %s", domain) + log.Printf("Could not get Certification expiration for domain %s", domain) } if int(expTime.Sub(time.Now()).Hours()/24.0) > c.Int("days") { @@ -393,19 +415,18 @@ func renew(c *cli.Context) error { metaBytes, err := ioutil.ReadFile(metaPath) if err != nil { - logger().Fatalf("Error while loading the meta data for domain %s\n\t%s", domain, err.Error()) + log.Fatalf("Error while loading the meta data for domain %s\n\t%v", domain, err) } var certRes acme.CertificateResource - err = json.Unmarshal(metaBytes, &certRes) - if err != nil { - logger().Fatalf("Error while marshalling the meta data for domain %s\n\t%s", domain, err.Error()) + if err := json.Unmarshal(metaBytes, &certRes); err != nil { + log.Fatalf("Error while marshalling the meta data for domain %s\n\t%v", domain, err) } if c.Bool("reuse-key") { keyBytes, err := ioutil.ReadFile(privPath) if err != nil { - logger().Fatalf("Error while loading the private key for domain %s\n\t%s", domain, err.Error()) + log.Fatalf("Error while loading the private key for domain %s\n\t%v", domain, err) } certRes.PrivateKey = keyBytes } @@ -414,7 +435,7 @@ func renew(c *cli.Context) error { newCert, err := client.RenewCertificate(certRes, !c.Bool("no-bundle"), c.Bool("must-staple")) if err != nil { - logger().Fatalf("%s", err.Error()) + log.Fatal(err) } saveCertRes(newCert, conf) diff --git a/crypto.go b/crypto.go index 8b23e2fc..0d42b173 100644 --- a/crypto.go +++ b/crypto.go @@ -52,5 +52,5 @@ func loadPrivateKey(file string) (crypto.PrivateKey, error) { return x509.ParseECPrivateKey(keyBlock.Bytes) } - return nil, errors.New("Unknown private key type.") + return nil, errors.New("unknown private key type") } diff --git a/log/logger.go b/log/logger.go new file mode 100644 index 00000000..291541c0 --- /dev/null +++ b/log/logger.go @@ -0,0 +1,59 @@ +package log + +import ( + "log" + "os" +) + +// Logger is an optional custom logger. +var Logger *log.Logger + +// Fatal writes a log entry. +// It uses Logger if not nil, otherwise it uses the default log.Logger. +func Fatal(args ...interface{}) { + if Logger == nil { + Logger = log.New(os.Stderr, "", log.LstdFlags) + } + + Logger.Fatal(args...) +} + +// Fatalf writes a log entry. +// It uses Logger if not nil, otherwise it uses the default log.Logger. +func Fatalf(format string, args ...interface{}) { + if Logger == nil { + Logger = log.New(os.Stderr, "", log.LstdFlags) + } + + Logger.Fatalf(format, args...) +} + +// Print writes a log entry. +// It uses Logger if not nil, otherwise it uses the default log.Logger. +func Print(args ...interface{}) { + if Logger == nil { + Logger = log.New(os.Stdout, "", log.LstdFlags) + } + + Logger.Print(args...) +} + +// Println writes a log entry. +// It uses Logger if not nil, otherwise it uses the default log.Logger. +func Println(args ...interface{}) { + if Logger == nil { + Logger = log.New(os.Stdout, "", log.LstdFlags) + } + + Logger.Println(args...) +} + +// Printf writes a log entry. +// It uses Logger if not nil, otherwise it uses the default log.Logger. +func Printf(format string, args ...interface{}) { + if Logger == nil { + Logger = log.New(os.Stdout, "", log.LstdFlags) + } + + Logger.Printf(format, args...) +} diff --git a/providers/dns/auroradns/auroradns.go b/providers/dns/auroradns/auroradns.go index 55b48f9b..03d31754 100644 --- a/providers/dns/auroradns/auroradns.go +++ b/providers/dns/auroradns/auroradns.go @@ -2,12 +2,13 @@ package auroradns import ( "fmt" + "os" + "sync" + "github.com/edeckers/auroradnsclient" "github.com/edeckers/auroradnsclient/records" "github.com/edeckers/auroradnsclient/zones" "github.com/xenolf/lego/acme" - "os" - "sync" ) // DNSProvider describes a provider for AuroraDNS @@ -59,7 +60,7 @@ func (provider *DNSProvider) getZoneInformationByName(name string) (zones.ZoneRe } } - return zones.ZoneRecord{}, fmt.Errorf("Could not find Zone record") + return zones.ZoneRecord{}, fmt.Errorf("could not find Zone record") } // Present creates a record with a secret @@ -83,6 +84,9 @@ func (provider *DNSProvider) Present(domain, token, keyAuth string) error { authZone = acme.UnFqdn(authZone) zoneRecord, err := provider.getZoneInformationByName(authZone) + if err != nil { + return fmt.Errorf("could not create record: %v", err) + } reqData := records.CreateRecordRequest{ @@ -94,7 +98,7 @@ func (provider *DNSProvider) Present(domain, token, keyAuth string) error { respData, err := provider.client.CreateRecord(zoneRecord.ID, reqData) if err != nil { - return fmt.Errorf("Could not create record: '%s'.", err) + return fmt.Errorf("could not create record: %v", err) } provider.recordIDsMu.Lock() @@ -113,12 +117,12 @@ func (provider *DNSProvider) CleanUp(domain, token, keyAuth string) error { provider.recordIDsMu.Unlock() if !ok { - return fmt.Errorf("Unknown recordID for '%s'", fqdn) + return fmt.Errorf("unknown recordID for %q", fqdn) } authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { - return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err) + return fmt.Errorf("could not determine zone for domain: %q. %v", domain, err) } authZone = acme.UnFqdn(authZone) diff --git a/providers/dns/auroradns/auroradns_test.go b/providers/dns/auroradns/auroradns_test.go index f4df7fa6..f93bc95d 100644 --- a/providers/dns/auroradns/auroradns_test.go +++ b/providers/dns/auroradns/auroradns_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -var fakeAuroraDNSUserId = "asdf1234" +var fakeAuroraDNSUserID = "asdf1234" var fakeAuroraDNSKey = "key" func TestAuroraDNSPresent(t *testing.T) { @@ -60,7 +60,7 @@ func TestAuroraDNSPresent(t *testing.T) { defer mock.Close() - auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserId, fakeAuroraDNSKey) + auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserID, fakeAuroraDNSKey) if auroraProvider == nil { t.Fatal("Expected non-nil AuroraDNS provider, but was nil") } @@ -123,7 +123,7 @@ func TestAuroraDNSCleanUp(t *testing.T) { })) defer mock.Close() - auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserId, fakeAuroraDNSKey) + auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserID, fakeAuroraDNSKey) if auroraProvider == nil { t.Fatal("Expected non-nil AuroraDNS provider, but was nil") } diff --git a/providers/dns/azure/azure.go b/providers/dns/azure/azure.go index 9022af47..b26d8526 100644 --- a/providers/dns/azure/azure.go +++ b/providers/dns/azure/azure.go @@ -7,12 +7,10 @@ import ( "context" "fmt" "os" + "strings" "time" "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2017-09-01/dns" - - "strings" - "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" "github.com/Azure/go-autorest/autorest/azure" @@ -22,32 +20,31 @@ import ( // DNSProvider is an implementation of the acme.ChallengeProvider interface type DNSProvider struct { - clientId string + clientID string clientSecret string - subscriptionId string - tenantId string + subscriptionID string + tenantID string resourceGroup string - - context context.Context + context context.Context } // NewDNSProvider returns a DNSProvider instance configured for azure. // Credentials must be passed in the environment variables: AZURE_CLIENT_ID, // AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP func NewDNSProvider() (*DNSProvider, error) { - clientId := os.Getenv("AZURE_CLIENT_ID") + clientID := os.Getenv("AZURE_CLIENT_ID") clientSecret := os.Getenv("AZURE_CLIENT_SECRET") - subscriptionId := os.Getenv("AZURE_SUBSCRIPTION_ID") - tenantId := os.Getenv("AZURE_TENANT_ID") + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + tenantID := os.Getenv("AZURE_TENANT_ID") resourceGroup := os.Getenv("AZURE_RESOURCE_GROUP") - return NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup) + return NewDNSProviderCredentials(clientID, clientSecret, subscriptionID, tenantID, resourceGroup) } // NewDNSProviderCredentials uses the supplied credentials to return a // DNSProvider instance configured for azure. -func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup string) (*DNSProvider, error) { - if clientId == "" || clientSecret == "" || subscriptionId == "" || tenantId == "" || resourceGroup == "" { - missingEnvVars := []string{} +func NewDNSProviderCredentials(clientID, clientSecret, subscriptionID, tenantID, resourceGroup string) (*DNSProvider, error) { + if clientID == "" || clientSecret == "" || subscriptionID == "" || tenantID == "" || resourceGroup == "" { + var missingEnvVars []string for _, envVar := range []string{"AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_SUBSCRIPTION_ID", "AZURE_TENANT_ID", "AZURE_RESOURCE_GROUP"} { if os.Getenv(envVar) == "" { missingEnvVars = append(missingEnvVars, envVar) @@ -57,10 +54,10 @@ func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, } return &DNSProvider{ - clientId: clientId, + clientID: clientID, clientSecret: clientSecret, - subscriptionId: subscriptionId, - tenantId: tenantId, + subscriptionID: subscriptionID, + tenantID: tenantID, resourceGroup: resourceGroup, // TODO: A timeout can be added here for cancellation purposes. context: context.Background(), @@ -81,8 +78,12 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error { return err } - rsc := dns.NewRecordSetsClient(c.subscriptionId) + rsc := dns.NewRecordSetsClient(c.subscriptionID) spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint) + if err != nil { + return err + } + rsc.Authorizer = autorest.NewBearerAuthorizer(spt) relative := toRelativeRecord(fqdn, acme.ToFqdn(zone)) @@ -90,16 +91,12 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error { Name: &relative, RecordSetProperties: &dns.RecordSetProperties{ TTL: to.Int64Ptr(60), - TxtRecords: &[]dns.TxtRecord{dns.TxtRecord{Value: &[]string{value}}}, + TxtRecords: &[]dns.TxtRecord{{Value: &[]string{value}}}, }, } + _, err = rsc.CreateOrUpdate(c.context, c.resourceGroup, zone, relative, dns.TXT, rec, "", "") - - if err != nil { - return err - } - - return nil + return err } // Returns the relative record to the domain @@ -117,15 +114,16 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { } relative := toRelativeRecord(fqdn, acme.ToFqdn(zone)) - rsc := dns.NewRecordSetsClient(c.subscriptionId) + rsc := dns.NewRecordSetsClient(c.subscriptionID) spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint) - rsc.Authorizer = autorest.NewBearerAuthorizer(spt) - _, err = rsc.Delete(c.context, c.resourceGroup, zone, relative, dns.TXT, "") if err != nil { return err } - return nil + rsc.Authorizer = autorest.NewBearerAuthorizer(spt) + + _, err = rsc.Delete(c.context, c.resourceGroup, zone, relative, dns.TXT, "") + return err } // Checks that azure has a zone for this domain name. @@ -137,12 +135,14 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) { // Now we want to to Azure and get the zone. spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint) + if err != nil { + return "", err + } - dc := dns.NewZonesClient(c.subscriptionId) + dc := dns.NewZonesClient(c.subscriptionID) dc.Authorizer = autorest.NewBearerAuthorizer(spt) zone, err := dc.Get(c.context, c.resourceGroup, acme.UnFqdn(authZone)) - if err != nil { return "", err } @@ -154,9 +154,9 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) { // NewServicePrincipalTokenFromCredentials creates a new ServicePrincipalToken using values of the // passed credentials map. func (c *DNSProvider) newServicePrincipalTokenFromCredentials(scope string) (*adal.ServicePrincipalToken, error) { - oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, c.tenantId) + oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, c.tenantID) if err != nil { - panic(err) + return nil, err } - return adal.NewServicePrincipalToken(*oauthConfig, c.clientId, c.clientSecret, scope) + return adal.NewServicePrincipalToken(*oauthConfig, c.clientID, c.clientSecret, scope) } diff --git a/providers/dns/bluecat/bluecat.go b/providers/dns/bluecat/bluecat.go index 92b8a21d..b0374b7e 100644 --- a/providers/dns/bluecat/bluecat.go +++ b/providers/dns/bluecat/bluecat.go @@ -13,18 +13,19 @@ import ( "strings" "time" - "github.com/xenolf/lego/acme" "io/ioutil" + + "github.com/xenolf/lego/acme" ) -const bluecatUrlTemplate = "%s/Services/REST/v1" +const bluecatURLTemplate = "%s/Services/REST/v1" const configType = "Configuration" const viewType = "View" const txtType = "TXTRecord" const zoneType = "Zone" type entityResponse struct { - Id uint `json:"id"` + ID uint `json:"id"` Name string `json:"name"` Type string `json:"type"` Properties string `json:"properties"` @@ -33,7 +34,7 @@ type entityResponse struct { // DNSProvider is an implementation of the acme.ChallengeProvider interface that uses // Bluecat's Address Manager REST API to manage TXT records for a domain. type DNSProvider struct { - baseUrl string + baseURL string userName string password string configName string @@ -55,7 +56,7 @@ func NewDNSProvider() (*DNSProvider, error) { password := os.Getenv("BLUECAT_PASSWORD") configName := os.Getenv("BLUECAT_CONFIG_NAME") dnsView := os.Getenv("BLUECAT_DNS_VIEW") - httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} + httpClient := http.Client{Timeout: 30 * time.Second} return NewDNSProviderCredentials(server, userName, password, configName, dnsView, httpClient) } @@ -67,7 +68,7 @@ func NewDNSProviderCredentials(server, userName, password, configName, dnsView s } return &DNSProvider{ - baseUrl: fmt.Sprintf(bluecatUrlTemplate, server), + baseURL: fmt.Sprintf(bluecatURLTemplate, server), userName: userName, password: password, configName: configName, @@ -79,7 +80,7 @@ func NewDNSProviderCredentials(server, userName, password, configName, dnsView s // Send a REST request, using query parameters specified. The Authorization // header will be set if we have an active auth token func (d *DNSProvider) sendRequest(method, resource string, payload interface{}, queryArgs map[string]string) (*http.Response, error) { - url := fmt.Sprintf("%s/%s", d.baseUrl, resource) + url := fmt.Sprintf("%s/%s", d.baseURL, resource) body, err := json.Marshal(payload) if err != nil { @@ -159,14 +160,14 @@ func (d *DNSProvider) logout() error { if resp.StatusCode != 200 { return fmt.Errorf("Bluecat API request failed to delete session with HTTP status code %d", resp.StatusCode) - } else { - authBytes, _ := ioutil.ReadAll(resp.Body) - authResp := string(authBytes) + } - if !strings.Contains(authResp, "successfully") { - msg := strings.Trim(authResp, "\"") - return fmt.Errorf("Bluecat API request failed to delete session: %s", msg) - } + authBytes, _ := ioutil.ReadAll(resp.Body) + authResp := string(authBytes) + + if !strings.Contains(authResp, "successfully") { + msg := strings.Trim(authResp, "\"") + return fmt.Errorf("Bluecat API request failed to delete session: %s", msg) } d.token = "" @@ -175,7 +176,7 @@ func (d *DNSProvider) logout() error { } // Lookup the entity ID of the configuration named in our properties -func (d *DNSProvider) lookupConfId() (uint, error) { +func (d *DNSProvider) lookupConfID() (uint, error) { queryArgs := map[string]string{ "parentId": strconv.Itoa(0), "name": d.configName, @@ -193,18 +194,18 @@ func (d *DNSProvider) lookupConfId() (uint, error) { if err != nil { return 0, err } - return conf.Id, nil + return conf.ID, nil } // Find the DNS view with the given name within -func (d *DNSProvider) lookupViewId(viewName string) (uint, error) { - confId, err := d.lookupConfId() +func (d *DNSProvider) lookupViewID(viewName string) (uint, error) { + confID, err := d.lookupConfID() if err != nil { return 0, err } queryArgs := map[string]string{ - "parentId": strconv.FormatUint(uint64(confId), 10), + "parentId": strconv.FormatUint(uint64(confID), 10), "name": d.dnsView, "type": viewType, } @@ -221,13 +222,13 @@ func (d *DNSProvider) lookupViewId(viewName string) (uint, error) { return 0, err } - return view.Id, nil + return view.ID, nil } // Return the entityId of the parent zone by recursing from the root view // Also return the simple name of the host -func (d *DNSProvider) lookupParentZoneId(viewId uint, fqdn string) (uint, string, error) { - parentViewId := viewId +func (d *DNSProvider) lookupParentZoneID(viewID uint, fqdn string) (uint, string, error) { + parentViewID := viewID name := "" if fqdn != "" { @@ -236,25 +237,24 @@ func (d *DNSProvider) lookupParentZoneId(viewId uint, fqdn string) (uint, string name = zones[0] for i := last; i > -1; i-- { - zoneId, err := d.getZone(parentViewId, zones[i]) - if err != nil || zoneId == 0 { - return parentViewId, name, err + zoneID, err := d.getZone(parentViewID, zones[i]) + if err != nil || zoneID == 0 { + return parentViewID, name, err } - if (i > 0) { - name = strings.Join(zones[0:i],".") + if i > 0 { + name = strings.Join(zones[0:i], ".") } - parentViewId = zoneId + parentViewID = zoneID } } - return parentViewId, name, nil + return parentViewID, name, nil } // Get the DNS zone with the specified name under the parentId -func (d *DNSProvider) getZone(parentId uint, name string) (uint, error) { - +func (d *DNSProvider) getZone(parentID uint, name string) (uint, error) { queryArgs := map[string]string{ - "parentId": strconv.FormatUint(uint64(parentId), 10), + "parentId": strconv.FormatUint(uint64(parentID), 10), "name": name, "type": zoneType, } @@ -275,7 +275,7 @@ func (d *DNSProvider) getZone(parentId uint, name string) (uint, error) { return 0, err } - return zone.Id, nil + return zone.ID, nil } // Present creates a TXT record using the specified parameters @@ -289,21 +289,24 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return err } - viewId, err := d.lookupViewId(d.dnsView) + viewID, err := d.lookupViewID(d.dnsView) if err != nil { return err } - parentZoneId, name, err := d.lookupParentZoneId(viewId, fqdn) + parentZoneID, name, err := d.lookupParentZoneID(viewID, fqdn) + if err != nil { + return err + } queryArgs := map[string]string{ - "parentId": strconv.FormatUint(uint64(parentZoneId), 10), + "parentId": strconv.FormatUint(uint64(parentZoneID), 10), } body := bluecatEntity{ - Name: name, - Type: "TXTRecord", - Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", ttl, fqdn, value), + Name: name, + Type: "TXTRecord", + Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", ttl, fqdn, value), } resp, err := d.sendRequest("POST", "addEntity", body, queryArgs) @@ -321,23 +324,18 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("Bluecat API addEntity request failed: %s", addTxtResp) } - err = d.deploy(uint(parentZoneId)) + err = d.deploy(parentZoneID) if err != nil { return err } - err = d.logout() - if err != nil { - return err - } - - return nil + return d.logout() } // Deploy the DNS config for the specified entity to the authoritative servers -func (d *DNSProvider) deploy(entityId uint) error { +func (d *DNSProvider) deploy(entityID uint) error { queryArgs := map[string]string{ - "entityId": strconv.FormatUint(uint64(entityId), 10), + "entityId": strconv.FormatUint(uint64(entityID), 10), } resp, err := d.sendRequest("POST", "quickDeploy", nil, queryArgs) @@ -359,18 +357,18 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return err } - viewId, err := d.lookupViewId(d.dnsView) + viewID, err := d.lookupViewID(d.dnsView) if err != nil { return err } - parentId, name, err := d.lookupParentZoneId(viewId, fqdn) + parentID, name, err := d.lookupParentZoneID(viewID, fqdn) if err != nil { return err } queryArgs := map[string]string{ - "parentId": strconv.FormatUint(uint64(parentId), 10), + "parentId": strconv.FormatUint(uint64(parentID), 10), "name": name, "type": txtType, } @@ -387,7 +385,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return err } queryArgs = map[string]string{ - "objectId": strconv.FormatUint(uint64(txtRec.Id), 10), + "objectId": strconv.FormatUint(uint64(txtRec.ID), 10), } resp, err = d.sendRequest("DELETE", "delete", nil, queryArgs) @@ -396,23 +394,18 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } defer resp.Body.Close() - err = d.deploy(parentId) + err = d.deploy(parentID) if err != nil { return err } - err = d.logout() - if err != nil { - return err - } - - return nil + return d.logout() } -//JSON body for Bluecat entity requests and responses +// JSON body for Bluecat entity requests and responses type bluecatEntity struct { - ID string `json:"id,omitempty"` - Name string `json:"name"` - Type string `json:"type"` + ID string `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` Properties string `json:"properties"` } diff --git a/providers/dns/bluecat/bluecat_test.go b/providers/dns/bluecat/bluecat_test.go index c1138ffc..0362e58a 100644 --- a/providers/dns/bluecat/bluecat_test.go +++ b/providers/dns/bluecat/bluecat_test.go @@ -4,8 +4,9 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" "time" + + "github.com/stretchr/testify/assert" ) var ( @@ -14,7 +15,7 @@ var ( bluecatUserName string bluecatPassword string bluecatConfigName string - bluecatDnsView string + bluecatDNSView string bluecatDomain string ) @@ -24,8 +25,13 @@ func init() { bluecatPassword = os.Getenv("BLUECAT_PASSWORD") bluecatDomain = os.Getenv("BLUECAT_DOMAIN") bluecatConfigName = os.Getenv("BLUECAT_CONFIG_NAME") - bluecatDnsView = os.Getenv("BLUECAT_DNS_VIEW") - if len(bluecatServer) > 0 && len(bluecatDomain) > 0 && len(bluecatUserName) > 0 && len(bluecatPassword) > 0 && len(bluecatConfigName) > 0 && len(bluecatDnsView) > 0 { + bluecatDNSView = os.Getenv("BLUECAT_DNS_VIEW") + if len(bluecatServer) > 0 && + len(bluecatDomain) > 0 && + len(bluecatUserName) > 0 && + len(bluecatPassword) > 0 && + len(bluecatConfigName) > 0 && + len(bluecatDNSView) > 0 { bluecatLiveTest = true } } diff --git a/providers/dns/cloudflare/cloudflare.go b/providers/dns/cloudflare/cloudflare.go index d62b26f0..7cc32faf 100644 --- a/providers/dns/cloudflare/cloudflare.go +++ b/providers/dns/cloudflare/cloudflare.go @@ -81,11 +81,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error { } _, err = c.makeRequest("POST", fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body)) - if err != nil { - return err - } - - return nil + return err } // CleanUp removes the TXT record matching the specified parameters @@ -98,11 +94,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { } _, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil) - if err != nil { - return err - } - - return nil + return err } func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) { @@ -162,7 +154,7 @@ func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) { } } - return nil, fmt.Errorf("No existing record found for %s", fqdn) + return nil, fmt.Errorf("no existing record found for %s", fqdn) } func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) { @@ -187,7 +179,6 @@ func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM req.Header.Set("X-Auth-Email", c.authEmail) req.Header.Set("X-Auth-Key", c.authKey) - //req.Header.Set("User-Agent", userAgent()) client := http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) diff --git a/providers/dns/cloudxns/cloudxns.go b/providers/dns/cloudxns/cloudxns.go index 59697417..1204af14 100644 --- a/providers/dns/cloudxns/cloudxns.go +++ b/providers/dns/cloudxns/cloudxns.go @@ -1,214 +1,210 @@ -// Package cloudxns implements a DNS provider for solving the DNS-01 challenge -// using cloudxns DNS. -package cloudxns - -import ( - "bytes" - "crypto/md5" - "encoding/hex" - "encoding/json" - "fmt" - "net/http" - "os" - "strconv" - "time" - - "github.com/xenolf/lego/acme" -) - -const cloudXNSBaseURL = "https://www.cloudxns.net/api2/" - -// DNSProvider is an implementation of the acme.ChallengeProvider interface -type DNSProvider struct { - apiKey string - secretKey string -} - -// NewDNSProvider returns a DNSProvider instance configured for cloudxns. -// Credentials must be passed in the environment variables: CLOUDXNS_API_KEY -// and CLOUDXNS_SECRET_KEY. -func NewDNSProvider() (*DNSProvider, error) { - apiKey := os.Getenv("CLOUDXNS_API_KEY") - secretKey := os.Getenv("CLOUDXNS_SECRET_KEY") - return NewDNSProviderCredentials(apiKey, secretKey) -} - -// NewDNSProviderCredentials uses the supplied credentials to return a -// DNSProvider instance configured for cloudxns. -func NewDNSProviderCredentials(apiKey, secretKey string) (*DNSProvider, error) { - if apiKey == "" || secretKey == "" { - return nil, fmt.Errorf("CloudXNS credentials missing") - } - - return &DNSProvider{ - apiKey: apiKey, - secretKey: secretKey, - }, nil -} - -// Present creates a TXT record to fulfil the dns-01 challenge. -func (c *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) - zoneID, err := c.getHostedZoneID(fqdn) - if err != nil { - return err - } - - return c.addTxtRecord(zoneID, fqdn, value, ttl) -} - -// CleanUp removes the TXT record matching the specified parameters. -func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) - zoneID, err := c.getHostedZoneID(fqdn) - if err != nil { - return err - } - - recordID, err := c.findTxtRecord(zoneID, fqdn) - if err != nil { - return err - } - - return c.delTxtRecord(recordID, zoneID) -} - -func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) { - type Data struct { - ID string `json:"id"` - Domain string `json:"domain"` - } - - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) - if err != nil { - return "", err - } - - result, err := c.makeRequest("GET", "domain", nil) - if err != nil { - return "", err - } - - var domains []Data - err = json.Unmarshal(result, &domains) - if err != nil { - return "", err - } - - for _, data := range domains { - if data.Domain == authZone { - return data.ID, nil - } - } - - return "", fmt.Errorf("Zone %s not found in cloudxns for domain %s", authZone, fqdn) -} - -func (c *DNSProvider) findTxtRecord(zoneID, fqdn string) (string, error) { - result, err := c.makeRequest("GET", fmt.Sprintf("record/%s?host_id=0&offset=0&row_num=2000", zoneID), nil) - if err != nil { - return "", err - } - - var records []cloudXNSRecord - err = json.Unmarshal(result, &records) - if err != nil { - return "", err - } - - for _, record := range records { - if record.Host == acme.UnFqdn(fqdn) && record.Type == "TXT" { - return record.RecordID, nil - } - } - - return "", fmt.Errorf("No existing record found for %s", fqdn) -} - -func (c *DNSProvider) addTxtRecord(zoneID, fqdn, value string, ttl int) error { - id, err := strconv.Atoi(zoneID) - if err != nil { - return err - } - - payload := cloudXNSRecord{ - ID: id, - Host: acme.UnFqdn(fqdn), - Value: value, - Type: "TXT", - LineID: 1, - TTL: ttl, - } - - body, err := json.Marshal(payload) - if err != nil { - return err - } - - _, err = c.makeRequest("POST", "record", body) - if err != nil { - return err - } - - return nil -} - -func (c *DNSProvider) delTxtRecord(recordID, zoneID string) error { - _, err := c.makeRequest("DELETE", fmt.Sprintf("record/%s/%s", recordID, zoneID), nil) - return err -} - -func (c *DNSProvider) hmac(url, date, body string) string { - sum := md5.Sum([]byte(c.apiKey + url + body + date + c.secretKey)) - return hex.EncodeToString(sum[:]) -} - -func (c *DNSProvider) makeRequest(method, uri string, body []byte) (json.RawMessage, error) { - type APIResponse struct { - Code int `json:"code"` - Message string `json:"message"` - Data json.RawMessage `json:"data,omitempty"` - } - - url := cloudXNSBaseURL + uri - req, err := http.NewRequest(method, url, bytes.NewReader(body)) - if err != nil { - return nil, err - } - - requestDate := time.Now().Format(time.RFC1123Z) - - req.Header.Set("API-KEY", c.apiKey) - req.Header.Set("API-REQUEST-DATE", requestDate) - req.Header.Set("API-HMAC", c.hmac(url, requestDate, string(body))) - req.Header.Set("API-FORMAT", "json") - - resp, err := acme.HTTPClient.Do(req) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - var r APIResponse - err = json.NewDecoder(resp.Body).Decode(&r) - if err != nil { - return nil, err - } - - if r.Code != 1 { - return nil, fmt.Errorf("CloudXNS API Error: %s", r.Message) - } - return r.Data, nil -} - -type cloudXNSRecord struct { - ID int `json:"domain_id,omitempty"` - RecordID string `json:"record_id,omitempty"` - - Host string `json:"host"` - Value string `json:"value"` - Type string `json:"type"` - LineID int `json:"line_id,string"` - TTL int `json:"ttl,string"` -} +// Package cloudxns implements a DNS provider for solving the DNS-01 challenge +// using cloudxns DNS. +package cloudxns + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "os" + "strconv" + "time" + + "github.com/xenolf/lego/acme" +) + +const cloudXNSBaseURL = "https://www.cloudxns.net/api2/" + +// DNSProvider is an implementation of the acme.ChallengeProvider interface +type DNSProvider struct { + apiKey string + secretKey string +} + +// NewDNSProvider returns a DNSProvider instance configured for cloudxns. +// Credentials must be passed in the environment variables: CLOUDXNS_API_KEY +// and CLOUDXNS_SECRET_KEY. +func NewDNSProvider() (*DNSProvider, error) { + apiKey := os.Getenv("CLOUDXNS_API_KEY") + secretKey := os.Getenv("CLOUDXNS_SECRET_KEY") + return NewDNSProviderCredentials(apiKey, secretKey) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for cloudxns. +func NewDNSProviderCredentials(apiKey, secretKey string) (*DNSProvider, error) { + if apiKey == "" || secretKey == "" { + return nil, fmt.Errorf("CloudXNS credentials missing") + } + + return &DNSProvider{ + apiKey: apiKey, + secretKey: secretKey, + }, nil +} + +// Present creates a TXT record to fulfil the dns-01 challenge. +func (c *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + zoneID, err := c.getHostedZoneID(fqdn) + if err != nil { + return err + } + + return c.addTxtRecord(zoneID, fqdn, value, ttl) +} + +// CleanUp removes the TXT record matching the specified parameters. +func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + zoneID, err := c.getHostedZoneID(fqdn) + if err != nil { + return err + } + + recordID, err := c.findTxtRecord(zoneID, fqdn) + if err != nil { + return err + } + + return c.delTxtRecord(recordID, zoneID) +} + +func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) { + type Data struct { + ID string `json:"id"` + Domain string `json:"domain"` + } + + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return "", err + } + + result, err := c.makeRequest("GET", "domain", nil) + if err != nil { + return "", err + } + + var domains []Data + err = json.Unmarshal(result, &domains) + if err != nil { + return "", err + } + + for _, data := range domains { + if data.Domain == authZone { + return data.ID, nil + } + } + + return "", fmt.Errorf("zone %s not found in cloudxns for domain %s", authZone, fqdn) +} + +func (c *DNSProvider) findTxtRecord(zoneID, fqdn string) (string, error) { + result, err := c.makeRequest("GET", fmt.Sprintf("record/%s?host_id=0&offset=0&row_num=2000", zoneID), nil) + if err != nil { + return "", err + } + + var records []cloudXNSRecord + err = json.Unmarshal(result, &records) + if err != nil { + return "", err + } + + for _, record := range records { + if record.Host == acme.UnFqdn(fqdn) && record.Type == "TXT" { + return record.RecordID, nil + } + } + + return "", fmt.Errorf("no existing record found for %s", fqdn) +} + +func (c *DNSProvider) addTxtRecord(zoneID, fqdn, value string, ttl int) error { + id, err := strconv.Atoi(zoneID) + if err != nil { + return err + } + + payload := cloudXNSRecord{ + ID: id, + Host: acme.UnFqdn(fqdn), + Value: value, + Type: "TXT", + LineID: 1, + TTL: ttl, + } + + body, err := json.Marshal(payload) + if err != nil { + return err + } + + _, err = c.makeRequest("POST", "record", body) + return err +} + +func (c *DNSProvider) delTxtRecord(recordID, zoneID string) error { + _, err := c.makeRequest("DELETE", fmt.Sprintf("record/%s/%s", recordID, zoneID), nil) + return err +} + +func (c *DNSProvider) hmac(url, date, body string) string { + sum := md5.Sum([]byte(c.apiKey + url + body + date + c.secretKey)) + return hex.EncodeToString(sum[:]) +} + +func (c *DNSProvider) makeRequest(method, uri string, body []byte) (json.RawMessage, error) { + type APIResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data,omitempty"` + } + + url := cloudXNSBaseURL + uri + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + requestDate := time.Now().Format(time.RFC1123Z) + + req.Header.Set("API-KEY", c.apiKey) + req.Header.Set("API-REQUEST-DATE", requestDate) + req.Header.Set("API-HMAC", c.hmac(url, requestDate, string(body))) + req.Header.Set("API-FORMAT", "json") + + resp, err := acme.HTTPClient.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var r APIResponse + err = json.NewDecoder(resp.Body).Decode(&r) + if err != nil { + return nil, err + } + + if r.Code != 1 { + return nil, fmt.Errorf("CloudXNS API Error: %s", r.Message) + } + return r.Data, nil +} + +type cloudXNSRecord struct { + ID int `json:"domain_id,omitempty"` + RecordID string `json:"record_id,omitempty"` + + Host string `json:"host"` + Value string `json:"value"` + Type string `json:"type"` + LineID int `json:"line_id,string"` + TTL int `json:"ttl,string"` +} diff --git a/providers/dns/cloudxns/cloudxns_test.go b/providers/dns/cloudxns/cloudxns_test.go index 8f26ba82..ee08df2b 100644 --- a/providers/dns/cloudxns/cloudxns_test.go +++ b/providers/dns/cloudxns/cloudxns_test.go @@ -1,80 +1,80 @@ -package cloudxns - -import ( - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -var ( - cxLiveTest bool - cxAPIKey string - cxSecretKey string - cxDomain string -) - -func init() { - cxAPIKey = os.Getenv("CLOUDXNS_API_KEY") - cxSecretKey = os.Getenv("CLOUDXNS_SECRET_KEY") - cxDomain = os.Getenv("CLOUDXNS_DOMAIN") - if len(cxAPIKey) > 0 && len(cxSecretKey) > 0 && len(cxDomain) > 0 { - cxLiveTest = true - } -} - -func restoreCloudXNSEnv() { - os.Setenv("CLOUDXNS_API_KEY", cxAPIKey) - os.Setenv("CLOUDXNS_SECRET_KEY", cxSecretKey) -} - -func TestNewDNSProviderValid(t *testing.T) { - os.Setenv("CLOUDXNS_API_KEY", "") - os.Setenv("CLOUDXNS_SECRET_KEY", "") - _, err := NewDNSProviderCredentials("123", "123") - assert.NoError(t, err) - restoreCloudXNSEnv() -} - -func TestNewDNSProviderValidEnv(t *testing.T) { - os.Setenv("CLOUDXNS_API_KEY", "123") - os.Setenv("CLOUDXNS_SECRET_KEY", "123") - _, err := NewDNSProvider() - assert.NoError(t, err) - restoreCloudXNSEnv() -} - -func TestNewDNSProviderMissingCredErr(t *testing.T) { - os.Setenv("CLOUDXNS_API_KEY", "") - os.Setenv("CLOUDXNS_SECRET_KEY", "") - _, err := NewDNSProvider() - assert.EqualError(t, err, "CloudXNS credentials missing") - restoreCloudXNSEnv() -} - -func TestCloudXNSPresent(t *testing.T) { - if !cxLiveTest { - t.Skip("skipping live test") - } - - provider, err := NewDNSProviderCredentials(cxAPIKey, cxSecretKey) - assert.NoError(t, err) - - err = provider.Present(cxDomain, "", "123d==") - assert.NoError(t, err) -} - -func TestCloudXNSCleanUp(t *testing.T) { - if !cxLiveTest { - t.Skip("skipping live test") - } - - time.Sleep(time.Second * 2) - - provider, err := NewDNSProviderCredentials(cxAPIKey, cxSecretKey) - assert.NoError(t, err) - - err = provider.CleanUp(cxDomain, "", "123d==") - assert.NoError(t, err) -} +package cloudxns + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + cxLiveTest bool + cxAPIKey string + cxSecretKey string + cxDomain string +) + +func init() { + cxAPIKey = os.Getenv("CLOUDXNS_API_KEY") + cxSecretKey = os.Getenv("CLOUDXNS_SECRET_KEY") + cxDomain = os.Getenv("CLOUDXNS_DOMAIN") + if len(cxAPIKey) > 0 && len(cxSecretKey) > 0 && len(cxDomain) > 0 { + cxLiveTest = true + } +} + +func restoreCloudXNSEnv() { + os.Setenv("CLOUDXNS_API_KEY", cxAPIKey) + os.Setenv("CLOUDXNS_SECRET_KEY", cxSecretKey) +} + +func TestNewDNSProviderValid(t *testing.T) { + os.Setenv("CLOUDXNS_API_KEY", "") + os.Setenv("CLOUDXNS_SECRET_KEY", "") + _, err := NewDNSProviderCredentials("123", "123") + assert.NoError(t, err) + restoreCloudXNSEnv() +} + +func TestNewDNSProviderValidEnv(t *testing.T) { + os.Setenv("CLOUDXNS_API_KEY", "123") + os.Setenv("CLOUDXNS_SECRET_KEY", "123") + _, err := NewDNSProvider() + assert.NoError(t, err) + restoreCloudXNSEnv() +} + +func TestNewDNSProviderMissingCredErr(t *testing.T) { + os.Setenv("CLOUDXNS_API_KEY", "") + os.Setenv("CLOUDXNS_SECRET_KEY", "") + _, err := NewDNSProvider() + assert.EqualError(t, err, "CloudXNS credentials missing") + restoreCloudXNSEnv() +} + +func TestCloudXNSPresent(t *testing.T) { + if !cxLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProviderCredentials(cxAPIKey, cxSecretKey) + assert.NoError(t, err) + + err = provider.Present(cxDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestCloudXNSCleanUp(t *testing.T) { + if !cxLiveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 2) + + provider, err := NewDNSProviderCredentials(cxAPIKey, cxSecretKey) + assert.NoError(t, err) + + err = provider.CleanUp(cxDomain, "", "123d==") + assert.NoError(t, err) +} diff --git a/providers/dns/digitalocean/digitalocean.go b/providers/dns/digitalocean/digitalocean.go index da261b39..ef749e6f 100644 --- a/providers/dns/digitalocean/digitalocean.go +++ b/providers/dns/digitalocean/digitalocean.go @@ -22,6 +22,12 @@ type DNSProvider struct { recordIDsMu sync.Mutex } +// Timeout returns the timeout and interval to use when checking for DNS +// propagation. Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return 60 * time.Second, 5 * time.Second +} + // NewDNSProvider returns a DNSProvider instance configured for Digital // Ocean. Credentials must be passed in the environment variable: // DO_AUTH_TOKEN. @@ -44,34 +50,17 @@ func NewDNSProviderCredentials(apiAuthToken string) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domain, token, keyAuth string) error { - // txtRecordRequest represents the request body to DO's API to make a TXT record - type txtRecordRequest struct { - RecordType string `json:"type"` - Name string `json:"name"` - Data string `json:"data"` - } - - // txtRecordResponse represents a response from DO's API after making a TXT record - type txtRecordResponse struct { - DomainRecord struct { - ID int `json:"id"` - Type string `json:"type"` - Name string `json:"name"` - Data string `json:"data"` - } `json:"domain_record"` - } - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { - return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err) + return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err) } authZone = acme.UnFqdn(authZone) reqURL := fmt.Sprintf("%s/v2/domains/%s/records", digitalOceanBaseURL, authZone) - reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value} + reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value, TTL: 30} body, err := json.Marshal(reqData) if err != nil { return err @@ -124,7 +113,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { - return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err) + return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err) } authZone = acme.UnFqdn(authZone) @@ -164,3 +153,21 @@ type digitalOceanAPIError struct { } var digitalOceanBaseURL = "https://api.digitalocean.com" + +// txtRecordRequest represents the request body to DO's API to make a TXT record +type txtRecordRequest struct { + RecordType string `json:"type"` + Name string `json:"name"` + Data string `json:"data"` + TTL int `json:"ttl"` +} + +// txtRecordResponse represents a response from DO's API after making a TXT record +type txtRecordResponse struct { + DomainRecord struct { + ID int `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Data string `json:"data"` + } `json:"domain_record"` +} diff --git a/providers/dns/digitalocean/digitalocean_test.go b/providers/dns/digitalocean/digitalocean_test.go index 7498508b..c2bef08b 100644 --- a/providers/dns/digitalocean/digitalocean_test.go +++ b/providers/dns/digitalocean/digitalocean_test.go @@ -33,7 +33,7 @@ func TestDigitalOceanPresent(t *testing.T) { if err != nil { t.Fatalf("Error reading request body: %v", err) } - if got, want := string(reqBody), `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI"}`; got != want { + if got, want := string(reqBody), `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}`; got != want { t.Errorf("Expected body data to be: `%s` but got `%s`", want, got) } diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 72ab0d8c..83693d20 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -1,4 +1,3 @@ -// Factory for DNS providers package dns import ( @@ -7,6 +6,7 @@ import ( "github.com/xenolf/lego/acme" "github.com/xenolf/lego/providers/dns/auroradns" "github.com/xenolf/lego/providers/dns/azure" + "github.com/xenolf/lego/providers/dns/bluecat" "github.com/xenolf/lego/providers/dns/cloudflare" "github.com/xenolf/lego/providers/dns/cloudxns" "github.com/xenolf/lego/providers/dns/digitalocean" @@ -35,9 +35,9 @@ import ( "github.com/xenolf/lego/providers/dns/rfc2136" "github.com/xenolf/lego/providers/dns/route53" "github.com/xenolf/lego/providers/dns/vultr" - "github.com/xenolf/lego/providers/dns/bluecat" ) +// NewDNSChallengeProviderByName Factory for DNS providers func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) { var err error var provider acme.ChallengeProvider @@ -107,7 +107,7 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) case "exec": provider, err = exec.NewDNSProvider() default: - err = fmt.Errorf("Unrecognised DNS provider: %s", name) + err = fmt.Errorf("unrecognised DNS provider: %s", name) } return provider, err } diff --git a/providers/dns/dnsimple/dnsimple.go b/providers/dns/dnsimple/dnsimple.go index df76a241..30c7fc2f 100644 --- a/providers/dns/dnsimple/dnsimple.go +++ b/providers/dns/dnsimple/dnsimple.go @@ -23,14 +23,14 @@ type DNSProvider struct { // See: https://developer.dnsimple.com/v2/#authentication func NewDNSProvider() (*DNSProvider, error) { accessToken := os.Getenv("DNSIMPLE_OAUTH_TOKEN") - baseUrl := os.Getenv("DNSIMPLE_BASE_URL") + baseURL := os.Getenv("DNSIMPLE_BASE_URL") - return NewDNSProviderCredentials(accessToken, baseUrl) + return NewDNSProviderCredentials(accessToken, baseURL) } // NewDNSProviderCredentials uses the supplied credentials to return a // DNSProvider instance configured for dnsimple. -func NewDNSProviderCredentials(accessToken, baseUrl string) (*DNSProvider, error) { +func NewDNSProviderCredentials(accessToken, baseURL string) (*DNSProvider, error) { if accessToken == "" { return nil, fmt.Errorf("DNSimple OAuth token is missing") } @@ -38,8 +38,8 @@ func NewDNSProviderCredentials(accessToken, baseUrl string) (*DNSProvider, error client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(accessToken)) client.UserAgent = "lego" - if baseUrl != "" { - client.BaseURL = baseUrl + if baseURL != "" { + client.BaseURL = baseURL } return &DNSProvider{client: client}, nil @@ -119,8 +119,7 @@ func (c *DNSProvider) getHostedZone(domain string) (string, error) { } if hostedZone.ID == 0 { - return "", fmt.Errorf("Zone %s not found in DNSimple for domain %s", authZone, domain) - + return "", fmt.Errorf("zone %s not found in DNSimple for domain %s", authZone, domain) } return hostedZone.Name, nil @@ -173,7 +172,7 @@ func (c *DNSProvider) getAccountID() (string, error) { } if whoamiResponse.Data.Account == nil { - return "", fmt.Errorf("DNSimple user tokens are not supported, please use an account token.") + return "", fmt.Errorf("DNSimple user tokens are not supported, please use an account token") } return strconv.FormatInt(whoamiResponse.Data.Account.ID, 10), nil diff --git a/providers/dns/dnsimple/dnsimple_test.go b/providers/dns/dnsimple/dnsimple_test.go index bd35790d..0d44410f 100644 --- a/providers/dns/dnsimple/dnsimple_test.go +++ b/providers/dns/dnsimple/dnsimple_test.go @@ -12,19 +12,19 @@ var ( dnsimpleLiveTest bool dnsimpleOauthToken string dnsimpleDomain string - dnsimpleBaseUrl string + dnsimpleBaseURL string ) func init() { dnsimpleOauthToken = os.Getenv("DNSIMPLE_OAUTH_TOKEN") dnsimpleDomain = os.Getenv("DNSIMPLE_DOMAIN") - dnsimpleBaseUrl = "https://api.sandbox.dnsimple.com" + dnsimpleBaseURL = "https://api.sandbox.dnsimple.com" if len(dnsimpleOauthToken) > 0 && len(dnsimpleDomain) > 0 { - baseUrl := os.Getenv("DNSIMPLE_BASE_URL") + baseURL := os.Getenv("DNSIMPLE_BASE_URL") - if baseUrl != "" { - dnsimpleBaseUrl = baseUrl + if baseURL != "" { + dnsimpleBaseURL = baseURL } dnsimpleLiveTest = true @@ -33,7 +33,7 @@ func init() { func restoreDNSimpleEnv() { os.Setenv("DNSIMPLE_OAUTH_TOKEN", dnsimpleOauthToken) - os.Setenv("DNSIMPLE_BASE_URL", dnsimpleBaseUrl) + os.Setenv("DNSIMPLE_BASE_URL", dnsimpleBaseURL) } // @@ -114,7 +114,7 @@ func TestLiveDNSimplePresent(t *testing.T) { t.Skip("skipping live test") } - provider, err := NewDNSProviderCredentials(dnsimpleOauthToken, dnsimpleBaseUrl) + provider, err := NewDNSProviderCredentials(dnsimpleOauthToken, dnsimpleBaseURL) assert.NoError(t, err) err = provider.Present(dnsimpleDomain, "", "123d==") @@ -132,7 +132,7 @@ func TestLiveDNSimpleCleanUp(t *testing.T) { time.Sleep(time.Second * 1) - provider, err := NewDNSProviderCredentials(dnsimpleOauthToken, dnsimpleBaseUrl) + provider, err := NewDNSProviderCredentials(dnsimpleOauthToken, dnsimpleBaseURL) assert.NoError(t, err) err = provider.CleanUp(dnsimpleDomain, "", "123d==") diff --git a/providers/dns/dnsmadeeasy/dnsmadeeasy.go b/providers/dns/dnsmadeeasy/dnsmadeeasy.go index c4363a4e..11b1ac44 100644 --- a/providers/dns/dnsmadeeasy/dnsmadeeasy.go +++ b/providers/dns/dnsmadeeasy/dnsmadeeasy.go @@ -95,11 +95,7 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error { record := &Record{Type: "TXT", Name: name, Value: value, TTL: ttl} err = d.createRecord(domain, record) - if err != nil { - return err - } - - return nil + return err } // CleanUp removes the TXT records matching the specified parameters @@ -226,7 +222,7 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) } client := &http.Client{ Transport: transport, - Timeout: time.Duration(10 * time.Second), + Timeout: 10 * time.Second, } resp, err := client.Do(req) if err != nil { diff --git a/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go b/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go index e860ecb6..8d6e27ac 100644 --- a/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go +++ b/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go @@ -28,6 +28,7 @@ func TestPresentAndCleanup(t *testing.T) { } provider, err := NewDNSProvider() + assert.NoError(t, err) err = provider.Present(testDomain, "", "123d==") assert.NoError(t, err) diff --git a/providers/dns/duckdns/duckdns.go b/providers/dns/duckdns/duckdns.go index 6e2102a7..855838ae 100644 --- a/providers/dns/duckdns/duckdns.go +++ b/providers/dns/duckdns/duckdns.go @@ -1,82 +1,81 @@ -// Adds lego support for http://duckdns.org . -// -// See http://www.duckdns.org/spec.jsp for more info on updating TXT records. -package duckdns - -import ( - "errors" - "fmt" - "io/ioutil" - "os" - - "github.com/xenolf/lego/acme" -) - -// DNSProvider adds and removes the record for the DNS challenge -type DNSProvider struct { - // The duckdns api token - token string -} - -// NewDNSProvider returns a new DNS provider using -// environment variable DUCKDNS_TOKEN for adding and removing the DNS record. -func NewDNSProvider() (*DNSProvider, error) { - duckdnsToken := os.Getenv("DUCKDNS_TOKEN") - - return NewDNSProviderCredentials(duckdnsToken) -} - -// NewDNSProviderCredentials uses the supplied credentials to return a -// DNSProvider instance configured for http://duckdns.org . -func NewDNSProviderCredentials(duckdnsToken string) (*DNSProvider, error) { - if duckdnsToken == "" { - return nil, errors.New("environment variable DUCKDNS_TOKEN not set") - } - - return &DNSProvider{token: duckdnsToken}, nil -} - -// makeDuckdnsURL creates a url to clear the set or unset the TXT record. -// txt == "" will clear the TXT record. -func makeDuckdnsURL(domain, token, txt string) string { - requestBase := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s", domain, token) - if txt == "" { - return requestBase + "&clear=true" - } - return requestBase + "&txt=" + txt -} - -func issueDuckdnsRequest(url string) error { - response, err := acme.HTTPClient.Get(url) - if err != nil { - return err - } - defer response.Body.Close() - - bodyBytes, err := ioutil.ReadAll(response.Body) - if err != nil { - return err - } - body := string(bodyBytes) - if body != "OK" { - return fmt.Errorf("Request to change TXT record for duckdns returned the following result (%s) this does not match expectation (OK) used url [%s]", body, url) - } - return nil -} - -// Present creates a TXT record to fulfil the dns-01 challenge. -// In duckdns you only have one TXT record shared with -// the domain and all sub domains. -// -// To update the TXT record we just need to make one simple get request. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - _, txtRecord, _ := acme.DNS01Record(domain, keyAuth) - url := makeDuckdnsURL(domain, d.token, txtRecord) - return issueDuckdnsRequest(url) -} - -// CleanUp clears duckdns TXT record -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - url := makeDuckdnsURL(domain, d.token, "") - return issueDuckdnsRequest(url) -} +// Package duckdns Adds lego support for http://duckdns.org . +// See http://www.duckdns.org/spec.jsp for more info on updating TXT records. +package duckdns + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + + "github.com/xenolf/lego/acme" +) + +// DNSProvider adds and removes the record for the DNS challenge +type DNSProvider struct { + // The duckdns api token + token string +} + +// NewDNSProvider returns a new DNS provider using +// environment variable DUCKDNS_TOKEN for adding and removing the DNS record. +func NewDNSProvider() (*DNSProvider, error) { + duckdnsToken := os.Getenv("DUCKDNS_TOKEN") + + return NewDNSProviderCredentials(duckdnsToken) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for http://duckdns.org . +func NewDNSProviderCredentials(duckdnsToken string) (*DNSProvider, error) { + if duckdnsToken == "" { + return nil, errors.New("environment variable DUCKDNS_TOKEN not set") + } + + return &DNSProvider{token: duckdnsToken}, nil +} + +// makeDuckdnsURL creates a url to clear the set or unset the TXT record. +// txt == "" will clear the TXT record. +func makeDuckdnsURL(domain, token, txt string) string { + requestBase := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s", domain, token) + if txt == "" { + return requestBase + "&clear=true" + } + return requestBase + "&txt=" + txt +} + +func issueDuckdnsRequest(url string) error { + response, err := acme.HTTPClient.Get(url) + if err != nil { + return err + } + defer response.Body.Close() + + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + body := string(bodyBytes) + if body != "OK" { + return fmt.Errorf("Request to change TXT record for duckdns returned the following result (%s) this does not match expectation (OK) used url [%s]", body, url) + } + return nil +} + +// Present creates a TXT record to fulfil the dns-01 challenge. +// In duckdns you only have one TXT record shared with +// the domain and all sub domains. +// +// To update the TXT record we just need to make one simple get request. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + _, txtRecord, _ := acme.DNS01Record(domain, keyAuth) + url := makeDuckdnsURL(domain, d.token, txtRecord) + return issueDuckdnsRequest(url) +} + +// CleanUp clears duckdns TXT record +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + url := makeDuckdnsURL(domain, d.token, "") + return issueDuckdnsRequest(url) +} diff --git a/providers/dns/duckdns/duckdns_test.go b/providers/dns/duckdns/duckdns_test.go index f1afed4f..aefc0943 100644 --- a/providers/dns/duckdns/duckdns_test.go +++ b/providers/dns/duckdns/duckdns_test.go @@ -1,65 +1,65 @@ -package duckdns - -import ( - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -var ( - duckdnsLiveTest bool - duckdnsToken string - duckdnsDomain string -) - -func init() { - duckdnsToken = os.Getenv("DUCKDNS_TOKEN") - duckdnsDomain = os.Getenv("DUCKDNS_DOMAIN") - if len(duckdnsDomain) > 0 && len(duckdnsDomain) > 0 { - duckdnsLiveTest = true - } -} - -func restoreDuckdnsEnv() { - os.Setenv("DUCKDNS_TOKEN", duckdnsToken) -} - -func TestNewDNSProviderValidEnv(t *testing.T) { - os.Setenv("DUCKDNS_TOKEN", "123") - _, err := NewDNSProvider() - assert.NoError(t, err) - restoreDuckdnsEnv() -} -func TestNewDNSProviderMissingCredErr(t *testing.T) { - os.Setenv("DUCKDNS_TOKEN", "") - _, err := NewDNSProvider() - assert.EqualError(t, err, "environment variable DUCKDNS_TOKEN not set") - restoreDuckdnsEnv() -} -func TestLiveDuckdnsPresent(t *testing.T) { - if !duckdnsLiveTest { - t.Skip("skipping live test") - } - - provider, err := NewDNSProvider() - assert.NoError(t, err) - - err = provider.Present(duckdnsDomain, "", "123d==") - assert.NoError(t, err) -} - -func TestLiveDuckdnsCleanUp(t *testing.T) { - if !duckdnsLiveTest { - t.Skip("skipping live test") - } - - time.Sleep(time.Second * 10) - - provider, err := NewDNSProvider() - assert.NoError(t, err) - - err = provider.CleanUp(duckdnsDomain, "", "123d==") - assert.NoError(t, err) -} +package duckdns + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + duckdnsLiveTest bool + duckdnsToken string + duckdnsDomain string +) + +func init() { + duckdnsToken = os.Getenv("DUCKDNS_TOKEN") + duckdnsDomain = os.Getenv("DUCKDNS_DOMAIN") + if len(duckdnsToken) > 0 && len(duckdnsDomain) > 0 { + duckdnsLiveTest = true + } +} + +func restoreDuckdnsEnv() { + os.Setenv("DUCKDNS_TOKEN", duckdnsToken) +} + +func TestNewDNSProviderValidEnv(t *testing.T) { + os.Setenv("DUCKDNS_TOKEN", "123") + _, err := NewDNSProvider() + assert.NoError(t, err) + restoreDuckdnsEnv() +} +func TestNewDNSProviderMissingCredErr(t *testing.T) { + os.Setenv("DUCKDNS_TOKEN", "") + _, err := NewDNSProvider() + assert.EqualError(t, err, "environment variable DUCKDNS_TOKEN not set") + restoreDuckdnsEnv() +} +func TestLiveDuckdnsPresent(t *testing.T) { + if !duckdnsLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.Present(duckdnsDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestLiveDuckdnsCleanUp(t *testing.T) { + if !duckdnsLiveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 10) + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.CleanUp(duckdnsDomain, "", "123d==") + assert.NoError(t, err) +} diff --git a/providers/dns/dyn/dyn.go b/providers/dns/dyn/dyn.go index 277dffb9..ad65fab6 100644 --- a/providers/dns/dyn/dyn.go +++ b/providers/dns/dyn/dyn.go @@ -80,7 +80,7 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) req.Header.Set("Auth-Token", d.token) } - client := &http.Client{Timeout: time.Duration(10 * time.Second)} + client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, err @@ -158,7 +158,7 @@ func (d *DNSProvider) logout() error { req.Header.Set("Content-Type", "application/json") req.Header.Set("Auth-Token", d.token) - client := &http.Client{Timeout: time.Duration(10 * time.Second)} + client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return err @@ -206,12 +206,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return err } - err = d.logout() - if err != nil { - return err - } - - return nil + return d.logout() } func (d *DNSProvider) publish(zone, notes string) error { @@ -222,12 +217,9 @@ func (d *DNSProvider) publish(zone, notes string) error { pub := &publish{Publish: true, Notes: notes} resource := fmt.Sprintf("Zone/%s/", zone) - _, err := d.sendRequest("PUT", resource, pub) - if err != nil { - return err - } - return nil + _, err := d.sendRequest("PUT", resource, pub) + return err } // CleanUp removes the TXT record matching the specified parameters @@ -253,7 +245,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { req.Header.Set("Content-Type", "application/json") req.Header.Set("Auth-Token", d.token) - client := &http.Client{Timeout: time.Duration(10 * time.Second)} + client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return err @@ -269,10 +261,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return err } - err = d.logout() - if err != nil { - return err - } - - return nil + return d.logout() } diff --git a/providers/dns/exoscale/exoscale.go b/providers/dns/exoscale/exoscale.go index 4b125e8d..1467e8d7 100644 --- a/providers/dns/exoscale/exoscale.go +++ b/providers/dns/exoscale/exoscale.go @@ -16,7 +16,7 @@ type DNSProvider struct { client *egoscale.Client } -// Credentials must be passed in the environment variables: +// NewDNSProvider Credentials must be passed in the environment variables: // EXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT. func NewDNSProvider() (*DNSProvider, error) { key := os.Getenv("EXOSCALE_API_KEY") @@ -25,7 +25,7 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderClient(key, secret, endpoint) } -// Uses the supplied parameters to return a DNSProvider instance +// NewDNSProviderClient Uses the supplied parameters to return a DNSProvider instance // configured for Exoscale. func NewDNSProviderClient(key, secret, endpoint string) (*DNSProvider, error) { if key == "" || secret == "" { @@ -48,7 +48,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error { return err } - recordID, err := c.FindExistingRecordId(zone, recordName) + recordID, err := c.FindExistingRecordID(zone, recordName) if err != nil { return err } @@ -84,7 +84,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { return err } - recordID, err := c.FindExistingRecordId(zone, recordName) + recordID, err := c.FindExistingRecordID(zone, recordName) if err != nil { return err } @@ -99,9 +99,9 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } -// Query Exoscale to find an existing record for this name. +// FindExistingRecordID Query Exoscale to find an existing record for this name. // Returns nil if no record could be found -func (c *DNSProvider) FindExistingRecordId(zone, recordName string) (int64, error) { +func (c *DNSProvider) FindExistingRecordID(zone, recordName string) (int64, error) { records, err := c.client.GetRecords(zone) if err != nil { return -1, errors.New("Error while retrievening DNS records: " + err.Error()) @@ -114,7 +114,7 @@ func (c *DNSProvider) FindExistingRecordId(zone, recordName string) (int64, erro return 0, nil } -// Extract DNS zone and DNS entry name +// FindZoneAndRecordName Extract DNS zone and DNS entry name func (c *DNSProvider) FindZoneAndRecordName(fqdn, domain string) (string, string, error) { zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { diff --git a/providers/dns/gandi/gandi.go b/providers/dns/gandi/gandi.go index 422b02a2..750bc8b4 100644 --- a/providers/dns/gandi/gandi.go +++ b/providers/dns/gandi/gandi.go @@ -74,15 +74,18 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if ttl < 300 { ttl = 300 // 300 is gandi minimum value for ttl } + // find authZone and Gandi zone_id for fqdn authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers) if err != nil { return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err) } + zoneID, err := d.getZoneID(authZone) if err != nil { return err } + // determine name of TXT record if !strings.HasSuffix( strings.ToLower(fqdn), strings.ToLower("."+authZone)) { @@ -90,40 +93,49 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { "Gandi DNS: unexpected authZone %s for fqdn %s", authZone, fqdn) } name := fqdn[:len(fqdn)-len("."+authZone)] + // acquire lock and check there is not a challenge already in // progress for this value of authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() + if _, ok := d.inProgressAuthZones[authZone]; ok { return fmt.Errorf( "Gandi DNS: challenge already in progress for authZone %s", authZone) } + // perform API actions to create and activate new gandi zone // containing the required TXT record newZoneName := fmt.Sprintf( "%s [ACME Challenge %s]", acme.UnFqdn(authZone), time.Now().Format(time.RFC822Z)) + newZoneID, err := d.cloneZone(zoneID, newZoneName) if err != nil { return err } + newZoneVersion, err := d.newZoneVersion(newZoneID) if err != nil { return err } + err = d.addTXTRecord(newZoneID, newZoneVersion, name, value, ttl) if err != nil { return err } + err = d.setZoneVersion(newZoneID, newZoneVersion) if err != nil { return err } + err = d.setZone(authZone, newZoneID) if err != nil { return err } + // save data necessary for CleanUp d.inProgressFQDNs[fqdn] = inProgressInfo{ zoneID: zoneID, @@ -142,25 +154,25 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // acquire lock and retrieve zoneID, newZoneID and authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() + if _, ok := d.inProgressFQDNs[fqdn]; !ok { // if there is no cleanup information then just return return nil } + zoneID := d.inProgressFQDNs[fqdn].zoneID newZoneID := d.inProgressFQDNs[fqdn].newZoneID authZone := d.inProgressFQDNs[fqdn].authZone delete(d.inProgressFQDNs, fqdn) delete(d.inProgressAuthZones, authZone) + // perform API actions to restore old gandi zone for authZone err := d.setZone(authZone, zoneID) if err != nil { return err } - err = d.deleteZone(newZoneID) - if err != nil { - return err - } - return nil + + return d.deleteZone(newZoneID) } // Timeout returns the values (40*time.Minute, 60*time.Second) which @@ -259,15 +271,18 @@ func (e rpcError) Error() string { func httpPost(url string, bodyType string, body io.Reader) ([]byte, error) { client := http.Client{Timeout: 60 * time.Second} + resp, err := client.Post(url, bodyType, body) if err != nil { return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err) } defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err) } + return b, nil } @@ -281,12 +296,14 @@ func rpcCall(call *methodCall, resp response) error { if err != nil { return fmt.Errorf("Gandi DNS: Marshal Error: %v", err) } + // post b = append([]byte(``+"\n"), b...) respBody, err := httpPost(endpoint, "text/xml", bytes.NewReader(b)) if err != nil { return err } + // unmarshal err = xml.Unmarshal(respBody, resp) if err != nil { @@ -313,12 +330,14 @@ func (d *DNSProvider) getZoneID(domain string) (int, error) { if err != nil { return 0, err } + var zoneID int for _, member := range resp.StructMembers { if member.Name == "zone_id" { zoneID = member.ValueInt } } + if zoneID == 0 { return 0, fmt.Errorf( "Gandi DNS: Could not determine zone_id for %s", domain) @@ -346,12 +365,14 @@ func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) { if err != nil { return 0, err } + var newZoneID int for _, member := range resp.StructMembers { if member.Name == "id" { newZoneID = member.ValueInt } } + if newZoneID == 0 { return 0, fmt.Errorf("Gandi DNS: Could not determine cloned zone_id") } @@ -370,6 +391,7 @@ func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) { if err != nil { return 0, err } + if resp.Value == 0 { return 0, fmt.Errorf("Gandi DNS: Could not create new zone version") } @@ -402,10 +424,7 @@ func (d *DNSProvider) addTXTRecord(zoneID int, version int, name string, value s }, }, }, resp) - if err != nil { - return err - } - return nil + return err } func (d *DNSProvider) setZoneVersion(zoneID int, version int) error { @@ -421,6 +440,7 @@ func (d *DNSProvider) setZoneVersion(zoneID int, version int) error { if err != nil { return err } + if !resp.Value { return fmt.Errorf("Gandi DNS: could not set zone version") } @@ -440,12 +460,14 @@ func (d *DNSProvider) setZone(domain string, zoneID int) error { if err != nil { return err } + var respZoneID int for _, member := range resp.StructMembers { if member.Name == "zone_id" { respZoneID = member.ValueInt } } + if respZoneID != zoneID { return fmt.Errorf( "Gandi DNS: Could not set new zone_id for %s", domain) @@ -465,6 +487,7 @@ func (d *DNSProvider) deleteZone(zoneID int) error { if err != nil { return err } + if !resp.Value { return fmt.Errorf("Gandi DNS: could not delete zone_id") } diff --git a/providers/dns/gandi/gandi_test.go b/providers/dns/gandi/gandi_test.go index 451333ca..520c7e37 100644 --- a/providers/dns/gandi/gandi_test.go +++ b/providers/dns/gandi/gandi_test.go @@ -1,41 +1,15 @@ package gandi import ( - "crypto" - "crypto/rand" - "crypto/rsa" "io" "io/ioutil" "net/http" "net/http/httptest" - "os" "regexp" "strings" "testing" - - "github.com/xenolf/lego/acme" ) -// stagingServer is the Let's Encrypt staging server used by the live test -const stagingServer = "https://acme-staging.api.letsencrypt.org/directory" - -// user implements acme.User and is used by the live test -type user struct { - Email string - Registration *acme.RegistrationResource - key crypto.PrivateKey -} - -func (u *user) GetEmail() string { - return u.Email -} -func (u *user) GetRegistration() *acme.RegistrationResource { - return u.Registration -} -func (u *user) GetPrivateKey() crypto.PrivateKey { - return u.key -} - // TestDNSProvider runs Present and CleanUp against a fake Gandi RPC // Server, whose responses are predetermined for particular requests. func TestDNSProvider(t *testing.T) { @@ -92,61 +66,6 @@ func TestDNSProvider(t *testing.T) { } } -// TestDNSProviderLive performs a live test to obtain a certificate -// using the Let's Encrypt staging server. It runs provided that both -// the environment variables GANDI_API_KEY and GANDI_TEST_DOMAIN are -// set. Otherwise the test is skipped. -// -// To complete this test, go test must be run with the -timeout=40m -// flag, since the default timeout of 10m is insufficient. -func TestDNSProviderLive(t *testing.T) { - apiKey := os.Getenv("GANDI_API_KEY") - domain := os.Getenv("GANDI_TEST_DOMAIN") - if apiKey == "" || domain == "" { - t.Skip("skipping live test") - } - // create a user. - const rsaKeySize = 2048 - privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize) - if err != nil { - t.Fatal(err) - } - myUser := user{ - Email: "test@example.com", - key: privateKey, - } - // create a client using staging server - client, err := acme.NewClient(stagingServer, &myUser, acme.RSA2048) - if err != nil { - t.Fatal(err) - } - provider, err := NewDNSProviderCredentials(apiKey) - if err != nil { - t.Fatal(err) - } - err = client.SetChallengeProvider(acme.DNS01, provider) - if err != nil { - t.Fatal(err) - } - client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) - // register and agree tos - reg, err := client.Register() - if err != nil { - t.Fatal(err) - } - myUser.Registration = reg - err = client.AgreeToTOS() - if err != nil { - t.Fatal(err) - } - // complete the challenge - bundle := false - _, failures := client.ObtainCertificate([]string{domain}, bundle, nil, false) - if len(failures) > 0 { - t.Fatal(failures) - } -} - // serverResponses is the XML-RPC Request->Response map used by the // fake RPC server. It was generated by recording a real RPC session // which resulted in the successful issue of a cert, and then diff --git a/providers/dns/gandiv5/gandiv5.go b/providers/dns/gandiv5/gandiv5.go index 86cc7bf3..4614723a 100644 --- a/providers/dns/gandiv5/gandiv5.go +++ b/providers/dns/gandiv5/gandiv5.go @@ -21,6 +21,7 @@ var ( // endpoint is the Gandi API endpoint used by Present and // CleanUp. It is overridden during tests. endpoint = "https://dns.api.gandi.net/api/v5" + // findZoneByFqdn determines the DNS zone of an fqdn. It is overridden // during tests. findZoneByFqdn = acme.FindZoneByFqdn @@ -66,11 +67,13 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if ttl < 300 { ttl = 300 // 300 is gandi minimum value for ttl } + // find authZone authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers) if err != nil { return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err) } + // determine name of TXT record if !strings.HasSuffix( strings.ToLower(fqdn), strings.ToLower("."+authZone)) { @@ -78,15 +81,18 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { "Gandi DNS: unexpected authZone %s for fqdn %s", authZone, fqdn) } name := fqdn[:len(fqdn)-len("."+authZone)] + // acquire lock and check there is not a challenge already in // progress for this value of authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() + // add TXT record into authZone err = d.addTXTRecord(acme.UnFqdn(authZone), name, value, ttl) if err != nil { return err } + // save data necessary for CleanUp d.inProgressFQDNs[fqdn] = inProgressInfo{ authZone: authZone, @@ -98,6 +104,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + // acquire lock and retrieve authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() @@ -105,15 +112,13 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // if there is no cleanup information then just return return nil } + fieldName := d.inProgressFQDNs[fqdn].fieldName authZone := d.inProgressFQDNs[fqdn].authZone delete(d.inProgressFQDNs, fqdn) + // delete TXT record from authZone - err := d.deleteTXTRecord(acme.UnFqdn(authZone), fieldName) - if err != nil { - return err - } - return nil + return d.deleteTXTRecord(acme.UnFqdn(authZone), fieldName) } // Timeout returns the values (20*time.Minute, 20*time.Second) which @@ -149,16 +154,18 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf if err != nil { return nil, err } + req, err := http.NewRequest(method, url, bytes.NewReader(body)) if err != nil { return nil, err } + req.Header.Set("Content-Type", "application/json") if len(d.apiKey) > 0 { req.Header.Set("X-Api-Key", d.apiKey) } - client := &http.Client{Timeout: time.Duration(10 * time.Second)} + client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, err diff --git a/providers/dns/gandiv5/gandiv5_test.go b/providers/dns/gandiv5/gandiv5_test.go index 56e63915..f6fa779f 100644 --- a/providers/dns/gandiv5/gandiv5_test.go +++ b/providers/dns/gandiv5/gandiv5_test.go @@ -1,41 +1,15 @@ package gandiv5 import ( - "crypto" - "crypto/rand" - "crypto/rsa" "io" "io/ioutil" "net/http" "net/http/httptest" - "os" "regexp" "strings" "testing" - - "github.com/xenolf/lego/acme" ) -// stagingServer is the Let's Encrypt staging server used by the live test -const stagingServer = "https://acme-staging.api.letsencrypt.org/directory" - -// user implements acme.User and is used by the live test -type user struct { - Email string - Registration *acme.RegistrationResource - key crypto.PrivateKey -} - -func (u *user) GetEmail() string { - return u.Email -} -func (u *user) GetRegistration() *acme.RegistrationResource { - return u.Registration -} -func (u *user) GetPrivateKey() crypto.PrivateKey { - return u.key -} - // TestDNSProvider runs Present and CleanUp against a fake Gandi RPC // Server, whose responses are predetermined for particular requests. func TestDNSProvider(t *testing.T) { @@ -92,61 +66,6 @@ func TestDNSProvider(t *testing.T) { } } -// TestDNSProviderLive performs a live test to obtain a certificate -// using the Let's Encrypt staging server. It runs provided that both -// the environment variables GANDIV5_API_KEY and GANDI_TEST_DOMAIN are -// set. Otherwise the test is skipped. -// -// To complete this test, go test must be run with the -timeout=40m -// flag, since the default timeout of 10m is insufficient. -func TestDNSProviderLive(t *testing.T) { - apiKey := os.Getenv("GANDIV5_API_KEY") - domain := os.Getenv("GANDI_TEST_DOMAIN") - if apiKey == "" || domain == "" { - t.Skip("skipping live test") - } - // create a user. - const rsaKeySize = 2048 - privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize) - if err != nil { - t.Fatal(err) - } - myUser := user{ - Email: "test@example.com", - key: privateKey, - } - // create a client using staging server - client, err := acme.NewClient(stagingServer, &myUser, acme.RSA2048) - if err != nil { - t.Fatal(err) - } - provider, err := NewDNSProviderCredentials(apiKey) - if err != nil { - t.Fatal(err) - } - err = client.SetChallengeProvider(acme.DNS01, provider) - if err != nil { - t.Fatal(err) - } - client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) - // register and agree tos - reg, err := client.Register() - if err != nil { - t.Fatal(err) - } - myUser.Registration = reg - err = client.AgreeToTOS() - if err != nil { - t.Fatal(err) - } - // complete the challenge - bundle := false - _, failures := client.ObtainCertificate([]string{domain}, bundle, nil, false) - if len(failures) > 0 { - t.Fatal(failures) - } -} - // serverResponses is the JSON Request->Response map used by the // fake JSON server. var serverResponses = map[string]string{ diff --git a/providers/dns/glesys/glesys.go b/providers/dns/glesys/glesys.go index 36c6c00d..7623b564 100644 --- a/providers/dns/glesys/glesys.go +++ b/providers/dns/glesys/glesys.go @@ -6,7 +6,6 @@ import ( "bytes" "encoding/json" "fmt" - "log" "net/http" "os" "strings" @@ -14,29 +13,14 @@ import ( "time" "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/log" ) // GleSYS API reference: https://github.com/GleSYS/API/wiki/API-Documentation -// domainAPI is the GleSYS API endpoint used by Present and CleanUp. +// domainAPI is the GleSYS API endpoint used by Present and CleanUp. const domainAPI = "https://api.glesys.com/domain" -var ( - // Logger is used to log API communication results; - // if nil, the default log.Logger is used. - Logger *log.Logger -) - -// logf writes a log entry. It uses Logger if not -// nil, otherwise it uses the default log.Logger. -func logf(format string, args ...interface{}) { - if Logger != nil { - Logger.Printf(format, args...) - } else { - log.Printf(format, args...) - } -} - // DNSProvider is an implementation of the // acme.ChallengeProviderTimeout interface that uses GleSYS // API to manage TXT records for a domain. @@ -80,6 +64,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("GleSYS DNS: findZoneByFqdn failure: %v", err) } + // determine name of TXT record if !strings.HasSuffix( strings.ToLower(fqdn), strings.ToLower("."+authZone)) { @@ -87,23 +72,27 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { "GleSYS DNS: unexpected authZone %s for fqdn %s", authZone, fqdn) } name := fqdn[:len(fqdn)-len("."+authZone)] + // acquire lock and check there is not a challenge already in // progress for this value of authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() + // add TXT record into authZone - recordId, err := d.addTXTRecord(domain, acme.UnFqdn(authZone), name, value, ttl) + recordID, err := d.addTXTRecord(domain, acme.UnFqdn(authZone), name, value, ttl) if err != nil { return err } + // save data necessary for CleanUp - d.activeRecords[fqdn] = recordId + d.activeRecords[fqdn] = recordID return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + // acquire lock and retrieve authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() @@ -111,14 +100,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // if there is no cleanup information then just return return nil } - recordId := d.activeRecords[fqdn] + + recordID := d.activeRecords[fqdn] delete(d.activeRecords, fqdn) + // delete TXT record from authZone - err := d.deleteTXTRecord(domain, recordId) - if err != nil { - return err - } - return nil + return d.deleteTXTRecord(domain, recordID) } // Timeout returns the values (20*time.Minute, 20*time.Second) which @@ -135,7 +122,7 @@ type addRecordRequest struct { Host string `json:"host"` Type string `json:"type"` Data string `json:"data"` - Ttl int `json:"ttl,omitempty"` + TTL int `json:"ttl,omitempty"` } type deleteRecordRequest struct { @@ -160,14 +147,16 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf if err != nil { return nil, err } + req, err := http.NewRequest(method, url, bytes.NewReader(body)) if err != nil { return nil, err } + req.Header.Set("Content-Type", "application/json") req.SetBasicAuth(d.apiUser, d.apiKey) - client := &http.Client{Timeout: time.Duration(10 * time.Second)} + client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, err @@ -177,6 +166,7 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf if resp.StatusCode >= 400 { return nil, fmt.Errorf("GleSYS DNS: request failed with HTTP status code %d", resp.StatusCode) } + var response responseStruct err = json.NewDecoder(resp.Body).Decode(&response) @@ -187,14 +177,14 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf func (d *DNSProvider) addTXTRecord(fqdn string, domain string, name string, value string, ttl int) (int, error) { response, err := d.sendRequest("POST", "addrecord", addRecordRequest{ - Domainname: domain, - Host: name, - Type: "TXT", - Data: value, - Ttl: ttl, + Domainname: domain, + Host: name, + Type: "TXT", + Data: value, + TTL: ttl, }) if response != nil && response.Response.Status.Code == 200 { - logf("[INFO][%s] GleSYS DNS: Successfully created recordid %d", fqdn, response.Response.Record.Recordid) + log.Printf("[INFO][%s] GleSYS DNS: Successfully created recordid %d", fqdn, response.Response.Record.Recordid) return response.Response.Record.Recordid, nil } return 0, err @@ -205,7 +195,7 @@ func (d *DNSProvider) deleteTXTRecord(fqdn string, recordid int) error { Recordid: recordid, }) if response != nil && response.Response.Status.Code == 200 { - logf("[INFO][%s] GleSYS DNS: Successfully deleted recordid %d", fqdn, recordid) + log.Printf("[INFO][%s] GleSYS DNS: Successfully deleted recordid %d", fqdn, recordid) } return err } diff --git a/providers/dns/godaddy/godaddy.go b/providers/dns/godaddy/godaddy.go index 4112f662..54fbad03 100644 --- a/providers/dns/godaddy/godaddy.go +++ b/providers/dns/godaddy/godaddy.go @@ -10,9 +10,10 @@ import ( "bytes" "encoding/json" - "github.com/xenolf/lego/acme" "io/ioutil" "strings" + + "github.com/xenolf/lego/acme" ) // GoDaddyAPIURL represents the API endpoint to call. @@ -75,7 +76,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error { Type: "TXT", Name: recordName, Data: value, - Ttl: ttl, + TTL: ttl, }, } @@ -98,7 +99,7 @@ func (c *DNSProvider) updateRecords(records []DNSRecord, domainZone string, reco if resp.StatusCode != http.StatusOK { bodyBytes, _ := ioutil.ReadAll(resp.Body) - return fmt.Errorf("Could not create record %v; Status: %v; Body: %s\n", string(body), resp.StatusCode, string(bodyBytes)) + return fmt.Errorf("could not create record %v; Status: %v; Body: %s", string(body), resp.StatusCode, string(bodyBytes)) } return nil } @@ -146,10 +147,11 @@ func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Res return client.Do(req) } +// DNSRecord a DNS record type DNSRecord struct { Type string `json:"type"` Name string `json:"name"` Data string `json:"data"` Priority int `json:"priority,omitempty"` - Ttl int `json:"ttl,omitempty"` + TTL int `json:"ttl,omitempty"` } diff --git a/providers/dns/googlecloud/googlecloud.go b/providers/dns/googlecloud/googlecloud.go index ba753f6d..dc46152f 100644 --- a/providers/dns/googlecloud/googlecloud.go +++ b/providers/dns/googlecloud/googlecloud.go @@ -11,7 +11,6 @@ import ( "github.com/xenolf/lego/acme" "golang.org/x/net/context" - "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/dns/v1" @@ -74,7 +73,7 @@ func NewDNSProviderServiceAccount(project string, saFile string) (*DNSProvider, if err != nil { return nil, fmt.Errorf("Unable to acquire config: %v", err) } - client := conf.Client(oauth2.NoContext) + client := conf.Client(context.Background()) svc, err := dns.New(client) if err != nil { diff --git a/providers/dns/googlecloud/googlecloud_test.go b/providers/dns/googlecloud/googlecloud_test.go index 75a10d9a..70b0e730 100644 --- a/providers/dns/googlecloud/googlecloud_test.go +++ b/providers/dns/googlecloud/googlecloud_test.go @@ -80,6 +80,7 @@ func TestLiveGoogleCloudPresentMultiple(t *testing.T) { // Check that we're able to create multiple entries err = provider.Present(gcloudDomain, "1", "123d==") + assert.NoError(t, err) err = provider.Present(gcloudDomain, "2", "123d==") assert.NoError(t, err) } diff --git a/providers/dns/linode/linode.go b/providers/dns/linode/linode.go index a91d2b48..1e8cbc50 100644 --- a/providers/dns/linode/linode.go +++ b/providers/dns/linode/linode.go @@ -19,7 +19,7 @@ const ( ) type hostedZoneInfo struct { - domainId int + domainID int resourceName string } @@ -72,7 +72,7 @@ func (p *DNSProvider) Present(domain, token, keyAuth string) error { return err } - if _, err = p.linode.CreateDomainResourceTXT(zone.domainId, acme.UnFqdn(fqdn), value, 60); err != nil { + if _, err = p.linode.CreateDomainResourceTXT(zone.domainID, acme.UnFqdn(fqdn), value, 60); err != nil { return err } @@ -88,7 +88,7 @@ func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error { } // Get all TXT records for the specified domain. - resources, err := p.linode.GetResourcesByType(zone.domainId, "TXT") + resources, err := p.linode.GetResourcesByType(zone.domainID, "TXT") if err != nil { return err } @@ -101,7 +101,7 @@ func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error { return err } if resp.ResourceID != resource.ResourceID { - return errors.New("Error deleting resource: resource IDs do not match!") + return errors.New("error deleting resource: resource IDs do not match") } break } @@ -125,7 +125,7 @@ func (p *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) { } return &hostedZoneInfo{ - domainId: domain.DomainID, + domainID: domain.DomainID, resourceName: resourceName, }, nil } diff --git a/providers/dns/namecheap/namecheap.go b/providers/dns/namecheap/namecheap.go index d7eb4093..2991d817 100644 --- a/providers/dns/namecheap/namecheap.go +++ b/providers/dns/namecheap/namecheap.go @@ -144,7 +144,7 @@ func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, e } } if longest < 1 { - return nil, fmt.Errorf("Invalid domain name '%s'", domain) + return nil, fmt.Errorf("invalid domain name %q", domain) } tld := strings.Join(parts[longest:], ".") @@ -318,7 +318,7 @@ func (d *DNSProvider) setHosts(ch *challenge, hosts []host) error { shr.Errors[0].Description, shr.Errors[0].Number) } if shr.Result.IsSuccess != "true" { - return fmt.Errorf("Namecheap setHosts failed.") + return fmt.Errorf("Namecheap setHosts failed") } return nil diff --git a/providers/dns/otc/mock.go b/providers/dns/otc/mock.go index 0f2acb4b..babc2b3a 100644 --- a/providers/dns/otc/mock.go +++ b/providers/dns/otc/mock.go @@ -2,11 +2,12 @@ package otc import ( "fmt" - "github.com/stretchr/testify/assert" "io/ioutil" "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) var fakeOTCUserName = "test" @@ -15,12 +16,14 @@ var fakeOTCDomainName = "test" var fakeOTCProjectName = "test" var fakeOTCToken = "62244bc21da68d03ebac94e6636ff01f" +// DNSMock mock type DNSMock struct { t *testing.T Server *httptest.Server Mux *http.ServeMux } +// NewDNSMock create a new DNSMock func NewDNSMock(t *testing.T) *DNSMock { return &DNSMock{ t: t, @@ -38,6 +41,7 @@ func (m *DNSMock) ShutdownServer() { m.Server.Close() } +// HandleAuthSuccessfully Handle auth successfully func (m *DNSMock) HandleAuthSuccessfully() { m.Mux.HandleFunc("/v3/auth/token", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Subject-Token", fakeOTCToken) @@ -64,6 +68,7 @@ func (m *DNSMock) HandleAuthSuccessfully() { }) } +// HandleListZonesSuccessfully Handle list zones successfully func (m *DNSMock) HandleListZonesSuccessfully() { m.Mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{ @@ -79,6 +84,7 @@ func (m *DNSMock) HandleListZonesSuccessfully() { }) } +// HandleListZonesEmpty Handle list zones empty func (m *DNSMock) HandleListZonesEmpty() { m.Mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{ @@ -93,6 +99,7 @@ func (m *DNSMock) HandleListZonesEmpty() { }) } +// HandleDeleteRecordsetsSuccessfully Handle delete recordsets successfully func (m *DNSMock) HandleDeleteRecordsetsSuccessfully() { m.Mux.HandleFunc("/v2/zones/123123/recordsets/321321", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{ @@ -107,6 +114,7 @@ func (m *DNSMock) HandleDeleteRecordsetsSuccessfully() { }) } +// HandleListRecordsetsEmpty Handle list recordsets empty func (m *DNSMock) HandleListRecordsetsEmpty() { m.Mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{ @@ -118,6 +126,8 @@ func (m *DNSMock) HandleListRecordsetsEmpty() { assert.Equal(m.t, r.URL.RawQuery, "type=TXT&name=_acme-challenge.example.com.") }) } + +// HandleListRecordsetsSuccessfully Handle list recordsets successfully func (m *DNSMock) HandleListRecordsetsSuccessfully() { m.Mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { diff --git a/providers/dns/otc/otc.go b/providers/dns/otc/otc.go index 86bcaa9b..86918730 100644 --- a/providers/dns/otc/otc.go +++ b/providers/dns/otc/otc.go @@ -59,6 +59,7 @@ func NewDNSProviderCredentials(domainName, userName, password, projectName, iden }, nil } +// SendRequest send request func (d *DNSProvider) SendRequest(method, resource string, payload interface{}) (io.Reader, error) { url := fmt.Sprintf("%s/%s", d.otcBaseURL, resource) @@ -81,7 +82,7 @@ func (d *DNSProvider) SendRequest(method, resource string, payload interface{}) tr.DisableKeepAlives = true client := &http.Client{ - Timeout: time.Duration(10 * time.Second), + Timeout: 10 * time.Second, Transport: tr, } resp, err := client.Do(req) @@ -168,7 +169,7 @@ func (d *DNSProvider) loginRequest() error { } req.Header.Set("Content-Type", "application/json") - client := &http.Client{Timeout: time.Duration(10 * time.Second)} + client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return err @@ -221,12 +222,7 @@ func (d *DNSProvider) loginRequest() error { // Starts a new OTC API Session. Authenticates using userName, password // and receives a token to be used in for subsequent requests. func (d *DNSProvider) login() error { - err := d.loginRequest() - if err != nil { - return err - } - - return nil + return d.loginRequest() } func (d *DNSProvider) getZoneID(zone string) (string, error) { @@ -305,10 +301,7 @@ func (d *DNSProvider) deleteRecordSet(zoneID, recordID string) error { resource := fmt.Sprintf("zones/%s/recordsets/%s", zoneID, recordID) _, err := d.SendRequest("DELETE", resource, nil) - if err != nil { - return err - } - return nil + return err } // Present creates a TXT record using the specified parameters @@ -340,7 +333,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` - Ttl int `json:"ttl"` + TTL int `json:"ttl"` Records []string `json:"records"` } @@ -348,16 +341,11 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Name: fqdn, Description: "Added TXT record for ACME dns-01 challenge using lego client", Type: "TXT", - Ttl: 300, + TTL: ttl, Records: []string{fmt.Sprintf("\"%s\"", value)}, } _, err = d.SendRequest("POST", resource, r1) - - if err != nil { - return err - } - - return nil + return err } // CleanUp removes the TXT record matching the specified parameters @@ -375,7 +363,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } zoneID, err := d.getZoneID(authZone) - if err != nil { return err } @@ -384,5 +371,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("unable go get record %s for zone %s: %s", fqdn, domain, err) } + return d.deleteRecordSet(zoneID, recordID) } diff --git a/providers/dns/ovh/ovh.go b/providers/dns/ovh/ovh.go index 290a8d7d..a49fc774 100644 --- a/providers/dns/ovh/ovh.go +++ b/providers/dns/ovh/ovh.go @@ -1,4 +1,4 @@ -// Package OVH implements a DNS provider for solving the DNS-01 +// Package ovh implements a DNS provider for solving the DNS-01 // challenge using OVH DNS. package ovh diff --git a/providers/dns/pdns/pdns.go b/providers/dns/pdns/pdns.go index a7856e6d..10124ae2 100644 --- a/providers/dns/pdns/pdns.go +++ b/providers/dns/pdns/pdns.go @@ -29,12 +29,12 @@ type DNSProvider struct { // PDNS_API_URL and PDNS_API_KEY. func NewDNSProvider() (*DNSProvider, error) { key := os.Getenv("PDNS_API_KEY") - hostUrl, err := url.Parse(os.Getenv("PDNS_API_URL")) + hostURL, err := url.Parse(os.Getenv("PDNS_API_URL")) if err != nil { return nil, err } - return NewDNSProviderCredentials(hostUrl, key) + return NewDNSProviderCredentials(hostURL, key) } // NewDNSProviderCredentials uses the supplied credentials to return a @@ -107,12 +107,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error { } _, err = c.makeRequest("PATCH", zone.URL, bytes.NewReader(body)) - if err != nil { - fmt.Println("here") - return err - } - - return nil + return err } // CleanUp removes the TXT record matching the specified parameters @@ -131,7 +126,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { rrsets := rrSets{ RRSets: []rrSet{ - rrSet{ + { Name: set.Name, Type: set.Type, ChangeType: "DELETE", @@ -220,7 +215,7 @@ func (c *DNSProvider) findTxtRecord(fqdn string) (*rrSet, error) { } } - return nil, fmt.Errorf("No existing record found for %s", fqdn) + return nil, fmt.Errorf("no existing record found for %s", fqdn) } func (c *DNSProvider) getAPIVersion() { diff --git a/providers/dns/rackspace/rackspace.go b/providers/dns/rackspace/rackspace.go index 13daa8c8..d30e582b 100644 --- a/providers/dns/rackspace/rackspace.go +++ b/providers/dns/rackspace/rackspace.go @@ -92,7 +92,7 @@ func NewDNSProviderCredentials(user, key string) (*DNSProvider, error) { client := http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { - return nil, fmt.Errorf("Error querying Rackspace Identity API: %v", err) + return nil, fmt.Errorf("error querying Rackspace Identity API: %v", err) } defer resp.Body.Close() @@ -115,7 +115,7 @@ func NewDNSProviderCredentials(user, key string) (*DNSProvider, error) { } } if dnsEndpoint == "" { - return nil, fmt.Errorf("Failed to populate DNS endpoint, check Rackspace API for changes.") + return nil, fmt.Errorf("failed to populate DNS endpoint, check Rackspace API for changes") } return &DNSProvider{ @@ -132,8 +132,8 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error { return err } - rec := RackspaceRecords{ - RackspaceRecord: []RackspaceRecord{{ + rec := Records{ + Record: []Record{{ Name: acme.UnFqdn(fqdn), Type: "TXT", Data: value, @@ -147,11 +147,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error { } _, err = c.makeRequest("POST", fmt.Sprintf("/domains/%d/records", zoneID), bytes.NewReader(body)) - if err != nil { - return err - } - - return nil + return err } // CleanUp removes the TXT record matching the specified parameters @@ -168,11 +164,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { } _, err = c.makeRequest("DELETE", fmt.Sprintf("/domains/%d/records?id=%s", zoneID, record.ID), nil) - if err != nil { - return err - } - - return nil + return err } // getHostedZoneID performs a lookup to get the DNS zone which needs @@ -205,36 +197,35 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (int, error) { // If nothing was returned, or for whatever reason more than 1 was returned (the search uses exact match, so should not occur) if zoneSearchResponse.TotalEntries != 1 { - return 0, fmt.Errorf("Found %d zones for %s in Rackspace for domain %s", zoneSearchResponse.TotalEntries, authZone, fqdn) + return 0, fmt.Errorf("found %d zones for %s in Rackspace for domain %s", zoneSearchResponse.TotalEntries, authZone, fqdn) } return zoneSearchResponse.HostedZones[0].ID, nil } // findTxtRecord searches a DNS zone for a TXT record with a specific name -func (c *DNSProvider) findTxtRecord(fqdn string, zoneID int) (*RackspaceRecord, error) { +func (c *DNSProvider) findTxtRecord(fqdn string, zoneID int) (*Record, error) { result, err := c.makeRequest("GET", fmt.Sprintf("/domains/%d/records?type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)), nil) if err != nil { return nil, err } - var records RackspaceRecords + var records Records err = json.Unmarshal(result, &records) if err != nil { return nil, err } - recordsLength := len(records.RackspaceRecord) + recordsLength := len(records.Record) switch recordsLength { case 1: - break case 0: - return nil, fmt.Errorf("No TXT record found for %s", fqdn) + return nil, fmt.Errorf("no TXT record found for %s", fqdn) default: - return nil, fmt.Errorf("More than 1 TXT record found for %s", fqdn) + return nil, fmt.Errorf("more than 1 TXT record found for %s", fqdn) } - return &records.RackspaceRecord[0], nil + return &records.Record[0], nil } // makeRequest is a wrapper function used for making DNS API requests @@ -251,13 +242,13 @@ func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM client := http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { - return nil, fmt.Errorf("Error querying DNS API: %v", err) + return nil, fmt.Errorf("error querying DNS API: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { - return nil, fmt.Errorf("Request failed for %s %s. Response code: %d", method, url, resp.StatusCode) + return nil, fmt.Errorf("request failed for %s %s. Response code: %d", method, url, resp.StatusCode) } var r json.RawMessage @@ -269,13 +260,13 @@ func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM return r, nil } -// RackspaceRecords is the list of records sent/received from the DNS API -type RackspaceRecords struct { - RackspaceRecord []RackspaceRecord `json:"records"` +// Records is the list of records sent/received from the DNS API +type Records struct { + Record []Record `json:"records"` } -// RackspaceRecord represents a Rackspace DNS record -type RackspaceRecord struct { +// Record represents a Rackspace DNS record +type Record struct { Name string `json:"name"` Type string `json:"type"` Data string `json:"data"` diff --git a/providers/dns/rackspace/rackspace_test.go b/providers/dns/rackspace/rackspace_test.go index 22c979ca..ed7a12f9 100644 --- a/providers/dns/rackspace/rackspace_test.go +++ b/providers/dns/rackspace/rackspace_test.go @@ -42,13 +42,14 @@ func liveRackspaceEnv() { os.Setenv("RACKSPACE_API_KEY", rackspaceAPIKey) } -func startTestServers() (identityAPI, dnsAPI *httptest.Server) { - dnsAPI = httptest.NewServer(dnsMux()) +func startTestServers() (*httptest.Server, *httptest.Server) { + dnsAPI := httptest.NewServer(dnsMux()) dnsEndpoint := dnsAPI.URL + "/123456" - identityAPI = httptest.NewServer(identityHandler(dnsEndpoint)) + identityAPI := httptest.NewServer(identityHandler(dnsEndpoint)) testAPIURL = identityAPI.URL + "/" - return + + return identityAPI, dnsAPI } func closeTestServers(identityAPI, dnsAPI *httptest.Server) { diff --git a/providers/dns/rfc2136/rfc2136.go b/providers/dns/rfc2136/rfc2136.go index dde42ddf..8f4231df 100644 --- a/providers/dns/rfc2136/rfc2136.go +++ b/providers/dns/rfc2136/rfc2136.go @@ -27,7 +27,7 @@ type DNSProvider struct { // dynamic update. Configured with environment variables: // RFC2136_NAMESERVER: Network address in the form "host" or "host:port". // RFC2136_TSIG_ALGORITHM: Defaults to hmac-md5.sig-alg.reg.int. (HMAC-MD5). -// See https://github.com/miekg/dns/blob/master/tsig.go for supported values. +// See https://github.com/miekg/dns/blob/master/tsig.go for supported values. // RFC2136_TSIG_KEY: Name of the secret key as defined in DNS server configuration. // RFC2136_TSIG_SECRET: Secret key payload. // RFC2136_TIMEOUT: DNS propagation timeout in time.ParseDuration format. (60s) @@ -77,7 +77,7 @@ func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret, t if err != nil { return nil, err } else if t < 0 { - return nil, fmt.Errorf("Invalid/negative RFC2136_TIMEOUT: %v", timeout) + return nil, fmt.Errorf("invalid/negative RFC2136_TIMEOUT: %v", timeout) } else { d.timeout = t } @@ -86,26 +86,26 @@ func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret, t return d, nil } -// Returns the timeout configured with RFC2136_TIMEOUT, or 60s. +// Timeout Returns the timeout configured with RFC2136_TIMEOUT, or 60s. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.timeout, 2 * time.Second + return d.timeout, 2 * time.Second } // Present creates a TXT record using the specified parameters -func (r *DNSProvider) Present(domain, token, keyAuth string) error { +func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) - return r.changeRecord("INSERT", fqdn, value, ttl) + return d.changeRecord("INSERT", fqdn, value, ttl) } // CleanUp removes the TXT record matching the specified parameters -func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error { +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) - return r.changeRecord("REMOVE", fqdn, value, ttl) + return d.changeRecord("REMOVE", fqdn, value, ttl) } -func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { +func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { // Find the zone for the given fqdn - zone, err := acme.FindZoneByFqdn(fqdn, []string{r.nameserver}) + zone, err := acme.FindZoneByFqdn(fqdn, []string{d.nameserver}) if err != nil { return err } @@ -127,20 +127,20 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { case "REMOVE": m.Remove(rrs) default: - return fmt.Errorf("Unexpected action: %s", action) + return fmt.Errorf("unexpected action: %s", action) } // Setup client c := new(dns.Client) c.SingleInflight = true // TSIG authentication / msg signing - if len(r.tsigKey) > 0 && len(r.tsigSecret) > 0 { - m.SetTsig(dns.Fqdn(r.tsigKey), r.tsigAlgorithm, 300, time.Now().Unix()) - c.TsigSecret = map[string]string{dns.Fqdn(r.tsigKey): r.tsigSecret} + if len(d.tsigKey) > 0 && len(d.tsigSecret) > 0 { + m.SetTsig(dns.Fqdn(d.tsigKey), d.tsigAlgorithm, 300, time.Now().Unix()) + c.TsigSecret = map[string]string{dns.Fqdn(d.tsigKey): d.tsigSecret} } // Send the query - reply, _, err := c.Exchange(m, r.nameserver) + reply, _, err := c.Exchange(m, d.nameserver) if err != nil { return fmt.Errorf("DNS update failed: %v", err) } diff --git a/providers/dns/route53/route53.go b/providers/dns/route53/route53.go index e16e12f0..adc15401 100644 --- a/providers/dns/route53/route53.go +++ b/providers/dns/route53/route53.go @@ -99,7 +99,7 @@ func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { hostedZoneID, err := r.getHostedZoneID(fqdn) if err != nil { - return fmt.Errorf("Failed to determine Route 53 hosted zone ID: %v", err) + return fmt.Errorf("failed to determine Route 53 hosted zone ID: %v", err) } recordSet := newTXTRecordSet(fqdn, value, ttl) @@ -118,7 +118,7 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { resp, err := r.client.ChangeResourceRecordSets(reqParams) if err != nil { - return fmt.Errorf("Failed to change Route 53 record set: %v", err) + return fmt.Errorf("failed to change Route 53 record set: %v", err) } statusID := resp.ChangeInfo.Id @@ -129,9 +129,9 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { } resp, err := r.client.GetChange(reqParams) if err != nil { - return false, fmt.Errorf("Failed to query Route 53 change status: %v", err) + return false, fmt.Errorf("failed to query Route 53 change status: %v", err) } - if *resp.ChangeInfo.Status == route53.ChangeStatusInsync { + if aws.StringValue(resp.ChangeInfo.Status) == route53.ChangeStatusInsync { return true, nil } return false, nil @@ -160,14 +160,14 @@ func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) { var hostedZoneID string for _, hostedZone := range resp.HostedZones { // .Name has a trailing dot - if !*hostedZone.Config.PrivateZone && *hostedZone.Name == authZone { - hostedZoneID = *hostedZone.Id + if !aws.BoolValue(hostedZone.Config.PrivateZone) && aws.StringValue(hostedZone.Name) == authZone { + hostedZoneID = aws.StringValue(hostedZone.Id) break } } if len(hostedZoneID) == 0 { - return "", fmt.Errorf("Zone %s not found in Route 53 for domain %s", authZone, fqdn) + return "", fmt.Errorf("zone %s not found in Route 53 for domain %s", authZone, fqdn) } if strings.HasPrefix(hostedZoneID, "/hostedzone/") { diff --git a/providers/dns/route53/route53_integration_test.go b/providers/dns/route53/route53_integration_test.go index 17ba4a08..6ece1888 100644 --- a/providers/dns/route53/route53_integration_test.go +++ b/providers/dns/route53/route53_integration_test.go @@ -11,7 +11,6 @@ import ( ) func TestRoute53TTL(t *testing.T) { - m, err := testGetAndPreCheck() if err != nil { t.Skip(err.Error()) @@ -19,13 +18,14 @@ func TestRoute53TTL(t *testing.T) { provider, err := NewDNSProvider() if err != nil { - t.Fatalf("Fatal: %s", err.Error()) + t.Fatal(err) } err = provider.Present(m["route53Domain"], "foo", "bar") if err != nil { - t.Fatalf("Fatal: %s", err.Error()) + t.Fatal(err) } + // we need a separate R53 client here as the one in the DNS provider is // unexported. fqdn := "_acme-challenge." + m["route53Domain"] + "." @@ -33,23 +33,25 @@ func TestRoute53TTL(t *testing.T) { zoneID, err := provider.getHostedZoneID(fqdn) if err != nil { provider.CleanUp(m["route53Domain"], "foo", "bar") - t.Fatalf("Fatal: %s", err.Error()) + t.Fatal(err) } + params := &route53.ListResourceRecordSetsInput{ HostedZoneId: aws.String(zoneID), } resp, err := svc.ListResourceRecordSets(params) if err != nil { provider.CleanUp(m["route53Domain"], "foo", "bar") - t.Fatalf("Fatal: %s", err.Error()) + t.Fatal(err) } for _, v := range resp.ResourceRecordSets { - if *v.Name == fqdn && *v.Type == "TXT" && *v.TTL == 10 { + if aws.StringValue(v.Name) == fqdn && aws.StringValue(v.Type) == "TXT" && aws.Int64Value(v.TTL) == 10 { provider.CleanUp(m["route53Domain"], "foo", "bar") return } } + provider.CleanUp(m["route53Domain"], "foo", "bar") t.Fatalf("Could not find a TXT record for _acme-challenge.%s with a TTL of 10", m["route53Domain"]) } diff --git a/providers/dns/route53/route53_test.go b/providers/dns/route53/route53_test.go index de4e28f3..e1505470 100644 --- a/providers/dns/route53/route53_test.go +++ b/providers/dns/route53/route53_test.go @@ -65,7 +65,7 @@ func TestRegionFromEnv(t *testing.T) { os.Setenv("AWS_REGION", "us-east-1") sess := session.New(aws.NewConfig()) - assert.Equal(t, "us-east-1", *sess.Config.Region, "Expected Region to be set from environment") + assert.Equal(t, "us-east-1", aws.StringValue(sess.Config.Region), "Expected Region to be set from environment") restoreRoute53Env() } diff --git a/providers/http/memcached/memcached.go b/providers/http/memcached/memcached.go index 9ac8b811..fc625bc5 100644 --- a/providers/http/memcached/memcached.go +++ b/providers/http/memcached/memcached.go @@ -10,18 +10,18 @@ import ( "github.com/xenolf/lego/acme" ) -// HTTPProvider implements ChallengeProvider for `http-01` challenge -type MemcachedProvider struct { +// HTTPProvider implements HTTPProvider for `http-01` challenge +type HTTPProvider struct { hosts []string } -// NewHTTPProvider returns a HTTPProvider instance with a configured webroot path -func NewMemcachedProvider(hosts []string) (*MemcachedProvider, error) { +// NewMemcachedProvider returns a HTTPProvider instance with a configured webroot path +func NewMemcachedProvider(hosts []string) (*HTTPProvider, error) { if len(hosts) == 0 { - return nil, fmt.Errorf("No memcached hosts provided") + return nil, fmt.Errorf("no memcached hosts provided") } - c := &MemcachedProvider{ + c := &HTTPProvider{ hosts: hosts, } @@ -29,7 +29,7 @@ func NewMemcachedProvider(hosts []string) (*MemcachedProvider, error) { } // Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path -func (w *MemcachedProvider) Present(domain, token, keyAuth string) error { +func (w *HTTPProvider) Present(domain, token, keyAuth string) error { var errs []error challengePath := path.Join("/", acme.HTTP01ChallengePath(token)) @@ -39,7 +39,7 @@ func (w *MemcachedProvider) Present(domain, token, keyAuth string) error { errs = append(errs, err) continue } - mc.Add(&memcache.Item{ + _ = mc.Add(&memcache.Item{ Key: challengePath, Value: []byte(keyAuth), Expiration: 60, @@ -47,14 +47,14 @@ func (w *MemcachedProvider) Present(domain, token, keyAuth string) error { } if len(errs) == len(w.hosts) { - return fmt.Errorf("Unable to store key in any of the memcache hosts -> %v", errs) + return fmt.Errorf("unable to store key in any of the memcache hosts -> %v", errs) } return nil } // CleanUp removes the file created for the challenge -func (w *MemcachedProvider) CleanUp(domain, token, keyAuth string) error { +func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error { // Memcached will clean up itself, that's what expiration is for. return nil } diff --git a/providers/http/memcached/memcached_test.go b/providers/http/memcached/memcached_test.go index 287a3330..f9488a1d 100644 --- a/providers/http/memcached/memcached_test.go +++ b/providers/http/memcached/memcached_test.go @@ -31,7 +31,7 @@ func init() { func TestNewMemcachedProviderEmpty(t *testing.T) { emptyHosts := make([]string, 0) _, err := NewMemcachedProvider(emptyHosts) - assert.EqualError(t, err, "No memcached hosts provided") + assert.EqualError(t, err, "no memcached hosts provided") } func TestNewMemcachedProviderValid(t *testing.T) { diff --git a/providers/http/webroot/webroot.go b/providers/http/webroot/webroot.go index 4bf211f3..f9dce06d 100644 --- a/providers/http/webroot/webroot.go +++ b/providers/http/webroot/webroot.go @@ -35,12 +35,12 @@ func (w *HTTPProvider) Present(domain, token, keyAuth string) error { challengeFilePath := path.Join(w.path, acme.HTTP01ChallengePath(token)) err = os.MkdirAll(path.Dir(challengeFilePath), 0755) if err != nil { - return fmt.Errorf("Could not create required directories in webroot for HTTP challenge -> %v", err) + return fmt.Errorf("could not create required directories in webroot for HTTP challenge -> %v", err) } err = ioutil.WriteFile(challengeFilePath, []byte(keyAuth), 0644) if err != nil { - return fmt.Errorf("Could not write file in webroot for HTTP challenge -> %v", err) + return fmt.Errorf("could not write file in webroot for HTTP challenge -> %v", err) } return nil @@ -48,10 +48,9 @@ func (w *HTTPProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the file created for the challenge func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error { - var err error - err = os.Remove(path.Join(w.path, acme.HTTP01ChallengePath(token))) + err := os.Remove(path.Join(w.path, acme.HTTP01ChallengePath(token))) if err != nil { - return fmt.Errorf("Could not remove file in webroot after HTTP challenge -> %v", err) + return fmt.Errorf("could not remove file in webroot after HTTP challenge -> %v", err) } return nil