mirror of
				https://github.com/coredns/coredns.git
				synced 2025-11-04 03:03:14 -05:00 
			
		
		
		
	
		
			
	
	
		
			216 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			216 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 
								 | 
							
								package https
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import (
							 | 
						||
| 
								 | 
							
									"encoding/json"
							 | 
						||
| 
								 | 
							
									"errors"
							 | 
						||
| 
								 | 
							
									"fmt"
							 | 
						||
| 
								 | 
							
									"io/ioutil"
							 | 
						||
| 
								 | 
							
									"net"
							 | 
						||
| 
								 | 
							
									"sync"
							 | 
						||
| 
								 | 
							
									"time"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									"github.com/miekg/coredns/server"
							 | 
						||
| 
								 | 
							
									"github.com/xenolf/lego/acme"
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// acmeMu ensures that only one ACME challenge occurs at a time.
							 | 
						||
| 
								 | 
							
								var acmeMu sync.Mutex
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// ACMEClient is an acme.Client with custom state attached.
							 | 
						||
| 
								 | 
							
								type ACMEClient struct {
							 | 
						||
| 
								 | 
							
									*acme.Client
							 | 
						||
| 
								 | 
							
									AllowPrompts bool // if false, we assume AlternatePort must be used
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// NewACMEClient creates a new ACMEClient given an email and whether
							 | 
						||
| 
								 | 
							
								// prompting the user is allowed. Clients should not be kept and
							 | 
						||
| 
								 | 
							
								// re-used over long periods of time, but immediate re-use is more
							 | 
						||
| 
								 | 
							
								// efficient than re-creating on every iteration.
							 | 
						||
| 
								 | 
							
								var NewACMEClient = func(email string, allowPrompts bool) (*ACMEClient, error) {
							 | 
						||
| 
								 | 
							
									// Look up or create the LE user account
							 | 
						||
| 
								 | 
							
									leUser, err := getUser(email)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// The client facilitates our communication with the CA server.
							 | 
						||
| 
								 | 
							
									client, err := acme.NewClient(CAUrl, &leUser, KeyType)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return nil, err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// If not registered, the user must register an account with the CA
							 | 
						||
| 
								 | 
							
									// and agree to terms
							 | 
						||
| 
								 | 
							
									if leUser.Registration == nil {
							 | 
						||
| 
								 | 
							
										reg, err := client.Register()
							 | 
						||
| 
								 | 
							
										if err != nil {
							 | 
						||
| 
								 | 
							
											return nil, errors.New("registration error: " + err.Error())
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
										leUser.Registration = reg
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										if allowPrompts { // can't prompt a user who isn't there
							 | 
						||
| 
								 | 
							
											if !Agreed && reg.TosURL == "" {
							 | 
						||
| 
								 | 
							
												Agreed = promptUserAgreement(saURL, false) // TODO - latest URL
							 | 
						||
| 
								 | 
							
											}
							 | 
						||
| 
								 | 
							
											if !Agreed && reg.TosURL == "" {
							 | 
						||
| 
								 | 
							
												return nil, errors.New("user must agree to terms")
							 | 
						||
| 
								 | 
							
											}
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										err = client.AgreeToTOS()
							 | 
						||
| 
								 | 
							
										if err != nil {
							 | 
						||
| 
								 | 
							
											saveUser(leUser) // Might as well try, right?
							 | 
						||
| 
								 | 
							
											return nil, errors.New("error agreeing to terms: " + err.Error())
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// save user to the file system
							 | 
						||
| 
								 | 
							
										err = saveUser(leUser)
							 | 
						||
| 
								 | 
							
										if err != nil {
							 | 
						||
| 
								 | 
							
											return nil, errors.New("could not save user: " + err.Error())
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return &ACMEClient{
							 | 
						||
| 
								 | 
							
										Client:       client,
							 | 
						||
| 
								 | 
							
										AllowPrompts: allowPrompts,
							 | 
						||
| 
								 | 
							
									}, nil
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// NewACMEClientGetEmail creates a new ACMEClient and gets an email
							 | 
						||
| 
								 | 
							
								// address at the same time (a server config is required, since it
							 | 
						||
| 
								 | 
							
								// may contain an email address in it).
							 | 
						||
| 
								 | 
							
								func NewACMEClientGetEmail(config server.Config, allowPrompts bool) (*ACMEClient, error) {
							 | 
						||
| 
								 | 
							
									return NewACMEClient(getEmail(config, allowPrompts), allowPrompts)
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// Configure configures c according to bindHost, which is the host (not
							 | 
						||
| 
								 | 
							
								// whole address) to bind the listener to in solving the http and tls-sni
							 | 
						||
| 
								 | 
							
								// challenges.
							 | 
						||
| 
								 | 
							
								func (c *ACMEClient) Configure(bindHost string) {
							 | 
						||
| 
								 | 
							
									// If we allow prompts, operator must be present. In our case,
							 | 
						||
| 
								 | 
							
									// that is synonymous with saying the server is not already
							 | 
						||
| 
								 | 
							
									// started. So if the user is still there, we don't use
							 | 
						||
| 
								 | 
							
									// AlternatePort because we don't need to proxy the challenges.
							 | 
						||
| 
								 | 
							
									// Conversely, if the operator is not there, the server has
							 | 
						||
| 
								 | 
							
									// already started and we need to proxy the challenge.
							 | 
						||
| 
								 | 
							
									if c.AllowPrompts {
							 | 
						||
| 
								 | 
							
										// Operator is present; server is not already listening
							 | 
						||
| 
								 | 
							
										c.SetHTTPAddress(net.JoinHostPort(bindHost, ""))
							 | 
						||
| 
								 | 
							
										c.SetTLSAddress(net.JoinHostPort(bindHost, ""))
							 | 
						||
| 
								 | 
							
										//c.ExcludeChallenges([]acme.Challenge{acme.DNS01})
							 | 
						||
| 
								 | 
							
									} else {
							 | 
						||
| 
								 | 
							
										// Operator is not present; server is started, so proxy challenges
							 | 
						||
| 
								 | 
							
										c.SetHTTPAddress(net.JoinHostPort(bindHost, AlternatePort))
							 | 
						||
| 
								 | 
							
										c.SetTLSAddress(net.JoinHostPort(bindHost, AlternatePort))
							 | 
						||
| 
								 | 
							
										//c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) // TODO: can we proxy TLS challenges? and we should support DNS...
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// Obtain obtains a single certificate for names. It stores the certificate
							 | 
						||
| 
								 | 
							
								// on the disk if successful.
							 | 
						||
| 
								 | 
							
								func (c *ACMEClient) Obtain(names []string) error {
							 | 
						||
| 
								 | 
							
								Attempts:
							 | 
						||
| 
								 | 
							
									for attempts := 0; attempts < 2; attempts++ {
							 | 
						||
| 
								 | 
							
										acmeMu.Lock()
							 | 
						||
| 
								 | 
							
										certificate, failures := c.ObtainCertificate(names, true, nil)
							 | 
						||
| 
								 | 
							
										acmeMu.Unlock()
							 | 
						||
| 
								 | 
							
										if len(failures) > 0 {
							 | 
						||
| 
								 | 
							
											// Error - try to fix it or report it to the user and abort
							 | 
						||
| 
								 | 
							
											var errMsg string             // we'll combine all the failures into a single error message
							 | 
						||
| 
								 | 
							
											var promptedForAgreement bool // only prompt user for agreement at most once
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
											for errDomain, obtainErr := range failures {
							 | 
						||
| 
								 | 
							
												// TODO: Double-check, will obtainErr ever be nil?
							 | 
						||
| 
								 | 
							
												if tosErr, ok := obtainErr.(acme.TOSError); ok {
							 | 
						||
| 
								 | 
							
													// Terms of Service agreement error; we can probably deal with this
							 | 
						||
| 
								 | 
							
													if !Agreed && !promptedForAgreement && c.AllowPrompts {
							 | 
						||
| 
								 | 
							
														Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
							 | 
						||
| 
								 | 
							
														promptedForAgreement = true
							 | 
						||
| 
								 | 
							
													}
							 | 
						||
| 
								 | 
							
													if Agreed || !c.AllowPrompts {
							 | 
						||
| 
								 | 
							
														err := c.AgreeToTOS()
							 | 
						||
| 
								 | 
							
														if err != nil {
							 | 
						||
| 
								 | 
							
															return errors.New("error agreeing to updated terms: " + err.Error())
							 | 
						||
| 
								 | 
							
														}
							 | 
						||
| 
								 | 
							
														continue Attempts
							 | 
						||
| 
								 | 
							
													}
							 | 
						||
| 
								 | 
							
												}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
												// If user did not agree or it was any other kind of error, just append to the list of errors
							 | 
						||
| 
								 | 
							
												errMsg += "[" + errDomain + "] failed to get certificate: " + obtainErr.Error() + "\n"
							 | 
						||
| 
								 | 
							
											}
							 | 
						||
| 
								 | 
							
											return errors.New(errMsg)
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// Success - immediately save the certificate resource
							 | 
						||
| 
								 | 
							
										err := saveCertResource(certificate)
							 | 
						||
| 
								 | 
							
										if err != nil {
							 | 
						||
| 
								 | 
							
											return fmt.Errorf("error saving assets for %v: %v", names, err)
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										break
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return nil
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// Renew renews the managed certificate for name. Right now our storage
							 | 
						||
| 
								 | 
							
								// mechanism only supports one name per certificate, so this function only
							 | 
						||
| 
								 | 
							
								// accepts one domain as input. It can be easily modified to support SAN
							 | 
						||
| 
								 | 
							
								// certificates if, one day, they become desperately needed enough that our
							 | 
						||
| 
								 | 
							
								// storage mechanism is upgraded to be more complex to support SAN certs.
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								// Anyway, this function is safe for concurrent use.
							 | 
						||
| 
								 | 
							
								func (c *ACMEClient) Renew(name string) error {
							 | 
						||
| 
								 | 
							
									// Prepare for renewal (load PEM cert, key, and meta)
							 | 
						||
| 
								 | 
							
									certBytes, err := ioutil.ReadFile(storage.SiteCertFile(name))
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									keyBytes, err := ioutil.ReadFile(storage.SiteKeyFile(name))
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(name))
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									var certMeta acme.CertificateResource
							 | 
						||
| 
								 | 
							
									err = json.Unmarshal(metaBytes, &certMeta)
							 | 
						||
| 
								 | 
							
									certMeta.Certificate = certBytes
							 | 
						||
| 
								 | 
							
									certMeta.PrivateKey = keyBytes
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Perform renewal and retry if necessary, but not too many times.
							 | 
						||
| 
								 | 
							
									var newCertMeta acme.CertificateResource
							 | 
						||
| 
								 | 
							
									var success bool
							 | 
						||
| 
								 | 
							
									for attempts := 0; attempts < 2; attempts++ {
							 | 
						||
| 
								 | 
							
										acmeMu.Lock()
							 | 
						||
| 
								 | 
							
										newCertMeta, err = c.RenewCertificate(certMeta, true)
							 | 
						||
| 
								 | 
							
										acmeMu.Unlock()
							 | 
						||
| 
								 | 
							
										if err == nil {
							 | 
						||
| 
								 | 
							
											success = true
							 | 
						||
| 
								 | 
							
											break
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// If the legal terms changed and need to be agreed to again,
							 | 
						||
| 
								 | 
							
										// we can handle that.
							 | 
						||
| 
								 | 
							
										if _, ok := err.(acme.TOSError); ok {
							 | 
						||
| 
								 | 
							
											err := c.AgreeToTOS()
							 | 
						||
| 
								 | 
							
											if err != nil {
							 | 
						||
| 
								 | 
							
												return err
							 | 
						||
| 
								 | 
							
											}
							 | 
						||
| 
								 | 
							
											continue
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// For any other kind of error, wait 10s and try again.
							 | 
						||
| 
								 | 
							
										time.Sleep(10 * time.Second)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if !success {
							 | 
						||
| 
								 | 
							
										return errors.New("too many renewal attempts; last error: " + err.Error())
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return saveCertResource(newCertMeta)
							 | 
						||
| 
								 | 
							
								}
							 |