400
vendor/github.com/mholt/certmagic/client.go
generated
vendored
Normal file
400
vendor/github.com/mholt/certmagic/client.go
generated
vendored
Normal file
@@ -0,0 +1,400 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/certificate"
|
||||
"github.com/xenolf/lego/challenge"
|
||||
"github.com/xenolf/lego/challenge/http01"
|
||||
"github.com/xenolf/lego/challenge/tlsalpn01"
|
||||
"github.com/xenolf/lego/lego"
|
||||
"github.com/xenolf/lego/registration"
|
||||
)
|
||||
|
||||
// acmeMu ensures that only one ACME challenge occurs at a time.
|
||||
var acmeMu sync.Mutex
|
||||
|
||||
// acmeClient is a wrapper over acme.Client with
|
||||
// some custom state attached. It is used to obtain,
|
||||
// renew, and revoke certificates with ACME.
|
||||
type acmeClient struct {
|
||||
config *Config
|
||||
acmeClient *lego.Client
|
||||
}
|
||||
|
||||
// listenerAddressInUse returns true if a TCP connection
|
||||
// can be made to addr within a short time interval.
|
||||
func listenerAddressInUse(addr string) bool {
|
||||
conn, err := net.DialTimeout("tcp", addr, 250*time.Millisecond)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
}
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) {
|
||||
// look up or create the user account
|
||||
leUser, err := cfg.getUser(cfg.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure key type and timeout are set
|
||||
keyType := cfg.KeyType
|
||||
if keyType == "" {
|
||||
keyType = KeyType
|
||||
}
|
||||
certObtainTimeout := cfg.CertObtainTimeout
|
||||
if certObtainTimeout == 0 {
|
||||
certObtainTimeout = CertObtainTimeout
|
||||
}
|
||||
|
||||
// ensure CA URL (directory endpoint) is set
|
||||
caURL := CA
|
||||
if cfg.CA != "" {
|
||||
caURL = cfg.CA
|
||||
}
|
||||
|
||||
// ensure endpoint is secure (assume HTTPS if scheme is missing)
|
||||
if !strings.Contains(caURL, "://") {
|
||||
caURL = "https://" + caURL
|
||||
}
|
||||
u, err := url.Parse(caURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme != "https" && !isLoopback(u.Host) && !isInternal(u.Host) {
|
||||
return nil, fmt.Errorf("%s: insecure CA URL (HTTPS required)", caURL)
|
||||
}
|
||||
|
||||
clientKey := caURL + leUser.Email + string(keyType)
|
||||
|
||||
// if an underlying client with this configuration already exists, reuse it
|
||||
cfg.acmeClientsMu.Lock()
|
||||
client, ok := cfg.acmeClients[clientKey]
|
||||
if !ok {
|
||||
// the client facilitates our communication with the CA server
|
||||
legoCfg := lego.NewConfig(&leUser)
|
||||
legoCfg.CADirURL = caURL
|
||||
legoCfg.UserAgent = buildUAString()
|
||||
legoCfg.HTTPClient.Timeout = HTTPTimeout
|
||||
legoCfg.Certificate = lego.CertificateConfig{
|
||||
KeyType: keyType,
|
||||
Timeout: certObtainTimeout,
|
||||
}
|
||||
client, err = lego.NewClient(legoCfg)
|
||||
if err != nil {
|
||||
cfg.acmeClientsMu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
cfg.acmeClients[clientKey] = client
|
||||
}
|
||||
cfg.acmeClientsMu.Unlock()
|
||||
|
||||
// if not registered, the user must register an account
|
||||
// with the CA and agree to terms
|
||||
if leUser.Registration == nil {
|
||||
if interactive { // can't prompt a user who isn't there
|
||||
termsURL := client.GetToSURL()
|
||||
if !cfg.Agreed && termsURL != "" {
|
||||
cfg.Agreed = cfg.askUserAgreement(client.GetToSURL())
|
||||
}
|
||||
if !cfg.Agreed && termsURL != "" {
|
||||
return nil, fmt.Errorf("user must agree to CA terms")
|
||||
}
|
||||
}
|
||||
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: cfg.Agreed})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("registration error: %v", err)
|
||||
}
|
||||
leUser.Registration = reg
|
||||
|
||||
// persist the user to storage
|
||||
err = cfg.saveUser(leUser)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not save user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
c := &acmeClient{
|
||||
config: cfg,
|
||||
acmeClient: client,
|
||||
}
|
||||
|
||||
if cfg.DNSProvider == nil {
|
||||
// Use HTTP and TLS-ALPN challenges by default
|
||||
|
||||
// figure out which ports we'll be serving the challenges on
|
||||
useHTTPPort := HTTPChallengePort
|
||||
useTLSALPNPort := TLSALPNChallengePort
|
||||
if HTTPPort > 0 && HTTPPort != HTTPChallengePort {
|
||||
useHTTPPort = HTTPPort
|
||||
}
|
||||
if HTTPSPort > 0 && HTTPSPort != TLSALPNChallengePort {
|
||||
useTLSALPNPort = HTTPSPort
|
||||
}
|
||||
if cfg.AltHTTPPort > 0 {
|
||||
useHTTPPort = cfg.AltHTTPPort
|
||||
}
|
||||
if cfg.AltTLSALPNPort > 0 {
|
||||
useTLSALPNPort = cfg.AltTLSALPNPort
|
||||
}
|
||||
|
||||
// If this machine is already listening on the HTTP or TLS-ALPN port
|
||||
// designated for the challenges, then we need to handle the challenges
|
||||
// a little differently: for HTTP, we will answer the challenge request
|
||||
// using our own HTTP handler (the HandleHTTPChallenge function - this
|
||||
// works only because challenge info is written to storage associated
|
||||
// with cfg when the challenge is initiated); for TLS-ALPN, we will add
|
||||
// the challenge cert to our cert cache and serve it up during the
|
||||
// handshake. As for the default solvers... we are careful to honor the
|
||||
// listener bind preferences by using cfg.ListenHost.
|
||||
var httpSolver, alpnSolver challenge.Provider
|
||||
httpSolver = http01.NewProviderServer(cfg.ListenHost, fmt.Sprintf("%d", useHTTPPort))
|
||||
alpnSolver = tlsalpn01.NewProviderServer(cfg.ListenHost, fmt.Sprintf("%d", useTLSALPNPort))
|
||||
if listenerAddressInUse(net.JoinHostPort(cfg.ListenHost, fmt.Sprintf("%d", useHTTPPort))) {
|
||||
httpSolver = nil
|
||||
}
|
||||
if listenerAddressInUse(net.JoinHostPort(cfg.ListenHost, fmt.Sprintf("%d", useTLSALPNPort))) {
|
||||
alpnSolver = tlsALPNSolver{certCache: cfg.certCache}
|
||||
}
|
||||
|
||||
// because of our nifty Storage interface, we can distribute the HTTP and
|
||||
// TLS-ALPN challenges across all instances that share the same storage -
|
||||
// in fact, this is required now for successful solving of the HTTP challenge
|
||||
// if the port is already in use, since we must write the challenge info
|
||||
// to storage for the HTTPChallengeHandler to solve it successfully
|
||||
c.acmeClient.Challenge.SetHTTP01Provider(distributedSolver{
|
||||
config: cfg,
|
||||
providerServer: httpSolver,
|
||||
})
|
||||
c.acmeClient.Challenge.SetTLSALPN01Provider(distributedSolver{
|
||||
config: cfg,
|
||||
providerServer: alpnSolver,
|
||||
})
|
||||
|
||||
// disable any challenges that should not be used
|
||||
if cfg.DisableHTTPChallenge {
|
||||
c.acmeClient.Challenge.Remove(challenge.HTTP01)
|
||||
}
|
||||
if cfg.DisableTLSALPNChallenge {
|
||||
c.acmeClient.Challenge.Remove(challenge.TLSALPN01)
|
||||
}
|
||||
} else {
|
||||
// Otherwise, use DNS challenge exclusively
|
||||
c.acmeClient.Challenge.Remove(challenge.HTTP01)
|
||||
c.acmeClient.Challenge.Remove(challenge.TLSALPN01)
|
||||
c.acmeClient.Challenge.SetDNS01Provider(cfg.DNSProvider)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// lockKey returns a key for a lock that is specific to the operation
|
||||
// named op being performed related to domainName and this config's CA.
|
||||
func (cfg *Config) lockKey(op, domainName string) string {
|
||||
return fmt.Sprintf("%s_%s_%s", op, domainName, cfg.CA)
|
||||
}
|
||||
|
||||
// Obtain obtains a single certificate for name. It stores the certificate
|
||||
// on the disk if successful. This function is safe for concurrent use.
|
||||
//
|
||||
// Our storage mechanism only supports one name per certificate, so this
|
||||
// function (along with Renew and Revoke) only accepts one domain as input.
|
||||
// It could be easily modified to support SAN certificates if our storage
|
||||
// mechanism is upgraded later, but that will increase logical complexity
|
||||
// in other areas.
|
||||
//
|
||||
// Callers who have access to a Config value should use the ObtainCert
|
||||
// method on that instead of this lower-level method.
|
||||
func (c *acmeClient) Obtain(name string) error {
|
||||
// ensure idempotency of the obtain operation for this name
|
||||
lockKey := c.config.lockKey("cert_acme", name)
|
||||
err := c.config.certCache.storage.Lock(lockKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := c.config.certCache.storage.Unlock(lockKey); err != nil {
|
||||
log.Printf("[ERROR][%s] Obtain: Unable to unlock '%s': %v", name, lockKey, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// check if obtain is still needed -- might have
|
||||
// been obtained during lock
|
||||
if c.config.storageHasCertResources(name) {
|
||||
log.Printf("[INFO][%s] Obtain: Certificate already exists in storage", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: []string{name},
|
||||
Bundle: true,
|
||||
MustStaple: c.config.MustStaple,
|
||||
}
|
||||
acmeMu.Lock()
|
||||
certificate, err := c.acmeClient.Certificate.Obtain(request)
|
||||
acmeMu.Unlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] failed to obtain certificate: %s", name, err)
|
||||
}
|
||||
|
||||
// double-check that we actually got a certificate, in case there's a bug upstream (see issue mholt/caddy#2121)
|
||||
if certificate.Domain == "" || certificate.Certificate == nil {
|
||||
return fmt.Errorf("returned certificate was empty; probably an unchecked error obtaining it")
|
||||
}
|
||||
|
||||
// Success - immediately save the certificate resource
|
||||
err = c.config.saveCertResource(certificate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving assets for %v: %v", name, err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if c.config.OnEvent != nil {
|
||||
c.config.OnEvent("acme_cert_obtained", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Renew renews the managed certificate for name. It puts the renewed
|
||||
// certificate into storage (not the cache). This function is safe for
|
||||
// concurrent use.
|
||||
//
|
||||
// Callers who have access to a Config value should use the RenewCert
|
||||
// method on that instead of this lower-level method.
|
||||
func (c *acmeClient) Renew(name string) error {
|
||||
// ensure idempotency of the renew operation for this name
|
||||
lockKey := c.config.lockKey("cert_acme", name)
|
||||
err := c.config.certCache.storage.Lock(lockKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := c.config.certCache.storage.Unlock(lockKey); err != nil {
|
||||
log.Printf("[ERROR][%s] Renew: Unable to unlock '%s': %v", name, lockKey, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Prepare for renewal (load PEM cert, key, and meta)
|
||||
certRes, err := c.config.loadCertResource(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if renew is still needed - might have been renewed while waiting for lock
|
||||
if !c.config.managedCertNeedsRenewal(certRes) {
|
||||
log.Printf("[INFO][%s] Renew: Certificate appears to have been renewed already", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform renewal and retry if necessary, but not too many times.
|
||||
var newCertMeta *certificate.Resource
|
||||
var success bool
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
acmeMu.Lock()
|
||||
newCertMeta, err = c.acmeClient.Certificate.Renew(certRes, true, c.config.MustStaple)
|
||||
acmeMu.Unlock()
|
||||
if err == nil {
|
||||
// double-check that we actually got a certificate; check a couple fields, just in case
|
||||
if newCertMeta == nil || newCertMeta.Domain == "" || newCertMeta.Certificate == nil {
|
||||
err = fmt.Errorf("returned certificate was empty; probably an unchecked error renewing it")
|
||||
} else {
|
||||
success = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// wait a little bit and try again
|
||||
wait := 10 * time.Second
|
||||
log.Printf("[ERROR] Renewing [%v]: %v; trying again in %s", name, err, wait)
|
||||
time.Sleep(wait)
|
||||
}
|
||||
|
||||
if !success {
|
||||
return fmt.Errorf("too many renewal attempts; last error: %v", err)
|
||||
}
|
||||
|
||||
if c.config.OnEvent != nil {
|
||||
c.config.OnEvent("acme_cert_renewed", name)
|
||||
}
|
||||
|
||||
return c.config.saveCertResource(newCertMeta)
|
||||
}
|
||||
|
||||
// Revoke revokes the certificate for name and deletes
|
||||
// it from storage.
|
||||
func (c *acmeClient) Revoke(name string) error {
|
||||
if !c.config.certCache.storage.Exists(StorageKeys.SitePrivateKey(c.config.CA, name)) {
|
||||
return fmt.Errorf("private key not found for %s", name)
|
||||
}
|
||||
|
||||
certRes, err := c.config.loadCertResource(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.acmeClient.Certificate.Revoke(certRes.Certificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.config.OnEvent != nil {
|
||||
c.config.OnEvent("acme_cert_revoked", name)
|
||||
}
|
||||
|
||||
err = c.config.certCache.storage.Delete(StorageKeys.SiteCert(c.config.CA, name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("certificate revoked, but unable to delete certificate file: %v", err)
|
||||
}
|
||||
err = c.config.certCache.storage.Delete(StorageKeys.SitePrivateKey(c.config.CA, name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("certificate revoked, but unable to delete private key: %v", err)
|
||||
}
|
||||
err = c.config.certCache.storage.Delete(StorageKeys.SiteMeta(c.config.CA, name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("certificate revoked, but unable to delete certificate metadata: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildUAString() string {
|
||||
ua := "CertMagic"
|
||||
if UserAgent != "" {
|
||||
ua += " " + UserAgent
|
||||
}
|
||||
return ua
|
||||
}
|
||||
|
||||
// Some default values passed down to the underlying lego client.
|
||||
var (
|
||||
UserAgent string
|
||||
HTTPTimeout = 30 * time.Second
|
||||
)
|
||||
Reference in New Issue
Block a user