aboutsummaryrefslogtreecommitdiff
path: root/core/https/client.go
diff options
context:
space:
mode:
Diffstat (limited to 'core/https/client.go')
-rw-r--r--core/https/client.go215
1 files changed, 215 insertions, 0 deletions
diff --git a/core/https/client.go b/core/https/client.go
new file mode 100644
index 000000000..e9e8cd82c
--- /dev/null
+++ b/core/https/client.go
@@ -0,0 +1,215 @@
+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)
+}