aboutsummaryrefslogtreecommitdiff
path: root/core/https
diff options
context:
space:
mode:
Diffstat (limited to 'core/https')
-rw-r--r--core/https/certificates.go234
-rw-r--r--core/https/certificates_test.go59
-rw-r--r--core/https/client.go215
-rw-r--r--core/https/crypto.go57
-rw-r--r--core/https/crypto_test.go111
-rw-r--r--core/https/handler.go42
-rw-r--r--core/https/handler_test.go63
-rw-r--r--core/https/handshake.go316
-rw-r--r--core/https/handshake_test.go54
-rw-r--r--core/https/https.go339
-rw-r--r--core/https/https_test.go323
-rw-r--r--core/https/maintain.go211
-rw-r--r--core/https/setup.go321
-rw-r--r--core/https/setup_test.go226
-rw-r--r--core/https/storage.go94
-rw-r--r--core/https/storage_test.go88
-rw-r--r--core/https/user.go200
-rw-r--r--core/https/user_test.go196
18 files changed, 0 insertions, 3149 deletions
diff --git a/core/https/certificates.go b/core/https/certificates.go
deleted file mode 100644
index 6a8f3adc6..000000000
--- a/core/https/certificates.go
+++ /dev/null
@@ -1,234 +0,0 @@
-package https
-
-import (
- "crypto/tls"
- "crypto/x509"
- "errors"
- "io/ioutil"
- "log"
- "strings"
- "sync"
- "time"
-
- "github.com/xenolf/lego/acme"
- "golang.org/x/crypto/ocsp"
-)
-
-// certCache stores certificates in memory,
-// keying certificates by name.
-var certCache = make(map[string]Certificate)
-var certCacheMu sync.RWMutex
-
-// Certificate is a tls.Certificate with associated metadata tacked on.
-// Even if the metadata can be obtained by parsing the certificate,
-// we can be more efficient by extracting the metadata once so it's
-// just there, ready to use.
-type Certificate struct {
- tls.Certificate
-
- // Names is the list of names this certificate is written for.
- // The first is the CommonName (if any), the rest are SAN.
- Names []string
-
- // NotAfter is when the certificate expires.
- NotAfter time.Time
-
- // Managed certificates are certificates that CoreDNS is managing,
- // as opposed to the user specifying a certificate and key file
- // or directory and managing the certificate resources themselves.
- Managed bool
-
- // OnDemand certificates are obtained or loaded on-demand during TLS
- // handshakes (as opposed to preloaded certificates, which are loaded
- // at startup). If OnDemand is true, Managed must necessarily be true.
- // OnDemand certificates are maintained in the background just like
- // preloaded ones, however, if an OnDemand certificate fails to renew,
- // it is removed from the in-memory cache.
- OnDemand bool
-
- // OCSP contains the certificate's parsed OCSP response.
- OCSP *ocsp.Response
-}
-
-// getCertificate gets a certificate that matches name (a server name)
-// from the in-memory cache. If there is no exact match for name, it
-// will be checked against names of the form '*.example.com' (wildcard
-// certificates) according to RFC 6125. If a match is found, matched will
-// be true. If no matches are found, matched will be false and a default
-// certificate will be returned with defaulted set to true. If no default
-// certificate is set, defaulted will be set to false.
-//
-// The logic in this function is adapted from the Go standard library,
-// which is by the Go Authors.
-//
-// This function is safe for concurrent use.
-func getCertificate(name string) (cert Certificate, matched, defaulted bool) {
- var ok bool
-
- // Not going to trim trailing dots here since RFC 3546 says,
- // "The hostname is represented ... without a trailing dot."
- // Just normalize to lowercase.
- name = strings.ToLower(name)
-
- certCacheMu.RLock()
- defer certCacheMu.RUnlock()
-
- // exact match? great, let's use it
- if cert, ok = certCache[name]; ok {
- matched = true
- return
- }
-
- // try replacing labels in the name with wildcards until we get a match
- labels := strings.Split(name, ".")
- for i := range labels {
- labels[i] = "*"
- candidate := strings.Join(labels, ".")
- if cert, ok = certCache[candidate]; ok {
- matched = true
- return
- }
- }
-
- // if nothing matches, use the default certificate or bust
- cert, defaulted = certCache[""]
- return
-}
-
-// cacheManagedCertificate loads the certificate for domain into the
-// cache, flagging it as Managed and, if onDemand is true, as OnDemand
-// (meaning that it was obtained or loaded during a TLS handshake).
-//
-// This function is safe for concurrent use.
-func cacheManagedCertificate(domain string, onDemand bool) (Certificate, error) {
- cert, err := makeCertificateFromDisk(storage.SiteCertFile(domain), storage.SiteKeyFile(domain))
- if err != nil {
- return cert, err
- }
- cert.Managed = true
- cert.OnDemand = onDemand
- cacheCertificate(cert)
- return cert, nil
-}
-
-// cacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
-// and keyFile, which must be in PEM format. It stores the certificate in
-// memory. The Managed and OnDemand flags of the certificate will be set to
-// false.
-//
-// This function is safe for concurrent use.
-func cacheUnmanagedCertificatePEMFile(certFile, keyFile string) error {
- cert, err := makeCertificateFromDisk(certFile, keyFile)
- if err != nil {
- return err
- }
- cacheCertificate(cert)
- return nil
-}
-
-// cacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes
-// of the certificate and key, then caches it in memory.
-//
-// This function is safe for concurrent use.
-func cacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) error {
- cert, err := makeCertificate(certBytes, keyBytes)
- if err != nil {
- return err
- }
- cacheCertificate(cert)
- return nil
-}
-
-// makeCertificateFromDisk makes a Certificate by loading the
-// certificate and key files. It fills out all the fields in
-// the certificate except for the Managed and OnDemand flags.
-// (It is up to the caller to set those.)
-func makeCertificateFromDisk(certFile, keyFile string) (Certificate, error) {
- certPEMBlock, err := ioutil.ReadFile(certFile)
- if err != nil {
- return Certificate{}, err
- }
- keyPEMBlock, err := ioutil.ReadFile(keyFile)
- if err != nil {
- return Certificate{}, err
- }
- return makeCertificate(certPEMBlock, keyPEMBlock)
-}
-
-// makeCertificate turns a certificate PEM bundle and a key PEM block into
-// a Certificate, with OCSP and other relevant metadata tagged with it,
-// except for the OnDemand and Managed flags. It is up to the caller to
-// set those properties.
-func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
- var cert Certificate
-
- // Convert to a tls.Certificate
- tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
- if err != nil {
- return cert, err
- }
- if len(tlsCert.Certificate) == 0 {
- return cert, errors.New("certificate is empty")
- }
-
- // Parse leaf certificate and extract relevant metadata
- leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
- if err != nil {
- return cert, err
- }
- if leaf.Subject.CommonName != "" {
- cert.Names = []string{strings.ToLower(leaf.Subject.CommonName)}
- }
- for _, name := range leaf.DNSNames {
- if name != leaf.Subject.CommonName {
- cert.Names = append(cert.Names, strings.ToLower(name))
- }
- }
- cert.NotAfter = leaf.NotAfter
-
- // Staple OCSP
- ocspBytes, ocspResp, err := acme.GetOCSPForCert(certPEMBlock)
- if err != nil {
- // An error here is not a problem because a certificate may simply
- // not contain a link to an OCSP server. But we should log it anyway.
- log.Printf("[WARNING] No OCSP stapling for %v: %v", cert.Names, err)
- } else if ocspResp.Status == ocsp.Good {
- tlsCert.OCSPStaple = ocspBytes
- cert.OCSP = ocspResp
- }
-
- cert.Certificate = tlsCert
- return cert, nil
-}
-
-// cacheCertificate adds cert to the in-memory cache. If the cache is
-// empty, cert will be used as the default certificate. If the cache is
-// full, random entries are deleted until there is room to map all the
-// names on the certificate.
-//
-// This certificate will be keyed to the names in cert.Names. Any name
-// that is already a key in the cache will be replaced with this cert.
-//
-// This function is safe for concurrent use.
-func cacheCertificate(cert Certificate) {
- certCacheMu.Lock()
- if _, ok := certCache[""]; !ok {
- // use as default
- cert.Names = append(cert.Names, "")
- certCache[""] = cert
- }
- for len(certCache)+len(cert.Names) > 10000 {
- // for simplicity, just remove random elements
- for key := range certCache {
- if key == "" { // ... but not the default cert
- continue
- }
- delete(certCache, key)
- break
- }
- }
- for _, name := range cert.Names {
- certCache[name] = cert
- }
- certCacheMu.Unlock()
-}
diff --git a/core/https/certificates_test.go b/core/https/certificates_test.go
deleted file mode 100644
index dbfb4efc1..000000000
--- a/core/https/certificates_test.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package https
-
-import "testing"
-
-func TestUnexportedGetCertificate(t *testing.T) {
- defer func() { certCache = make(map[string]Certificate) }()
-
- // When cache is empty
- if _, matched, defaulted := getCertificate("example.com"); matched || defaulted {
- t.Errorf("Got a certificate when cache was empty; matched=%v, defaulted=%v", matched, defaulted)
- }
-
- // When cache has one certificate in it (also is default)
- defaultCert := Certificate{Names: []string{"example.com", ""}}
- certCache[""] = defaultCert
- certCache["example.com"] = defaultCert
- if cert, matched, defaulted := getCertificate("Example.com"); !matched || defaulted || cert.Names[0] != "example.com" {
- t.Errorf("Didn't get a cert for 'Example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
- }
- if cert, matched, defaulted := getCertificate(""); !matched || defaulted || cert.Names[0] != "example.com" {
- t.Errorf("Didn't get a cert for '' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
- }
-
- // When retrieving wildcard certificate
- certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}}
- if cert, matched, defaulted := getCertificate("sub.example.com"); !matched || defaulted || cert.Names[0] != "*.example.com" {
- t.Errorf("Didn't get wildcard cert for 'sub.example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
- }
-
- // When no certificate matches, the default is returned
- if cert, matched, defaulted := getCertificate("nomatch"); matched || !defaulted {
- t.Errorf("Expected matched=false, defaulted=true; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert)
- } else if cert.Names[0] != "example.com" {
- t.Errorf("Expected default cert, got: %v", cert)
- }
-}
-
-func TestCacheCertificate(t *testing.T) {
- defer func() { certCache = make(map[string]Certificate) }()
-
- cacheCertificate(Certificate{Names: []string{"example.com", "sub.example.com"}})
- if _, ok := certCache["example.com"]; !ok {
- t.Error("Expected first cert to be cached by key 'example.com', but it wasn't")
- }
- if _, ok := certCache["sub.example.com"]; !ok {
- t.Error("Expected first cert to be cached by key 'sub.exmaple.com', but it wasn't")
- }
- if cert, ok := certCache[""]; !ok || cert.Names[2] != "" {
- t.Error("Expected first cert to be cached additionally as the default certificate with empty name added, but it wasn't")
- }
-
- cacheCertificate(Certificate{Names: []string{"example2.com"}})
- if _, ok := certCache["example2.com"]; !ok {
- t.Error("Expected second cert to be cached by key 'exmaple2.com', but it wasn't")
- }
- if cert, ok := certCache[""]; ok && cert.Names[0] == "example2.com" {
- t.Error("Expected second cert to NOT be cached as default, but it was")
- }
-}
diff --git a/core/https/client.go b/core/https/client.go
deleted file mode 100644
index e9e8cd82c..000000000
--- a/core/https/client.go
+++ /dev/null
@@ -1,215 +0,0 @@
-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)
-}
diff --git a/core/https/crypto.go b/core/https/crypto.go
deleted file mode 100644
index 7971bda36..000000000
--- a/core/https/crypto.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package https
-
-import (
- "crypto"
- "crypto/ecdsa"
- "crypto/rsa"
- "crypto/x509"
- "encoding/pem"
- "errors"
- "io/ioutil"
- "os"
-)
-
-// loadPrivateKey loads a PEM-encoded ECC/RSA private key from file.
-func loadPrivateKey(file string) (crypto.PrivateKey, error) {
- keyBytes, err := ioutil.ReadFile(file)
- if err != nil {
- return nil, err
- }
- keyBlock, _ := pem.Decode(keyBytes)
-
- switch keyBlock.Type {
- case "RSA PRIVATE KEY":
- return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
- case "EC PRIVATE KEY":
- return x509.ParseECPrivateKey(keyBlock.Bytes)
- }
-
- return nil, errors.New("unknown private key type")
-}
-
-// savePrivateKey saves a PEM-encoded ECC/RSA private key to file.
-func savePrivateKey(key crypto.PrivateKey, file string) error {
- var pemType string
- var keyBytes []byte
- switch key := key.(type) {
- case *ecdsa.PrivateKey:
- var err error
- pemType = "EC"
- keyBytes, err = x509.MarshalECPrivateKey(key)
- if err != nil {
- return err
- }
- case *rsa.PrivateKey:
- pemType = "RSA"
- keyBytes = x509.MarshalPKCS1PrivateKey(key)
- }
-
- pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes}
- keyOut, err := os.Create(file)
- if err != nil {
- return err
- }
- keyOut.Chmod(0600)
- defer keyOut.Close()
- return pem.Encode(keyOut, &pemKey)
-}
diff --git a/core/https/crypto_test.go b/core/https/crypto_test.go
deleted file mode 100644
index 07d2af5c7..000000000
--- a/core/https/crypto_test.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package https
-
-import (
- "bytes"
- "crypto"
- "crypto/ecdsa"
- "crypto/elliptic"
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509"
- "os"
- "runtime"
- "testing"
-)
-
-func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
- keyFile := "test.key"
- defer os.Remove(keyFile)
-
- privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
- if err != nil {
- t.Fatal(err)
- }
-
- // test save
- err = savePrivateKey(privateKey, keyFile)
- if err != nil {
- t.Fatal("error saving private key:", err)
- }
-
- // it doesn't make sense to test file permission on windows
- if runtime.GOOS != "windows" {
- // get info of the key file
- info, err := os.Stat(keyFile)
- if err != nil {
- t.Fatal("error stating private key:", err)
- }
- // verify permission of key file is correct
- if info.Mode().Perm() != 0600 {
- t.Error("Expected key file to have permission 0600, but it wasn't")
- }
- }
-
- // test load
- loadedKey, err := loadPrivateKey(keyFile)
- if err != nil {
- t.Error("error loading private key:", err)
- }
-
- // verify loaded key is correct
- if !PrivateKeysSame(privateKey, loadedKey) {
- t.Error("Expected key bytes to be the same, but they weren't")
- }
-}
-
-func TestSaveAndLoadECCPrivateKey(t *testing.T) {
- keyFile := "test.key"
- defer os.Remove(keyFile)
-
- privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
- if err != nil {
- t.Fatal(err)
- }
-
- // test save
- err = savePrivateKey(privateKey, keyFile)
- if err != nil {
- t.Fatal("error saving private key:", err)
- }
-
- // it doesn't make sense to test file permission on windows
- if runtime.GOOS != "windows" {
- // get info of the key file
- info, err := os.Stat(keyFile)
- if err != nil {
- t.Fatal("error stating private key:", err)
- }
- // verify permission of key file is correct
- if info.Mode().Perm() != 0600 {
- t.Error("Expected key file to have permission 0600, but it wasn't")
- }
- }
-
- // test load
- loadedKey, err := loadPrivateKey(keyFile)
- if err != nil {
- t.Error("error loading private key:", err)
- }
-
- // verify loaded key is correct
- if !PrivateKeysSame(privateKey, loadedKey) {
- t.Error("Expected key bytes to be the same, but they weren't")
- }
-}
-
-// PrivateKeysSame compares the bytes of a and b and returns true if they are the same.
-func PrivateKeysSame(a, b crypto.PrivateKey) bool {
- return bytes.Equal(PrivateKeyBytes(a), PrivateKeyBytes(b))
-}
-
-// PrivateKeyBytes returns the bytes of DER-encoded key.
-func PrivateKeyBytes(key crypto.PrivateKey) []byte {
- var keyBytes []byte
- switch key := key.(type) {
- case *rsa.PrivateKey:
- keyBytes = x509.MarshalPKCS1PrivateKey(key)
- case *ecdsa.PrivateKey:
- keyBytes, _ = x509.MarshalECPrivateKey(key)
- }
- return keyBytes
-}
diff --git a/core/https/handler.go b/core/https/handler.go
deleted file mode 100644
index f3139f54e..000000000
--- a/core/https/handler.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package https
-
-import (
- "crypto/tls"
- "log"
- "net/http"
- "net/http/httputil"
- "net/url"
- "strings"
-)
-
-const challengeBasePath = "/.well-known/acme-challenge"
-
-// RequestCallback proxies challenge requests to ACME client if the
-// request path starts with challengeBasePath. It returns true if it
-// handled the request and no more needs to be done; it returns false
-// if this call was a no-op and the request still needs handling.
-func RequestCallback(w http.ResponseWriter, r *http.Request) bool {
- if strings.HasPrefix(r.URL.Path, challengeBasePath) {
- scheme := "http"
- if r.TLS != nil {
- scheme = "https"
- }
-
- upstream, err := url.Parse(scheme + "://localhost:" + AlternatePort)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- log.Printf("[ERROR] ACME proxy handler: %v", err)
- return true
- }
-
- proxy := httputil.NewSingleHostReverseProxy(upstream)
- proxy.Transport = &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs
- }
- proxy.ServeHTTP(w, r)
-
- return true
- }
-
- return false
-}
diff --git a/core/https/handler_test.go b/core/https/handler_test.go
deleted file mode 100644
index 016799ffb..000000000
--- a/core/https/handler_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package https
-
-import (
- "net"
- "net/http"
- "net/http/httptest"
- "testing"
-)
-
-func TestRequestCallbackNoOp(t *testing.T) {
- // try base paths that aren't handled by this handler
- for _, url := range []string{
- "http://localhost/",
- "http://localhost/foo.html",
- "http://localhost/.git",
- "http://localhost/.well-known/",
- "http://localhost/.well-known/acme-challenging",
- } {
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- t.Fatalf("Could not craft request, got error: %v", err)
- }
- rw := httptest.NewRecorder()
- if RequestCallback(rw, req) {
- t.Errorf("Got true with this URL, but shouldn't have: %s", url)
- }
- }
-}
-
-func TestRequestCallbackSuccess(t *testing.T) {
- expectedPath := challengeBasePath + "/asdf"
-
- // Set up fake acme handler backend to make sure proxying succeeds
- var proxySuccess bool
- ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- proxySuccess = true
- if r.URL.Path != expectedPath {
- t.Errorf("Expected path '%s' but got '%s' instead", expectedPath, r.URL.Path)
- }
- }))
-
- // Custom listener that uses the port we expect
- ln, err := net.Listen("tcp", "127.0.0.1:"+AlternatePort)
- if err != nil {
- t.Fatalf("Unable to start test server listener: %v", err)
- }
- ts.Listener = ln
-
- // Start our engines and run the test
- ts.Start()
- defer ts.Close()
- req, err := http.NewRequest("GET", "http://127.0.0.1:"+AlternatePort+expectedPath, nil)
- if err != nil {
- t.Fatalf("Could not craft request, got error: %v", err)
- }
- rw := httptest.NewRecorder()
-
- RequestCallback(rw, req)
-
- if !proxySuccess {
- t.Fatal("Expected request to be proxied, but it wasn't")
- }
-}
diff --git a/core/https/handshake.go b/core/https/handshake.go
deleted file mode 100644
index a05231c49..000000000
--- a/core/https/handshake.go
+++ /dev/null
@@ -1,316 +0,0 @@
-package https
-
-import (
- "bytes"
- "crypto/tls"
- "encoding/pem"
- "errors"
- "fmt"
- "log"
- "strings"
- "sync"
- "sync/atomic"
- "time"
-
- "github.com/miekg/coredns/server"
- "github.com/xenolf/lego/acme"
-)
-
-// GetCertificate gets a certificate to satisfy clientHello as long as
-// the certificate is already cached in memory. It will not be loaded
-// from disk or obtained from the CA during the handshake.
-//
-// This function is safe for use as a tls.Config.GetCertificate callback.
-func GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
- cert, err := getCertDuringHandshake(clientHello.ServerName, false, false)
- return &cert.Certificate, err
-}
-
-// GetOrObtainCertificate will get a certificate to satisfy clientHello, even
-// if that means obtaining a new certificate from a CA during the handshake.
-// It first checks the in-memory cache, then accesses disk, then accesses the
-// network if it must. An obtained certificate will be stored on disk and
-// cached in memory.
-//
-// This function is safe for use as a tls.Config.GetCertificate callback.
-func GetOrObtainCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
- cert, err := getCertDuringHandshake(clientHello.ServerName, true, true)
- return &cert.Certificate, err
-}
-
-// getCertDuringHandshake will get a certificate for name. It first tries
-// the in-memory cache. If no certificate for name is in the cache and if
-// loadIfNecessary == true, it goes to disk to load it into the cache and
-// serve it. If it's not on disk and if obtainIfNecessary == true, the
-// certificate will be obtained from the CA, cached, and served. If
-// obtainIfNecessary is true, then loadIfNecessary must also be set to true.
-// An error will be returned if and only if no certificate is available.
-//
-// This function is safe for concurrent use.
-func getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) {
- // First check our in-memory cache to see if we've already loaded it
- cert, matched, defaulted := getCertificate(name)
- if matched {
- return cert, nil
- }
-
- if loadIfNecessary {
- // Then check to see if we have one on disk
- loadedCert, err := cacheManagedCertificate(name, true)
- if err == nil {
- loadedCert, err = handshakeMaintenance(name, loadedCert)
- if err != nil {
- log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err)
- }
- return loadedCert, nil
- }
-
- if obtainIfNecessary {
- // By this point, we need to ask the CA for a certificate
-
- name = strings.ToLower(name)
-
- // Make sure aren't over any applicable limits
- err := checkLimitsForObtainingNewCerts(name)
- if err != nil {
- return Certificate{}, err
- }
-
- // TODO(miek): deleted, tls will be enabled when a keyword is specified.
- // Obtain certificate from the CA
- return obtainOnDemandCertificate(name)
- }
- }
-
- if defaulted {
- return cert, nil
- }
-
- return Certificate{}, errors.New("no certificate for " + name)
-}
-
-// checkLimitsForObtainingNewCerts checks to see if name can be issued right
-// now according to mitigating factors we keep track of and preferences the
-// user has set. If a non-nil error is returned, do not issue a new certificate
-// for name.
-func checkLimitsForObtainingNewCerts(name string) error {
- // User can set hard limit for number of certs for the process to issue
- if onDemandMaxIssue > 0 && atomic.LoadInt32(OnDemandIssuedCount) >= onDemandMaxIssue {
- return fmt.Errorf("%s: maximum certificates issued (%d)", name, onDemandMaxIssue)
- }
-
- // Make sure name hasn't failed a challenge recently
- failedIssuanceMu.RLock()
- when, ok := failedIssuance[name]
- failedIssuanceMu.RUnlock()
- if ok {
- return fmt.Errorf("%s: throttled; refusing to issue cert since last attempt on %s failed", name, when.String())
- }
-
- // Make sure, if we've issued a few certificates already, that we haven't
- // issued any recently
- lastIssueTimeMu.Lock()
- since := time.Since(lastIssueTime)
- lastIssueTimeMu.Unlock()
- if atomic.LoadInt32(OnDemandIssuedCount) >= 10 && since < 10*time.Minute {
- return fmt.Errorf("%s: throttled; last certificate was obtained %v ago", name, since)
- }
-
- // 👍Good to go
- return nil
-}
-
-// obtainOnDemandCertificate obtains a certificate for name for the given
-// name. If another goroutine has already started obtaining a cert for
-// name, it will wait and use what the other goroutine obtained.
-//
-// This function is safe for use by multiple concurrent goroutines.
-func obtainOnDemandCertificate(name string) (Certificate, error) {
- // We must protect this process from happening concurrently, so synchronize.
- obtainCertWaitChansMu.Lock()
- wait, ok := obtainCertWaitChans[name]
- if ok {
- // lucky us -- another goroutine is already obtaining the certificate.
- // wait for it to finish obtaining the cert and then we'll use it.
- obtainCertWaitChansMu.Unlock()
- <-wait
- return getCertDuringHandshake(name, true, false)
- }
-
- // looks like it's up to us to do all the work and obtain the cert
- wait = make(chan struct{})
- obtainCertWaitChans[name] = wait
- obtainCertWaitChansMu.Unlock()
-
- // Unblock waiters and delete waitgroup when we return
- defer func() {
- obtainCertWaitChansMu.Lock()
- close(wait)
- delete(obtainCertWaitChans, name)
- obtainCertWaitChansMu.Unlock()
- }()
-
- log.Printf("[INFO] Obtaining new certificate for %s", name)
-
- // obtain cert
- client, err := NewACMEClientGetEmail(server.Config{}, false)
- if err != nil {
- return Certificate{}, errors.New("error creating client: " + err.Error())
- }
- client.Configure("") // TODO: which BindHost?
- err = client.Obtain([]string{name})
- if err != nil {
- // Failed to solve challenge, so don't allow another on-demand
- // issue for this name to be attempted for a little while.
- failedIssuanceMu.Lock()
- failedIssuance[name] = time.Now()
- go func(name string) {
- time.Sleep(5 * time.Minute)
- failedIssuanceMu.Lock()
- delete(failedIssuance, name)
- failedIssuanceMu.Unlock()
- }(name)
- failedIssuanceMu.Unlock()
- return Certificate{}, err
- }
-
- // Success - update counters and stuff
- atomic.AddInt32(OnDemandIssuedCount, 1)
- lastIssueTimeMu.Lock()
- lastIssueTime = time.Now()
- lastIssueTimeMu.Unlock()
-
- // The certificate is already on disk; now just start over to load it and serve it
- return getCertDuringHandshake(name, true, false)
-}
-
-// handshakeMaintenance performs a check on cert for expiration and OCSP
-// validity.
-//
-// This function is safe for use by multiple concurrent goroutines.
-func handshakeMaintenance(name string, cert Certificate) (Certificate, error) {
- // Check cert expiration
- timeLeft := cert.NotAfter.Sub(time.Now().UTC())
- if timeLeft < renewDurationBefore {
- log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft)
- return renewDynamicCertificate(name)
- }
-
- // Check OCSP staple validity
- if cert.OCSP != nil {
- refreshTime := cert.OCSP.ThisUpdate.Add(cert.OCSP.NextUpdate.Sub(cert.OCSP.ThisUpdate) / 2)
- if time.Now().After(refreshTime) {
- err := stapleOCSP(&cert, nil)
- if err != nil {
- // An error with OCSP stapling is not the end of the world, and in fact, is
- // quite common considering not all certs have issuer URLs that support it.
- log.Printf("[ERROR] Getting OCSP for %s: %v", name, err)
- }
- certCacheMu.Lock()
- certCache[name] = cert
- certCacheMu.Unlock()
- }
- }
-
- return cert, nil
-}
-
-// renewDynamicCertificate renews currentCert using the clientHello. It returns the
-// certificate to use and an error, if any. currentCert may be returned even if an
-// error occurs, since we perform renewals before they expire and it may still be
-// usable. name should already be lower-cased before calling this function.
-//
-// This function is safe for use by multiple concurrent goroutines.
-func renewDynamicCertificate(name string) (Certificate, error) {
- obtainCertWaitChansMu.Lock()
- wait, ok := obtainCertWaitChans[name]
- if ok {
- // lucky us -- another goroutine is already renewing the certificate.
- // wait for it to finish, then we'll use the new one.
- obtainCertWaitChansMu.Unlock()
- <-wait
- return getCertDuringHandshake(name, true, false)
- }
-
- // looks like it's up to us to do all the work and renew the cert
- wait = make(chan struct{})
- obtainCertWaitChans[name] = wait
- obtainCertWaitChansMu.Unlock()
-
- // unblock waiters and delete waitgroup when we return
- defer func() {
- obtainCertWaitChansMu.Lock()
- close(wait)
- delete(obtainCertWaitChans, name)
- obtainCertWaitChansMu.Unlock()
- }()
-
- log.Printf("[INFO] Renewing certificate for %s", name)
-
- client, err := NewACMEClientGetEmail(server.Config{}, false)
- if err != nil {
- return Certificate{}, err
- }
- client.Configure("") // TODO: Bind address of relevant listener, yuck
- err = client.Renew(name)
- if err != nil {
- return Certificate{}, err
- }
-
- return getCertDuringHandshake(name, true, false)
-}
-
-// stapleOCSP staples OCSP information to cert for hostname name.
-// If you have it handy, you should pass in the PEM-encoded certificate
-// bundle; otherwise the DER-encoded cert will have to be PEM-encoded.
-// If you don't have the PEM blocks handy, just pass in nil.
-//
-// Errors here are not necessarily fatal, it could just be that the
-// certificate doesn't have an issuer URL.
-func stapleOCSP(cert *Certificate, pemBundle []byte) error {
- if pemBundle == nil {
- // The function in the acme package that gets OCSP requires a PEM-encoded cert
- bundle := new(bytes.Buffer)
- for _, derBytes := range cert.Certificate.Certificate {
- pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
- }
- pemBundle = bundle.Bytes()
- }
-
- ocspBytes, ocspResp, err := acme.GetOCSPForCert(pemBundle)
- if err != nil {
- return err
- }
-
- cert.Certificate.OCSPStaple = ocspBytes
- cert.OCSP = ocspResp
-
- return nil
-}
-
-// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
-var obtainCertWaitChans = make(map[string]chan struct{})
-var obtainCertWaitChansMu sync.Mutex
-
-// OnDemandIssuedCount is the number of certificates that have been issued
-// on-demand by this process. It is only safe to modify this count atomically.
-// If it reaches onDemandMaxIssue, on-demand issuances will fail.
-var OnDemandIssuedCount = new(int32)
-
-// onDemandMaxIssue is set based on max_certs in tls config. It specifies the
-// maximum number of certificates that can be issued.
-// TODO: This applies globally, but we should probably make a server-specific
-// way to keep track of these limits and counts, since it's specified in the
-// Corefile...
-var onDemandMaxIssue int32
-
-// failedIssuance is a set of names that we recently failed to get a
-// certificate for from the ACME CA. They are removed after some time.
-// When a name is in this map, do not issue a certificate for it on-demand.
-var failedIssuance = make(map[string]time.Time)
-var failedIssuanceMu sync.RWMutex
-
-// lastIssueTime records when we last obtained a certificate successfully.
-// If this value is recent, do not make any on-demand certificate requests.
-var lastIssueTime time.Time
-var lastIssueTimeMu sync.Mutex
diff --git a/core/https/handshake_test.go b/core/https/handshake_test.go
deleted file mode 100644
index cf70eb17d..000000000
--- a/core/https/handshake_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package https
-
-import (
- "crypto/tls"
- "crypto/x509"
- "testing"
-)
-
-func TestGetCertificate(t *testing.T) {
- defer func() { certCache = make(map[string]Certificate) }()
-
- hello := &tls.ClientHelloInfo{ServerName: "example.com"}
- helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com"}
- helloNoSNI := &tls.ClientHelloInfo{}
- helloNoMatch := &tls.ClientHelloInfo{ServerName: "nomatch"}
-
- // When cache is empty
- if cert, err := GetCertificate(hello); err == nil {
- t.Errorf("GetCertificate should return error when cache is empty, got: %v", cert)
- }
- if cert, err := GetCertificate(helloNoSNI); err == nil {
- t.Errorf("GetCertificate should return error when cache is empty even if server name is blank, got: %v", cert)
- }
-
- // When cache has one certificate in it (also is default)
- defaultCert := Certificate{Names: []string{"example.com", ""}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"example.com"}}}}
- certCache[""] = defaultCert
- certCache["example.com"] = defaultCert
- if cert, err := GetCertificate(hello); err != nil {
- t.Errorf("Got an error but shouldn't have, when cert exists in cache: %v", err)
- } else if cert.Leaf.DNSNames[0] != "example.com" {
- t.Errorf("Got wrong certificate with exact match; expected 'example.com', got: %v", cert)
- }
- if cert, err := GetCertificate(helloNoSNI); err != nil {
- t.Errorf("Got an error with no SNI but shouldn't have, when cert exists in cache: %v", err)
- } else if cert.Leaf.DNSNames[0] != "example.com" {
- t.Errorf("Got wrong certificate for no SNI; expected 'example.com' as default, got: %v", cert)
- }
-
- // When retrieving wildcard certificate
- certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"*.example.com"}}}}
- if cert, err := GetCertificate(helloSub); err != nil {
- t.Errorf("Didn't get wildcard cert, got: cert=%v, err=%v ", cert, err)
- } else if cert.Leaf.DNSNames[0] != "*.example.com" {
- t.Errorf("Got wrong certificate, expected wildcard: %v", cert)
- }
-
- // When no certificate matches, the default is returned
- if cert, err := GetCertificate(helloNoMatch); err != nil {
- t.Errorf("Expected default certificate with no error when no matches, got err: %v", err)
- } else if cert.Leaf.DNSNames[0] != "example.com" {
- t.Errorf("Expected default cert with no matches, got: %v", cert)
- }
-}
diff --git a/core/https/https.go b/core/https/https.go
deleted file mode 100644
index 99ef2fef6..000000000
--- a/core/https/https.go
+++ /dev/null
@@ -1,339 +0,0 @@
-// Package https facilitates the management of TLS assets and integrates
-// Let's Encrypt functionality into CoreDNS with first-class support for
-// creating and renewing certificates automatically. It is designed to
-// configure sites for HTTPS by default.
-package https
-
-import (
- "encoding/json"
- "errors"
- "io/ioutil"
- "net"
- "os"
-
- "github.com/miekg/coredns/server"
- "github.com/xenolf/lego/acme"
-)
-
-// Activate sets up TLS for each server config in configs
-// as needed; this consists of acquiring and maintaining
-// certificates and keys for qualifying configs and enabling
-// OCSP stapling for all TLS-enabled configs.
-//
-// This function may prompt the user to provide an email
-// address if none is available through other means. It
-// prefers the email address specified in the config, but
-// if that is not available it will check the command line
-// argument. If absent, it will use the most recent email
-// address from last time. If there isn't one, the user
-// will be prompted and shown SA link.
-//
-// Also note that calling this function activates asset
-// management automatically, which keeps certificates
-// renewed and OCSP stapling updated.
-//
-// Activate returns the updated list of configs, since
-// some may have been appended, for example, to redirect
-// plaintext HTTP requests to their HTTPS counterpart.
-// This function only appends; it does not splice.
-func Activate(configs []server.Config) ([]server.Config, error) {
- // just in case previous caller forgot...
- Deactivate()
-
- // pre-screen each config and earmark the ones that qualify for managed TLS
- MarkQualified(configs)
-
- // place certificates and keys on disk
- err := ObtainCerts(configs, true, false)
- if err != nil {
- return configs, err
- }
-
- // update TLS configurations
- err = EnableTLS(configs, true)
- if err != nil {
- return configs, err
- }
-
- // renew all relevant certificates that need renewal. this is important
- // to do right away for a couple reasons, mainly because each restart,
- // the renewal ticker is reset, so if restarts happen more often than
- // the ticker interval, renewals would never happen. but doing
- // it right away at start guarantees that renewals aren't missed.
- err = renewManagedCertificates(true)
- if err != nil {
- return configs, err
- }
-
- // keep certificates renewed and OCSP stapling updated
- go maintainAssets(stopChan)
-
- return configs, nil
-}
-
-// Deactivate cleans up long-term, in-memory resources
-// allocated by calling Activate(). Essentially, it stops
-// the asset maintainer from running, meaning that certificates
-// will not be renewed, OCSP staples will not be updated, etc.
-func Deactivate() (err error) {
- defer func() {
- if rec := recover(); rec != nil {
- err = errors.New("already deactivated")
- }
- }()
- close(stopChan)
- stopChan = make(chan struct{})
- return
-}
-
-// MarkQualified scans each config and, if it qualifies for managed
-// TLS, it sets the Managed field of the TLSConfig to true.
-func MarkQualified(configs []server.Config) {
- for i := 0; i < len(configs); i++ {
- if ConfigQualifies(configs[i]) {
- configs[i].TLS.Managed = true
- }
- }
-}
-
-// ObtainCerts obtains certificates for all these configs as long as a
-// certificate does not already exist on disk. It does not modify the
-// configs at all; it only obtains and stores certificates and keys to
-// the disk. If allowPrompts is true, the user may be shown a prompt.
-// If proxyACME is true, the ACME challenges will be proxied to our alt port.
-func ObtainCerts(configs []server.Config, allowPrompts, proxyACME bool) error {
- // We group configs by email so we don't make the same clients over and
- // over. This has the potential to prompt the user for an email, but we
- // prevent that by assuming that if we already have a listener that can
- // proxy ACME challenge requests, then the server is already running and
- // the operator is no longer present.
- groupedConfigs := groupConfigsByEmail(configs, allowPrompts)
-
- for email, group := range groupedConfigs {
- // Wait as long as we can before creating the client, because it
- // may not be needed, for example, if we already have what we
- // need on disk. Creating a client involves the network and
- // potentially prompting the user, etc., so only do if necessary.
- var client *ACMEClient
-
- for _, cfg := range group {
- if existingCertAndKey(cfg.Host) {
- continue
- }
-
- // Now we definitely do need a client
- if client == nil {
- var err error
- client, err = NewACMEClient(email, allowPrompts)
- if err != nil {
- return errors.New("error creating client: " + err.Error())
- }
- }
-
- // c.Configure assumes that allowPrompts == !proxyACME,
- // but that's not always true. For example, a restart where
- // the user isn't present and we're not listening on port 80.
- // TODO: This could probably be refactored better.
- if proxyACME {
- client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
- client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
- client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
- } else {
- client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, ""))
- client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, ""))
- client.ExcludeChallenges([]acme.Challenge{acme.DNS01})
- }
-
- err := client.Obtain([]string{cfg.Host})
- if err != nil {
- return err
- }
- }
- }
-
- return nil
-}
-
-// groupConfigsByEmail groups configs by the email address to be used by an
-// ACME client. It only groups configs that have TLS enabled and that are
-// marked as Managed. If userPresent is true, the operator MAY be prompted
-// for an email address.
-func groupConfigsByEmail(configs []server.Config, userPresent bool) map[string][]server.Config {
- initMap := make(map[string][]server.Config)
- for _, cfg := range configs {
- if !cfg.TLS.Managed {
- continue
- }
- leEmail := getEmail(cfg, userPresent)
- initMap[leEmail] = append(initMap[leEmail], cfg)
- }
- return initMap
-}
-
-// EnableTLS configures each config to use TLS according to default settings.
-// It will only change configs that are marked as managed, and assumes that
-// certificates and keys are already on disk. If loadCertificates is true,
-// the certificates will be loaded from disk into the cache for this process
-// to use. If false, TLS will still be enabled and configured with default
-// settings, but no certificates will be parsed loaded into the cache, and
-// the returned error value will always be nil.
-func EnableTLS(configs []server.Config, loadCertificates bool) error {
- for i := 0; i < len(configs); i++ {
- if !configs[i].TLS.Managed {
- continue
- }
- configs[i].TLS.Enabled = true
- if loadCertificates {
- _, err := cacheManagedCertificate(configs[i].Host, false)
- if err != nil {
- return err
- }
- }
- setDefaultTLSParams(&configs[i])
- }
- return nil
-}
-
-// hostHasOtherPort returns true if there is another config in the list with the same
-// hostname that has port otherPort, or false otherwise. All the configs are checked
-// against the hostname of allConfigs[thisConfigIdx].
-func hostHasOtherPort(allConfigs []server.Config, thisConfigIdx int, otherPort string) bool {
- for i, otherCfg := range allConfigs {
- if i == thisConfigIdx {
- continue // has to be a config OTHER than the one we're comparing against
- }
- if otherCfg.Host == allConfigs[thisConfigIdx].Host && otherCfg.Port == otherPort {
- return true
- }
- }
- return false
-}
-
-// ConfigQualifies returns true if cfg qualifies for
-// fully managed TLS (but not on-demand TLS, which is
-// not considered here). It does NOT check to see if a
-// cert and key already exist for the config. If the
-// config does qualify, you should set cfg.TLS.Managed
-// to true and check that instead, because the process of
-// setting up the config may make it look like it
-// doesn't qualify even though it originally did.
-func ConfigQualifies(cfg server.Config) bool {
- return (!cfg.TLS.Manual || cfg.TLS.OnDemand) && // user might provide own cert and key
-
- // user can force-disable automatic HTTPS for this host
- cfg.Port != "80" &&
- cfg.TLS.LetsEncryptEmail != "off" &&
-
- // we get can't certs for some kinds of hostnames, but
- // on-demand TLS allows empty hostnames at startup
- cfg.TLS.OnDemand
-}
-
-// existingCertAndKey returns true if the host has a certificate
-// and private key in storage already, false otherwise.
-func existingCertAndKey(host string) bool {
- _, err := os.Stat(storage.SiteCertFile(host))
- if err != nil {
- return false
- }
- _, err = os.Stat(storage.SiteKeyFile(host))
- if err != nil {
- return false
- }
- return true
-}
-
-// saveCertResource saves the certificate resource to disk. This
-// includes the certificate file itself, the private key, and the
-// metadata file.
-func saveCertResource(cert acme.CertificateResource) error {
- err := os.MkdirAll(storage.Site(cert.Domain), 0700)
- if err != nil {
- return err
- }
-
- // Save cert
- err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
- if err != nil {
- return err
- }
-
- // Save private key
- err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
- if err != nil {
- return err
- }
-
- // Save cert metadata
- jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
- if err != nil {
- return err
- }
- err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
- if err != nil {
- return err
- }
-
- return nil
-}
-
-// Revoke revokes the certificate for host via ACME protocol.
-func Revoke(host string) error {
- if !existingCertAndKey(host) {
- return errors.New("no certificate and key for " + host)
- }
-
- email := getEmail(server.Config{Host: host}, true)
- if email == "" {
- return errors.New("email is required to revoke")
- }
-
- client, err := NewACMEClient(email, true)
- if err != nil {
- return err
- }
-
- certFile := storage.SiteCertFile(host)
- certBytes, err := ioutil.ReadFile(certFile)
- if err != nil {
- return err
- }
-
- err = client.RevokeCertificate(certBytes)
- if err != nil {
- return err
- }
-
- err = os.Remove(certFile)
- if err != nil {
- return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error())
- }
-
- return nil
-}
-
-var (
- // DefaultEmail represents the Let's Encrypt account email to use if none provided
- DefaultEmail string
-
- // Agreed indicates whether user has agreed to the Let's Encrypt SA
- Agreed bool
-
- // CAUrl represents the base URL to the CA's ACME endpoint
- CAUrl string
-)
-
-// AlternatePort is the port on which the acme client will open a
-// listener and solve the CA's challenges. If this alternate port
-// is used instead of the default port (80 or 443), then the
-// default port for the challenge must be forwarded to this one.
-const AlternatePort = "5033"
-
-// KeyType is the type to use for new keys.
-// This shouldn't need to change except for in tests;
-// the size can be drastically reduced for speed.
-var KeyType = acme.EC384
-
-// stopChan is used to signal the maintenance goroutine
-// to terminate.
-var stopChan chan struct{}
diff --git a/core/https/https_test.go b/core/https/https_test.go
deleted file mode 100644
index f19b3cde0..000000000
--- a/core/https/https_test.go
+++ /dev/null
@@ -1,323 +0,0 @@
-package https
-
-/*
-func TestHostQualifies(t *testing.T) {
- for i, test := range []struct {
- host string
- expect bool
- }{
- {"localhost", false},
- {"127.0.0.1", false},
- {"127.0.1.5", false},
- {"::1", false},
- {"[::1]", false},
- {"[::]", false},
- {"::", false},
- {"", false},
- {" ", false},
- {"0.0.0.0", false},
- {"192.168.1.3", false},
- {"10.0.2.1", false},
- {"169.112.53.4", false},
- {"foobar.com", true},
- {"sub.foobar.com", true},
- } {
- if HostQualifies(test.host) && !test.expect {
- t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
- }
- if !HostQualifies(test.host) && test.expect {
- t.Errorf("Test %d: Expected '%s' to qualify, but it did NOT", i, test.host)
- }
- }
-}
-
-func TestConfigQualifies(t *testing.T) {
- for i, test := range []struct {
- cfg server.Config
- expect bool
- }{
- {server.Config{Host: ""}, false},
- {server.Config{Host: "localhost"}, false},
- {server.Config{Host: "123.44.3.21"}, false},
- {server.Config{Host: "example.com"}, true},
- {server.Config{Host: "example.com", TLS: server.TLSConfig{Manual: true}}, false},
- {server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}}, false},
- {server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}}, true},
- {server.Config{Host: "example.com", Scheme: "http"}, false},
- {server.Config{Host: "example.com", Port: "80"}, false},
- {server.Config{Host: "example.com", Port: "1234"}, true},
- {server.Config{Host: "example.com", Scheme: "https"}, true},
- {server.Config{Host: "example.com", Port: "80", Scheme: "https"}, false},
- } {
- if test.expect && !ConfigQualifies(test.cfg) {
- t.Errorf("Test %d: Expected config to qualify, but it did NOT: %#v", i, test.cfg)
- }
- if !test.expect && ConfigQualifies(test.cfg) {
- t.Errorf("Test %d: Expected config to NOT qualify, but it did: %#v", i, test.cfg)
- }
- }
-}
-
-func TestRedirPlaintextHost(t *testing.T) {
- cfg := redirPlaintextHost(server.Config{
- Host: "example.com",
- BindHost: "93.184.216.34",
- Port: "1234",
- })
-
- // Check host and port
- if actual, expected := cfg.Host, "example.com"; actual != expected {
- t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
- }
- if actual, expected := cfg.BindHost, "93.184.216.34"; actual != expected {
- t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual)
- }
- if actual, expected := cfg.Port, "80"; actual != expected {
- t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
- }
-
- // Make sure redirect handler is set up properly
- if cfg.Middleware == nil || len(cfg.Middleware) != 1 {
- t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.Middleware)
- }
-
- handler, ok := cfg.Middleware[0](nil).(redirect.Redirect)
- if !ok {
- t.Fatalf("Expected a redirect.Redirect middleware, but got: %#v", handler)
- }
- if len(handler.Rules) != 1 {
- t.Fatalf("Expected one redirect rule, got: %#v", handler.Rules)
- }
-
- // Check redirect rule for correctness
- if actual, expected := handler.Rules[0].FromScheme, "http"; actual != expected {
- t.Errorf("Expected redirect rule to be from scheme '%s' but is actually from '%s'", expected, actual)
- }
- if actual, expected := handler.Rules[0].FromPath, "/"; actual != expected {
- t.Errorf("Expected redirect rule to be for path '%s' but is actually for '%s'", expected, actual)
- }
- if actual, expected := handler.Rules[0].To, "https://{host}:1234{uri}"; actual != expected {
- t.Errorf("Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
- }
- if actual, expected := handler.Rules[0].Code, http.StatusMovedPermanently; actual != expected {
- t.Errorf("Expected redirect rule to have code %d but was %d", expected, actual)
- }
-
- // browsers can infer a default port from scheme, so make sure the port
- // doesn't get added in explicitly for default ports like 443 for https.
- cfg = redirPlaintextHost(server.Config{Host: "example.com", Port: "443"})
- handler, ok = cfg.Middleware[0](nil).(redirect.Redirect)
- if actual, expected := handler.Rules[0].To, "https://{host}{uri}"; actual != expected {
- t.Errorf("(Default Port) Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
- }
-}
-
-func TestSaveCertResource(t *testing.T) {
- storage = Storage("./le_test_save")
- defer func() {
- err := os.RemoveAll(string(storage))
- if err != nil {
- t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
- }
- }()
-
- domain := "example.com"
- certContents := "certificate"
- keyContents := "private key"
- metaContents := `{
- "domain": "example.com",
- "certUrl": "https://example.com/cert",
- "certStableUrl": "https://example.com/cert/stable"
-}`
-
- cert := acme.CertificateResource{
- Domain: domain,
- CertURL: "https://example.com/cert",
- CertStableURL: "https://example.com/cert/stable",
- PrivateKey: []byte(keyContents),
- Certificate: []byte(certContents),
- }
-
- err := saveCertResource(cert)
- if err != nil {
- t.Fatalf("Expected no error, got: %v", err)
- }
-
- certFile, err := ioutil.ReadFile(storage.SiteCertFile(domain))
- if err != nil {
- t.Errorf("Expected no error reading certificate file, got: %v", err)
- }
- if string(certFile) != certContents {
- t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(certFile))
- }
-
- keyFile, err := ioutil.ReadFile(storage.SiteKeyFile(domain))
- if err != nil {
- t.Errorf("Expected no error reading private key file, got: %v", err)
- }
- if string(keyFile) != keyContents {
- t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(keyFile))
- }
-
- metaFile, err := ioutil.ReadFile(storage.SiteMetaFile(domain))
- if err != nil {
- t.Errorf("Expected no error reading meta file, got: %v", err)
- }
- if string(metaFile) != metaContents {
- t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(metaFile))
- }
-}
-
-func TestExistingCertAndKey(t *testing.T) {
- storage = Storage("./le_test_existing")
- defer func() {
- err := os.RemoveAll(string(storage))
- if err != nil {
- t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
- }
- }()
-
- domain := "example.com"
-
- if existingCertAndKey(domain) {
- t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain)
- }
-
- err := saveCertResource(acme.CertificateResource{
- Domain: domain,
- PrivateKey: []byte("key"),
- Certificate: []byte("cert"),
- })
- if err != nil {
- t.Fatalf("Expected no error, got: %v", err)
- }
-
- if !existingCertAndKey(domain) {
- t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain)
- }
-}
-
-func TestHostHasOtherPort(t *testing.T) {
- configs := []server.Config{
- {Host: "example.com", Port: "80"},
- {Host: "sub1.example.com", Port: "80"},
- {Host: "sub1.example.com", Port: "443"},
- }
-
- if hostHasOtherPort(configs, 0, "80") {
- t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`)
- }
- if hostHasOtherPort(configs, 0, "443") {
- t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`)
- }
- if !hostHasOtherPort(configs, 1, "443") {
- t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`)
- }
-}
-
-func TestMakePlaintextRedirects(t *testing.T) {
- configs := []server.Config{
- // Happy path = standard redirect from 80 to 443
- {Host: "example.com", TLS: server.TLSConfig{Managed: true}},
-
- // Host on port 80 already defined; don't change it (no redirect)
- {Host: "sub1.example.com", Port: "80", Scheme: "http"},
- {Host: "sub1.example.com", TLS: server.TLSConfig{Managed: true}},
-
- // Redirect from port 80 to port 5000 in this case
- {Host: "sub2.example.com", Port: "5000", TLS: server.TLSConfig{Managed: true}},
-
- // Can redirect from 80 to either 443 or 5001, but choose 443
- {Host: "sub3.example.com", Port: "443", TLS: server.TLSConfig{Managed: true}},
- {Host: "sub3.example.com", Port: "5001", Scheme: "https", TLS: server.TLSConfig{Managed: true}},
- }
-
- result := MakePlaintextRedirects(configs)
- expectedRedirCount := 3
-
- if len(result) != len(configs)+expectedRedirCount {
- t.Errorf("Expected %d redirect(s) to be added, but got %d",
- expectedRedirCount, len(result)-len(configs))
- }
-}
-
-func TestEnableTLS(t *testing.T) {
- configs := []server.Config{
- {Host: "example.com", TLS: server.TLSConfig{Managed: true}},
- {}, // not managed - no changes!
- }
-
- EnableTLS(configs, false)
-
- if !configs[0].TLS.Enabled {
- t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
- }
- if configs[1].TLS.Enabled {
- t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true")
- }
-}
-
-func TestGroupConfigsByEmail(t *testing.T) {
- if groupConfigsByEmail([]server.Config{}, false) == nil {
- t.Errorf("With empty input, returned map was nil, but expected non-nil map")
- }
-
- configs := []server.Config{
- {Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
- {Host: "sub1.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
- {Host: "sub2.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
- {Host: "sub3.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
- {Host: "sub4.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
- {Host: "sub5.example.com", TLS: server.TLSConfig{LetsEncryptEmail: ""}}, // not managed
- }
- DefaultEmail = "test@example.com"
-
- groups := groupConfigsByEmail(configs, true)
-
- if groups == nil {
- t.Fatalf("Returned map was nil, but expected values")
- }
-
- if len(groups) != 2 {
- t.Errorf("Expected 2 groups, got %d: %#v", len(groups), groups)
- }
- if len(groups["foo@bar"]) != 2 {
- t.Errorf("Expected 2 configs for foo@bar, got %d: %#v", len(groups["foobar"]), groups["foobar"])
- }
- if len(groups[DefaultEmail]) != 3 {
- t.Errorf("Expected 3 configs for %s, got %d: %#v", DefaultEmail, len(groups["foobar"]), groups["foobar"])
- }
-}
-
-func TestMarkQualified(t *testing.T) {
- // TODO: TestConfigQualifies and this test share the same config list...
- configs := []server.Config{
- {Host: ""},
- {Host: "localhost"},
- {Host: "123.44.3.21"},
- {Host: "example.com"},
- {Host: "example.com", TLS: server.TLSConfig{Manual: true}},
- {Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}},
- {Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}},
- {Host: "example.com", Scheme: "http"},
- {Host: "example.com", Port: "80"},
- {Host: "example.com", Port: "1234"},
- {Host: "example.com", Scheme: "https"},
- {Host: "example.com", Port: "80", Scheme: "https"},
- }
- expectedManagedCount := 4
-
- MarkQualified(configs)
-
- count := 0
- for _, cfg := range configs {
- if cfg.TLS.Managed {
- count++
- }
- }
-
- if count != expectedManagedCount {
- t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
- }
-}
-*/
diff --git a/core/https/maintain.go b/core/https/maintain.go
deleted file mode 100644
index 46fd3d1f5..000000000
--- a/core/https/maintain.go
+++ /dev/null
@@ -1,211 +0,0 @@
-package https
-
-import (
- "log"
- "time"
-
- "github.com/miekg/coredns/server"
-
- "golang.org/x/crypto/ocsp"
-)
-
-const (
- // RenewInterval is how often to check certificates for renewal.
- RenewInterval = 12 * time.Hour
-
- // OCSPInterval is how often to check if OCSP stapling needs updating.
- OCSPInterval = 1 * time.Hour
-)
-
-// maintainAssets is a permanently-blocking function
-// that loops indefinitely and, on a regular schedule, checks
-// certificates for expiration and initiates a renewal of certs
-// that are expiring soon. It also updates OCSP stapling and
-// performs other maintenance of assets.
-//
-// You must pass in the channel which you'll close when
-// maintenance should stop, to allow this goroutine to clean up
-// after itself and unblock.
-func maintainAssets(stopChan chan struct{}) {
- renewalTicker := time.NewTicker(RenewInterval)
- ocspTicker := time.NewTicker(OCSPInterval)
-
- for {
- select {
- case <-renewalTicker.C:
- log.Println("[INFO] Scanning for expiring certificates")
- renewManagedCertificates(false)
- log.Println("[INFO] Done checking certificates")
- case <-ocspTicker.C:
- log.Println("[INFO] Scanning for stale OCSP staples")
- updateOCSPStaples()
- log.Println("[INFO] Done checking OCSP staples")
- case <-stopChan:
- renewalTicker.Stop()
- ocspTicker.Stop()
- log.Println("[INFO] Stopped background maintenance routine")
- return
- }
- }
-}
-
-func renewManagedCertificates(allowPrompts bool) (err error) {
- var renewed, deleted []Certificate
- var client *ACMEClient
- visitedNames := make(map[string]struct{})
-
- certCacheMu.RLock()
- for name, cert := range certCache {
- if !cert.Managed {
- continue
- }
-
- // the list of names on this cert should never be empty...
- if cert.Names == nil || len(cert.Names) == 0 {
- log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v", name, cert.Names)
- deleted = append(deleted, cert)
- continue
- }
-
- // skip names whose certificate we've already renewed
- if _, ok := visitedNames[name]; ok {
- continue
- }
- for _, name := range cert.Names {
- visitedNames[name] = struct{}{}
- }
-
- timeLeft := cert.NotAfter.Sub(time.Now().UTC())
- if timeLeft < renewDurationBefore {
- log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft)
-
- if client == nil {
- client, err = NewACMEClientGetEmail(server.Config{}, allowPrompts)
- if err != nil {
- return err
- }
- client.Configure("") // TODO: Bind address of relevant listener, yuck
- }
-
- err := client.Renew(cert.Names[0]) // managed certs better have only one name
- if err != nil {
- if client.AllowPrompts && timeLeft < 0 {
- // Certificate renewal failed, the operator is present, and the certificate
- // is already expired; we should stop immediately and return the error. Note
- // that we used to do this any time a renewal failed at startup. However,
- // after discussion in https://github.com/miekg/coredns/issues/642 we decided to
- // only stop startup if the certificate is expired. We still log the error
- // otherwise.
- certCacheMu.RUnlock()
- return err
- }
- log.Printf("[ERROR] %v", err)
- if cert.OnDemand {
- deleted = append(deleted, cert)
- }
- } else {
- renewed = append(renewed, cert)
- }
- }
- }
- certCacheMu.RUnlock()
-
- // Apply changes to the cache
- for _, cert := range renewed {
- _, err := cacheManagedCertificate(cert.Names[0], cert.OnDemand)
- if err != nil {
- if client.AllowPrompts {
- return err // operator is present, so report error immediately
- }
- log.Printf("[ERROR] %v", err)
- }
- }
- for _, cert := range deleted {
- certCacheMu.Lock()
- for _, name := range cert.Names {
- delete(certCache, name)
- }
- certCacheMu.Unlock()
- }
-
- return nil
-}
-
-func updateOCSPStaples() {
- // Create a temporary place to store updates
- // until we release the potentially long-lived
- // read lock and use a short-lived write lock.
- type ocspUpdate struct {
- rawBytes []byte
- parsed *ocsp.Response
- }
- updated := make(map[string]ocspUpdate)
-
- // A single SAN certificate maps to multiple names, so we use this
- // set to make sure we don't waste cycles checking OCSP for the same
- // certificate multiple times.
- visited := make(map[string]struct{})
-
- certCacheMu.RLock()
- for name, cert := range certCache {
- // skip this certificate if we've already visited it,
- // and if not, mark all the names as visited
- if _, ok := visited[name]; ok {
- continue
- }
- for _, n := range cert.Names {
- visited[n] = struct{}{}
- }
-
- // no point in updating OCSP for expired certificates
- if time.Now().After(cert.NotAfter) {
- continue
- }
-
- var lastNextUpdate time.Time
- if cert.OCSP != nil {
- // start checking OCSP staple about halfway through validity period for good measure
- lastNextUpdate = cert.OCSP.NextUpdate
- refreshTime := cert.OCSP.ThisUpdate.Add(lastNextUpdate.Sub(cert.OCSP.ThisUpdate) / 2)
-
- // since OCSP is already stapled, we need only check if we're in that "refresh window"
- if time.Now().Before(refreshTime) {
- continue
- }
- }
-
- err := stapleOCSP(&cert, nil)
- if err != nil {
- if cert.OCSP != nil {
- // if it was no staple before, that's fine, otherwise we should log the error
- log.Printf("[ERROR] Checking OCSP for %s: %v", name, err)
- }
- continue
- }
-
- // By this point, we've obtained the latest OCSP response.
- // If there was no staple before, or if the response is updated, make
- // sure we apply the update to all names on the certificate.
- if lastNextUpdate.IsZero() || lastNextUpdate != cert.OCSP.NextUpdate {
- log.Printf("[INFO] Advancing OCSP staple for %v from %s to %s",
- cert.Names, lastNextUpdate, cert.OCSP.NextUpdate)
- for _, n := range cert.Names {
- updated[n] = ocspUpdate{rawBytes: cert.Certificate.OCSPStaple, parsed: cert.OCSP}
- }
- }
- }
- certCacheMu.RUnlock()
-
- // This write lock should be brief since we have all the info we need now.
- certCacheMu.Lock()
- for name, update := range updated {
- cert := certCache[name]
- cert.OCSP = update.parsed
- cert.Certificate.OCSPStaple = update.rawBytes
- certCache[name] = cert
- }
- certCacheMu.Unlock()
-}
-
-// renewDurationBefore is how long before expiration to renew certificates.
-const renewDurationBefore = (24 * time.Hour) * 30
diff --git a/core/https/setup.go b/core/https/setup.go
deleted file mode 100644
index ec90e0284..000000000
--- a/core/https/setup.go
+++ /dev/null
@@ -1,321 +0,0 @@
-package https
-
-import (
- "bytes"
- "crypto/tls"
- "encoding/pem"
- "io/ioutil"
- "log"
- "os"
- "path/filepath"
- "strconv"
- "strings"
-
- "github.com/miekg/coredns/core/setup"
- "github.com/miekg/coredns/middleware"
- "github.com/miekg/coredns/server"
-)
-
-// Setup sets up the TLS configuration and installs certificates that
-// are specified by the user in the config file. All the automatic HTTPS
-// stuff comes later outside of this function.
-func Setup(c *setup.Controller) (middleware.Middleware, error) {
- if c.Port == "80" {
- c.TLS.Enabled = false
- log.Printf("[WARNING] TLS disabled for %s.", c.Address())
- return nil, nil
- }
- c.TLS.Enabled = true
-
- // TODO(miek): disabled for now
- return nil, nil
-
- for c.Next() {
- var certificateFile, keyFile, loadDir, maxCerts string
-
- args := c.RemainingArgs()
- switch len(args) {
- case 1:
- c.TLS.LetsEncryptEmail = args[0]
-
- // user can force-disable managed TLS this way
- if c.TLS.LetsEncryptEmail == "off" {
- c.TLS.Enabled = false
- return nil, nil
- }
- case 2:
- certificateFile = args[0]
- keyFile = args[1]
- c.TLS.Manual = true
- }
-
- // Optional block with extra parameters
- var hadBlock bool
- for c.NextBlock() {
- hadBlock = true
- switch c.Val() {
- case "protocols":
- args := c.RemainingArgs()
- if len(args) != 2 {
- return nil, c.ArgErr()
- }
- value, ok := supportedProtocols[strings.ToLower(args[0])]
- if !ok {
- return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
- }
- c.TLS.ProtocolMinVersion = value
- value, ok = supportedProtocols[strings.ToLower(args[1])]
- if !ok {
- return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
- }
- c.TLS.ProtocolMaxVersion = value
- case "ciphers":
- for c.NextArg() {
- value, ok := supportedCiphersMap[strings.ToUpper(c.Val())]
- if !ok {
- return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val())
- }
- c.TLS.Ciphers = append(c.TLS.Ciphers, value)
- }
- case "clients":
- c.TLS.ClientCerts = c.RemainingArgs()
- if len(c.TLS.ClientCerts) == 0 {
- return nil, c.ArgErr()
- }
- case "load":
- c.Args(&loadDir)
- c.TLS.Manual = true
- case "max_certs":
- c.Args(&maxCerts)
- c.TLS.OnDemand = true
- default:
- return nil, c.Errf("Unknown keyword '%s'", c.Val())
- }
- }
-
- // tls requires at least one argument if a block is not opened
- if len(args) == 0 && !hadBlock {
- return nil, c.ArgErr()
- }
-
- // set certificate limit if on-demand TLS is enabled
- if maxCerts != "" {
- maxCertsNum, err := strconv.Atoi(maxCerts)
- if err != nil || maxCertsNum < 1 {
- return nil, c.Err("max_certs must be a positive integer")
- }
- if onDemandMaxIssue == 0 || int32(maxCertsNum) < onDemandMaxIssue { // keep the minimum; TODO: We have to do this because it is global; should be per-server or per-vhost...
- onDemandMaxIssue = int32(maxCertsNum)
- }
- }
-
- // don't try to load certificates unless we're supposed to
- if !c.TLS.Enabled || !c.TLS.Manual {
- continue
- }
-
- // load a single certificate and key, if specified
- if certificateFile != "" && keyFile != "" {
- err := cacheUnmanagedCertificatePEMFile(certificateFile, keyFile)
- if err != nil {
- return nil, c.Errf("Unable to load certificate and key files for %s: %v", c.Host, err)
- }
- log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile)
- }
-
- // load a directory of certificates, if specified
- if loadDir != "" {
- err := loadCertsInDir(c, loadDir)
- if err != nil {
- return nil, err
- }
- }
- }
-
- setDefaultTLSParams(c.Config)
-
- return nil, nil
-}
-
-// loadCertsInDir loads all the certificates/keys in dir, as long as
-// the file ends with .pem. This method of loading certificates is
-// modeled after haproxy, which expects the certificate and key to
-// be bundled into the same file:
-// https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#5.1-crt
-//
-// This function may write to the log as it walks the directory tree.
-func loadCertsInDir(c *setup.Controller, dir string) error {
- return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- log.Printf("[WARNING] Unable to traverse into %s; skipping", path)
- return nil
- }
- if info.IsDir() {
- return nil
- }
- if strings.HasSuffix(strings.ToLower(info.Name()), ".pem") {
- certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer)
- var foundKey bool // use only the first key in the file
-
- bundle, err := ioutil.ReadFile(path)
- if err != nil {
- return err
- }
-
- for {
- // Decode next block so we can see what type it is
- var derBlock *pem.Block
- derBlock, bundle = pem.Decode(bundle)
- if derBlock == nil {
- break
- }
-
- if derBlock.Type == "CERTIFICATE" {
- // Re-encode certificate as PEM, appending to certificate chain
- pem.Encode(certBuilder, derBlock)
- } else if derBlock.Type == "EC PARAMETERS" {
- // EC keys generated from openssl can be composed of two blocks:
- // parameters and key (parameter block should come first)
- if !foundKey {
- // Encode parameters
- pem.Encode(keyBuilder, derBlock)
-
- // Key must immediately follow
- derBlock, bundle = pem.Decode(bundle)
- if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" {
- return c.Errf("%s: expected elliptic private key to immediately follow EC parameters", path)
- }
- pem.Encode(keyBuilder, derBlock)
- foundKey = true
- }
- } else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") {
- // RSA key
- if !foundKey {
- pem.Encode(keyBuilder, derBlock)
- foundKey = true
- }
- } else {
- return c.Errf("%s: unrecognized PEM block type: %s", path, derBlock.Type)
- }
- }
-
- certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes()
- if len(certPEMBytes) == 0 {
- return c.Errf("%s: failed to parse PEM data", path)
- }
- if len(keyPEMBytes) == 0 {
- return c.Errf("%s: no private key block found", path)
- }
-
- err = cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes)
- if err != nil {
- return c.Errf("%s: failed to load cert and key for %s: %v", path, c.Host, err)
- }
- log.Printf("[INFO] Successfully loaded TLS assets from %s", path)
- }
- return nil
- })
-}
-
-// setDefaultTLSParams sets the default TLS cipher suites, protocol versions,
-// and server preferences of a server.Config if they were not previously set
-// (it does not overwrite; only fills in missing values). It will also set the
-// port to 443 if not already set, TLS is enabled, TLS is manual, and the host
-// does not equal localhost.
-func setDefaultTLSParams(c *server.Config) {
- // If no ciphers provided, use default list
- if len(c.TLS.Ciphers) == 0 {
- c.TLS.Ciphers = defaultCiphers
- }
-
- // Not a cipher suite, but still important for mitigating protocol downgrade attacks
- // (prepend since having it at end breaks http2 due to non-h2-approved suites before it)
- c.TLS.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, c.TLS.Ciphers...)
-
- // Set default protocol min and max versions - must balance compatibility and security
- if c.TLS.ProtocolMinVersion == 0 {
- c.TLS.ProtocolMinVersion = tls.VersionTLS10
- }
- if c.TLS.ProtocolMaxVersion == 0 {
- c.TLS.ProtocolMaxVersion = tls.VersionTLS12
- }
-
- // Prefer server cipher suites
- c.TLS.PreferServerCipherSuites = true
-
- // Default TLS port is 443; only use if port is not manually specified,
- // TLS is enabled, and the host is not localhost
- if c.Port == "" && c.TLS.Enabled && (!c.TLS.Manual || c.TLS.OnDemand) && c.Host != "localhost" {
- c.Port = "443"
- }
-}
-
-// Map of supported protocols.
-// SSLv3 will be not supported in future release.
-// HTTP/2 only supports TLS 1.2 and higher.
-var supportedProtocols = map[string]uint16{
- "ssl3.0": tls.VersionSSL30,
- "tls1.0": tls.VersionTLS10,
- "tls1.1": tls.VersionTLS11,
- "tls1.2": tls.VersionTLS12,
-}
-
-// Map of supported ciphers, used only for parsing config.
-//
-// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites,
-// including all but two of the suites below (the two GCM suites).
-// See https://http2.github.io/http2-spec/#BadCipherSuites
-//
-// TLS_FALLBACK_SCSV is not in this list because we manually ensure
-// it is always added (even though it is not technically a cipher suite).
-//
-// This map, like any map, is NOT ORDERED. Do not range over this map.
-var supportedCiphersMap = map[string]uint16{
- "ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
- "ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
- "ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
- "ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- "ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
- "ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
- "ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- "ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
- "RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
- "RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
- "ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
- "RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
-}
-
-// List of supported cipher suites in descending order of preference.
-// Ordering is very important! Getting the wrong order will break
-// mainstream clients, especially with HTTP/2.
-//
-// Note that TLS_FALLBACK_SCSV is not in this list since it is always
-// added manually.
-var supportedCiphers = []uint16{
- tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
- tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
- tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
- tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
- tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
- tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
- tls.TLS_RSA_WITH_AES_256_CBC_SHA,
- tls.TLS_RSA_WITH_AES_128_CBC_SHA,
- tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
- tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
-}
-
-// List of all the ciphers we want to use by default
-var defaultCiphers = []uint16{
- tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
- tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
- tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
- tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
- tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
- tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
- tls.TLS_RSA_WITH_AES_256_CBC_SHA,
- tls.TLS_RSA_WITH_AES_128_CBC_SHA,
-}
diff --git a/core/https/setup_test.go b/core/https/setup_test.go
deleted file mode 100644
index 7640eb524..000000000
--- a/core/https/setup_test.go
+++ /dev/null
@@ -1,226 +0,0 @@
-package https
-
-// TODO(miek): all fail
-
-/*
-func TestMain(m *testing.M) {
- // Write test certificates to disk before tests, and clean up
- // when we're done.
- err := ioutil.WriteFile(certFile, testCert, 0644)
- if err != nil {
- log.Fatal(err)
- }
- err = ioutil.WriteFile(keyFile, testKey, 0644)
- if err != nil {
- os.Remove(certFile)
- log.Fatal(err)
- }
-
- result := m.Run()
-
- os.Remove(certFile)
- os.Remove(keyFile)
- os.Exit(result)
-}
-
-func TestSetupParseBasic(t *testing.T) {
- c := setup.NewTestController(`tls ` + certFile + ` ` + keyFile + ``)
-
- _, err := Setup(c)
- if err != nil {
- t.Errorf("Expected no errors, got: %v", err)
- }
-
- // Basic checks
- if !c.TLS.Manual {
- t.Error("Expected TLS Manual=true, but was false")
- }
- if !c.TLS.Enabled {
- t.Error("Expected TLS Enabled=true, but was false")
- }
-
- // Security defaults
- if c.TLS.ProtocolMinVersion != tls.VersionTLS10 {
- t.Errorf("Expected 'tls1.0 (0x0301)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion)
- }
- if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 {
- t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %v", c.TLS.ProtocolMaxVersion)
- }
-
- // Cipher checks
- expectedCiphers := []uint16{
- tls.TLS_FALLBACK_SCSV,
- tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
- tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
- tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
- tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
- tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
- tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
- tls.TLS_RSA_WITH_AES_256_CBC_SHA,
- tls.TLS_RSA_WITH_AES_128_CBC_SHA,
- }
-
- // Ensure count is correct (plus one for TLS_FALLBACK_SCSV)
- if len(c.TLS.Ciphers) != len(expectedCiphers) {
- t.Errorf("Expected %v Ciphers (including TLS_FALLBACK_SCSV), got %v",
- len(expectedCiphers), len(c.TLS.Ciphers))
- }
-
- // Ensure ordering is correct
- for i, actual := range c.TLS.Ciphers {
- if actual != expectedCiphers[i] {
- t.Errorf("Expected cipher in position %d to be %0x, got %0x", i, expectedCiphers[i], actual)
- }
- }
-
- if !c.TLS.PreferServerCipherSuites {
- t.Error("Expected PreferServerCipherSuites = true, but was false")
- }
-}
-
-func TestSetupParseIncompleteParams(t *testing.T) {
- // Using tls without args is an error because it's unnecessary.
- c := setup.NewTestController(`tls`)
- _, err := Setup(c)
- if err == nil {
- t.Error("Expected an error, but didn't get one")
- }
-}
-
-func TestSetupParseWithOptionalParams(t *testing.T) {
- params := `tls ` + certFile + ` ` + keyFile + ` {
- protocols ssl3.0 tls1.2
- ciphers RSA-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384
- }`
- c := setup.NewTestController(params)
-
- _, err := Setup(c)
- if err != nil {
- t.Errorf("Expected no errors, got: %v", err)
- }
-
- if c.TLS.ProtocolMinVersion != tls.VersionSSL30 {
- t.Errorf("Expected 'ssl3.0 (0x0300)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion)
- }
-
- if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 {
- t.Errorf("Expected 'tls1.2 (0x0302)' as ProtocolMaxVersion, got %#v", c.TLS.ProtocolMaxVersion)
- }
-
- if len(c.TLS.Ciphers)-1 != 3 {
- t.Errorf("Expected 3 Ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers)-1)
- }
-}
-
-func TestSetupDefaultWithOptionalParams(t *testing.T) {
- params := `tls {
- ciphers RSA-3DES-EDE-CBC-SHA
- }`
- c := setup.NewTestController(params)
-
- _, err := Setup(c)
- if err != nil {
- t.Errorf("Expected no errors, got: %v", err)
- }
- if len(c.TLS.Ciphers)-1 != 1 {
- t.Errorf("Expected 1 ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers)-1)
- }
-}
-
-// TODO: If we allow this... but probably not a good idea.
-// func TestSetupDisableHTTPRedirect(t *testing.T) {
-// c := NewTestController(`tls {
-// allow_http
-// }`)
-// _, err := TLS(c)
-// if err != nil {
-// t.Errorf("Expected no error, but got %v", err)
-// }
-// if !c.TLS.DisableHTTPRedir {
-// t.Error("Expected HTTP redirect to be disabled, but it wasn't")
-// }
-// }
-
-func TestSetupParseWithWrongOptionalParams(t *testing.T) {
- // Test protocols wrong params
- params := `tls ` + certFile + ` ` + keyFile + ` {
- protocols ssl tls
- }`
- c := setup.NewTestController(params)
- _, err := Setup(c)
- if err == nil {
- t.Errorf("Expected errors, but no error returned")
- }
-
- // Test ciphers wrong params
- params = `tls ` + certFile + ` ` + keyFile + ` {
- ciphers not-valid-cipher
- }`
- c = setup.NewTestController(params)
- _, err = Setup(c)
- if err == nil {
- t.Errorf("Expected errors, but no error returned")
- }
-}
-
-func TestSetupParseWithClientAuth(t *testing.T) {
- params := `tls ` + certFile + ` ` + keyFile + ` {
- clients client_ca.crt client2_ca.crt
- }`
- c := setup.NewTestController(params)
- _, err := Setup(c)
- if err != nil {
- t.Errorf("Expected no errors, got: %v", err)
- }
-
- if count := len(c.TLS.ClientCerts); count != 2 {
- t.Fatalf("Expected two client certs, had %d", count)
- }
- if actual := c.TLS.ClientCerts[0]; actual != "client_ca.crt" {
- t.Errorf("Expected first client cert file to be '%s', but was '%s'", "client_ca.crt", actual)
- }
- if actual := c.TLS.ClientCerts[1]; actual != "client2_ca.crt" {
- t.Errorf("Expected second client cert file to be '%s', but was '%s'", "client2_ca.crt", actual)
- }
-
- // Test missing client cert file
- params = `tls ` + certFile + ` ` + keyFile + ` {
- clients
- }`
- c = setup.NewTestController(params)
- _, err = Setup(c)
- if err == nil {
- t.Errorf("Expected an error, but no error returned")
- }
-}
-
-const (
- certFile = "test_cert.pem"
- keyFile = "test_key.pem"
-)
-
-var testCert = []byte(`-----BEGIN CERTIFICATE-----
-MIIBkjCCATmgAwIBAgIJANfFCBcABL6LMAkGByqGSM49BAEwFDESMBAGA1UEAxMJ
-bG9jYWxob3N0MB4XDTE2MDIxMDIyMjAyNFoXDTE4MDIwOTIyMjAyNFowFDESMBAG
-A1UEAxMJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs22MtnG7
-9K1mvIyjEO9GLx7BFD0tBbGnwQ0VPsuCxC6IeVuXbQDLSiVQvFZ6lUszTlczNxVk
-pEfqrM6xAupB7qN1MHMwHQYDVR0OBBYEFHxYDvAxUwL4XrjPev6qZ/BiLDs5MEQG
-A1UdIwQ9MDuAFHxYDvAxUwL4XrjPev6qZ/BiLDs5oRikFjAUMRIwEAYDVQQDEwls
-b2NhbGhvc3SCCQDXxQgXAAS+izAMBgNVHRMEBTADAQH/MAkGByqGSM49BAEDSAAw
-RQIgRvBqbyJM2JCJqhA1FmcoZjeMocmhxQHTt1c+1N2wFUgCIQDtvrivbBPA688N
-Qh3sMeAKNKPsx5NxYdoWuu9KWcKz9A==
------END CERTIFICATE-----
-`)
-
-var testKey = []byte(`-----BEGIN EC PARAMETERS-----
-BggqhkjOPQMBBw==
------END EC PARAMETERS-----
------BEGIN EC PRIVATE KEY-----
-MHcCAQEEIGLtRmwzYVcrH3J0BnzYbGPdWVF10i9p6mxkA4+b2fURoAoGCCqGSM49
-AwEHoUQDQgAEs22MtnG79K1mvIyjEO9GLx7BFD0tBbGnwQ0VPsuCxC6IeVuXbQDL
-SiVQvFZ6lUszTlczNxVkpEfqrM6xAupB7g==
------END EC PRIVATE KEY-----
-`)
-*/
diff --git a/core/https/storage.go b/core/https/storage.go
deleted file mode 100644
index 5d8e949da..000000000
--- a/core/https/storage.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package https
-
-import (
- "path/filepath"
- "strings"
-
- "github.com/miekg/coredns/core/assets"
-)
-
-// storage is used to get file paths in a consistent,
-// cross-platform way for persisting Let's Encrypt assets
-// on the file system.
-var storage = Storage(filepath.Join(assets.Path(), "letsencrypt"))
-
-// Storage is a root directory and facilitates
-// forming file paths derived from it.
-type Storage string
-
-// Sites gets the directory that stores site certificate and keys.
-func (s Storage) Sites() string {
- return filepath.Join(string(s), "sites")
-}
-
-// Site returns the path to the folder containing assets for domain.
-func (s Storage) Site(domain string) string {
- return filepath.Join(s.Sites(), domain)
-}
-
-// SiteCertFile returns the path to the certificate file for domain.
-func (s Storage) SiteCertFile(domain string) string {
- return filepath.Join(s.Site(domain), domain+".crt")
-}
-
-// SiteKeyFile returns the path to domain's private key file.
-func (s Storage) SiteKeyFile(domain string) string {
- return filepath.Join(s.Site(domain), domain+".key")
-}
-
-// SiteMetaFile returns the path to the domain's asset metadata file.
-func (s Storage) SiteMetaFile(domain string) string {
- return filepath.Join(s.Site(domain), domain+".json")
-}
-
-// Users gets the directory that stores account folders.
-func (s Storage) Users() string {
- return filepath.Join(string(s), "users")
-}
-
-// User gets the account folder for the user with email.
-func (s Storage) User(email string) string {
- if email == "" {
- email = emptyEmail
- }
- return filepath.Join(s.Users(), email)
-}
-
-// UserRegFile gets the path to the registration file for
-// the user with the given email address.
-func (s Storage) UserRegFile(email string) string {
- if email == "" {
- email = emptyEmail
- }
- fileName := emailUsername(email)
- if fileName == "" {
- fileName = "registration"
- }
- return filepath.Join(s.User(email), fileName+".json")
-}
-
-// UserKeyFile gets the path to the private key file for
-// the user with the given email address.
-func (s Storage) UserKeyFile(email string) string {
- if email == "" {
- email = emptyEmail
- }
- fileName := emailUsername(email)
- if fileName == "" {
- fileName = "private"
- }
- return filepath.Join(s.User(email), fileName+".key")
-}
-
-// emailUsername returns the username portion of an
-// email address (part before '@') or the original
-// input if it can't find the "@" symbol.
-func emailUsername(email string) string {
- at := strings.Index(email, "@")
- if at == -1 {
- return email
- } else if at == 0 {
- return email[1:]
- }
- return email[:at]
-}
diff --git a/core/https/storage_test.go b/core/https/storage_test.go
deleted file mode 100644
index 85c2220eb..000000000
--- a/core/https/storage_test.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package https
-
-import (
- "path/filepath"
- "testing"
-)
-
-func TestStorage(t *testing.T) {
- storage = Storage("./le_test")
-
- if expected, actual := filepath.Join("le_test", "sites"), storage.Sites(); actual != expected {
- t.Errorf("Expected Sites() to return '%s' but got '%s'", expected, actual)
- }
- if expected, actual := filepath.Join("le_test", "sites", "test.com"), storage.Site("test.com"); actual != expected {
- t.Errorf("Expected Site() to return '%s' but got '%s'", expected, actual)
- }
- if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.crt"), storage.SiteCertFile("test.com"); actual != expected {
- t.Errorf("Expected SiteCertFile() to return '%s' but got '%s'", expected, actual)
- }
- if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.key"), storage.SiteKeyFile("test.com"); actual != expected {
- t.Errorf("Expected SiteKeyFile() to return '%s' but got '%s'", expected, actual)
- }
- if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.json"), storage.SiteMetaFile("test.com"); actual != expected {
- t.Errorf("Expected SiteMetaFile() to return '%s' but got '%s'", expected, actual)
- }
- if expected, actual := filepath.Join("le_test", "users"), storage.Users(); actual != expected {
- t.Errorf("Expected Users() to return '%s' but got '%s'", expected, actual)
- }
- if expected, actual := filepath.Join("le_test", "users", "me@example.com"), storage.User("me@example.com"); actual != expected {
- t.Errorf("Expected User() to return '%s' but got '%s'", expected, actual)
- }
- if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.json"), storage.UserRegFile("me@example.com"); actual != expected {
- t.Errorf("Expected UserRegFile() to return '%s' but got '%s'", expected, actual)
- }
- if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.key"), storage.UserKeyFile("me@example.com"); actual != expected {
- t.Errorf("Expected UserKeyFile() to return '%s' but got '%s'", expected, actual)
- }
-
- // Test with empty emails
- if expected, actual := filepath.Join("le_test", "users", emptyEmail), storage.User(emptyEmail); actual != expected {
- t.Errorf("Expected User(\"\") to return '%s' but got '%s'", expected, actual)
- }
- if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".json"), storage.UserRegFile(""); actual != expected {
- t.Errorf("Expected UserRegFile(\"\") to return '%s' but got '%s'", expected, actual)
- }
- if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".key"), storage.UserKeyFile(""); actual != expected {
- t.Errorf("Expected UserKeyFile(\"\") to return '%s' but got '%s'", expected, actual)
- }
-}
-
-func TestEmailUsername(t *testing.T) {
- for i, test := range []struct {
- input, expect string
- }{
- {
- input: "username@example.com",
- expect: "username",
- },
- {
- input: "plus+addressing@example.com",
- expect: "plus+addressing",
- },
- {
- input: "me+plus-addressing@example.com",
- expect: "me+plus-addressing",
- },
- {
- input: "not-an-email",
- expect: "not-an-email",
- },
- {
- input: "@foobar.com",
- expect: "foobar.com",
- },
- {
- input: emptyEmail,
- expect: emptyEmail,
- },
- {
- input: "",
- expect: "",
- },
- } {
- if actual := emailUsername(test.input); actual != test.expect {
- t.Errorf("Test %d: Expected username to be '%s' but was '%s'", i, test.expect, actual)
- }
- }
-}
diff --git a/core/https/user.go b/core/https/user.go
deleted file mode 100644
index 9c30c656c..000000000
--- a/core/https/user.go
+++ /dev/null
@@ -1,200 +0,0 @@
-package https
-
-import (
- "bufio"
- "crypto"
- "crypto/ecdsa"
- "crypto/elliptic"
- "crypto/rand"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "strings"
-
- "github.com/miekg/coredns/server"
- "github.com/xenolf/lego/acme"
-)
-
-// User represents a Let's Encrypt user account.
-type User struct {
- Email string
- Registration *acme.RegistrationResource
- key crypto.PrivateKey
-}
-
-// GetEmail gets u's email.
-func (u User) GetEmail() string {
- return u.Email
-}
-
-// GetRegistration gets u's registration resource.
-func (u User) GetRegistration() *acme.RegistrationResource {
- return u.Registration
-}
-
-// GetPrivateKey gets u's private key.
-func (u User) GetPrivateKey() crypto.PrivateKey {
- return u.key
-}
-
-// getUser loads the user with the given email from disk.
-// If the user does not exist, it will create a new one,
-// but it does NOT save new users to the disk or register
-// them via ACME. It does NOT prompt the user.
-func getUser(email string) (User, error) {
- var user User
-
- // open user file
- regFile, err := os.Open(storage.UserRegFile(email))
- if err != nil {
- if os.IsNotExist(err) {
- // create a new user
- return newUser(email)
- }
- return user, err
- }
- defer regFile.Close()
-
- // load user information
- err = json.NewDecoder(regFile).Decode(&user)
- if err != nil {
- return user, err
- }
-
- // load their private key
- user.key, err = loadPrivateKey(storage.UserKeyFile(email))
- if err != nil {
- return user, err
- }
-
- return user, nil
-}
-
-// saveUser persists a user's key and account registration
-// to the file system. It does NOT register the user via ACME
-// or prompt the user.
-func saveUser(user User) error {
- // make user account folder
- err := os.MkdirAll(storage.User(user.Email), 0700)
- if err != nil {
- return err
- }
-
- // save private key file
- err = savePrivateKey(user.key, storage.UserKeyFile(user.Email))
- if err != nil {
- return err
- }
-
- // save registration file
- jsonBytes, err := json.MarshalIndent(&user, "", "\t")
- if err != nil {
- return err
- }
-
- return ioutil.WriteFile(storage.UserRegFile(user.Email), jsonBytes, 0600)
-}
-
-// newUser creates a new User for the given email address
-// with a new private key. This function does NOT save the
-// user to disk or register it via ACME. If you want to use
-// a user account that might already exist, call getUser
-// instead. It does NOT prompt the user.
-func newUser(email string) (User, error) {
- user := User{Email: email}
- privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
- if err != nil {
- return user, errors.New("error generating private key: " + err.Error())
- }
- user.key = privateKey
- return user, nil
-}
-
-// getEmail does everything it can to obtain an email
-// address from the user to use for TLS for cfg. If it
-// cannot get an email address, it returns empty string.
-// (It will warn the user of the consequences of an
-// empty email.) This function MAY prompt the user for
-// input. If userPresent is false, the operator will
-// NOT be prompted and an empty email may be returned.
-func getEmail(cfg server.Config, userPresent bool) string {
- // First try the tls directive from the Corefile
- leEmail := cfg.TLS.LetsEncryptEmail
- if leEmail == "" {
- // Then try memory (command line flag or typed by user previously)
- leEmail = DefaultEmail
- }
- if leEmail == "" {
- // Then try to get most recent user email ~/.coredns/users file
- userDirs, err := ioutil.ReadDir(storage.Users())
- if err == nil {
- var mostRecent os.FileInfo
- for _, dir := range userDirs {
- if !dir.IsDir() {
- continue
- }
- if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) {
- leEmail = dir.Name()
- DefaultEmail = leEmail // save for next time
- }
- }
- }
- }
- if leEmail == "" && userPresent {
- // Alas, we must bother the user and ask for an email address;
- // if they proceed they also agree to the SA.
- reader := bufio.NewReader(stdin)
- fmt.Println("\nYour sites will be served over HTTPS automatically using Let's Encrypt.")
- fmt.Println("By continuing, you agree to the Let's Encrypt Subscriber Agreement at:")
- fmt.Println(" " + saURL) // TODO: Show current SA link
- fmt.Println("Please enter your email address so you can recover your account if needed.")
- fmt.Println("You can leave it blank, but you'll lose the ability to recover your account.")
- fmt.Print("Email address: ")
- var err error
- leEmail, err = reader.ReadString('\n')
- if err != nil {
- return ""
- }
- leEmail = strings.TrimSpace(leEmail)
- DefaultEmail = leEmail
- Agreed = true
- }
- return leEmail
-}
-
-// promptUserAgreement prompts the user to agree to the agreement
-// at agreementURL via stdin. If the agreement has changed, then pass
-// true as the second argument. If this is the user's first time
-// agreeing, pass false. It returns whether the user agreed or not.
-func promptUserAgreement(agreementURL string, changed bool) bool {
- if changed {
- fmt.Printf("The Let's Encrypt Subscriber Agreement has changed:\n %s\n", agreementURL)
- fmt.Print("Do you agree to the new terms? (y/n): ")
- } else {
- fmt.Printf("To continue, you must agree to the Let's Encrypt Subscriber Agreement:\n %s\n", agreementURL)
- fmt.Print("Do you agree to the terms? (y/n): ")
- }
-
- reader := bufio.NewReader(stdin)
- answer, err := reader.ReadString('\n')
- if err != nil {
- return false
- }
- answer = strings.ToLower(strings.TrimSpace(answer))
-
- return answer == "y" || answer == "yes"
-}
-
-// stdin is used to read the user's input if prompted;
-// this is changed by tests during tests.
-var stdin = io.ReadWriter(os.Stdin)
-
-// The name of the folder for accounts where the email
-// address was not provided; default 'username' if you will.
-const emptyEmail = "default"
-
-// TODO: Use latest
-const saURL = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
diff --git a/core/https/user_test.go b/core/https/user_test.go
deleted file mode 100644
index 3e1af5007..000000000
--- a/core/https/user_test.go
+++ /dev/null
@@ -1,196 +0,0 @@
-package https
-
-import (
- "bytes"
- "crypto/rand"
- "crypto/rsa"
- "io"
- "os"
- "strings"
- "testing"
- "time"
-
- "github.com/miekg/coredns/server"
- "github.com/xenolf/lego/acme"
-)
-
-func TestUser(t *testing.T) {
- privateKey, err := rsa.GenerateKey(rand.Reader, 128)
- if err != nil {
- t.Fatalf("Could not generate test private key: %v", err)
- }
- u := User{
- Email: "me@mine.com",
- Registration: new(acme.RegistrationResource),
- key: privateKey,
- }
-
- if expected, actual := "me@mine.com", u.GetEmail(); actual != expected {
- t.Errorf("Expected email '%s' but got '%s'", expected, actual)
- }
- if u.GetRegistration() == nil {
- t.Error("Expected a registration resource, but got nil")
- }
- if expected, actual := privateKey, u.GetPrivateKey(); actual != expected {
- t.Errorf("Expected the private key at address %p but got one at %p instead ", expected, actual)
- }
-}
-
-func TestNewUser(t *testing.T) {
- email := "me@foobar.com"
- user, err := newUser(email)
- if err != nil {
- t.Fatalf("Error creating user: %v", err)
- }
- if user.key == nil {
- t.Error("Private key is nil")
- }
- if user.Email != email {
- t.Errorf("Expected email to be %s, but was %s", email, user.Email)
- }
- if user.Registration != nil {
- t.Error("New user already has a registration resource; it shouldn't")
- }
-}
-
-func TestSaveUser(t *testing.T) {
- storage = Storage("./testdata")
- defer os.RemoveAll(string(storage))
-
- email := "me@foobar.com"
- user, err := newUser(email)
- if err != nil {
- t.Fatalf("Error creating user: %v", err)
- }
-
- err = saveUser(user)
- if err != nil {
- t.Fatalf("Error saving user: %v", err)
- }
- _, err = os.Stat(storage.UserRegFile(email))
- if err != nil {
- t.Errorf("Cannot access user registration file, error: %v", err)
- }
- _, err = os.Stat(storage.UserKeyFile(email))
- if err != nil {
- t.Errorf("Cannot access user private key file, error: %v", err)
- }
-}
-
-func TestGetUserDoesNotAlreadyExist(t *testing.T) {
- storage = Storage("./testdata")
- defer os.RemoveAll(string(storage))
-
- user, err := getUser("user_does_not_exist@foobar.com")
- if err != nil {
- t.Fatalf("Error getting user: %v", err)
- }
-
- if user.key == nil {
- t.Error("Expected user to have a private key, but it was nil")
- }
-}
-
-func TestGetUserAlreadyExists(t *testing.T) {
- storage = Storage("./testdata")
- defer os.RemoveAll(string(storage))
-
- email := "me@foobar.com"
-
- // Set up test
- user, err := newUser(email)
- if err != nil {
- t.Fatalf("Error creating user: %v", err)
- }
- err = saveUser(user)
- if err != nil {
- t.Fatalf("Error saving user: %v", err)
- }
-
- // Expect to load user from disk
- user2, err := getUser(email)
- if err != nil {
- t.Fatalf("Error getting user: %v", err)
- }
-
- // Assert keys are the same
- if !PrivateKeysSame(user.key, user2.key) {
- t.Error("Expected private key to be the same after loading, but it wasn't")
- }
-
- // Assert emails are the same
- if user.Email != user2.Email {
- t.Errorf("Expected emails to be equal, but was '%s' before and '%s' after loading", user.Email, user2.Email)
- }
-}
-
-func TestGetEmail(t *testing.T) {
- // let's not clutter up the output
- origStdout := os.Stdout
- os.Stdout = nil
- defer func() { os.Stdout = origStdout }()
-
- storage = Storage("./testdata")
- defer os.RemoveAll(string(storage))
- DefaultEmail = "test2@foo.com"
-
- // Test1: Use email in config
- config := server.Config{
- TLS: server.TLSConfig{
- LetsEncryptEmail: "test1@foo.com",
- },
- }
- actual := getEmail(config, true)
- if actual != "test1@foo.com" {
- t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", "test1@foo.com", actual)
- }
-
- // Test2: Use default email from flag (or user previously typing it)
- actual = getEmail(server.Config{}, true)
- if actual != DefaultEmail {
- t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", DefaultEmail, actual)
- }
-
- // Test3: Get input from user
- DefaultEmail = ""
- stdin = new(bytes.Buffer)
- _, err := io.Copy(stdin, strings.NewReader("test3@foo.com\n"))
- if err != nil {
- t.Fatalf("Could not simulate user input, error: %v", err)
- }
- actual = getEmail(server.Config{}, true)
- if actual != "test3@foo.com" {
- t.Errorf("Did not get correct email from user input prompt; expected '%s' but got '%s'", "test3@foo.com", actual)
- }
-
- // Test4: Get most recent email from before
- DefaultEmail = ""
- for i, eml := range []string{
- "test4-3@foo.com",
- "test4-2@foo.com",
- "test4-1@foo.com",
- } {
- u, err := newUser(eml)
- if err != nil {
- t.Fatalf("Error creating user %d: %v", i, err)
- }
- err = saveUser(u)
- if err != nil {
- t.Fatalf("Error saving user %d: %v", i, err)
- }
-
- // Change modified time so they're all different, so the test becomes deterministic
- f, err := os.Stat(storage.User(eml))
- if err != nil {
- t.Fatalf("Could not access user folder for '%s': %v", eml, err)
- }
- chTime := f.ModTime().Add(-(time.Duration(i) * time.Second))
- if err := os.Chtimes(storage.User(eml), chTime, chTime); err != nil {
- t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err)
- }
- }
- actual = getEmail(server.Config{}, true)
- if actual != "test4-3@foo.com" {
- t.Errorf("Did not get correct email from storage; expected '%s' but got '%s'", "test4-3@foo.com", actual)
- }
-}