mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-07-28 20:59:56 +00:00
TUN-528: Move cloudflared into a separate repo
This commit is contained in:
390
vendor/github.com/mholt/caddy/caddytls/certificates.go
generated
vendored
Normal file
390
vendor/github.com/mholt/caddy/caddytls/certificates.go
generated
vendored
Normal file
@@ -0,0 +1,390 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/telemetry"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
// certificateCache is to be an instance-wide cache of certs
|
||||
// that site-specific TLS configs can refer to. Using a
|
||||
// central map like this avoids duplication of certs in
|
||||
// memory when the cert is used by multiple sites, and makes
|
||||
// maintenance easier. Because these are not to be global,
|
||||
// the cache will get garbage collected after a config reload
|
||||
// (a new instance will take its place).
|
||||
type certificateCache struct {
|
||||
sync.RWMutex
|
||||
cache map[string]Certificate // keyed by certificate hash
|
||||
}
|
||||
|
||||
// replaceCertificate replaces oldCert with newCert in the cache, and
|
||||
// updates all configs that are pointing to the old certificate to
|
||||
// point to the new one instead. newCert must already be loaded into
|
||||
// the cache (this method does NOT load it into the cache).
|
||||
//
|
||||
// Note that all the names on the old certificate will be deleted
|
||||
// from the name lookup maps of each config, then all the names on
|
||||
// the new certificate will be added to the lookup maps as long as
|
||||
// they do not overwrite any entries.
|
||||
//
|
||||
// The newCert may be modified and its cache entry updated.
|
||||
//
|
||||
// This method is safe for concurrent use.
|
||||
func (certCache *certificateCache) replaceCertificate(oldCert, newCert Certificate) error {
|
||||
certCache.Lock()
|
||||
defer certCache.Unlock()
|
||||
|
||||
// have all the configs that are pointing to the old
|
||||
// certificate point to the new certificate instead
|
||||
for _, cfg := range oldCert.configs {
|
||||
// first delete all the name lookup entries that
|
||||
// pointed to the old certificate
|
||||
for name, certKey := range cfg.Certificates {
|
||||
if certKey == oldCert.Hash {
|
||||
delete(cfg.Certificates, name)
|
||||
}
|
||||
}
|
||||
|
||||
// then add name lookup entries for the names
|
||||
// on the new certificate, but don't overwrite
|
||||
// entries that may already exist, not only as
|
||||
// a courtesy, but importantly: because if we
|
||||
// overwrote a value here, and this config no
|
||||
// longer pointed to a certain certificate in
|
||||
// the cache, that certificate's list of configs
|
||||
// referring to it would be incorrect; so just
|
||||
// insert entries, don't overwrite any
|
||||
for _, name := range newCert.Names {
|
||||
if _, ok := cfg.Certificates[name]; !ok {
|
||||
cfg.Certificates[name] = newCert.Hash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// since caching a new certificate attaches only the config
|
||||
// that loaded it, the new certificate needs to be given the
|
||||
// list of all the configs that use it, so copy the list
|
||||
// over from the old certificate to the new certificate
|
||||
// in the cache
|
||||
newCert.configs = oldCert.configs
|
||||
certCache.cache[newCert.Hash] = newCert
|
||||
|
||||
// finally, delete the old certificate from the cache
|
||||
delete(certCache.cache, oldCert.Hash)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reloadManagedCertificate reloads the certificate corresponding to the name(s)
|
||||
// on oldCert into the cache, from storage. This also replaces the old certificate
|
||||
// with the new one, so that all configurations that used the old cert now point
|
||||
// to the new cert.
|
||||
func (certCache *certificateCache) reloadManagedCertificate(oldCert Certificate) error {
|
||||
// get the certificate from storage and cache it
|
||||
newCert, err := oldCert.configs[0].CacheManagedCertificate(oldCert.Names[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to reload certificate for %v into cache: %v", oldCert.Names, err)
|
||||
}
|
||||
|
||||
// and replace the old certificate with the new one
|
||||
err = certCache.replaceCertificate(oldCert, newCert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("replacing certificate %v: %v", oldCert.Names, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Certificate is a tls.Certificate with associated metadata tacked on.
|
||||
// Even if the metadata can be obtained by parsing the certificate,
|
||||
// we are more efficient by extracting the metadata onto this struct.
|
||||
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
|
||||
|
||||
// OCSP contains the certificate's parsed OCSP response.
|
||||
OCSP *ocsp.Response
|
||||
|
||||
// The hex-encoded hash of this cert's chain's bytes.
|
||||
Hash string
|
||||
|
||||
// configs is the list of configs that use or refer to
|
||||
// The first one is assumed to be the config that is
|
||||
// "in charge" of this certificate (i.e. determines
|
||||
// whether it is managed, how it is managed, etc).
|
||||
// This field will be populated by cacheCertificate.
|
||||
// Only meddle with it if you know what you're doing!
|
||||
configs []*Config
|
||||
}
|
||||
|
||||
// CacheManagedCertificate loads the certificate for domain into the
|
||||
// cache, from the TLS storage for managed certificates. It returns a
|
||||
// copy of the Certificate that was put into the cache.
|
||||
//
|
||||
// This method is safe for concurrent use.
|
||||
func (cfg *Config) CacheManagedCertificate(domain string) (Certificate, error) {
|
||||
storage, err := cfg.StorageFor(cfg.CAUrl)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
siteData, err := storage.LoadSite(domain)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
cert, err := makeCertificateWithOCSP(siteData.Cert, siteData.Key)
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
telemetry.Increment("tls_managed_cert_count")
|
||||
return cfg.cacheCertificate(cert), nil
|
||||
}
|
||||
|
||||
// cacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
|
||||
// and keyFile, which must be in PEM format. It stores the certificate in
|
||||
// the in-memory cache.
|
||||
//
|
||||
// This function is safe for concurrent use.
|
||||
func (cfg *Config) cacheUnmanagedCertificatePEMFile(certFile, keyFile string) error {
|
||||
cert, err := makeCertificateFromDiskWithOCSP(certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.cacheCertificate(cert)
|
||||
telemetry.Increment("tls_manual_cert_count")
|
||||
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 (cfg *Config) cacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) error {
|
||||
cert, err := makeCertificateWithOCSP(certBytes, keyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.cacheCertificate(cert)
|
||||
telemetry.Increment("tls_manual_cert_count")
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeCertificateFromDiskWithOCSP 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.) It staples OCSP.
|
||||
func makeCertificateFromDiskWithOCSP(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 makeCertificateWithOCSP(certPEMBlock, keyPEMBlock)
|
||||
}
|
||||
|
||||
// makeCertificate turns a certificate PEM bundle and a key PEM block into
|
||||
// a Certificate with necessary metadata from parsing its bytes filled into
|
||||
// its struct fields for convenience (except for the OnDemand and Managed
|
||||
// flags; it is up to the caller to set those properties!). This function
|
||||
// does NOT staple OCSP.
|
||||
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
|
||||
}
|
||||
|
||||
// Extract necessary metadata
|
||||
err = fillCertFromLeaf(&cert, tlsCert)
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// makeCertificateWithOCSP is the same as makeCertificate except that it also
|
||||
// staples OCSP to the certificate.
|
||||
func makeCertificateWithOCSP(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
|
||||
cert, err := makeCertificate(certPEMBlock, keyPEMBlock)
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
err = stapleOCSP(&cert, certPEMBlock)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Stapling OCSP: %v", err)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// fillCertFromLeaf populates metadata fields on cert from tlsCert.
|
||||
func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error {
|
||||
if len(tlsCert.Certificate) == 0 {
|
||||
return errors.New("certificate is empty")
|
||||
}
|
||||
cert.Certificate = tlsCert
|
||||
|
||||
// the leaf cert should be the one for the site; it has what we need
|
||||
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if leaf.Subject.CommonName != "" { // TODO: CommonName is deprecated
|
||||
cert.Names = []string{strings.ToLower(leaf.Subject.CommonName)}
|
||||
}
|
||||
for _, name := range leaf.DNSNames {
|
||||
if name != leaf.Subject.CommonName { // TODO: CommonName is deprecated
|
||||
cert.Names = append(cert.Names, strings.ToLower(name))
|
||||
}
|
||||
}
|
||||
for _, ip := range leaf.IPAddresses {
|
||||
if ipStr := ip.String(); ipStr != leaf.Subject.CommonName { // TODO: CommonName is deprecated
|
||||
cert.Names = append(cert.Names, strings.ToLower(ipStr))
|
||||
}
|
||||
}
|
||||
for _, email := range leaf.EmailAddresses {
|
||||
if email != leaf.Subject.CommonName { // TODO: CommonName is deprecated
|
||||
cert.Names = append(cert.Names, strings.ToLower(email))
|
||||
}
|
||||
}
|
||||
if len(cert.Names) == 0 {
|
||||
return errors.New("certificate has no names")
|
||||
}
|
||||
|
||||
// save the hash of this certificate (chain) and
|
||||
// expiration date, for necessity and efficiency
|
||||
cert.Hash = hashCertificateChain(cert.Certificate.Certificate)
|
||||
cert.NotAfter = leaf.NotAfter
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashCertificateChain computes the unique hash of certChain,
|
||||
// which is the chain of DER-encoded bytes. It returns the
|
||||
// hex encoding of the hash.
|
||||
func hashCertificateChain(certChain [][]byte) string {
|
||||
h := sha256.New()
|
||||
for _, certInChain := range certChain {
|
||||
h.Write(certInChain)
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// managedCertInStorageExpiresSoon returns true if cert (being a
|
||||
// managed certificate) is expiring within RenewDurationBefore.
|
||||
// It returns false if there was an error checking the expiration
|
||||
// of the certificate as found in storage, or if the certificate
|
||||
// in storage is NOT expiring soon. A certificate that is expiring
|
||||
// soon in our cache but is not expiring soon in storage probably
|
||||
// means that another instance renewed the certificate in the
|
||||
// meantime, and it would be a good idea to simply load the cert
|
||||
// into our cache rather than repeating the renewal process again.
|
||||
func managedCertInStorageExpiresSoon(cert Certificate) (bool, error) {
|
||||
if len(cert.configs) == 0 {
|
||||
return false, fmt.Errorf("no configs for certificate")
|
||||
}
|
||||
storage, err := cert.configs[0].StorageFor(cert.configs[0].CAUrl)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
siteData, err := storage.LoadSite(cert.Names[0])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
tlsCert, err := tls.X509KeyPair(siteData.Cert, siteData.Key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
timeLeft := leaf.NotAfter.Sub(time.Now().UTC())
|
||||
return timeLeft < RenewDurationBefore, nil
|
||||
}
|
||||
|
||||
// cacheCertificate adds cert to the in-memory cache. If a certificate
|
||||
// with the same hash is already cached, it is NOT overwritten; instead,
|
||||
// cfg is added to the existing certificate's list of configs if not
|
||||
// already in the list. Then all the names on cert are used to add
|
||||
// entries to cfg.Certificates (the config's name lookup map).
|
||||
// Then the certificate is stored/updated in the cache. It returns
|
||||
// a copy of the certificate that ends up being stored in the cache.
|
||||
//
|
||||
// It is VERY important, even for some test cases, that the Hash field
|
||||
// of the cert be set properly.
|
||||
//
|
||||
// This function is safe for concurrent use.
|
||||
func (cfg *Config) cacheCertificate(cert Certificate) Certificate {
|
||||
cfg.certCache.Lock()
|
||||
defer cfg.certCache.Unlock()
|
||||
|
||||
// if this certificate already exists in the cache,
|
||||
// use it instead of overwriting it -- very important!
|
||||
if existingCert, ok := cfg.certCache.cache[cert.Hash]; ok {
|
||||
cert = existingCert
|
||||
}
|
||||
|
||||
// attach this config to the certificate so we know which
|
||||
// configs are referencing/using the certificate, but don't
|
||||
// duplicate entries
|
||||
var found bool
|
||||
for _, c := range cert.configs {
|
||||
if c == cfg {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
cert.configs = append(cert.configs, cfg)
|
||||
}
|
||||
|
||||
// key the certificate by all its names for this config only,
|
||||
// this is how we find the certificate during handshakes
|
||||
// (yes, if certs overlap in the names they serve, one will
|
||||
// overwrite another here, but that's just how it goes)
|
||||
for _, name := range cert.Names {
|
||||
cfg.Certificates[name] = cert.Hash
|
||||
}
|
||||
|
||||
// store the certificate
|
||||
cfg.certCache.cache[cert.Hash] = cert
|
||||
|
||||
return cert
|
||||
}
|
88
vendor/github.com/mholt/caddy/caddytls/certificates_test.go
generated
vendored
Normal file
88
vendor/github.com/mholt/caddy/caddytls/certificates_test.go
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestUnexportedGetCertificate(t *testing.T) {
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
|
||||
// When cache is empty
|
||||
if _, matched, defaulted := cfg.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
|
||||
firstCert := Certificate{Names: []string{"example.com"}}
|
||||
certCache.cache["0xdeadbeef"] = firstCert
|
||||
cfg.Certificates["example.com"] = "0xdeadbeef"
|
||||
if cert, matched, defaulted := cfg.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 := cfg.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)
|
||||
}
|
||||
|
||||
// When retrieving wildcard certificate
|
||||
certCache.cache["0xb01dface"] = Certificate{Names: []string{"*.example.com"}}
|
||||
cfg.Certificates["*.example.com"] = "0xb01dface"
|
||||
if cert, matched, defaulted := cfg.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)
|
||||
}
|
||||
|
||||
// TODO: Re-implement this behavior when I'm not in the middle of upgrading for ACMEv2 support. :) (it was reverted in #2037)
|
||||
// // When no certificate matches and SNI is provided, return no certificate (should be TLS alert)
|
||||
// if cert, matched, defaulted := cfg.getCertificate("nomatch"); matched || defaulted {
|
||||
// t.Errorf("Expected matched=false, defaulted=false; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert)
|
||||
// }
|
||||
|
||||
// When no certificate matches and SNI is NOT provided, a random is returned
|
||||
if cert, matched, defaulted := cfg.getCertificate(""); matched || !defaulted {
|
||||
t.Errorf("Expected matched=false, defaulted=true; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheCertificate(t *testing.T) {
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
|
||||
cfg.cacheCertificate(Certificate{Names: []string{"example.com", "sub.example.com"}, Hash: "foobar"})
|
||||
if len(certCache.cache) != 1 {
|
||||
t.Errorf("Expected length of certificate cache to be 1")
|
||||
}
|
||||
if _, ok := certCache.cache["foobar"]; !ok {
|
||||
t.Error("Expected first cert to be cached by key 'foobar', but it wasn't")
|
||||
}
|
||||
if _, ok := cfg.Certificates["example.com"]; !ok {
|
||||
t.Error("Expected first cert to be keyed by 'example.com', but it wasn't")
|
||||
}
|
||||
if _, ok := cfg.Certificates["sub.example.com"]; !ok {
|
||||
t.Error("Expected first cert to be keyed by 'sub.example.com', but it wasn't")
|
||||
}
|
||||
|
||||
// different config, but using same cache; and has cert with overlapping name,
|
||||
// but different hash
|
||||
cfg2 := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
cfg2.cacheCertificate(Certificate{Names: []string{"example.com"}, Hash: "barbaz"})
|
||||
if _, ok := certCache.cache["barbaz"]; !ok {
|
||||
t.Error("Expected second cert to be cached by key 'barbaz.com', but it wasn't")
|
||||
}
|
||||
if hash, ok := cfg2.Certificates["example.com"]; !ok {
|
||||
t.Error("Expected second cert to be keyed by 'example.com', but it wasn't")
|
||||
} else if hash != "barbaz" {
|
||||
t.Errorf("Expected second cert to map to 'barbaz' but it was %s instead", hash)
|
||||
}
|
||||
}
|
432
vendor/github.com/mholt/caddy/caddytls/client.go
generated
vendored
Normal file
432
vendor/github.com/mholt/caddy/caddytls/client.go
generated
vendored
Normal file
@@ -0,0 +1,432 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/telemetry"
|
||||
"github.com/xenolf/lego/acmev2"
|
||||
)
|
||||
|
||||
// acmeMu ensures that only one ACME challenge occurs at a time.
|
||||
var acmeMu sync.Mutex
|
||||
|
||||
// ACMEClient is a wrapper over acme.Client with
|
||||
// some custom state attached. It is used to obtain,
|
||||
// renew, and revoke certificates with ACME.
|
||||
type ACMEClient struct {
|
||||
AllowPrompts bool
|
||||
config *Config
|
||||
acmeClient *acme.Client
|
||||
storage Storage
|
||||
}
|
||||
|
||||
// newACMEClient creates a new ACMEClient given an email and whether
|
||||
// prompting the user is allowed. It's a variable so we can mock in tests.
|
||||
var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) {
|
||||
storage, err := config.StorageFor(config.CAUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Look up or create the LE user account
|
||||
leUser, err := getUser(storage, config.ACMEEmail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure key type is set
|
||||
keyType := DefaultKeyType
|
||||
if config.KeyType != "" {
|
||||
keyType = config.KeyType
|
||||
}
|
||||
|
||||
// ensure CA URL (directory endpoint) is set
|
||||
caURL := DefaultCAUrl
|
||||
if config.CAUrl != "" {
|
||||
caURL = config.CAUrl
|
||||
}
|
||||
|
||||
// ensure endpoint is secure (assume HTTPS if scheme is missing)
|
||||
if !strings.Contains(caURL, "://") {
|
||||
caURL = "https://" + caURL
|
||||
}
|
||||
u, err := url.Parse(caURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Scheme != "https" && !caddy.IsLoopback(u.Host) && !caddy.IsInternal(u.Host) {
|
||||
return nil, fmt.Errorf("%s: insecure CA URL (HTTPS required)", caURL)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if allowPrompts { // can't prompt a user who isn't there
|
||||
termsURL := client.GetToSURL()
|
||||
if !Agreed && termsURL != "" {
|
||||
Agreed = askUserAgreement(client.GetToSURL())
|
||||
}
|
||||
if !Agreed && termsURL != "" {
|
||||
return nil, errors.New("user must agree to CA terms (use -agree flag)")
|
||||
}
|
||||
}
|
||||
|
||||
reg, err := client.Register(Agreed)
|
||||
if err != nil {
|
||||
return nil, errors.New("registration error: " + err.Error())
|
||||
}
|
||||
leUser.Registration = reg
|
||||
|
||||
// save user to the file system
|
||||
err = saveUser(storage, leUser)
|
||||
if err != nil {
|
||||
return nil, errors.New("could not save user: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
c := &ACMEClient{
|
||||
AllowPrompts: allowPrompts,
|
||||
config: config,
|
||||
acmeClient: client,
|
||||
storage: storage,
|
||||
}
|
||||
|
||||
if config.DNSProvider == "" {
|
||||
// Use HTTP and TLS-SNI challenges by default
|
||||
|
||||
// See if HTTP challenge needs to be proxied
|
||||
useHTTPPort := HTTPChallengePort
|
||||
if config.AltHTTPPort != "" {
|
||||
useHTTPPort = config.AltHTTPPort
|
||||
}
|
||||
if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, useHTTPPort)) {
|
||||
useHTTPPort = DefaultHTTPAlternatePort
|
||||
}
|
||||
|
||||
// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return
|
||||
// See which port TLS-SNI challenges will be accomplished on
|
||||
// useTLSSNIPort := TLSSNIChallengePort
|
||||
// if config.AltTLSSNIPort != "" {
|
||||
// useTLSSNIPort = config.AltTLSSNIPort
|
||||
// }
|
||||
// err := c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, useTLSSNIPort))
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// if using file storage, we can distribute the HTTP challenge across
|
||||
// all instances sharing the acme folder; either way, we must still set
|
||||
// the address for the default HTTP provider server
|
||||
var useDistributedHTTPSolver bool
|
||||
if storage, err := c.config.StorageFor(c.config.CAUrl); err == nil {
|
||||
if _, ok := storage.(*FileStorage); ok {
|
||||
useDistributedHTTPSolver = true
|
||||
}
|
||||
}
|
||||
if useDistributedHTTPSolver {
|
||||
c.acmeClient.SetChallengeProvider(acme.HTTP01, distributedHTTPSolver{
|
||||
// being careful to respect user's listener bind preferences
|
||||
httpProviderServer: acme.NewHTTPProviderServer(config.ListenHost, useHTTPPort),
|
||||
})
|
||||
} else {
|
||||
// Always respect user's bind preferences by using config.ListenHost.
|
||||
// NOTE(Sep'16): At time of writing, SetHTTPAddress() and SetTLSAddress()
|
||||
// must be called before SetChallengeProvider() (see above), since they reset
|
||||
// the challenge provider back to the default one! (still true in March 2018)
|
||||
err := c.acmeClient.SetHTTPAddress(net.JoinHostPort(config.ListenHost, useHTTPPort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return
|
||||
// See if TLS challenge needs to be handled by our own facilities
|
||||
// if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, useTLSSNIPort)) {
|
||||
// c.acmeClient.SetChallengeProvider(acme.TLSSNI01, tlsSNISolver{certCache: config.certCache})
|
||||
// }
|
||||
|
||||
// Disable any challenges that should not be used
|
||||
var disabledChallenges []acme.Challenge
|
||||
if DisableHTTPChallenge {
|
||||
disabledChallenges = append(disabledChallenges, acme.HTTP01)
|
||||
}
|
||||
// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return
|
||||
// if DisableTLSSNIChallenge {
|
||||
// disabledChallenges = append(disabledChallenges, acme.TLSSNI01)
|
||||
// }
|
||||
if len(disabledChallenges) > 0 {
|
||||
c.acmeClient.ExcludeChallenges(disabledChallenges)
|
||||
}
|
||||
} else {
|
||||
// Otherwise, use DNS challenge exclusively
|
||||
|
||||
// Load provider constructor function
|
||||
provFn, ok := dnsProviders[config.DNSProvider]
|
||||
if !ok {
|
||||
return nil, errors.New("unknown DNS provider by name '" + config.DNSProvider + "'")
|
||||
}
|
||||
|
||||
// We could pass credentials to create the provider, but for now
|
||||
// just let the solver package get them from the environment
|
||||
prov, err := provFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use the DNS challenge exclusively
|
||||
// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return
|
||||
// c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
||||
c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01})
|
||||
c.acmeClient.SetChallengeProvider(acme.DNS01, prov)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Obtain obtains a single certificate for name. It stores the certificate
|
||||
// on the disk if successful. This function is safe for concurrent use.
|
||||
//
|
||||
// Right now our storage mechanism only supports one name per certificate,
|
||||
// so this function (along with Renew and Revoke) only accepts one domain
|
||||
// as input. It can be easily modified to support SAN certificates if our
|
||||
// storage mechanism is upgraded later.
|
||||
//
|
||||
// Callers who have access to a Config value should use the ObtainCert
|
||||
// method on that instead of this lower-level method.
|
||||
func (c *ACMEClient) Obtain(name string) error {
|
||||
waiter, err := c.storage.TryLock(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if waiter != nil {
|
||||
log.Printf("[INFO] Certificate for %s is already being obtained elsewhere and stored; waiting", name)
|
||||
waiter.Wait()
|
||||
return nil // we assume the process with the lock succeeded, rather than hammering this execution path again
|
||||
}
|
||||
defer func() {
|
||||
if err := c.storage.Unlock(name); err != nil {
|
||||
log.Printf("[ERROR] Unable to unlock obtain call for %s: %v", name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
namesObtaining.Add([]string{name})
|
||||
acmeMu.Lock()
|
||||
certificate, err := c.acmeClient.ObtainCertificate([]string{name}, true, nil, c.config.MustStaple)
|
||||
acmeMu.Unlock()
|
||||
namesObtaining.Remove([]string{name})
|
||||
if err != nil {
|
||||
// for a certain kind of error, we can enumerate the error per-domain
|
||||
if failures, ok := err.(acme.ObtainError); ok && len(failures) > 0 {
|
||||
var errMsg string // combine all the failures into a single error message
|
||||
for errDomain, obtainErr := range failures {
|
||||
if obtainErr == nil {
|
||||
continue
|
||||
}
|
||||
errMsg += fmt.Sprintf("[%s] failed to get certificate: %v\n", errDomain, obtainErr)
|
||||
}
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
|
||||
return fmt.Errorf("[%s] failed to obtain certificate: %v", name, err)
|
||||
}
|
||||
|
||||
// double-check that we actually got a certificate, in case there's a bug upstream (see issue #2121)
|
||||
if certificate.Domain == "" || certificate.Certificate == nil {
|
||||
return errors.New("returned certificate was empty; probably an unchecked error obtaining it")
|
||||
}
|
||||
|
||||
// Success - immediately save the certificate resource
|
||||
err = saveCertResource(c.storage, certificate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving assets for %v: %v", name, err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
go telemetry.Increment("tls_acme_certs_obtained")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Renew renews the managed certificate for name. It puts the renewed
|
||||
// certificate into storage (not the cache). This function is safe for
|
||||
// concurrent use.
|
||||
//
|
||||
// Callers who have access to a Config value should use the RenewCert
|
||||
// method on that instead of this lower-level method.
|
||||
func (c *ACMEClient) Renew(name string) error {
|
||||
waiter, err := c.storage.TryLock(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if waiter != nil {
|
||||
log.Printf("[INFO] Certificate for %s is already being renewed elsewhere and stored; waiting", name)
|
||||
waiter.Wait()
|
||||
return nil // assume that the worker that renewed the cert succeeded; avoid hammering this path over and over
|
||||
}
|
||||
defer func() {
|
||||
if err := c.storage.Unlock(name); err != nil {
|
||||
log.Printf("[ERROR] Unable to unlock renew call for %s: %v", name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Prepare for renewal (load PEM cert, key, and meta)
|
||||
siteData, err := c.storage.LoadSite(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var certMeta acme.CertificateResource
|
||||
err = json.Unmarshal(siteData.Meta, &certMeta)
|
||||
certMeta.Certificate = siteData.Cert
|
||||
certMeta.PrivateKey = siteData.Key
|
||||
|
||||
// Perform renewal and retry if necessary, but not too many times.
|
||||
var newCertMeta acme.CertificateResource
|
||||
var success bool
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
namesObtaining.Add([]string{name})
|
||||
acmeMu.Lock()
|
||||
newCertMeta, err = c.acmeClient.RenewCertificate(certMeta, true, c.config.MustStaple)
|
||||
acmeMu.Unlock()
|
||||
namesObtaining.Remove([]string{name})
|
||||
if err == nil {
|
||||
// double-check that we actually got a certificate; check a couple fields
|
||||
// TODO: This is a temporary workaround for what I think is a bug in the acmev2 package (March 2018)
|
||||
// but it might not hurt to keep this extra check in place
|
||||
if newCertMeta.Domain == "" || newCertMeta.Certificate == nil {
|
||||
err = errors.New("returned certificate was empty; probably an unchecked error renewing it")
|
||||
} else {
|
||||
success = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// wait a little bit and try again
|
||||
wait := 10 * time.Second
|
||||
log.Printf("[ERROR] Renewing [%v]: %v; trying again in %s", name, err, wait)
|
||||
time.Sleep(wait)
|
||||
}
|
||||
|
||||
if !success {
|
||||
return errors.New("too many renewal attempts; last error: " + err.Error())
|
||||
}
|
||||
|
||||
caddy.EmitEvent(caddy.CertRenewEvent, name)
|
||||
go telemetry.Increment("tls_acme_certs_renewed")
|
||||
|
||||
return saveCertResource(c.storage, newCertMeta)
|
||||
}
|
||||
|
||||
// Revoke revokes the certificate for name and deletes
|
||||
// it from storage.
|
||||
func (c *ACMEClient) Revoke(name string) error {
|
||||
siteExists, err := c.storage.SiteExists(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !siteExists {
|
||||
return errors.New("no certificate and key for " + name)
|
||||
}
|
||||
|
||||
siteData, err := c.storage.LoadSite(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.acmeClient.RevokeCertificate(siteData.Cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go telemetry.Increment("tls_acme_certs_revoked")
|
||||
|
||||
err = c.storage.DeleteSite(name)
|
||||
if err != nil {
|
||||
return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// namesObtaining is a set of hostnames with thread-safe
|
||||
// methods. A name should be in this set only while this
|
||||
// package is in the process of obtaining a certificate
|
||||
// for the name. ACME challenges that are received for
|
||||
// names which are not in this set were not initiated by
|
||||
// this package and probably should not be handled by
|
||||
// this package.
|
||||
var namesObtaining = nameCoordinator{names: make(map[string]struct{})}
|
||||
|
||||
type nameCoordinator struct {
|
||||
names map[string]struct{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Add adds names to c. It is safe for concurrent use.
|
||||
func (c *nameCoordinator) Add(names []string) {
|
||||
c.mu.Lock()
|
||||
for _, name := range names {
|
||||
c.names[strings.ToLower(name)] = struct{}{}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Remove removes names from c. It is safe for concurrent use.
|
||||
func (c *nameCoordinator) Remove(names []string) {
|
||||
c.mu.Lock()
|
||||
for _, name := range names {
|
||||
delete(c.names, strings.ToLower(name))
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Has returns true if c has name. It is safe for concurrent use.
|
||||
func (c *nameCoordinator) Has(name string) bool {
|
||||
hostname, _, err := net.SplitHostPort(name)
|
||||
if err != nil {
|
||||
hostname = name
|
||||
}
|
||||
c.mu.RLock()
|
||||
_, ok := c.names[strings.ToLower(hostname)]
|
||||
c.mu.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
// KnownACMECAs is a list of ACME directory endpoints of
|
||||
// known, public, and trusted ACME-compatible certificate
|
||||
// authorities.
|
||||
var KnownACMECAs = []string{
|
||||
"https://acme-v02.api.letsencrypt.org/directory",
|
||||
}
|
17
vendor/github.com/mholt/caddy/caddytls/client_test.go
generated
vendored
Normal file
17
vendor/github.com/mholt/caddy/caddytls/client_test.go
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
// TODO
|
698
vendor/github.com/mholt/caddy/caddytls/config.go
generated
vendored
Normal file
698
vendor/github.com/mholt/caddy/caddytls/config.go
generated
vendored
Normal file
@@ -0,0 +1,698 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/klauspost/cpuid"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/xenolf/lego/acmev2"
|
||||
)
|
||||
|
||||
// Config describes how TLS should be configured and used.
|
||||
type Config struct {
|
||||
// The hostname or class of hostnames this config is
|
||||
// designated for; can contain wildcard characters
|
||||
// according to RFC 6125 §6.4.3 - this field MUST
|
||||
// be set in order for things to work as expected
|
||||
Hostname string
|
||||
|
||||
// Whether TLS is enabled
|
||||
Enabled bool
|
||||
|
||||
// Minimum and maximum protocol versions to allow
|
||||
ProtocolMinVersion uint16
|
||||
ProtocolMaxVersion uint16
|
||||
|
||||
// The list of cipher suites; first should be
|
||||
// TLS_FALLBACK_SCSV to prevent degrade attacks
|
||||
Ciphers []uint16
|
||||
|
||||
// Whether to prefer server cipher suites
|
||||
PreferServerCipherSuites bool
|
||||
|
||||
// The list of preferred curves
|
||||
CurvePreferences []tls.CurveID
|
||||
|
||||
// Client authentication policy
|
||||
ClientAuth tls.ClientAuthType
|
||||
|
||||
// List of client CA certificates to allow, if
|
||||
// client authentication is enabled
|
||||
ClientCerts []string
|
||||
|
||||
// Manual means user provides own certs and keys
|
||||
Manual bool
|
||||
|
||||
// Managed means config qualifies for implicit,
|
||||
// automatic, managed TLS; as opposed to the user
|
||||
// providing and managing the certificate manually
|
||||
Managed bool
|
||||
|
||||
// OnDemand means the class of hostnames this
|
||||
// config applies to may obtain and manage
|
||||
// certificates at handshake-time (as opposed
|
||||
// to pre-loaded at startup); OnDemand certs
|
||||
// will be managed the same way as preloaded
|
||||
// ones, however, if an OnDemand cert fails to
|
||||
// renew, it is removed from the in-memory
|
||||
// cache; if this is true, Managed must
|
||||
// necessarily be true
|
||||
OnDemand bool
|
||||
|
||||
// SelfSigned means that this hostname is
|
||||
// served with a self-signed certificate
|
||||
// that we generated in memory for convenience
|
||||
SelfSigned bool
|
||||
|
||||
// The endpoint of the directory for the ACME
|
||||
// CA we are to use
|
||||
CAUrl string
|
||||
|
||||
// The host (ONLY the host, not port) to listen
|
||||
// on if necessary to start a listener to solve
|
||||
// an ACME challenge
|
||||
ListenHost string
|
||||
|
||||
// The alternate port (ONLY port, not host) to
|
||||
// use for the ACME HTTP challenge; if non-empty,
|
||||
// this port will be used instead of
|
||||
// HTTPChallengePort to spin up a listener for
|
||||
// the HTTP challenge
|
||||
AltHTTPPort string
|
||||
|
||||
// The alternate port (ONLY port, not host)
|
||||
// to use for the ACME TLS-SNI challenge.
|
||||
// The system must forward TLSSNIChallengePort
|
||||
// to this port for challenge to succeed
|
||||
AltTLSSNIPort string
|
||||
|
||||
// The string identifier of the DNS provider
|
||||
// to use when solving the ACME DNS challenge
|
||||
DNSProvider string
|
||||
|
||||
// The email address to use when creating or
|
||||
// using an ACME account (fun fact: if this
|
||||
// is set to "off" then this config will not
|
||||
// qualify for managed TLS)
|
||||
ACMEEmail string
|
||||
|
||||
// The type of key to use when generating
|
||||
// certificates
|
||||
KeyType acme.KeyType
|
||||
|
||||
// The storage creator; use StorageFor() to get a guaranteed
|
||||
// non-nil Storage instance. Note, Caddy may call this frequently
|
||||
// so implementors are encouraged to cache any heavy instantiations.
|
||||
StorageProvider string
|
||||
|
||||
// The state needed to operate on-demand TLS
|
||||
OnDemandState OnDemandState
|
||||
|
||||
// Add the must staple TLS extension to the CSR generated by lego/acme
|
||||
MustStaple bool
|
||||
|
||||
// The list of protocols to choose from for Application Layer
|
||||
// Protocol Negotiation (ALPN).
|
||||
ALPN []string
|
||||
|
||||
// The map of hostname to certificate hash. This is used to complete
|
||||
// handshakes and serve the right certificate given the SNI.
|
||||
Certificates map[string]string
|
||||
|
||||
certCache *certificateCache // pointer to the Instance's certificate store
|
||||
tlsConfig *tls.Config // the final tls.Config created with buildStandardTLSConfig()
|
||||
}
|
||||
|
||||
// OnDemandState contains some state relevant for providing
|
||||
// on-demand TLS.
|
||||
type OnDemandState struct {
|
||||
// The number of certificates that have been issued on-demand
|
||||
// by this config. It is only safe to modify this count atomically.
|
||||
// If it reaches MaxObtain, on-demand issuances must fail.
|
||||
ObtainedCount int32
|
||||
|
||||
// Set from max_certs in tls config, it specifies the
|
||||
// maximum number of certificates that can be issued.
|
||||
MaxObtain int32
|
||||
|
||||
// The url to call to check if an on-demand tls certificate should
|
||||
// be issued. If a request to the URL fails or returns a non 2xx
|
||||
// status on-demand issuances must fail.
|
||||
AskURL *url.URL
|
||||
}
|
||||
|
||||
// NewConfig returns a new Config with a pointer to the instance's
|
||||
// certificate cache. You will usually need to set Other fields on
|
||||
// the returned Config for successful practical use.
|
||||
func NewConfig(inst *caddy.Instance) *Config {
|
||||
inst.StorageMu.RLock()
|
||||
certCache, ok := inst.Storage[CertCacheInstStorageKey].(*certificateCache)
|
||||
inst.StorageMu.RUnlock()
|
||||
if !ok || certCache == nil {
|
||||
certCache = &certificateCache{cache: make(map[string]Certificate)}
|
||||
inst.StorageMu.Lock()
|
||||
inst.Storage[CertCacheInstStorageKey] = certCache
|
||||
inst.StorageMu.Unlock()
|
||||
}
|
||||
cfg := new(Config)
|
||||
cfg.Certificates = make(map[string]string)
|
||||
cfg.certCache = certCache
|
||||
return cfg
|
||||
}
|
||||
|
||||
// ObtainCert obtains a certificate for name using c, as long
|
||||
// as a certificate does not already exist in storage for that
|
||||
// name. The name must qualify and c must be flagged as Managed.
|
||||
// This function is a no-op if storage already has a certificate
|
||||
// for name.
|
||||
//
|
||||
// It only obtains and stores certificates (and their keys),
|
||||
// it does not load them into memory. If allowPrompts is true,
|
||||
// the user may be shown a prompt.
|
||||
func (c *Config) ObtainCert(name string, allowPrompts bool) error {
|
||||
skip, err := c.preObtainOrRenewChecks(name, allowPrompts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// we expect this to be a new (non-existent) site
|
||||
storage, err := c.StorageFor(c.CAUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
siteExists, err := storage.SiteExists(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if siteExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
client, err := newACMEClient(c, allowPrompts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Obtain(name)
|
||||
}
|
||||
|
||||
// RenewCert renews the certificate for name using c. It stows the
|
||||
// renewed certificate and its assets in storage if successful.
|
||||
func (c *Config) RenewCert(name string, allowPrompts bool) error {
|
||||
skip, err := c.preObtainOrRenewChecks(name, allowPrompts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
client, err := newACMEClient(c, allowPrompts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Renew(name)
|
||||
}
|
||||
|
||||
// preObtainOrRenewChecks perform a few simple checks before
|
||||
// obtaining or renewing a certificate with ACME, and returns
|
||||
// whether this name should be skipped (like if it's not
|
||||
// managed TLS) as well as any error. It ensures that the
|
||||
// config is Managed, that the name qualifies for a certificate,
|
||||
// and that an email address is available.
|
||||
func (c *Config) preObtainOrRenewChecks(name string, allowPrompts bool) (bool, error) {
|
||||
if !c.Managed || !HostQualifies(name) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// wildcard certificates require DNS challenge (as of March 2018)
|
||||
if strings.Contains(name, "*") && c.DNSProvider == "" {
|
||||
return false, fmt.Errorf("wildcard domain name (%s) requires DNS challenge; use dns subdirective to configure it", name)
|
||||
}
|
||||
|
||||
if c.ACMEEmail == "" {
|
||||
var err error
|
||||
c.ACMEEmail, err = getEmail(c, allowPrompts)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// StorageFor obtains a TLS Storage instance for the given CA URL which should
|
||||
// be unique for every different ACME CA. If a StorageCreator is set on this
|
||||
// Config, it will be used. Otherwise the default file storage implementation
|
||||
// is used. When the error is nil, this is guaranteed to return a non-nil
|
||||
// Storage instance.
|
||||
func (c *Config) StorageFor(caURL string) (Storage, error) {
|
||||
// Validate CA URL
|
||||
if caURL == "" {
|
||||
caURL = DefaultCAUrl
|
||||
}
|
||||
if caURL == "" {
|
||||
return nil, fmt.Errorf("cannot create storage without CA URL")
|
||||
}
|
||||
caURL = strings.ToLower(caURL)
|
||||
|
||||
// scheme required or host will be parsed as path (as of Go 1.6)
|
||||
if !strings.Contains(caURL, "://") {
|
||||
caURL = "https://" + caURL
|
||||
}
|
||||
|
||||
u, err := url.Parse(caURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to parse CA URL: %v", caURL, err)
|
||||
}
|
||||
|
||||
if u.Host == "" {
|
||||
return nil, fmt.Errorf("%s: no host in CA URL", caURL)
|
||||
}
|
||||
|
||||
// Create the storage based on the URL
|
||||
var s Storage
|
||||
if c.StorageProvider == "" {
|
||||
c.StorageProvider = "file"
|
||||
}
|
||||
|
||||
creator, ok := storageProviders[c.StorageProvider]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: Unknown storage: %v", caURL, c.StorageProvider)
|
||||
}
|
||||
|
||||
s, err = creator(u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create custom storage '%v': %v", caURL, c.StorageProvider, err)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// buildStandardTLSConfig converts cfg (*caddytls.Config) to a *tls.Config
|
||||
// and stores it in cfg so it can be used in servers. If TLS is disabled,
|
||||
// no tls.Config is created.
|
||||
func (c *Config) buildStandardTLSConfig() error {
|
||||
if !c.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := new(tls.Config)
|
||||
|
||||
ciphersAdded := make(map[uint16]struct{})
|
||||
curvesAdded := make(map[tls.CurveID]struct{})
|
||||
|
||||
// add cipher suites
|
||||
for _, ciph := range c.Ciphers {
|
||||
if _, ok := ciphersAdded[ciph]; !ok {
|
||||
ciphersAdded[ciph] = struct{}{}
|
||||
config.CipherSuites = append(config.CipherSuites, ciph)
|
||||
}
|
||||
}
|
||||
|
||||
config.PreferServerCipherSuites = c.PreferServerCipherSuites
|
||||
|
||||
// add curve preferences
|
||||
for _, curv := range c.CurvePreferences {
|
||||
if _, ok := curvesAdded[curv]; !ok {
|
||||
curvesAdded[curv] = struct{}{}
|
||||
config.CurvePreferences = append(config.CurvePreferences, curv)
|
||||
}
|
||||
}
|
||||
|
||||
config.MinVersion = c.ProtocolMinVersion
|
||||
config.MaxVersion = c.ProtocolMaxVersion
|
||||
config.ClientAuth = c.ClientAuth
|
||||
config.NextProtos = c.ALPN
|
||||
config.GetCertificate = c.GetCertificate
|
||||
|
||||
// set up client authentication if enabled
|
||||
if config.ClientAuth != tls.NoClientCert {
|
||||
pool := x509.NewCertPool()
|
||||
clientCertsAdded := make(map[string]struct{})
|
||||
|
||||
for _, caFile := range c.ClientCerts {
|
||||
// don't add cert to pool more than once
|
||||
if _, ok := clientCertsAdded[caFile]; ok {
|
||||
continue
|
||||
}
|
||||
clientCertsAdded[caFile] = struct{}{}
|
||||
|
||||
// Any client with a certificate from this CA will be allowed to connect
|
||||
caCrt, err := ioutil.ReadFile(caFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !pool.AppendCertsFromPEM(caCrt) {
|
||||
return fmt.Errorf("error loading client certificate '%s': no certificates were successfully parsed", caFile)
|
||||
}
|
||||
}
|
||||
|
||||
config.ClientCAs = pool
|
||||
}
|
||||
|
||||
// default cipher suites
|
||||
if len(config.CipherSuites) == 0 {
|
||||
config.CipherSuites = getPreferredDefaultCiphers()
|
||||
}
|
||||
|
||||
// for security, ensure TLS_FALLBACK_SCSV is always included first
|
||||
if len(config.CipherSuites) == 0 || config.CipherSuites[0] != tls.TLS_FALLBACK_SCSV {
|
||||
config.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, config.CipherSuites...)
|
||||
}
|
||||
|
||||
// store the resulting new tls.Config
|
||||
c.tlsConfig = config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeTLSConfig makes a tls.Config from configs. The returned
|
||||
// tls.Config is programmed to load the matching caddytls.Config
|
||||
// based on the hostname in SNI, but that's all. This is used
|
||||
// to create a single TLS configuration for a listener (a group
|
||||
// of sites).
|
||||
func MakeTLSConfig(configs []*Config) (*tls.Config, error) {
|
||||
if len(configs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
configMap := make(configGroup)
|
||||
|
||||
for i, cfg := range configs {
|
||||
if cfg == nil {
|
||||
// avoid nil pointer dereference below this loop
|
||||
configs[i] = new(Config)
|
||||
continue
|
||||
}
|
||||
|
||||
// can't serve TLS and non-TLS on same port
|
||||
if i > 0 && cfg.Enabled != configs[i-1].Enabled {
|
||||
thisConfProto, lastConfProto := "not TLS", "not TLS"
|
||||
if cfg.Enabled {
|
||||
thisConfProto = "TLS"
|
||||
}
|
||||
if configs[i-1].Enabled {
|
||||
lastConfProto = "TLS"
|
||||
}
|
||||
return nil, fmt.Errorf("cannot multiplex %s (%s) and %s (%s) on same listener",
|
||||
configs[i-1].Hostname, lastConfProto, cfg.Hostname, thisConfProto)
|
||||
}
|
||||
|
||||
// convert this caddytls.Config into a tls.Config
|
||||
if err := cfg.buildStandardTLSConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if an existing config with this hostname was already
|
||||
// configured, then they must be identical (or at least
|
||||
// compatible), otherwise that is a configuration error
|
||||
if otherConfig, ok := configMap[cfg.Hostname]; ok {
|
||||
if err := assertConfigsCompatible(cfg, otherConfig); err != nil {
|
||||
return nil, fmt.Errorf("incompatible TLS configurations for the same SNI "+
|
||||
"name (%s) on the same listener: %v",
|
||||
cfg.Hostname, err)
|
||||
}
|
||||
}
|
||||
|
||||
// key this config by its hostname (overwrites
|
||||
// configs with the same hostname pattern; should
|
||||
// be OK since we already asserted they are roughly
|
||||
// the same); during TLS handshakes, configs are
|
||||
// loaded based on the hostname pattern, according
|
||||
// to client's SNI
|
||||
configMap[cfg.Hostname] = cfg
|
||||
}
|
||||
|
||||
// Is TLS disabled? By now, we know that all
|
||||
// configs agree whether it is or not, so we
|
||||
// can just look at the first one. If so,
|
||||
// we're done here.
|
||||
if len(configs) == 0 || !configs[0].Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
GetConfigForClient: configMap.GetConfigForClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// assertConfigsCompatible returns an error if the two Configs
|
||||
// do not have the same (or roughly compatible) configurations.
|
||||
// If one of the tlsConfig pointers on either Config is nil,
|
||||
// an error will be returned. If both are nil, no error.
|
||||
func assertConfigsCompatible(cfg1, cfg2 *Config) error {
|
||||
c1, c2 := cfg1.tlsConfig, cfg2.tlsConfig
|
||||
|
||||
if (c1 == nil && c2 != nil) || (c1 != nil && c2 == nil) {
|
||||
return fmt.Errorf("one config is not made")
|
||||
}
|
||||
if c1 == nil && c2 == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(c1.CipherSuites) != len(c2.CipherSuites) {
|
||||
return fmt.Errorf("different number of allowed cipher suites")
|
||||
}
|
||||
for i, ciph := range c1.CipherSuites {
|
||||
if c2.CipherSuites[i] != ciph {
|
||||
return fmt.Errorf("different cipher suites or different order")
|
||||
}
|
||||
}
|
||||
|
||||
if len(c1.CurvePreferences) != len(c2.CurvePreferences) {
|
||||
return fmt.Errorf("different number of allowed cipher suites")
|
||||
}
|
||||
for i, curve := range c1.CurvePreferences {
|
||||
if c2.CurvePreferences[i] != curve {
|
||||
return fmt.Errorf("different curve preferences or different order")
|
||||
}
|
||||
}
|
||||
|
||||
if len(c1.NextProtos) != len(c2.NextProtos) {
|
||||
return fmt.Errorf("different number of ALPN (NextProtos) values")
|
||||
}
|
||||
for i, proto := range c1.NextProtos {
|
||||
if c2.NextProtos[i] != proto {
|
||||
return fmt.Errorf("different ALPN (NextProtos) values or different order")
|
||||
}
|
||||
}
|
||||
|
||||
if c1.PreferServerCipherSuites != c2.PreferServerCipherSuites {
|
||||
return fmt.Errorf("one prefers server cipher suites, the other does not")
|
||||
}
|
||||
if c1.MinVersion != c2.MinVersion {
|
||||
return fmt.Errorf("minimum TLS version mismatch")
|
||||
}
|
||||
if c1.MaxVersion != c2.MaxVersion {
|
||||
return fmt.Errorf("maximum TLS version mismatch")
|
||||
}
|
||||
if c1.ClientAuth != c2.ClientAuth {
|
||||
return fmt.Errorf("client authentication policy mismatch")
|
||||
}
|
||||
if c1.ClientAuth != tls.NoClientCert && c2.ClientAuth != tls.NoClientCert && c1.ClientCAs != c2.ClientCAs {
|
||||
// Two hosts defined on the same listener are not compatible if they
|
||||
// have ClientAuth enabled, because there's no guarantee beyond the
|
||||
// hostname which config will be used (because SNI only has server name).
|
||||
// To prevent clients from bypassing authentication, require that
|
||||
// ClientAuth be configured in an unambiguous manner.
|
||||
return fmt.Errorf("multiple hosts requiring client authentication ambiguously configured")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigGetter gets a Config keyed by key.
|
||||
type ConfigGetter func(c *caddy.Controller) *Config
|
||||
|
||||
var configGetters = make(map[string]ConfigGetter)
|
||||
|
||||
// RegisterConfigGetter registers fn as the way to get a
|
||||
// Config for server type serverType.
|
||||
func RegisterConfigGetter(serverType string, fn ConfigGetter) {
|
||||
configGetters[serverType] = fn
|
||||
}
|
||||
|
||||
// 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).
|
||||
func SetDefaultTLSParams(config *Config) {
|
||||
// If no ciphers provided, use default list
|
||||
if len(config.Ciphers) == 0 {
|
||||
config.Ciphers = getPreferredDefaultCiphers()
|
||||
}
|
||||
|
||||
// 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)
|
||||
config.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, config.Ciphers...)
|
||||
|
||||
// If no curves provided, use default list
|
||||
if len(config.CurvePreferences) == 0 {
|
||||
config.CurvePreferences = defaultCurves
|
||||
}
|
||||
|
||||
// Set default protocol min and max versions - must balance compatibility and security
|
||||
if config.ProtocolMinVersion == 0 {
|
||||
config.ProtocolMinVersion = tls.VersionTLS12
|
||||
}
|
||||
if config.ProtocolMaxVersion == 0 {
|
||||
config.ProtocolMaxVersion = tls.VersionTLS12
|
||||
}
|
||||
|
||||
// Prefer server cipher suites
|
||||
config.PreferServerCipherSuites = true
|
||||
}
|
||||
|
||||
// Map of supported key types
|
||||
var supportedKeyTypes = map[string]acme.KeyType{
|
||||
"P384": acme.EC384,
|
||||
"P256": acme.EC256,
|
||||
"RSA8192": acme.RSA8192,
|
||||
"RSA4096": acme.RSA4096,
|
||||
"RSA2048": acme.RSA2048,
|
||||
}
|
||||
|
||||
// Map of supported protocols.
|
||||
// HTTP/2 only supports TLS 1.2 and higher.
|
||||
// If updating this map, also update tlsProtocolStringToMap in caddyhttp/fastcgi/fastcgi.go
|
||||
var SupportedProtocols = map[string]uint16{
|
||||
"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 four of the suites below (the four 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-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
"ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"ECDHE-ECDSA-WITH-CHACHA20-POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
"ECDHE-RSA-WITH-CHACHA20-POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
"ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_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-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
"RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_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 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_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
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,
|
||||
}
|
||||
|
||||
// List of ciphers we should prefer if native AESNI support is missing
|
||||
var defaultCiphersNonAESNI = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
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,
|
||||
}
|
||||
|
||||
// getPreferredDefaultCiphers returns an appropriate cipher suite to use, depending on
|
||||
// the hardware support available for AES-NI.
|
||||
//
|
||||
// See https://github.com/mholt/caddy/issues/1674
|
||||
func getPreferredDefaultCiphers() []uint16 {
|
||||
if cpuid.CPU.AesNi() {
|
||||
return defaultCiphers
|
||||
}
|
||||
|
||||
// Return a cipher suite that prefers ChaCha20
|
||||
return defaultCiphersNonAESNI
|
||||
}
|
||||
|
||||
// Map of supported curves
|
||||
// https://golang.org/pkg/crypto/tls/#CurveID
|
||||
var supportedCurvesMap = map[string]tls.CurveID{
|
||||
"X25519": tls.X25519,
|
||||
"P256": tls.CurveP256,
|
||||
"P384": tls.CurveP384,
|
||||
"P521": tls.CurveP521,
|
||||
}
|
||||
|
||||
// List of all the curves we want to use by default.
|
||||
//
|
||||
// This list should only include curves which are fast by design (e.g. X25519)
|
||||
// and those for which an optimized assembly implementation exists (e.g. P256).
|
||||
// The latter ones can be found here: https://github.com/golang/go/tree/master/src/crypto/elliptic
|
||||
var defaultCurves = []tls.CurveID{
|
||||
tls.X25519,
|
||||
tls.CurveP256,
|
||||
}
|
||||
|
||||
const (
|
||||
// HTTPChallengePort is the officially designated port for
|
||||
// the HTTP challenge according to the ACME spec.
|
||||
HTTPChallengePort = "80"
|
||||
|
||||
// TLSSNIChallengePort is the officially designated port for
|
||||
// the TLS-SNI challenge according to the ACME spec.
|
||||
TLSSNIChallengePort = "443"
|
||||
|
||||
// DefaultHTTPAlternatePort is the port on which the ACME
|
||||
// client will open a listener and solve the HTTP challenge.
|
||||
// If this alternate port is used instead of the default
|
||||
// port, then whatever is listening on the default port must
|
||||
// be capable of proxying or forwarding the request to this
|
||||
// alternate port.
|
||||
DefaultHTTPAlternatePort = "5033"
|
||||
|
||||
// CertCacheInstStorageKey is the name of the key for
|
||||
// accessing the certificate storage on the *caddy.Instance.
|
||||
CertCacheInstStorageKey = "tls_cert_cache"
|
||||
)
|
229
vendor/github.com/mholt/caddy/caddytls/config_test.go
generated
vendored
Normal file
229
vendor/github.com/mholt/caddy/caddytls/config_test.go
generated
vendored
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/klauspost/cpuid"
|
||||
)
|
||||
|
||||
func TestConvertTLSConfigProtocolVersions(t *testing.T) {
|
||||
// same min and max protocol versions
|
||||
config := &Config{
|
||||
Enabled: true,
|
||||
ProtocolMinVersion: tls.VersionTLS12,
|
||||
ProtocolMaxVersion: tls.VersionTLS12,
|
||||
}
|
||||
err := config.buildStandardTLSConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("Did not expect an error, but got %v", err)
|
||||
}
|
||||
if got, want := config.tlsConfig.MinVersion, uint16(tls.VersionTLS12); got != want {
|
||||
t.Errorf("Expected min version to be %x, got %x", want, got)
|
||||
}
|
||||
if got, want := config.tlsConfig.MaxVersion, uint16(tls.VersionTLS12); got != want {
|
||||
t.Errorf("Expected max version to be %x, got %x", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertTLSConfigPreferServerCipherSuites(t *testing.T) {
|
||||
// prefer server cipher suites
|
||||
config := Config{Enabled: true, PreferServerCipherSuites: true}
|
||||
err := config.buildStandardTLSConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("Did not expect an error, but got %v", err)
|
||||
}
|
||||
if got, want := config.tlsConfig.PreferServerCipherSuites, true; got != want {
|
||||
t.Errorf("Expected PreferServerCipherSuites==%v but got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTLSConfigTLSEnabledDisabledError(t *testing.T) {
|
||||
// verify handling when Enabled is true and false
|
||||
configs := []*Config{
|
||||
{Enabled: true},
|
||||
{Enabled: false},
|
||||
}
|
||||
_, err := MakeTLSConfig(configs)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected an error, but got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertTLSConfigCipherSuites(t *testing.T) {
|
||||
// ensure cipher suites are unioned and
|
||||
// that TLS_FALLBACK_SCSV is prepended
|
||||
configs := []*Config{
|
||||
{Enabled: true, Ciphers: []uint16{0xc02c, 0xc030}},
|
||||
{Enabled: true, Ciphers: []uint16{0xc012, 0xc030, 0xc00a}},
|
||||
{Enabled: true, Ciphers: nil},
|
||||
}
|
||||
|
||||
defaultCiphersExpected := getPreferredDefaultCiphers()
|
||||
expectedCiphers := [][]uint16{
|
||||
{tls.TLS_FALLBACK_SCSV, 0xc02c, 0xc030},
|
||||
{tls.TLS_FALLBACK_SCSV, 0xc012, 0xc030, 0xc00a},
|
||||
append([]uint16{tls.TLS_FALLBACK_SCSV}, defaultCiphersExpected...),
|
||||
}
|
||||
|
||||
for i, config := range configs {
|
||||
err := config.buildStandardTLSConfig()
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: Expected no error, got: %v", i, err)
|
||||
}
|
||||
if !reflect.DeepEqual(config.tlsConfig.CipherSuites, expectedCiphers[i]) {
|
||||
t.Errorf("Test %d: Expected ciphers %v but got %v",
|
||||
i, expectedCiphers[i], config.tlsConfig.CipherSuites)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPreferredDefaultCiphers(t *testing.T) {
|
||||
expectedCiphers := defaultCiphers
|
||||
if !cpuid.CPU.AesNi() {
|
||||
expectedCiphers = defaultCiphersNonAESNI
|
||||
}
|
||||
|
||||
// Ensure ordering is correct and ciphers are what we expected.
|
||||
result := getPreferredDefaultCiphers()
|
||||
for i, actual := range result {
|
||||
if actual != expectedCiphers[i] {
|
||||
t.Errorf("Expected cipher in position %d to be %0x, got %0x", i, expectedCiphers[i], actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageForNoURL(t *testing.T) {
|
||||
c := &Config{}
|
||||
if _, err := c.StorageFor(""); err == nil {
|
||||
t.Fatal("Expected error on empty URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageForLowercasesAndPrefixesScheme(t *testing.T) {
|
||||
resultStr := ""
|
||||
RegisterStorageProvider("fake-TestStorageForLowercasesAndPrefixesScheme", func(caURL *url.URL) (Storage, error) {
|
||||
resultStr = caURL.String()
|
||||
return nil, nil
|
||||
})
|
||||
c := &Config{
|
||||
StorageProvider: "fake-TestStorageForLowercasesAndPrefixesScheme",
|
||||
}
|
||||
if _, err := c.StorageFor("EXAMPLE.COM/BLAH"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resultStr != "https://example.com/blah" {
|
||||
t.Fatalf("Unexpected CA URL string: %v", resultStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageForBadURL(t *testing.T) {
|
||||
c := &Config{}
|
||||
if _, err := c.StorageFor("http://192.168.0.%31/"); err == nil {
|
||||
t.Fatal("Expected error for bad URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageForDefault(t *testing.T) {
|
||||
c := &Config{}
|
||||
s, err := c.StorageFor("example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := s.(*FileStorage); !ok {
|
||||
t.Fatalf("Unexpected storage type: %#v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageForCustom(t *testing.T) {
|
||||
storage := fakeStorage("fake-TestStorageForCustom")
|
||||
RegisterStorageProvider("fake-TestStorageForCustom", func(caURL *url.URL) (Storage, error) { return storage, nil })
|
||||
c := &Config{
|
||||
StorageProvider: "fake-TestStorageForCustom",
|
||||
}
|
||||
s, err := c.StorageFor("example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s != storage {
|
||||
t.Fatal("Unexpected storage")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageForCustomError(t *testing.T) {
|
||||
RegisterStorageProvider("fake-TestStorageForCustomError", func(caURL *url.URL) (Storage, error) { return nil, errors.New("some error") })
|
||||
c := &Config{
|
||||
StorageProvider: "fake-TestStorageForCustomError",
|
||||
}
|
||||
if _, err := c.StorageFor("example.com"); err == nil {
|
||||
t.Fatal("Expecting error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageForCustomNil(t *testing.T) {
|
||||
// Should fall through to the default
|
||||
c := &Config{StorageProvider: ""}
|
||||
s, err := c.StorageFor("example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := s.(*FileStorage); !ok {
|
||||
t.Fatalf("Unexpected storage type: %#v", s)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeStorage string
|
||||
|
||||
func (s fakeStorage) SiteExists(domain string) (bool, error) {
|
||||
panic("no impl")
|
||||
}
|
||||
|
||||
func (s fakeStorage) LoadSite(domain string) (*SiteData, error) {
|
||||
panic("no impl")
|
||||
}
|
||||
|
||||
func (s fakeStorage) StoreSite(domain string, data *SiteData) error {
|
||||
panic("no impl")
|
||||
}
|
||||
|
||||
func (s fakeStorage) DeleteSite(domain string) error {
|
||||
panic("no impl")
|
||||
}
|
||||
|
||||
func (s fakeStorage) TryLock(domain string) (Waiter, error) {
|
||||
panic("no impl")
|
||||
}
|
||||
|
||||
func (s fakeStorage) Unlock(domain string) error {
|
||||
panic("no impl")
|
||||
}
|
||||
|
||||
func (s fakeStorage) LoadUser(email string) (*UserData, error) {
|
||||
panic("no impl")
|
||||
}
|
||||
|
||||
func (s fakeStorage) StoreUser(email string, data *UserData) error {
|
||||
panic("no impl")
|
||||
}
|
||||
|
||||
func (s fakeStorage) MostRecentUserEmail() string {
|
||||
panic("no impl")
|
||||
}
|
356
vendor/github.com/mholt/caddy/caddytls/crypto.go
generated
vendored
Normal file
356
vendor/github.com/mholt/caddy/caddytls/crypto.go
generated
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/xenolf/lego/acmev2"
|
||||
)
|
||||
|
||||
// loadPrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
|
||||
func loadPrivateKey(keyBytes []byte) (crypto.PrivateKey, error) {
|
||||
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 an array of bytes.
|
||||
func savePrivateKey(key crypto.PrivateKey) ([]byte, 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 nil, err
|
||||
}
|
||||
case *rsa.PrivateKey:
|
||||
pemType = "RSA"
|
||||
keyBytes = x509.MarshalPKCS1PrivateKey(key)
|
||||
}
|
||||
|
||||
pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes}
|
||||
return pem.EncodeToMemory(&pemKey), nil
|
||||
}
|
||||
|
||||
// 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 already, 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()
|
||||
}
|
||||
|
||||
var ocspBytes []byte
|
||||
var ocspResp *ocsp.Response
|
||||
var ocspErr error
|
||||
var gotNewOCSP bool
|
||||
|
||||
// First try to load OCSP staple from storage and see if
|
||||
// we can still use it.
|
||||
// TODO: Use Storage interface instead of disk directly
|
||||
var ocspFileNamePrefix string
|
||||
if len(cert.Names) > 0 {
|
||||
firstName := strings.Replace(cert.Names[0], "*", "wildcard_", -1)
|
||||
ocspFileNamePrefix = firstName + "-"
|
||||
}
|
||||
ocspFileName := ocspFileNamePrefix + fastHash(pemBundle)
|
||||
ocspCachePath := filepath.Join(ocspFolder, ocspFileName)
|
||||
cachedOCSP, err := ioutil.ReadFile(ocspCachePath)
|
||||
if err == nil {
|
||||
resp, err := ocsp.ParseResponse(cachedOCSP, nil)
|
||||
if err == nil {
|
||||
if freshOCSP(resp) {
|
||||
// staple is still fresh; use it
|
||||
ocspBytes = cachedOCSP
|
||||
ocspResp = resp
|
||||
}
|
||||
} else {
|
||||
// invalid contents; delete the file
|
||||
// (we do this independently of the maintenance routine because
|
||||
// in this case we know for sure this should be a staple file
|
||||
// because we loaded it by name, whereas the maintenance routine
|
||||
// just iterates the list of files, even if somehow a non-staple
|
||||
// file gets in the folder. in this case we are sure it is corrupt.)
|
||||
err := os.Remove(ocspCachePath)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Unable to delete invalid OCSP staple file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't get a fresh staple by reading the cache,
|
||||
// then we need to request it from the OCSP responder
|
||||
if ocspResp == nil || len(ocspBytes) == 0 {
|
||||
ocspBytes, ocspResp, ocspErr = acme.GetOCSPForCert(pemBundle)
|
||||
if ocspErr != 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.
|
||||
// There's nothing else we can do to get OCSP for this certificate,
|
||||
// so we can return here with the error.
|
||||
return fmt.Errorf("no OCSP stapling for %v: %v", cert.Names, ocspErr)
|
||||
}
|
||||
gotNewOCSP = true
|
||||
}
|
||||
|
||||
// By now, we should have a response. If good, staple it to
|
||||
// the certificate. If the OCSP response was not loaded from
|
||||
// storage, we persist it for next time.
|
||||
if ocspResp.Status == ocsp.Good {
|
||||
if ocspResp.NextUpdate.After(cert.NotAfter) {
|
||||
// uh oh, this OCSP response expires AFTER the certificate does, that's kinda bogus.
|
||||
// it was the reason a lot of Symantec-validated sites (not Caddy) went down
|
||||
// in October 2017. https://twitter.com/mattiasgeniar/status/919432824708648961
|
||||
return fmt.Errorf("invalid: OCSP response for %v valid after certificate expiration (%s)",
|
||||
cert.Names, cert.NotAfter.Sub(ocspResp.NextUpdate))
|
||||
}
|
||||
cert.Certificate.OCSPStaple = ocspBytes
|
||||
cert.OCSP = ocspResp
|
||||
if gotNewOCSP {
|
||||
err := os.MkdirAll(filepath.Join(caddy.AssetsPath(), "ocsp"), 0700)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to make OCSP staple path for %v: %v", cert.Names, err)
|
||||
}
|
||||
err = ioutil.WriteFile(ocspCachePath, ocspBytes, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write OCSP staple file for %v: %v", cert.Names, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeSelfSignedCert makes a self-signed certificate according
|
||||
// to the parameters in config. It then caches the certificate
|
||||
// in our cache.
|
||||
func makeSelfSignedCert(config *Config) error {
|
||||
// start by generating private key
|
||||
var privKey interface{}
|
||||
var err error
|
||||
switch config.KeyType {
|
||||
case "", acme.EC256:
|
||||
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
case acme.EC384:
|
||||
privKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
case acme.RSA2048:
|
||||
privKey, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||
case acme.RSA4096:
|
||||
privKey, err = rsa.GenerateKey(rand.Reader, 4096)
|
||||
case acme.RSA8192:
|
||||
privKey, err = rsa.GenerateKey(rand.Reader, 8192)
|
||||
default:
|
||||
return fmt.Errorf("cannot generate private key; unknown key type %v", config.KeyType)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate private key: %v", err)
|
||||
}
|
||||
|
||||
// create certificate structure with proper values
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(24 * time.Hour * 7)
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate serial number: %v", err)
|
||||
}
|
||||
cert := &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{Organization: []string{"Caddy Self-Signed"}},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}
|
||||
var names []string
|
||||
if ip := net.ParseIP(config.Hostname); ip != nil {
|
||||
names = append(names, strings.ToLower(ip.String()))
|
||||
cert.IPAddresses = append(cert.IPAddresses, ip)
|
||||
} else {
|
||||
names = append(names, strings.ToLower(config.Hostname))
|
||||
cert.DNSNames = append(cert.DNSNames, strings.ToLower(config.Hostname))
|
||||
}
|
||||
|
||||
publicKey := func(privKey interface{}) interface{} {
|
||||
switch k := privKey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &k.PublicKey
|
||||
case *ecdsa.PrivateKey:
|
||||
return &k.PublicKey
|
||||
default:
|
||||
return errors.New("unknown key type")
|
||||
}
|
||||
}
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, publicKey(privKey), privKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create certificate: %v", err)
|
||||
}
|
||||
|
||||
chain := [][]byte{derBytes}
|
||||
|
||||
config.cacheCertificate(Certificate{
|
||||
Certificate: tls.Certificate{
|
||||
Certificate: chain,
|
||||
PrivateKey: privKey,
|
||||
Leaf: cert,
|
||||
},
|
||||
Names: names,
|
||||
NotAfter: cert.NotAfter,
|
||||
Hash: hashCertificateChain(chain),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RotateSessionTicketKeys rotates the TLS session ticket keys
|
||||
// on cfg every TicketRotateInterval. It spawns a new goroutine so
|
||||
// this function does NOT block. It returns a channel you should
|
||||
// close when you are ready to stop the key rotation, like when the
|
||||
// server using cfg is no longer running.
|
||||
func RotateSessionTicketKeys(cfg *tls.Config) chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
ticker := time.NewTicker(TicketRotateInterval)
|
||||
go runTLSTicketKeyRotation(cfg, ticker, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
// Functions that may be swapped out for testing
|
||||
var (
|
||||
runTLSTicketKeyRotation = standaloneTLSTicketKeyRotation
|
||||
setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte { return keys }
|
||||
setSessionTicketKeysTestHookMu sync.Mutex
|
||||
)
|
||||
|
||||
// standaloneTLSTicketKeyRotation governs over the array of TLS ticket keys used to de/crypt TLS tickets.
|
||||
// It periodically sets a new ticket key as the first one, used to encrypt (and decrypt),
|
||||
// pushing any old ticket keys to the back, where they are considered for decryption only.
|
||||
//
|
||||
// Lack of entropy for the very first ticket key results in the feature being disabled (as does Go),
|
||||
// later lack of entropy temporarily disables ticket key rotation.
|
||||
// Old ticket keys are still phased out, though.
|
||||
//
|
||||
// Stops the ticker when returning.
|
||||
func standaloneTLSTicketKeyRotation(c *tls.Config, ticker *time.Ticker, exitChan chan struct{}) {
|
||||
defer ticker.Stop()
|
||||
|
||||
// The entire page should be marked as sticky, but Go cannot do that
|
||||
// without resorting to syscall#Mlock. And, we don't have madvise (for NODUMP), too. ☹
|
||||
keys := make([][32]byte, 1, NumTickets)
|
||||
|
||||
rng := c.Rand
|
||||
if rng == nil {
|
||||
rng = rand.Reader
|
||||
}
|
||||
if _, err := io.ReadFull(rng, keys[0][:]); err != nil {
|
||||
c.SessionTicketsDisabled = true // bail if we don't have the entropy for the first one
|
||||
return
|
||||
}
|
||||
setSessionTicketKeysTestHookMu.Lock()
|
||||
setSessionTicketKeysHook := setSessionTicketKeysTestHook
|
||||
setSessionTicketKeysTestHookMu.Unlock()
|
||||
c.SetSessionTicketKeys(setSessionTicketKeysHook(keys))
|
||||
|
||||
for {
|
||||
select {
|
||||
case _, isOpen := <-exitChan:
|
||||
if !isOpen {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
rng = c.Rand // could've changed since the start
|
||||
if rng == nil {
|
||||
rng = rand.Reader
|
||||
}
|
||||
var newTicketKey [32]byte
|
||||
_, err := io.ReadFull(rng, newTicketKey[:])
|
||||
|
||||
if len(keys) < NumTickets {
|
||||
keys = append(keys, keys[0]) // manipulates the internal length
|
||||
}
|
||||
for idx := len(keys) - 1; idx >= 1; idx-- {
|
||||
keys[idx] = keys[idx-1] // yes, this makes copies
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
keys[0] = newTicketKey
|
||||
}
|
||||
// pushes the last key out, doesn't matter that we don't have a new one
|
||||
c.SetSessionTicketKeys(setSessionTicketKeysHook(keys))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fastHash hashes input using a hashing algorithm that
|
||||
// is fast, and returns the hash as a hex-encoded string.
|
||||
// Do not use this for cryptographic purposes.
|
||||
func fastHash(input []byte) string {
|
||||
h := fnv.New32a()
|
||||
h.Write(input)
|
||||
return fmt.Sprintf("%x", h.Sum32())
|
||||
}
|
||||
|
||||
const (
|
||||
// NumTickets is how many tickets to hold and consider
|
||||
// to decrypt TLS sessions.
|
||||
NumTickets = 4
|
||||
|
||||
// TicketRotateInterval is how often to generate
|
||||
// new ticket for TLS PFS encryption
|
||||
TicketRotateInterval = 10 * time.Hour
|
||||
)
|
152
vendor/github.com/mholt/caddy/caddytls/crypto_test.go
generated
vendored
Normal file
152
vendor/github.com/mholt/caddy/caddytls/crypto_test.go
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 128) // make tests faster; small key size OK for testing
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test save
|
||||
savedBytes, err := savePrivateKey(privateKey)
|
||||
if err != nil {
|
||||
t.Fatal("error saving private key:", err)
|
||||
}
|
||||
|
||||
// test load
|
||||
loadedKey, err := loadPrivateKey(savedBytes)
|
||||
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) {
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test save
|
||||
savedBytes, err := savePrivateKey(privateKey)
|
||||
if err != nil {
|
||||
t.Fatal("error saving private key:", err)
|
||||
}
|
||||
|
||||
// test load
|
||||
loadedKey, err := loadPrivateKey(savedBytes)
|
||||
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
|
||||
}
|
||||
|
||||
func TestStandaloneTLSTicketKeyRotation(t *testing.T) {
|
||||
type syncPkt struct {
|
||||
ticketKey [32]byte
|
||||
keysInUse int
|
||||
}
|
||||
|
||||
tlsGovChan := make(chan struct{})
|
||||
defer close(tlsGovChan)
|
||||
callSync := make(chan syncPkt)
|
||||
|
||||
setSessionTicketKeysTestHookMu.Lock()
|
||||
oldHook := setSessionTicketKeysTestHook
|
||||
defer func() {
|
||||
setSessionTicketKeysTestHookMu.Lock()
|
||||
setSessionTicketKeysTestHook = oldHook
|
||||
setSessionTicketKeysTestHookMu.Unlock()
|
||||
}()
|
||||
setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte {
|
||||
callSync <- syncPkt{keys[0], len(keys)}
|
||||
return keys
|
||||
}
|
||||
setSessionTicketKeysTestHookMu.Unlock()
|
||||
|
||||
c := new(tls.Config)
|
||||
timer := time.NewTicker(time.Millisecond * 1)
|
||||
|
||||
go standaloneTLSTicketKeyRotation(c, timer, tlsGovChan)
|
||||
|
||||
rounds := 0
|
||||
var lastTicketKey [32]byte
|
||||
for {
|
||||
select {
|
||||
case pkt := <-callSync:
|
||||
if lastTicketKey == pkt.ticketKey {
|
||||
close(tlsGovChan)
|
||||
t.Errorf("The same TLS ticket key has been used again (not rotated): %x.", lastTicketKey)
|
||||
return
|
||||
}
|
||||
lastTicketKey = pkt.ticketKey
|
||||
rounds++
|
||||
if rounds <= NumTickets && pkt.keysInUse != rounds {
|
||||
close(tlsGovChan)
|
||||
t.Errorf("Expected TLS ticket keys in use: %d; Got instead: %d.", rounds, pkt.keysInUse)
|
||||
return
|
||||
}
|
||||
if c.SessionTicketsDisabled {
|
||||
t.Error("Session tickets have been disabled unexpectedly.")
|
||||
return
|
||||
}
|
||||
if rounds >= NumTickets+1 {
|
||||
return
|
||||
}
|
||||
case <-time.After(time.Second * 1):
|
||||
t.Errorf("Timeout after %d rounds.", rounds)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
304
vendor/github.com/mholt/caddy/caddytls/filestorage.go
generated
vendored
Normal file
304
vendor/github.com/mholt/caddy/caddytls/filestorage.go
generated
vendored
Normal file
@@ -0,0 +1,304 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterStorageProvider("file", NewFileStorage)
|
||||
}
|
||||
|
||||
// NewFileStorage is a StorageConstructor function that creates a new
|
||||
// Storage instance backed by the local disk. The resulting Storage
|
||||
// instance is guaranteed to be non-nil if there is no error.
|
||||
func NewFileStorage(caURL *url.URL) (Storage, error) {
|
||||
// storageBasePath is the root path in which all TLS/ACME assets are
|
||||
// stored. Do not change this value during the lifetime of the program.
|
||||
storageBasePath := filepath.Join(caddy.AssetsPath(), "acme")
|
||||
|
||||
storage := &FileStorage{Path: filepath.Join(storageBasePath, caURL.Host)}
|
||||
storage.Locker = &fileStorageLock{caURL: caURL.Host, storage: storage}
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// FileStorage facilitates forming file paths derived from a root
|
||||
// directory. It is used to get file paths in a consistent,
|
||||
// cross-platform way or persisting ACME assets on the file system.
|
||||
type FileStorage struct {
|
||||
Path string
|
||||
Locker
|
||||
}
|
||||
|
||||
// sites gets the directory that stores site certificate and keys.
|
||||
func (s *FileStorage) sites() string {
|
||||
return filepath.Join(s.Path, "sites")
|
||||
}
|
||||
|
||||
// site returns the path to the folder containing assets for domain.
|
||||
func (s *FileStorage) site(domain string) string {
|
||||
domain = fileSafe(domain)
|
||||
return filepath.Join(s.sites(), domain)
|
||||
}
|
||||
|
||||
// siteCertFile returns the path to the certificate file for domain.
|
||||
func (s *FileStorage) siteCertFile(domain string) string {
|
||||
domain = fileSafe(domain)
|
||||
return filepath.Join(s.site(domain), domain+".crt")
|
||||
}
|
||||
|
||||
// siteKeyFile returns the path to domain's private key file.
|
||||
func (s *FileStorage) siteKeyFile(domain string) string {
|
||||
domain = fileSafe(domain)
|
||||
return filepath.Join(s.site(domain), domain+".key")
|
||||
}
|
||||
|
||||
// siteMetaFile returns the path to the domain's asset metadata file.
|
||||
func (s *FileStorage) siteMetaFile(domain string) string {
|
||||
domain = fileSafe(domain)
|
||||
return filepath.Join(s.site(domain), domain+".json")
|
||||
}
|
||||
|
||||
// users gets the directory that stores account folders.
|
||||
func (s *FileStorage) users() string {
|
||||
return filepath.Join(s.Path, "users")
|
||||
}
|
||||
|
||||
// user gets the account folder for the user with email
|
||||
func (s *FileStorage) user(email string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
email = fileSafe(email)
|
||||
return filepath.Join(s.users(), email)
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
// userRegFile gets the path to the registration file for the user with the
|
||||
// given email address.
|
||||
func (s *FileStorage) userRegFile(email string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
email = strings.ToLower(email)
|
||||
fileName := emailUsername(email)
|
||||
if fileName == "" {
|
||||
fileName = "registration"
|
||||
}
|
||||
fileName = fileSafe(fileName)
|
||||
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 *FileStorage) userKeyFile(email string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
email = strings.ToLower(email)
|
||||
fileName := emailUsername(email)
|
||||
if fileName == "" {
|
||||
fileName = "private"
|
||||
}
|
||||
fileName = fileSafe(fileName)
|
||||
return filepath.Join(s.user(email), fileName+".key")
|
||||
}
|
||||
|
||||
// readFile abstracts a simple ioutil.ReadFile, making sure to return an
|
||||
// ErrNotExist instance when the file is not found.
|
||||
func (s *FileStorage) readFile(file string) ([]byte, error) {
|
||||
b, err := ioutil.ReadFile(file)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ErrNotExist(err)
|
||||
}
|
||||
return b, err
|
||||
}
|
||||
|
||||
// SiteExists implements Storage.SiteExists by checking for the presence of
|
||||
// cert and key files.
|
||||
func (s *FileStorage) SiteExists(domain string) (bool, error) {
|
||||
_, err := os.Stat(s.siteCertFile(domain))
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, err = os.Stat(s.siteKeyFile(domain))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// LoadSite implements Storage.LoadSite by loading it from disk. If it is not
|
||||
// present, an instance of ErrNotExist is returned.
|
||||
func (s *FileStorage) LoadSite(domain string) (*SiteData, error) {
|
||||
var err error
|
||||
siteData := new(SiteData)
|
||||
siteData.Cert, err = s.readFile(s.siteCertFile(domain))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
siteData.Key, err = s.readFile(s.siteKeyFile(domain))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
siteData.Meta, err = s.readFile(s.siteMetaFile(domain))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return siteData, nil
|
||||
}
|
||||
|
||||
// StoreSite implements Storage.StoreSite by writing it to disk. The base
|
||||
// directories needed for the file are automatically created as needed.
|
||||
func (s *FileStorage) StoreSite(domain string, data *SiteData) error {
|
||||
err := os.MkdirAll(s.site(domain), 0700)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making site directory: %v", err)
|
||||
}
|
||||
err = ioutil.WriteFile(s.siteCertFile(domain), data.Cert, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing certificate file: %v", err)
|
||||
}
|
||||
err = ioutil.WriteFile(s.siteKeyFile(domain), data.Key, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing key file: %v", err)
|
||||
}
|
||||
err = ioutil.WriteFile(s.siteMetaFile(domain), data.Meta, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing cert meta file: %v", err)
|
||||
}
|
||||
log.Printf("[INFO][%v] Certificate written to disk: %v", domain, s.siteCertFile(domain))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSite implements Storage.DeleteSite by deleting just the cert from
|
||||
// disk. If it is not present, an instance of ErrNotExist is returned.
|
||||
func (s *FileStorage) DeleteSite(domain string) error {
|
||||
err := os.Remove(s.siteCertFile(domain))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return ErrNotExist(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadUser implements Storage.LoadUser by loading it from disk. If it is not
|
||||
// present, an instance of ErrNotExist is returned.
|
||||
func (s *FileStorage) LoadUser(email string) (*UserData, error) {
|
||||
var err error
|
||||
userData := new(UserData)
|
||||
userData.Reg, err = s.readFile(s.userRegFile(email))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userData.Key, err = s.readFile(s.userKeyFile(email))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return userData, nil
|
||||
}
|
||||
|
||||
// StoreUser implements Storage.StoreUser by writing it to disk. The base
|
||||
// directories needed for the file are automatically created as needed.
|
||||
func (s *FileStorage) StoreUser(email string, data *UserData) error {
|
||||
err := os.MkdirAll(s.user(email), 0700)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making user directory: %v", err)
|
||||
}
|
||||
err = ioutil.WriteFile(s.userRegFile(email), data.Reg, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing user registration file: %v", err)
|
||||
}
|
||||
err = ioutil.WriteFile(s.userKeyFile(email), data.Key, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing user key file: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MostRecentUserEmail implements Storage.MostRecentUserEmail by finding the
|
||||
// most recently written sub directory in the users' directory. It is named
|
||||
// after the email address. This corresponds to the most recent call to
|
||||
// StoreUser.
|
||||
func (s *FileStorage) MostRecentUserEmail() string {
|
||||
userDirs, err := ioutil.ReadDir(s.users())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var mostRecent os.FileInfo
|
||||
for _, dir := range userDirs {
|
||||
if !dir.IsDir() {
|
||||
continue
|
||||
}
|
||||
if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) {
|
||||
mostRecent = dir
|
||||
}
|
||||
}
|
||||
if mostRecent != nil {
|
||||
return mostRecent.Name()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// fileSafe standardizes and sanitizes str for use in a file path.
|
||||
func fileSafe(str string) string {
|
||||
str = strings.ToLower(str)
|
||||
str = strings.TrimSpace(str)
|
||||
repl := strings.NewReplacer("..", "",
|
||||
"/", "",
|
||||
"\\", "",
|
||||
// TODO: Consider also replacing "@" with "_at_" (but migrate existing accounts...)
|
||||
"+", "_plus_",
|
||||
"%", "",
|
||||
"$", "",
|
||||
"`", "",
|
||||
"~", "",
|
||||
":", "",
|
||||
";", "",
|
||||
"=", "",
|
||||
"!", "",
|
||||
"#", "",
|
||||
"&", "",
|
||||
"|", "",
|
||||
"\"", "",
|
||||
"'", "",
|
||||
"*", "wildcard_")
|
||||
return repl.Replace(str)
|
||||
}
|
84
vendor/github.com/mholt/caddy/caddytls/filestorage_test.go
generated
vendored
Normal file
84
vendor/github.com/mholt/caddy/caddytls/filestorage_test.go
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// *********************************** NOTE ********************************
|
||||
// Due to circular package dependencies with the storagetest sub package and
|
||||
// the fact that we want to use that harness to test file storage, most of
|
||||
// the tests for file storage are done in the storagetest package.
|
||||
|
||||
func TestPathBuilders(t *testing.T) {
|
||||
fs := FileStorage{Path: "test"}
|
||||
|
||||
for i, testcase := range []struct {
|
||||
in, folder, certFile, keyFile, metaFile string
|
||||
}{
|
||||
{
|
||||
in: "example.com",
|
||||
folder: filepath.Join("test", "sites", "example.com"),
|
||||
certFile: filepath.Join("test", "sites", "example.com", "example.com.crt"),
|
||||
keyFile: filepath.Join("test", "sites", "example.com", "example.com.key"),
|
||||
metaFile: filepath.Join("test", "sites", "example.com", "example.com.json"),
|
||||
},
|
||||
{
|
||||
in: "*.example.com",
|
||||
folder: filepath.Join("test", "sites", "wildcard_.example.com"),
|
||||
certFile: filepath.Join("test", "sites", "wildcard_.example.com", "wildcard_.example.com.crt"),
|
||||
keyFile: filepath.Join("test", "sites", "wildcard_.example.com", "wildcard_.example.com.key"),
|
||||
metaFile: filepath.Join("test", "sites", "wildcard_.example.com", "wildcard_.example.com.json"),
|
||||
},
|
||||
{
|
||||
// prevent directory traversal! very important, esp. with on-demand TLS
|
||||
// see issue #2092
|
||||
in: "a/../../../foo",
|
||||
folder: filepath.Join("test", "sites", "afoo"),
|
||||
certFile: filepath.Join("test", "sites", "afoo", "afoo.crt"),
|
||||
keyFile: filepath.Join("test", "sites", "afoo", "afoo.key"),
|
||||
metaFile: filepath.Join("test", "sites", "afoo", "afoo.json"),
|
||||
},
|
||||
{
|
||||
in: "b\\..\\..\\..\\foo",
|
||||
folder: filepath.Join("test", "sites", "bfoo"),
|
||||
certFile: filepath.Join("test", "sites", "bfoo", "bfoo.crt"),
|
||||
keyFile: filepath.Join("test", "sites", "bfoo", "bfoo.key"),
|
||||
metaFile: filepath.Join("test", "sites", "bfoo", "bfoo.json"),
|
||||
},
|
||||
{
|
||||
in: "c/foo",
|
||||
folder: filepath.Join("test", "sites", "cfoo"),
|
||||
certFile: filepath.Join("test", "sites", "cfoo", "cfoo.crt"),
|
||||
keyFile: filepath.Join("test", "sites", "cfoo", "cfoo.key"),
|
||||
metaFile: filepath.Join("test", "sites", "cfoo", "cfoo.json"),
|
||||
},
|
||||
} {
|
||||
if actual := fs.site(testcase.in); actual != testcase.folder {
|
||||
t.Errorf("Test %d: site folder: Expected '%s' but got '%s'", i, testcase.folder, actual)
|
||||
}
|
||||
if actual := fs.siteCertFile(testcase.in); actual != testcase.certFile {
|
||||
t.Errorf("Test %d: site cert file: Expected '%s' but got '%s'", i, testcase.certFile, actual)
|
||||
}
|
||||
if actual := fs.siteKeyFile(testcase.in); actual != testcase.keyFile {
|
||||
t.Errorf("Test %d: site key file: Expected '%s' but got '%s'", i, testcase.keyFile, actual)
|
||||
}
|
||||
if actual := fs.siteMetaFile(testcase.in); actual != testcase.metaFile {
|
||||
t.Errorf("Test %d: site meta file: Expected '%s' but got '%s'", i, testcase.metaFile, actual)
|
||||
}
|
||||
}
|
||||
}
|
140
vendor/github.com/mholt/caddy/caddytls/filestoragesync.go
generated
vendored
Normal file
140
vendor/github.com/mholt/caddy/caddytls/filestoragesync.go
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// be sure to remove lock files when exiting the process!
|
||||
caddy.OnProcessExit = append(caddy.OnProcessExit, func() {
|
||||
fileStorageNameLocksMu.Lock()
|
||||
defer fileStorageNameLocksMu.Unlock()
|
||||
for key, fw := range fileStorageNameLocks {
|
||||
os.Remove(fw.filename)
|
||||
delete(fileStorageNameLocks, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// fileStorageLock facilitates ACME-related locking by using
|
||||
// the associated FileStorage, so multiple processes can coordinate
|
||||
// renewals on the certificates on a shared file system.
|
||||
type fileStorageLock struct {
|
||||
caURL string
|
||||
storage *FileStorage
|
||||
}
|
||||
|
||||
// TryLock attempts to get a lock for name, otherwise it returns
|
||||
// a Waiter value to wait until the other process is finished.
|
||||
func (s *fileStorageLock) TryLock(name string) (Waiter, error) {
|
||||
fileStorageNameLocksMu.Lock()
|
||||
defer fileStorageNameLocksMu.Unlock()
|
||||
|
||||
// see if lock already exists within this process
|
||||
fw, ok := fileStorageNameLocks[s.caURL+name]
|
||||
if ok {
|
||||
// lock already created within process, let caller wait on it
|
||||
return fw, nil
|
||||
}
|
||||
|
||||
// attempt to persist lock to disk by creating lock file
|
||||
fw = &fileWaiter{
|
||||
filename: s.storage.siteCertFile(name) + ".lock",
|
||||
wg: new(sync.WaitGroup),
|
||||
}
|
||||
// parent dir must exist
|
||||
if err := os.MkdirAll(s.storage.site(name), 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lf, err := os.OpenFile(fw.filename, os.O_CREATE|os.O_EXCL, 0644)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
// another process has the lock; use it to wait
|
||||
return fw, nil
|
||||
}
|
||||
// otherwise, this was some unexpected error
|
||||
return nil, err
|
||||
}
|
||||
lf.Close()
|
||||
|
||||
// looks like we get the lock
|
||||
fw.wg.Add(1)
|
||||
fileStorageNameLocks[s.caURL+name] = fw
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Unlock unlocks name.
|
||||
func (s *fileStorageLock) Unlock(name string) error {
|
||||
fileStorageNameLocksMu.Lock()
|
||||
defer fileStorageNameLocksMu.Unlock()
|
||||
fw, ok := fileStorageNameLocks[s.caURL+name]
|
||||
if !ok {
|
||||
return fmt.Errorf("FileStorage: no lock to release for %s", name)
|
||||
}
|
||||
// remove lock file
|
||||
os.Remove(fw.filename)
|
||||
|
||||
// if parent folder is now empty, remove it too to keep it tidy
|
||||
lockParentFolder := s.storage.site(name)
|
||||
dir, err := os.Open(lockParentFolder)
|
||||
if err == nil {
|
||||
items, _ := dir.Readdirnames(3) // OK to ignore error here
|
||||
if len(items) == 0 {
|
||||
os.Remove(lockParentFolder)
|
||||
}
|
||||
dir.Close()
|
||||
}
|
||||
|
||||
fw.wg.Done()
|
||||
delete(fileStorageNameLocks, s.caURL+name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// fileWaiter waits for a file to disappear; it polls
|
||||
// the file system to check for the existence of a file.
|
||||
// It also has a WaitGroup which will be faster than
|
||||
// polling, for when locking need only happen within this
|
||||
// process.
|
||||
type fileWaiter struct {
|
||||
filename string
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// Wait waits until the lock is released.
|
||||
func (fw *fileWaiter) Wait() {
|
||||
start := time.Now()
|
||||
fw.wg.Wait()
|
||||
for time.Since(start) < 1*time.Hour {
|
||||
_, err := os.Stat(fw.filename)
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
var fileStorageNameLocks = make(map[string]*fileWaiter) // keyed by CA + name
|
||||
var fileStorageNameLocksMu sync.Mutex
|
||||
|
||||
var _ Locker = &fileStorageLock{}
|
||||
var _ Waiter = &fileWaiter{}
|
542
vendor/github.com/mholt/caddy/caddytls/handshake.go
generated
vendored
Normal file
542
vendor/github.com/mholt/caddy/caddytls/handshake.go
generated
vendored
Normal file
@@ -0,0 +1,542 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/telemetry"
|
||||
)
|
||||
|
||||
// configGroup is a type that keys configs by their hostname
|
||||
// (hostnames can have wildcard characters; use the getConfig
|
||||
// method to get a config by matching its hostname).
|
||||
type configGroup map[string]*Config
|
||||
|
||||
// getConfig gets the config by the first key match for name.
|
||||
// In other words, "sub.foo.bar" will get the config for "*.foo.bar"
|
||||
// if that is the closest match. If no match is found, the first
|
||||
// (random) config will be loaded, which will defer any TLS alerts
|
||||
// to the certificate validation (this may or may not be ideal;
|
||||
// let's talk about it if this becomes problematic).
|
||||
//
|
||||
// This function follows nearly the same logic to lookup
|
||||
// a hostname as the getCertificate function uses.
|
||||
func (cg configGroup) getConfig(name string) *Config {
|
||||
name = strings.ToLower(name)
|
||||
|
||||
// exact match? great, let's use it
|
||||
if config, ok := cg[name]; ok {
|
||||
return config
|
||||
}
|
||||
|
||||
// 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 config, ok := cg[candidate]; ok {
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
// try a config that serves all names (the above
|
||||
// loop doesn't try empty string; for hosts defined
|
||||
// with only a port, for instance, like ":443")
|
||||
if config, ok := cg[""]; ok {
|
||||
return config
|
||||
}
|
||||
|
||||
// no matches, so just serve up a random config
|
||||
for _, config := range cg {
|
||||
return config
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfigForClient gets a TLS configuration satisfying clientHello.
|
||||
// In getting the configuration, it abides the rules and settings
|
||||
// defined in the Config that matches clientHello.ServerName. If no
|
||||
// tls.Config is set on the matching Config, a nil value is returned.
|
||||
//
|
||||
// This method is safe for use as a tls.Config.GetConfigForClient callback.
|
||||
func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
config := cg.getConfig(clientHello.ServerName)
|
||||
if config != nil {
|
||||
return config.tlsConfig, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetCertificate gets a certificate to satisfy clientHello. In getting
|
||||
// the certificate, it abides the rules and settings defined in the
|
||||
// Config that matches clientHello.ServerName. It first checks the in-
|
||||
// memory cache, then, if the config enables "OnDemand", it accesses
|
||||
// disk, then accesses the network if it must obtain a new certificate
|
||||
// via ACME.
|
||||
//
|
||||
// This method is safe for use as a tls.Config.GetCertificate callback.
|
||||
func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if ClientHelloTelemetry && len(clientHello.SupportedVersions) > 0 {
|
||||
// If no other plugin (such as the HTTP server type) is implementing ClientHello telemetry, we do it.
|
||||
// NOTE: The values in the Go standard lib's ClientHelloInfo aren't guaranteed to be in order.
|
||||
info := ClientHelloInfo{
|
||||
Version: clientHello.SupportedVersions[0], // report the highest
|
||||
CipherSuites: clientHello.CipherSuites,
|
||||
ExtensionsUnknown: true, // no extension info... :(
|
||||
CompressionMethodsUnknown: true, // no compression methods... :(
|
||||
Curves: clientHello.SupportedCurves,
|
||||
Points: clientHello.SupportedPoints,
|
||||
// We also have, but do not yet use: SignatureSchemes, ServerName, and SupportedProtos (ALPN)
|
||||
// because the standard lib parses some extensions, but our MITM detector generally doesn't.
|
||||
}
|
||||
go telemetry.SetNested("tls_client_hello", info.Key(), info)
|
||||
}
|
||||
|
||||
// get the certificate and serve it up
|
||||
cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true)
|
||||
if err == nil {
|
||||
go telemetry.Increment("tls_handshake_count") // TODO: This is a "best guess" for now, we need something listener-level
|
||||
}
|
||||
return &cert.Certificate, err
|
||||
}
|
||||
|
||||
// getCertificate gets a certificate that matches name (a server name)
|
||||
// from the in-memory cache, according to the lookup table associated with
|
||||
// cfg. The lookup then points to a certificate in the Instance certificate
|
||||
// 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 defaulted is false, then no certificates were available.
|
||||
//
|
||||
// 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 (cfg *Config) getCertificate(name string) (cert Certificate, matched, defaulted bool) {
|
||||
var certKey string
|
||||
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)
|
||||
|
||||
cfg.certCache.RLock()
|
||||
defer cfg.certCache.RUnlock()
|
||||
|
||||
// exact match? great, let's use it
|
||||
if certKey, ok = cfg.Certificates[name]; ok {
|
||||
cert = cfg.certCache.cache[certKey]
|
||||
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 certKey, ok = cfg.Certificates[candidate]; ok {
|
||||
cert = cfg.certCache.cache[certKey]
|
||||
matched = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// check the certCache directly to see if the SNI name is
|
||||
// already the key of the certificate it wants! this is vital
|
||||
// for supporting the TLS-SNI challenge, since the tlsSNISolver
|
||||
// just puts the temporary certificate in the instance cache,
|
||||
// with no regard for configs; this also means that the SNI
|
||||
// can contain the hash of a specific cert (chain) it wants
|
||||
// and we will still be able to serve it up
|
||||
// (this behavior, by the way, could be controversial as to
|
||||
// whether it complies with RFC 6066 about SNI, but I think
|
||||
// it does soooo...)
|
||||
// NOTE/TODO: TLS-SNI challenge is changing, as of Jan. 2018
|
||||
// but what will be different, if it ever returns, is unclear
|
||||
if directCert, ok := cfg.certCache.cache[name]; ok {
|
||||
cert = directCert
|
||||
matched = true
|
||||
return
|
||||
}
|
||||
|
||||
// if nothing matches, use a random certificate
|
||||
// TODO: This is not my favorite behavior; I would rather serve
|
||||
// no certificate if SNI is provided and cause a TLS alert, than
|
||||
// serve the wrong certificate (but sometimes the 'wrong' cert
|
||||
// is what is wanted, but in those cases I would prefer that the
|
||||
// site owner explicitly configure a "default" certificate).
|
||||
// (See issue 2035; any change to this behavior must account for
|
||||
// hosts defined like ":443" or "0.0.0.0:443" where the hostname
|
||||
// is empty or a catch-all IP or something.)
|
||||
for _, certKey := range cfg.Certificates {
|
||||
cert = cfg.certCache.cache[certKey]
|
||||
defaulted = true
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getCertDuringHandshake will get a certificate for name. It first tries
|
||||
// the in-memory cache. If no certificate for name is in the cache, the
|
||||
// config most closely corresponding to name will be loaded. If that config
|
||||
// allows it (OnDemand==true) 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 (cfg *Config) 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 := cfg.getCertificate(name)
|
||||
if matched {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// If OnDemand is enabled, then we might be able to load or
|
||||
// obtain a needed certificate
|
||||
if cfg.OnDemand && loadIfNecessary {
|
||||
// Then check to see if we have one on disk
|
||||
loadedCert, err := cfg.CacheManagedCertificate(name)
|
||||
if err == nil {
|
||||
loadedCert, err = cfg.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 the certificate should be obtained based on config
|
||||
err := cfg.checkIfCertShouldBeObtained(name)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
|
||||
// Name has to qualify for a certificate
|
||||
if !HostQualifies(name) {
|
||||
return cert, errors.New("hostname '" + name + "' does not qualify for certificate")
|
||||
}
|
||||
|
||||
// Obtain certificate from the CA
|
||||
return cfg.obtainOnDemandCertificate(name)
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the default certificate if there is one
|
||||
if defaulted {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
return Certificate{}, fmt.Errorf("no certificate available for %s", name)
|
||||
}
|
||||
|
||||
// checkIfCertShouldBeObtained checks to see if an on-demand tls certificate
|
||||
// should be obtained for a given domain based upon the config settings. If
|
||||
// a non-nil error is returned, do not issue a new certificate for name.
|
||||
func (cfg *Config) checkIfCertShouldBeObtained(name string) error {
|
||||
// If the "ask" URL is defined in the config, use to determine if a
|
||||
// cert should obtained
|
||||
if cfg.OnDemandState.AskURL != nil {
|
||||
return cfg.checkURLForObtainingNewCerts(name)
|
||||
}
|
||||
|
||||
// Otherwise use the limit defined by the "max_certs" setting
|
||||
return cfg.checkLimitsForObtainingNewCerts(name)
|
||||
}
|
||||
|
||||
func (cfg *Config) checkURLForObtainingNewCerts(name string) error {
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return errors.New("following http redirects is not allowed")
|
||||
},
|
||||
}
|
||||
|
||||
// Copy the URL from the config in order to modify it for this request
|
||||
askURL := new(url.URL)
|
||||
*askURL = *cfg.OnDemandState.AskURL
|
||||
|
||||
query := askURL.Query()
|
||||
query.Set("domain", name)
|
||||
askURL.RawQuery = query.Encode()
|
||||
|
||||
resp, err := client.Get(askURL.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking %v to deterine if certificate for hostname '%s' should be allowed: %v", cfg.OnDemandState.AskURL, name, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return fmt.Errorf("certificate for hostname '%s' not allowed, non-2xx status code %d returned from %v", name, resp.StatusCode, cfg.OnDemandState.AskURL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkLimitsForObtainingNewCerts checks to see if name can be issued right
|
||||
// now according the maximum count defined in the configuration. If a non-nil
|
||||
// error is returned, do not issue a new certificate for name.
|
||||
func (cfg *Config) checkLimitsForObtainingNewCerts(name string) error {
|
||||
// User can set hard limit for number of certs for the process to issue
|
||||
if cfg.OnDemandState.MaxObtain > 0 &&
|
||||
atomic.LoadInt32(&cfg.OnDemandState.ObtainedCount) >= cfg.OnDemandState.MaxObtain {
|
||||
return fmt.Errorf("%s: maximum certificates issued (%d)", name, cfg.OnDemandState.MaxObtain)
|
||||
}
|
||||
|
||||
// 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(&cfg.OnDemandState.ObtainedCount) >= 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 (cfg *Config) 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 cfg.getCertDuringHandshake(name, true, false)
|
||||
}
|
||||
|
||||
// looks like it's up to us to do all the work and obtain the cert.
|
||||
// make a chan others can wait on if needed
|
||||
wait = make(chan struct{})
|
||||
obtainCertWaitChans[name] = wait
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
|
||||
// obtain the certificate
|
||||
log.Printf("[INFO] Obtaining new certificate for %s", name)
|
||||
err := cfg.ObtainCert(name, false)
|
||||
|
||||
// immediately unblock anyone waiting for it; doing this in
|
||||
// a defer would risk deadlock because of the recursive call
|
||||
// to getCertDuringHandshake below when we return!
|
||||
obtainCertWaitChansMu.Lock()
|
||||
close(wait)
|
||||
delete(obtainCertWaitChans, name)
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
|
||||
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(&cfg.OnDemandState.ObtainedCount, 1)
|
||||
lastIssueTimeMu.Lock()
|
||||
lastIssueTime = time.Now()
|
||||
lastIssueTimeMu.Unlock()
|
||||
|
||||
// certificate is already on disk; now just start over to load it and serve it
|
||||
return cfg.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 (cfg *Config) 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 cfg.renewDynamicCertificate(name, cert)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
cfg.certCache.Lock()
|
||||
cfg.certCache.cache[cert.Hash] = cert
|
||||
cfg.certCache.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// renewDynamicCertificate renews the certificate for name using cfg. It returns the
|
||||
// certificate to use and an error, if any. name should already be lower-cased before
|
||||
// calling this function. name is the name obtained directly from the handshake's
|
||||
// ClientHello.
|
||||
//
|
||||
// This function is safe for use by multiple concurrent goroutines.
|
||||
func (cfg *Config) renewDynamicCertificate(name string, currentCert Certificate) (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 cfg.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()
|
||||
|
||||
// renew and reload the certificate
|
||||
log.Printf("[INFO] Renewing certificate for %s", name)
|
||||
err := cfg.RenewCert(name, false)
|
||||
if err == nil {
|
||||
// even though the recursive nature of the dynamic cert loading
|
||||
// would just call this function anyway, we do it here to
|
||||
// make the replacement as atomic as possible.
|
||||
newCert, err := currentCert.configs[0].CacheManagedCertificate(name)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] loading renewed certificate for %s: %v", name, err)
|
||||
} else {
|
||||
// replace the old certificate with the new one
|
||||
err = cfg.certCache.replaceCertificate(currentCert, newCert)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Replacing certificate for %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// immediately unblock anyone waiting for it; doing this in
|
||||
// a defer would risk deadlock because of the recursive call
|
||||
// to getCertDuringHandshake below when we return!
|
||||
obtainCertWaitChansMu.Lock()
|
||||
close(wait)
|
||||
delete(obtainCertWaitChans, name)
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
|
||||
return cfg.getCertDuringHandshake(name, true, false)
|
||||
}
|
||||
|
||||
// ClientHelloInfo is our own version of the standard lib's
|
||||
// tls.ClientHelloInfo. As of May 2018, any fields populated
|
||||
// by the Go standard library are not guaranteed to have their
|
||||
// values in the original order as on the wire.
|
||||
type ClientHelloInfo struct {
|
||||
Version uint16 `json:"version,omitempty"`
|
||||
CipherSuites []uint16 `json:"cipher_suites,omitempty"`
|
||||
Extensions []uint16 `json:"extensions,omitempty"`
|
||||
CompressionMethods []byte `json:"compression,omitempty"`
|
||||
Curves []tls.CurveID `json:"curves,omitempty"`
|
||||
Points []uint8 `json:"points,omitempty"`
|
||||
|
||||
// Whether a couple of fields are unknown; if not, the key will encode
|
||||
// differently to reflect that, as opposed to being known empty values.
|
||||
// (some fields may be unknown depending on what package is being used;
|
||||
// i.e. the Go standard lib doesn't expose some things)
|
||||
// (very important to NOT encode these to JSON)
|
||||
ExtensionsUnknown bool `json:"-"`
|
||||
CompressionMethodsUnknown bool `json:"-"`
|
||||
}
|
||||
|
||||
// Key returns a standardized string form of the data in info,
|
||||
// useful for identifying duplicates.
|
||||
func (info ClientHelloInfo) Key() string {
|
||||
extensions, compressionMethods := "?", "?"
|
||||
if !info.ExtensionsUnknown {
|
||||
extensions = fmt.Sprintf("%x", info.Extensions)
|
||||
}
|
||||
if !info.CompressionMethodsUnknown {
|
||||
compressionMethods = fmt.Sprintf("%x", info.CompressionMethods)
|
||||
}
|
||||
return telemetry.FastHash([]byte(fmt.Sprintf("%x-%x-%s-%s-%x-%x",
|
||||
info.Version, info.CipherSuites, extensions,
|
||||
compressionMethods, info.Curves, info.Points)))
|
||||
}
|
||||
|
||||
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
|
||||
var obtainCertWaitChans = make(map[string]chan struct{})
|
||||
var obtainCertWaitChansMu sync.Mutex
|
||||
|
||||
// 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
|
||||
|
||||
// ClientHelloTelemetry determines whether to report
|
||||
// TLS ClientHellos to telemetry. Disable if doing
|
||||
// it from a different package.
|
||||
var ClientHelloTelemetry = true
|
77
vendor/github.com/mholt/caddy/caddytls/handshake_test.go
generated
vendored
Normal file
77
vendor/github.com/mholt/caddy/caddytls/handshake_test.go
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetCertificate(t *testing.T) {
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
|
||||
hello := &tls.ClientHelloInfo{ServerName: "example.com"}
|
||||
helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com"}
|
||||
helloNoSNI := &tls.ClientHelloInfo{}
|
||||
// helloNoMatch := &tls.ClientHelloInfo{ServerName: "nomatch"} // TODO (see below)
|
||||
|
||||
// When cache is empty
|
||||
if cert, err := cfg.GetCertificate(hello); err == nil {
|
||||
t.Errorf("GetCertificate should return error when cache is empty, got: %v", cert)
|
||||
}
|
||||
if cert, err := cfg.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
|
||||
firstCert := Certificate{Names: []string{"example.com"}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"example.com"}}}}
|
||||
cfg.cacheCertificate(firstCert)
|
||||
if cert, err := cfg.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 _, err := cfg.GetCertificate(helloNoSNI); err != nil {
|
||||
t.Errorf("Got an error with no SNI but shouldn't have, when cert exists in cache: %v", err)
|
||||
}
|
||||
|
||||
// When retrieving wildcard certificate
|
||||
wildcardCert := Certificate{
|
||||
Names: []string{"*.example.com"},
|
||||
Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"*.example.com"}}},
|
||||
Hash: "(don't overwrite the first one)",
|
||||
}
|
||||
cfg.cacheCertificate(wildcardCert)
|
||||
if cert, err := cfg.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 cache is NOT empty but there's no SNI
|
||||
if cert, err := cfg.GetCertificate(helloNoSNI); err != nil {
|
||||
t.Errorf("Expected random certificate with no error when no SNI, got err: %v", err)
|
||||
} else if cert == nil || len(cert.Leaf.DNSNames) == 0 {
|
||||
t.Errorf("Expected random cert with no matches, got: %v", cert)
|
||||
}
|
||||
|
||||
// TODO: Re-implement this behavior (it was reverted in #2037)
|
||||
// When no certificate matches, raise an alert
|
||||
// if _, err := cfg.GetCertificate(helloNoMatch); err == nil {
|
||||
// t.Errorf("Expected an error when no certificate matched the SNI, got: %v", err)
|
||||
// }
|
||||
}
|
120
vendor/github.com/mholt/caddy/caddytls/httphandler.go
generated
vendored
Normal file
120
vendor/github.com/mholt/caddy/caddytls/httphandler.go
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/xenolf/lego/acmev2"
|
||||
)
|
||||
|
||||
const challengeBasePath = "/.well-known/acme-challenge"
|
||||
|
||||
// HTTPChallengeHandler proxies challenge requests to ACME client if the
|
||||
// request path starts with challengeBasePath, if the HTTP challenge is not
|
||||
// disabled, and if we are known to be obtaining a certificate for the name.
|
||||
// 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 HTTPChallengeHandler(w http.ResponseWriter, r *http.Request, listenHost string) bool {
|
||||
if !strings.HasPrefix(r.URL.Path, challengeBasePath) {
|
||||
return false
|
||||
}
|
||||
if DisableHTTPChallenge {
|
||||
return false
|
||||
}
|
||||
|
||||
// see if another instance started the HTTP challenge for this name
|
||||
if tryDistributedChallengeSolver(w, r) {
|
||||
return true
|
||||
}
|
||||
|
||||
// otherwise, if we aren't getting the name, then ignore this challenge
|
||||
if !namesObtaining.Has(r.Host) {
|
||||
return false
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
if listenHost == "" {
|
||||
listenHost = "localhost"
|
||||
}
|
||||
|
||||
// always proxy to the DefaultHTTPAlternatePort because obviously the
|
||||
// ACME challenge request already got into one of our HTTP handlers, so
|
||||
// it means we must have started a HTTP listener on the alternate
|
||||
// port instead; which is only accessible via listenHost
|
||||
upstream, err := url.Parse(fmt.Sprintf("%s://%s:%s", scheme, listenHost, DefaultHTTPAlternatePort))
|
||||
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},
|
||||
}
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// tryDistributedChallengeSolver checks to see if this challenge
|
||||
// request was initiated by another instance that shares file
|
||||
// storage, and attempts to complete the challenge for it. It
|
||||
// returns true if the challenge was handled; false otherwise.
|
||||
func tryDistributedChallengeSolver(w http.ResponseWriter, r *http.Request) bool {
|
||||
filePath := distributedHTTPSolver{}.challengeTokensPath(r.Host)
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.Printf("[ERROR][%s] Opening distributed challenge token file: %v", r.Host, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var chalInfo challengeInfo
|
||||
err = json.NewDecoder(f).Decode(&chalInfo)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR][%s] Decoding challenge token file %s (corrupted?): %v", r.Host, filePath, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// this part borrowed from xenolf/lego's built-in HTTP-01 challenge solver (March 2018)
|
||||
challengeReqPath := acme.HTTP01ChallengePath(chalInfo.Token)
|
||||
if r.URL.Path == challengeReqPath &&
|
||||
strings.HasPrefix(r.Host, chalInfo.Domain) &&
|
||||
r.Method == "GET" {
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte(chalInfo.KeyAuth))
|
||||
r.Close = true
|
||||
log.Printf("[INFO][%s] Served key authentication", chalInfo.Domain)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
84
vendor/github.com/mholt/caddy/caddytls/httphandler_test.go
generated
vendored
Normal file
84
vendor/github.com/mholt/caddy/caddytls/httphandler_test.go
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPChallengeHandlerNoOp(t *testing.T) {
|
||||
namesObtaining.Add([]string{"localhost"})
|
||||
|
||||
// try base paths and host names 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",
|
||||
"http://other/.well-known/acme-challenge/foo",
|
||||
} {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not craft request, got error: %v", err)
|
||||
}
|
||||
rw := httptest.NewRecorder()
|
||||
if HTTPChallengeHandler(rw, req, "") {
|
||||
t.Errorf("Got true with this URL, but shouldn't have: %s", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPChallengeHandlerSuccess(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:"+DefaultHTTPAlternatePort)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to start test server listener: %v", err)
|
||||
}
|
||||
ts.Listener = ln
|
||||
|
||||
// Tell this package that we are handling a challenge for 127.0.0.1
|
||||
namesObtaining.Add([]string{"127.0.0.1"})
|
||||
|
||||
// Start our engines and run the test
|
||||
ts.Start()
|
||||
defer ts.Close()
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:"+DefaultHTTPAlternatePort+expectedPath, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not craft request, got error: %v", err)
|
||||
}
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
HTTPChallengeHandler(rw, req, "")
|
||||
|
||||
if !proxySuccess {
|
||||
t.Fatal("Expected request to be proxied, but it wasn't")
|
||||
}
|
||||
}
|
365
vendor/github.com/mholt/caddy/caddytls/maintain.go
generated
vendored
Normal file
365
vendor/github.com/mholt/caddy/caddytls/maintain.go
generated
vendored
Normal file
@@ -0,0 +1,365 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// maintain assets while this package is imported, which is
|
||||
// always. we don't ever stop it, since we need it running.
|
||||
go maintainAssets(make(chan struct{}))
|
||||
}
|
||||
|
||||
const (
|
||||
// RenewInterval is how often to check certificates for renewal.
|
||||
RenewInterval = 12 * time.Hour
|
||||
|
||||
// RenewDurationBefore is how long before expiration to renew certificates.
|
||||
RenewDurationBefore = (24 * time.Hour) * 30
|
||||
|
||||
// RenewDurationBeforeAtStartup is how long before expiration to require
|
||||
// a renewed certificate when the process is first starting up (see #1680).
|
||||
// A wider window between RenewDurationBefore and this value will allow
|
||||
// Caddy to start under duress but hopefully this duration will give it
|
||||
// enough time for the blockage to be relieved.
|
||||
RenewDurationBeforeAtStartup = (24 * time.Hour) * 7
|
||||
|
||||
// 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. It should only be
|
||||
// called once per process.
|
||||
//
|
||||
// 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. (Not that you HAVE to stop it...)
|
||||
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()
|
||||
DeleteOldStapleFiles()
|
||||
log.Println("[INFO] Done checking OCSP staples")
|
||||
case <-stopChan:
|
||||
renewalTicker.Stop()
|
||||
ocspTicker.Stop()
|
||||
log.Println("[INFO] Stopped background maintenance routine")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RenewManagedCertificates renews managed certificates,
|
||||
// including ones loaded on-demand.
|
||||
func RenewManagedCertificates(allowPrompts bool) (err error) {
|
||||
for _, inst := range caddy.Instances() {
|
||||
inst.StorageMu.RLock()
|
||||
certCache, ok := inst.Storage[CertCacheInstStorageKey].(*certificateCache)
|
||||
inst.StorageMu.RUnlock()
|
||||
if !ok || certCache == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// we use the queues for a very important reason: to do any and all
|
||||
// operations that could require an exclusive write lock outside
|
||||
// of the read lock! otherwise we get a deadlock, yikes. in other
|
||||
// words, our first iteration through the certificate cache does NOT
|
||||
// perform any operations--only queues them--so that more fine-grained
|
||||
// write locks may be obtained during the actual operations.
|
||||
var renewQueue, reloadQueue, deleteQueue []Certificate
|
||||
|
||||
certCache.RLock()
|
||||
for certKey, cert := range certCache.cache {
|
||||
if len(cert.configs) == 0 {
|
||||
// this is bad if this happens, probably a programmer error (oops)
|
||||
log.Printf("[ERROR] No associated TLS config for certificate with names %v; unable to manage", cert.Names)
|
||||
continue
|
||||
}
|
||||
if !cert.configs[0].Managed || cert.configs[0].SelfSigned {
|
||||
continue
|
||||
}
|
||||
|
||||
// the list of names on this cert should never be empty... programmer error?
|
||||
if cert.Names == nil || len(cert.Names) == 0 {
|
||||
log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v - removing from cache", certKey, cert.Names)
|
||||
deleteQueue = append(deleteQueue, cert)
|
||||
continue
|
||||
}
|
||||
|
||||
// if time is up or expires soon, we need to try to renew it
|
||||
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
|
||||
if timeLeft < RenewDurationBefore {
|
||||
// see if the certificate in storage has already been renewed, possibly by another
|
||||
// instance of Caddy that didn't coordinate with this one; if so, just load it (this
|
||||
// might happen if another instance already renewed it - kinda sloppy but checking disk
|
||||
// first is a simple way to possibly drastically reduce rate limit problems)
|
||||
storedCertExpiring, err := managedCertInStorageExpiresSoon(cert)
|
||||
if err != nil {
|
||||
// hmm, weird, but not a big deal, maybe it was deleted or something
|
||||
log.Printf("[NOTICE] Error while checking if certificate for %v in storage is also expiring soon: %v",
|
||||
cert.Names, err)
|
||||
} else if !storedCertExpiring {
|
||||
// if the certificate is NOT expiring soon and there was no error, then we
|
||||
// are good to just reload the certificate from storage instead of repeating
|
||||
// a likely-unnecessary renewal procedure
|
||||
reloadQueue = append(reloadQueue, cert)
|
||||
continue
|
||||
}
|
||||
|
||||
// the certificate in storage has not been renewed yet, so we will do it
|
||||
// NOTE 1: This is not correct 100% of the time, if multiple Caddy instances
|
||||
// happen to run their maintenance checks at approximately the same times;
|
||||
// both might start renewal at about the same time and do two renewals and one
|
||||
// will overwrite the other. Hence TLS storage plugins. This is sort of a TODO.
|
||||
// NOTE 2: It is super-important to note that the TLS-SNI challenge requires
|
||||
// a write lock on the cache in order to complete its challenge, so it is extra
|
||||
// vital that this renew operation does not happen inside our read lock!
|
||||
renewQueue = append(renewQueue, cert)
|
||||
}
|
||||
}
|
||||
certCache.RUnlock()
|
||||
|
||||
// Reload certificates that merely need to be updated in memory
|
||||
for _, oldCert := range reloadQueue {
|
||||
timeLeft := oldCert.NotAfter.Sub(time.Now().UTC())
|
||||
log.Printf("[INFO] Certificate for %v expires in %v, but is already renewed in storage; reloading stored certificate",
|
||||
oldCert.Names, timeLeft)
|
||||
|
||||
err = certCache.reloadManagedCertificate(oldCert)
|
||||
if err != nil {
|
||||
if allowPrompts {
|
||||
return err // operator is present, so report error immediately
|
||||
}
|
||||
log.Printf("[ERROR] Loading renewed certificate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Renewal queue
|
||||
for _, oldCert := range renewQueue {
|
||||
timeLeft := oldCert.NotAfter.Sub(time.Now().UTC())
|
||||
log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", oldCert.Names, timeLeft)
|
||||
|
||||
// Get the name which we should use to renew this certificate;
|
||||
// we only support managing certificates with one name per cert,
|
||||
// so this should be easy. We can't rely on cert.Config.Hostname
|
||||
// because it may be a wildcard value from the Caddyfile (e.g.
|
||||
// *.something.com) which, as of Jan. 2017, is not supported by ACME.
|
||||
// TODO: ^ ^ ^ (wildcards)
|
||||
renewName := oldCert.Names[0]
|
||||
|
||||
// perform renewal
|
||||
err := oldCert.configs[0].RenewCert(renewName, allowPrompts)
|
||||
if err != nil {
|
||||
if allowPrompts {
|
||||
// Certificate renewal failed and the operator is present. See a discussion
|
||||
// about this in issue 642. For a while, we only stopped if the certificate
|
||||
// was expired, but in reality, there is no difference between reporting
|
||||
// it now versus later, except that there's somebody present to deal with
|
||||
// it right now. Follow-up: See issue 1680. Only fail in this case if the
|
||||
// certificate is dangerously close to expiration.
|
||||
timeLeft := oldCert.NotAfter.Sub(time.Now().UTC())
|
||||
if timeLeft < RenewDurationBeforeAtStartup {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Printf("[ERROR] %v", err)
|
||||
if oldCert.configs[0].OnDemand {
|
||||
// loaded dynamically, remove dynamically
|
||||
deleteQueue = append(deleteQueue, oldCert)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// successful renewal, so update in-memory cache by loading
|
||||
// renewed certificate so it will be used with handshakes
|
||||
err = certCache.reloadManagedCertificate(oldCert)
|
||||
if err != nil {
|
||||
if allowPrompts {
|
||||
return err // operator is present, so report error immediately
|
||||
}
|
||||
log.Printf("[ERROR] %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Deletion queue
|
||||
for _, cert := range deleteQueue {
|
||||
certCache.Lock()
|
||||
// remove any pointers to this certificate from Configs
|
||||
for _, cfg := range cert.configs {
|
||||
for name, certKey := range cfg.Certificates {
|
||||
if certKey == cert.Hash {
|
||||
delete(cfg.Certificates, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
// then delete the certificate from the cache
|
||||
delete(certCache.cache, cert.Hash)
|
||||
certCache.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateOCSPStaples updates the OCSP stapling in all
|
||||
// eligible, cached certificates.
|
||||
//
|
||||
// OCSP maintenance strives to abide the relevant points on
|
||||
// Ryan Sleevi's recommendations for good OCSP support:
|
||||
// https://gist.github.com/sleevi/5efe9ef98961ecfb4da8
|
||||
func UpdateOCSPStaples() {
|
||||
for _, inst := range caddy.Instances() {
|
||||
inst.StorageMu.RLock()
|
||||
certCache, ok := inst.Storage[CertCacheInstStorageKey].(*certificateCache)
|
||||
inst.StorageMu.RUnlock()
|
||||
if !ok || certCache == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a temporary place to store updates
|
||||
// until we release the potentially long-lived
|
||||
// read lock and use a short-lived write lock
|
||||
// on the certificate cache.
|
||||
type ocspUpdate struct {
|
||||
rawBytes []byte
|
||||
parsed *ocsp.Response
|
||||
}
|
||||
updated := make(map[string]ocspUpdate)
|
||||
|
||||
certCache.RLock()
|
||||
for certHash, cert := range certCache.cache {
|
||||
// no point in updating OCSP for expired certificates
|
||||
if time.Now().After(cert.NotAfter) {
|
||||
continue
|
||||
}
|
||||
|
||||
var lastNextUpdate time.Time
|
||||
if cert.OCSP != nil {
|
||||
lastNextUpdate = cert.OCSP.NextUpdate
|
||||
if freshOCSP(cert.OCSP) {
|
||||
continue // no need to update staple if ours is still fresh
|
||||
}
|
||||
}
|
||||
|
||||
err := stapleOCSP(&cert, nil)
|
||||
if err != nil {
|
||||
if cert.OCSP != nil {
|
||||
// if there was no staple before, that's fine; otherwise we should log the error
|
||||
log.Printf("[ERROR] Checking OCSP: %v", 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 cert.OCSP != nil && (lastNextUpdate.IsZero() || lastNextUpdate != cert.OCSP.NextUpdate) {
|
||||
log.Printf("[INFO] Advancing OCSP staple for %v from %s to %s",
|
||||
cert.Names, lastNextUpdate, cert.OCSP.NextUpdate)
|
||||
updated[certHash] = ocspUpdate{rawBytes: cert.Certificate.OCSPStaple, parsed: cert.OCSP}
|
||||
}
|
||||
}
|
||||
certCache.RUnlock()
|
||||
|
||||
// These write locks should be brief since we have all the info we need now.
|
||||
for certKey, update := range updated {
|
||||
certCache.Lock()
|
||||
cert := certCache.cache[certKey]
|
||||
cert.OCSP = update.parsed
|
||||
cert.Certificate.OCSPStaple = update.rawBytes
|
||||
certCache.cache[certKey] = cert
|
||||
certCache.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteOldStapleFiles deletes cached OCSP staples that have expired.
|
||||
// TODO: Should we do this for certificates too?
|
||||
func DeleteOldStapleFiles() {
|
||||
// TODO: Upgrade caddytls.Storage to support OCSP operations too
|
||||
files, err := ioutil.ReadDir(ocspFolder)
|
||||
if err != nil {
|
||||
// maybe just hasn't been created yet; no big deal
|
||||
return
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// weird, what's a folder doing inside the OCSP cache?
|
||||
continue
|
||||
}
|
||||
stapleFile := filepath.Join(ocspFolder, file.Name())
|
||||
ocspBytes, err := ioutil.ReadFile(stapleFile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp, err := ocsp.ParseResponse(ocspBytes, nil)
|
||||
if err != nil {
|
||||
// contents are invalid; delete it
|
||||
err = os.Remove(stapleFile)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Purging corrupt staple file %s: %v", stapleFile, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if time.Now().After(resp.NextUpdate) {
|
||||
// response has expired; delete it
|
||||
err = os.Remove(stapleFile)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Purging expired staple file %s: %v", stapleFile, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// freshOCSP returns true if resp is still fresh,
|
||||
// meaning that it is not expedient to get an
|
||||
// updated response from the OCSP server.
|
||||
func freshOCSP(resp *ocsp.Response) bool {
|
||||
nextUpdate := resp.NextUpdate
|
||||
// If there is an OCSP responder certificate, and it expires before the
|
||||
// OCSP response, use its expiration date as the end of the OCSP
|
||||
// response's validity period.
|
||||
if resp.Certificate != nil && resp.Certificate.NotAfter.Before(nextUpdate) {
|
||||
nextUpdate = resp.Certificate.NotAfter
|
||||
}
|
||||
// start checking OCSP staple about halfway through validity period for good measure
|
||||
refreshTime := resp.ThisUpdate.Add(nextUpdate.Sub(resp.ThisUpdate) / 2)
|
||||
return time.Now().Before(refreshTime)
|
||||
}
|
||||
|
||||
var ocspFolder = filepath.Join(caddy.AssetsPath(), "ocsp")
|
373
vendor/github.com/mholt/caddy/caddytls/setup.go
generated
vendored
Normal file
373
vendor/github.com/mholt/caddy/caddytls/setup.go
generated
vendored
Normal file
@@ -0,0 +1,373 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/telemetry"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("tls", caddy.Plugin{Action: setupTLS})
|
||||
}
|
||||
|
||||
// setupTLS 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 setupTLS(c *caddy.Controller) error {
|
||||
// obtain the configGetter, which loads the config we're, uh, configuring
|
||||
configGetter, ok := configGetters[c.ServerType()]
|
||||
if !ok {
|
||||
return fmt.Errorf("no caddytls.ConfigGetter for %s server type; must call RegisterConfigGetter", c.ServerType())
|
||||
}
|
||||
config := configGetter(c)
|
||||
if config == nil {
|
||||
return fmt.Errorf("no caddytls.Config to set up for %s", c.Key)
|
||||
}
|
||||
|
||||
// the certificate cache is tied to the current caddy.Instance; get a pointer to it
|
||||
certCache, ok := c.Get(CertCacheInstStorageKey).(*certificateCache)
|
||||
if !ok || certCache == nil {
|
||||
certCache = &certificateCache{cache: make(map[string]Certificate)}
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
}
|
||||
config.certCache = certCache
|
||||
|
||||
config.Enabled = true
|
||||
|
||||
for c.Next() {
|
||||
var certificateFile, keyFile, loadDir, maxCerts, askURL string
|
||||
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 1:
|
||||
// even if the email is one of the special values below,
|
||||
// it is still necessary for future analysis that we store
|
||||
// that value in the ACMEEmail field.
|
||||
config.ACMEEmail = args[0]
|
||||
|
||||
// user can force-disable managed TLS this way
|
||||
if args[0] == "off" {
|
||||
config.Enabled = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// user might want a temporary, in-memory, self-signed cert
|
||||
if args[0] == "self_signed" {
|
||||
config.SelfSigned = true
|
||||
}
|
||||
case 2:
|
||||
certificateFile = args[0]
|
||||
keyFile = args[1]
|
||||
config.Manual = true
|
||||
}
|
||||
|
||||
// Optional block with extra parameters
|
||||
var hadBlock bool
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
switch c.Val() {
|
||||
case "ca":
|
||||
arg := c.RemainingArgs()
|
||||
if len(arg) != 1 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
config.CAUrl = arg[0]
|
||||
case "key_type":
|
||||
arg := c.RemainingArgs()
|
||||
value, ok := supportedKeyTypes[strings.ToUpper(arg[0])]
|
||||
if !ok {
|
||||
return c.Errf("Wrong key type name or key type not supported: '%s'", c.Val())
|
||||
}
|
||||
config.KeyType = value
|
||||
case "protocols":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 1 {
|
||||
value, ok := SupportedProtocols[strings.ToLower(args[0])]
|
||||
if !ok {
|
||||
return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
|
||||
}
|
||||
|
||||
config.ProtocolMinVersion, config.ProtocolMaxVersion = value, value
|
||||
} else {
|
||||
value, ok := SupportedProtocols[strings.ToLower(args[0])]
|
||||
if !ok {
|
||||
return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
|
||||
}
|
||||
config.ProtocolMinVersion = value
|
||||
value, ok = SupportedProtocols[strings.ToLower(args[1])]
|
||||
if !ok {
|
||||
return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
|
||||
}
|
||||
config.ProtocolMaxVersion = value
|
||||
if config.ProtocolMinVersion > config.ProtocolMaxVersion {
|
||||
return c.Errf("Minimum protocol version cannot be higher than maximum (reverse the order)")
|
||||
}
|
||||
}
|
||||
case "ciphers":
|
||||
for c.NextArg() {
|
||||
value, ok := SupportedCiphersMap[strings.ToUpper(c.Val())]
|
||||
if !ok {
|
||||
return c.Errf("Wrong cipher name or cipher not supported: '%s'", c.Val())
|
||||
}
|
||||
config.Ciphers = append(config.Ciphers, value)
|
||||
}
|
||||
case "curves":
|
||||
for c.NextArg() {
|
||||
value, ok := supportedCurvesMap[strings.ToUpper(c.Val())]
|
||||
if !ok {
|
||||
return c.Errf("Wrong curve name or curve not supported: '%s'", c.Val())
|
||||
}
|
||||
config.CurvePreferences = append(config.CurvePreferences, value)
|
||||
}
|
||||
case "clients":
|
||||
clientCertList := c.RemainingArgs()
|
||||
if len(clientCertList) == 0 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
|
||||
listStart, mustProvideCA := 1, true
|
||||
switch clientCertList[0] {
|
||||
case "request":
|
||||
config.ClientAuth = tls.RequestClientCert
|
||||
mustProvideCA = false
|
||||
case "require":
|
||||
config.ClientAuth = tls.RequireAnyClientCert
|
||||
mustProvideCA = false
|
||||
case "verify_if_given":
|
||||
config.ClientAuth = tls.VerifyClientCertIfGiven
|
||||
default:
|
||||
config.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
listStart = 0
|
||||
}
|
||||
if mustProvideCA && len(clientCertList) <= listStart {
|
||||
return c.ArgErr()
|
||||
}
|
||||
|
||||
config.ClientCerts = clientCertList[listStart:]
|
||||
case "load":
|
||||
c.Args(&loadDir)
|
||||
config.Manual = true
|
||||
case "max_certs":
|
||||
c.Args(&maxCerts)
|
||||
config.OnDemand = true
|
||||
telemetry.Increment("tls_on_demand_count")
|
||||
case "ask":
|
||||
c.Args(&askURL)
|
||||
config.OnDemand = true
|
||||
telemetry.Increment("tls_on_demand_count")
|
||||
case "dns":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
dnsProvName := args[0]
|
||||
if _, ok := dnsProviders[dnsProvName]; !ok {
|
||||
return c.Errf("Unsupported DNS provider '%s'", args[0])
|
||||
}
|
||||
config.DNSProvider = args[0]
|
||||
case "storage":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
storageProvName := args[0]
|
||||
if _, ok := storageProviders[storageProvName]; !ok {
|
||||
return c.Errf("Unsupported Storage provider '%s'", args[0])
|
||||
}
|
||||
config.StorageProvider = args[0]
|
||||
case "alpn":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
for _, arg := range args {
|
||||
config.ALPN = append(config.ALPN, arg)
|
||||
}
|
||||
case "must_staple":
|
||||
config.MustStaple = true
|
||||
case "wildcard":
|
||||
if !HostQualifies(config.Hostname) {
|
||||
return c.Errf("Hostname '%s' does not qualify for managed TLS, so cannot manage wildcard certificate for it", config.Hostname)
|
||||
}
|
||||
if strings.Contains(config.Hostname, "*") {
|
||||
return c.Errf("Cannot convert domain name '%s' to a valid wildcard: already has a wildcard label", config.Hostname)
|
||||
}
|
||||
parts := strings.Split(config.Hostname, ".")
|
||||
if len(parts) < 3 {
|
||||
return c.Errf("Cannot convert domain name '%s' to a valid wildcard: too few labels", config.Hostname)
|
||||
}
|
||||
parts[0] = "*"
|
||||
config.Hostname = strings.Join(parts, ".")
|
||||
default:
|
||||
return c.Errf("Unknown subdirective '%s'", c.Val())
|
||||
}
|
||||
}
|
||||
|
||||
// tls requires at least one argument if a block is not opened
|
||||
if len(args) == 0 && !hadBlock {
|
||||
return c.ArgErr()
|
||||
}
|
||||
|
||||
// set certificate limit if on-demand TLS is enabled
|
||||
if maxCerts != "" {
|
||||
maxCertsNum, err := strconv.Atoi(maxCerts)
|
||||
if err != nil || maxCertsNum < 1 {
|
||||
return c.Err("max_certs must be a positive integer")
|
||||
}
|
||||
config.OnDemandState.MaxObtain = int32(maxCertsNum)
|
||||
}
|
||||
|
||||
if askURL != "" {
|
||||
parsedURL, err := url.Parse(askURL)
|
||||
if err != nil {
|
||||
return c.Err("ask must be a valid url")
|
||||
}
|
||||
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return c.Err("ask URL must use http or https")
|
||||
}
|
||||
|
||||
config.OnDemandState.AskURL = parsedURL
|
||||
}
|
||||
|
||||
// don't try to load certificates unless we're supposed to
|
||||
if !config.Enabled || !config.Manual {
|
||||
continue
|
||||
}
|
||||
|
||||
// load a single certificate and key, if specified
|
||||
if certificateFile != "" && keyFile != "" {
|
||||
err := config.cacheUnmanagedCertificatePEMFile(certificateFile, keyFile)
|
||||
if err != nil {
|
||||
return c.Errf("Unable to load certificate and key files for '%s': %v", c.Key, 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(config, c, loadDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetDefaultTLSParams(config)
|
||||
|
||||
// generate self-signed cert if needed
|
||||
if config.SelfSigned {
|
||||
err := makeSelfSignedCert(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("self-signed: %v", err)
|
||||
}
|
||||
telemetry.Increment("tls_self_signed_count")
|
||||
}
|
||||
|
||||
return 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(cfg *Config, c *caddy.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 = cfg.cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes)
|
||||
if err != nil {
|
||||
return c.Errf("%s: failed to load cert and key for '%s': %v", path, c.Key, err)
|
||||
}
|
||||
log.Printf("[INFO] Successfully loaded TLS assets from %s", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
451
vendor/github.com/mholt/caddy/caddytls/setup_test.go
generated
vendored
Normal file
451
vendor/github.com/mholt/caddy/caddytls/setup_test.go
generated
vendored
Normal file
@@ -0,0 +1,451 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/xenolf/lego/acmev2"
|
||||
)
|
||||
|
||||
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) {
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c := caddy.NewTestController("", `tls `+certFile+` `+keyFile+``)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
|
||||
err := setupTLS(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
// Basic checks
|
||||
if !cfg.Manual {
|
||||
t.Error("Expected TLS Manual=true, but was false")
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
t.Error("Expected TLS Enabled=true, but was false")
|
||||
}
|
||||
|
||||
// Security defaults
|
||||
if cfg.ProtocolMinVersion != tls.VersionTLS12 {
|
||||
t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMinVersion, got %#v", cfg.ProtocolMinVersion)
|
||||
}
|
||||
if cfg.ProtocolMaxVersion != tls.VersionTLS12 {
|
||||
t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %v", cfg.ProtocolMaxVersion)
|
||||
}
|
||||
|
||||
// Cipher checks
|
||||
expectedCiphers := append([]uint16{tls.TLS_FALLBACK_SCSV}, getPreferredDefaultCiphers()...)
|
||||
|
||||
// Ensure count is correct (plus one for TLS_FALLBACK_SCSV)
|
||||
if len(cfg.Ciphers) != len(expectedCiphers) {
|
||||
t.Errorf("Expected %v Ciphers (including TLS_FALLBACK_SCSV), got %v",
|
||||
len(expectedCiphers), len(cfg.Ciphers))
|
||||
}
|
||||
|
||||
// Ensure ordering is correct
|
||||
for i, actual := range cfg.Ciphers {
|
||||
if actual != expectedCiphers[i] {
|
||||
t.Errorf("Expected cipher in position %d to be %0x, got %0x", i, expectedCiphers[i], actual)
|
||||
}
|
||||
}
|
||||
|
||||
if !cfg.PreferServerCipherSuites {
|
||||
t.Error("Expected PreferServerCipherSuites = true, but was false")
|
||||
}
|
||||
|
||||
if len(cfg.ALPN) != 0 {
|
||||
t.Error("Expected ALPN empty by default")
|
||||
}
|
||||
|
||||
// Ensure curve count is correct
|
||||
if len(cfg.CurvePreferences) != len(defaultCurves) {
|
||||
t.Errorf("Expected %v Curves, got %v", len(defaultCurves), len(cfg.CurvePreferences))
|
||||
}
|
||||
|
||||
// Ensure curve ordering is correct
|
||||
for i, actual := range cfg.CurvePreferences {
|
||||
if actual != defaultCurves[i] {
|
||||
t.Errorf("Expected curve in position %d to be %0x, got %0x", i, defaultCurves[i], actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseIncompleteParams(t *testing.T) {
|
||||
// Using tls without args is an error because it's unnecessary.
|
||||
c := caddy.NewTestController("", `tls`)
|
||||
err := setupTLS(c)
|
||||
if err == nil {
|
||||
t.Error("Expected an error, but didn't get one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseWithOptionalParams(t *testing.T) {
|
||||
params := `tls ` + certFile + ` ` + keyFile + ` {
|
||||
protocols tls1.0 tls1.2
|
||||
ciphers RSA-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384
|
||||
must_staple
|
||||
alpn http/1.1
|
||||
}`
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c := caddy.NewTestController("", params)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
|
||||
err := setupTLS(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if cfg.ProtocolMinVersion != tls.VersionTLS10 {
|
||||
t.Errorf("Expected 'tls1.0 (0x0301)' as ProtocolMinVersion, got %#v", cfg.ProtocolMinVersion)
|
||||
}
|
||||
|
||||
if cfg.ProtocolMaxVersion != tls.VersionTLS12 {
|
||||
t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %#v", cfg.ProtocolMaxVersion)
|
||||
}
|
||||
|
||||
if len(cfg.Ciphers)-1 != 3 {
|
||||
t.Errorf("Expected 3 Ciphers (not including TLS_FALLBACK_SCSV), got %v", len(cfg.Ciphers)-1)
|
||||
}
|
||||
|
||||
if !cfg.MustStaple {
|
||||
t.Error("Expected must staple to be true")
|
||||
}
|
||||
|
||||
if len(cfg.ALPN) != 1 || cfg.ALPN[0] != "http/1.1" {
|
||||
t.Errorf("Expected ALPN to contain only 'http/1.1' but got: %v", cfg.ALPN)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupDefaultWithOptionalParams(t *testing.T) {
|
||||
params := `tls {
|
||||
ciphers RSA-3DES-EDE-CBC-SHA
|
||||
}`
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c := caddy.NewTestController("", params)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
|
||||
err := setupTLS(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
if len(cfg.Ciphers)-1 != 1 {
|
||||
t.Errorf("Expected 1 ciphers (not including TLS_FALLBACK_SCSV), got %v", len(cfg.Ciphers)-1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseWithWrongOptionalParams(t *testing.T) {
|
||||
// Test protocols wrong params
|
||||
params := `tls ` + certFile + ` ` + keyFile + ` {
|
||||
protocols ssl tls
|
||||
}`
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c := caddy.NewTestController("", params)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
|
||||
err := setupTLS(c)
|
||||
if err == nil {
|
||||
t.Errorf("Expected errors, but no error returned")
|
||||
}
|
||||
|
||||
// Test ciphers wrong params
|
||||
params = `tls ` + certFile + ` ` + keyFile + ` {
|
||||
ciphers not-valid-cipher
|
||||
}`
|
||||
cfg = new(Config)
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c = caddy.NewTestController("", params)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
err = setupTLS(c)
|
||||
if err == nil {
|
||||
t.Error("Expected errors, but no error returned")
|
||||
}
|
||||
|
||||
// Test key_type wrong params
|
||||
params = `tls {
|
||||
key_type ab123
|
||||
}`
|
||||
cfg = new(Config)
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c = caddy.NewTestController("", params)
|
||||
err = setupTLS(c)
|
||||
if err == nil {
|
||||
t.Error("Expected errors, but no error returned")
|
||||
}
|
||||
|
||||
// Test curves wrong params
|
||||
params = `tls {
|
||||
curves ab123, cd456, ef789
|
||||
}`
|
||||
cfg = new(Config)
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c = caddy.NewTestController("", params)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
err = setupTLS(c)
|
||||
if err == nil {
|
||||
t.Error("Expected errors, but no error returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseWithClientAuth(t *testing.T) {
|
||||
// Test missing client cert file
|
||||
params := `tls ` + certFile + ` ` + keyFile + ` {
|
||||
clients
|
||||
}`
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c := caddy.NewTestController("", params)
|
||||
err := setupTLS(c)
|
||||
if err == nil {
|
||||
t.Error("Expected an error, but no error returned")
|
||||
}
|
||||
|
||||
noCAs, twoCAs := []string{}, []string{"client_ca.crt", "client2_ca.crt"}
|
||||
for caseNumber, caseData := range []struct {
|
||||
params string
|
||||
clientAuthType tls.ClientAuthType
|
||||
expectedErr bool
|
||||
expectedCAs []string
|
||||
}{
|
||||
{"", tls.NoClientCert, false, noCAs},
|
||||
{`tls ` + certFile + ` ` + keyFile + ` {
|
||||
clients client_ca.crt client2_ca.crt
|
||||
}`, tls.RequireAndVerifyClientCert, false, twoCAs},
|
||||
// now come modifier
|
||||
{`tls ` + certFile + ` ` + keyFile + ` {
|
||||
clients request
|
||||
}`, tls.RequestClientCert, false, noCAs},
|
||||
{`tls ` + certFile + ` ` + keyFile + ` {
|
||||
clients require
|
||||
}`, tls.RequireAnyClientCert, false, noCAs},
|
||||
{`tls ` + certFile + ` ` + keyFile + ` {
|
||||
clients verify_if_given client_ca.crt client2_ca.crt
|
||||
}`, tls.VerifyClientCertIfGiven, false, twoCAs},
|
||||
{`tls ` + certFile + ` ` + keyFile + ` {
|
||||
clients verify_if_given
|
||||
}`, tls.VerifyClientCertIfGiven, true, noCAs},
|
||||
} {
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c := caddy.NewTestController("", caseData.params)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
err := setupTLS(c)
|
||||
if caseData.expectedErr {
|
||||
if err == nil {
|
||||
t.Errorf("In case %d: Expected an error, got: %v", caseNumber, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("In case %d: Expected no errors, got: %v", caseNumber, err)
|
||||
}
|
||||
|
||||
if caseData.clientAuthType != cfg.ClientAuth {
|
||||
t.Errorf("In case %d: Expected TLS client auth type %v, got: %v",
|
||||
caseNumber, caseData.clientAuthType, cfg.ClientAuth)
|
||||
}
|
||||
|
||||
if count := len(cfg.ClientCerts); count < len(caseData.expectedCAs) {
|
||||
t.Fatalf("In case %d: Expected %d client certs, had %d", caseNumber, len(caseData.expectedCAs), count)
|
||||
}
|
||||
|
||||
for idx, expected := range caseData.expectedCAs {
|
||||
if actual := cfg.ClientCerts[idx]; actual != expected {
|
||||
t.Errorf("In case %d: Expected %dth client cert file to be '%s', but was '%s'",
|
||||
caseNumber, idx, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseWithCAUrl(t *testing.T) {
|
||||
testURL := "https://acme-staging.api.letsencrypt.org/directory"
|
||||
for caseNumber, caseData := range []struct {
|
||||
params string
|
||||
expectedErr bool
|
||||
expectedCAUrl string
|
||||
}{
|
||||
// Test working case
|
||||
{`tls {
|
||||
ca ` + testURL + `
|
||||
}`, false, testURL},
|
||||
// Test too few args
|
||||
{`tls {
|
||||
ca
|
||||
}`, true, ""},
|
||||
// Test too many args
|
||||
{`tls {
|
||||
ca 1 2
|
||||
}`, true, ""},
|
||||
} {
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c := caddy.NewTestController("", caseData.params)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
err := setupTLS(c)
|
||||
if caseData.expectedErr {
|
||||
if err == nil {
|
||||
t.Errorf("In case %d: Expected an error, got: %v", caseNumber, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("In case %d: Expected no errors, got: %v", caseNumber, err)
|
||||
}
|
||||
|
||||
if cfg.CAUrl != caseData.expectedCAUrl {
|
||||
t.Errorf("Expected '%v' as CAUrl, got %#v", caseData.expectedCAUrl, cfg.CAUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseWithKeyType(t *testing.T) {
|
||||
params := `tls {
|
||||
key_type p384
|
||||
}`
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c := caddy.NewTestController("", params)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
|
||||
err := setupTLS(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if cfg.KeyType != acme.EC384 {
|
||||
t.Errorf("Expected 'P384' as KeyType, got %#v", cfg.KeyType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseWithCurves(t *testing.T) {
|
||||
params := `tls {
|
||||
curves x25519 p256 p384 p521
|
||||
}`
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c := caddy.NewTestController("", params)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
|
||||
err := setupTLS(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.CurvePreferences) != 4 {
|
||||
t.Errorf("Expected 4 curves, got %v", len(cfg.CurvePreferences))
|
||||
}
|
||||
|
||||
expectedCurves := []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384, tls.CurveP521}
|
||||
|
||||
// Ensure ordering is correct
|
||||
for i, actual := range cfg.CurvePreferences {
|
||||
if actual != expectedCurves[i] {
|
||||
t.Errorf("Expected curve in position %d to be %v, got %v", i, expectedCurves[i], actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseWithOneTLSProtocol(t *testing.T) {
|
||||
params := `tls {
|
||||
protocols tls1.2
|
||||
}`
|
||||
certCache := &certificateCache{cache: make(map[string]Certificate)}
|
||||
cfg := &Config{Certificates: make(map[string]string), certCache: certCache}
|
||||
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
|
||||
c := caddy.NewTestController("", params)
|
||||
c.Set(CertCacheInstStorageKey, certCache)
|
||||
|
||||
err := setupTLS(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if cfg.ProtocolMinVersion != cfg.ProtocolMaxVersion {
|
||||
t.Errorf("Expected ProtocolMinVersion to be the same as ProtocolMaxVersion")
|
||||
}
|
||||
|
||||
if cfg.ProtocolMinVersion != tls.VersionTLS12 && cfg.ProtocolMaxVersion != tls.VersionTLS12 {
|
||||
t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMinVersion/ProtocolMaxVersion, got %v/%v", cfg.ProtocolMinVersion, cfg.ProtocolMaxVersion)
|
||||
}
|
||||
}
|
||||
|
||||
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-----
|
||||
`)
|
127
vendor/github.com/mholt/caddy/caddytls/storage.go
generated
vendored
Normal file
127
vendor/github.com/mholt/caddy/caddytls/storage.go
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import "net/url"
|
||||
|
||||
// StorageConstructor is a function type that is used in the Config to
|
||||
// instantiate a new Storage instance. This function can return a nil
|
||||
// Storage even without an error.
|
||||
type StorageConstructor func(caURL *url.URL) (Storage, error)
|
||||
|
||||
// SiteData contains persisted items pertaining to an individual site.
|
||||
type SiteData struct {
|
||||
// Cert is the public cert byte array.
|
||||
Cert []byte
|
||||
// Key is the private key byte array.
|
||||
Key []byte
|
||||
// Meta is metadata about the site used by Caddy.
|
||||
Meta []byte
|
||||
}
|
||||
|
||||
// UserData contains persisted items pertaining to a user.
|
||||
type UserData struct {
|
||||
// Reg is the user registration byte array.
|
||||
Reg []byte
|
||||
// Key is the user key byte array.
|
||||
Key []byte
|
||||
}
|
||||
|
||||
// Locker provides support for mutual exclusion
|
||||
type Locker interface {
|
||||
// TryLock will return immediatedly with or without acquiring the lock.
|
||||
// If a lock could be obtained, (nil, nil) is returned and you may
|
||||
// continue normally. If not (meaning another process is already
|
||||
// working on that name), a Waiter value will be returned upon
|
||||
// which you can Wait() until it is finished, and then return
|
||||
// when it unblocks. If waiting, do not unlock!
|
||||
//
|
||||
// To prevent deadlocks, all implementations (where this concern
|
||||
// is relevant) should put a reasonable expiration on the lock in
|
||||
// case Unlock is unable to be called due to some sort of storage
|
||||
// system failure or crash.
|
||||
TryLock(name string) (Waiter, error)
|
||||
|
||||
// Unlock unlocks the mutex for name. Only callers of TryLock who
|
||||
// successfully obtained the lock (no Waiter value was returned)
|
||||
// should call this method, and it should be called only after
|
||||
// the obtain/renew and store are finished, even if there was
|
||||
// an error (or a timeout). Unlock should also clean up any
|
||||
// unused resources allocated during TryLock.
|
||||
Unlock(name string) error
|
||||
}
|
||||
|
||||
// Storage is an interface abstracting all storage used by Caddy's TLS
|
||||
// subsystem. Implementations of this interface store both site and
|
||||
// user data.
|
||||
type Storage interface {
|
||||
// SiteExists returns true if this site exists in storage.
|
||||
// Site data is considered present when StoreSite has been called
|
||||
// successfully (without DeleteSite having been called, of course).
|
||||
SiteExists(domain string) (bool, error)
|
||||
|
||||
// LoadSite obtains the site data from storage for the given domain and
|
||||
// returns it. If data for the domain does not exist, an error value
|
||||
// of type ErrNotExist is returned. For multi-server storage, care
|
||||
// should be taken to make this load atomic to prevent race conditions
|
||||
// that happen with multiple data loads.
|
||||
LoadSite(domain string) (*SiteData, error)
|
||||
|
||||
// StoreSite persists the given site data for the given domain in
|
||||
// storage. For multi-server storage, care should be taken to make this
|
||||
// call atomic to prevent half-written data on failure of an internal
|
||||
// intermediate storage step. Implementers can trust that at runtime
|
||||
// this function will only be invoked after LockRegister and before
|
||||
// UnlockRegister of the same domain.
|
||||
StoreSite(domain string, data *SiteData) error
|
||||
|
||||
// DeleteSite deletes the site for the given domain from storage.
|
||||
// Multi-server implementations should attempt to make this atomic. If
|
||||
// the site does not exist, an error value of type ErrNotExist is returned.
|
||||
DeleteSite(domain string) error
|
||||
|
||||
// LoadUser obtains user data from storage for the given email and
|
||||
// returns it. If data for the email does not exist, an error value
|
||||
// of type ErrNotExist is returned. Multi-server implementations
|
||||
// should take care to make this operation atomic for all loaded
|
||||
// data items.
|
||||
LoadUser(email string) (*UserData, error)
|
||||
|
||||
// StoreUser persists the given user data for the given email in
|
||||
// storage. Multi-server implementations should take care to make this
|
||||
// operation atomic for all stored data items.
|
||||
StoreUser(email string, data *UserData) error
|
||||
|
||||
// MostRecentUserEmail provides the most recently used email parameter
|
||||
// in StoreUser. The result is an empty string if there are no
|
||||
// persisted users in storage.
|
||||
MostRecentUserEmail() string
|
||||
|
||||
// Locker is necessary because synchronizing certificate maintenance
|
||||
// depends on how storage is implemented.
|
||||
Locker
|
||||
}
|
||||
|
||||
// ErrNotExist is returned by Storage implementations when
|
||||
// a resource is not found. It is similar to os.ErrNotExist
|
||||
// except this is a type, not a variable.
|
||||
type ErrNotExist interface {
|
||||
error
|
||||
}
|
||||
|
||||
// Waiter is a type that can block until a storage lock is released.
|
||||
type Waiter interface {
|
||||
Wait()
|
||||
}
|
148
vendor/github.com/mholt/caddy/caddytls/storagetest/memorystorage.go
generated
vendored
Normal file
148
vendor/github.com/mholt/caddy/caddytls/storagetest/memorystorage.go
generated
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package storagetest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
// memoryMutex is a mutex used to control access to memoryStoragesByCAURL.
|
||||
var memoryMutex sync.Mutex
|
||||
|
||||
// memoryStoragesByCAURL is a map keyed by a CA URL string with values of
|
||||
// instantiated memory stores. Do not access this directly, it is used by
|
||||
// InMemoryStorageCreator.
|
||||
var memoryStoragesByCAURL = make(map[string]*InMemoryStorage)
|
||||
|
||||
// InMemoryStorageCreator is a caddytls.Storage.StorageCreator to create
|
||||
// InMemoryStorage instances for testing.
|
||||
func InMemoryStorageCreator(caURL *url.URL) (caddytls.Storage, error) {
|
||||
urlStr := caURL.String()
|
||||
memoryMutex.Lock()
|
||||
defer memoryMutex.Unlock()
|
||||
storage := memoryStoragesByCAURL[urlStr]
|
||||
if storage == nil {
|
||||
storage = NewInMemoryStorage()
|
||||
memoryStoragesByCAURL[urlStr] = storage
|
||||
}
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// InMemoryStorage is a caddytls.Storage implementation for use in testing.
|
||||
// It simply stores information in runtime memory.
|
||||
type InMemoryStorage struct {
|
||||
// Sites are exposed for testing purposes.
|
||||
Sites map[string]*caddytls.SiteData
|
||||
// Users are exposed for testing purposes.
|
||||
Users map[string]*caddytls.UserData
|
||||
// LastUserEmail is exposed for testing purposes.
|
||||
LastUserEmail string
|
||||
}
|
||||
|
||||
// NewInMemoryStorage constructs an InMemoryStorage instance. For use with
|
||||
// caddytls, the InMemoryStorageCreator should be used instead.
|
||||
func NewInMemoryStorage() *InMemoryStorage {
|
||||
return &InMemoryStorage{
|
||||
Sites: make(map[string]*caddytls.SiteData),
|
||||
Users: make(map[string]*caddytls.UserData),
|
||||
}
|
||||
}
|
||||
|
||||
// SiteExists implements caddytls.Storage.SiteExists in memory.
|
||||
func (s *InMemoryStorage) SiteExists(domain string) (bool, error) {
|
||||
_, siteExists := s.Sites[domain]
|
||||
return siteExists, nil
|
||||
}
|
||||
|
||||
// Clear completely clears all values associated with this storage.
|
||||
func (s *InMemoryStorage) Clear() {
|
||||
s.Sites = make(map[string]*caddytls.SiteData)
|
||||
s.Users = make(map[string]*caddytls.UserData)
|
||||
s.LastUserEmail = ""
|
||||
}
|
||||
|
||||
// LoadSite implements caddytls.Storage.LoadSite in memory.
|
||||
func (s *InMemoryStorage) LoadSite(domain string) (*caddytls.SiteData, error) {
|
||||
siteData, ok := s.Sites[domain]
|
||||
if !ok {
|
||||
return nil, caddytls.ErrNotExist(errors.New("not found"))
|
||||
}
|
||||
return siteData, nil
|
||||
}
|
||||
|
||||
func copyBytes(from []byte) []byte {
|
||||
copiedBytes := make([]byte, len(from))
|
||||
copy(copiedBytes, from)
|
||||
return copiedBytes
|
||||
}
|
||||
|
||||
// StoreSite implements caddytls.Storage.StoreSite in memory.
|
||||
func (s *InMemoryStorage) StoreSite(domain string, data *caddytls.SiteData) error {
|
||||
copiedData := new(caddytls.SiteData)
|
||||
copiedData.Cert = copyBytes(data.Cert)
|
||||
copiedData.Key = copyBytes(data.Key)
|
||||
copiedData.Meta = copyBytes(data.Meta)
|
||||
s.Sites[domain] = copiedData
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSite implements caddytls.Storage.DeleteSite in memory.
|
||||
func (s *InMemoryStorage) DeleteSite(domain string) error {
|
||||
if _, ok := s.Sites[domain]; !ok {
|
||||
return caddytls.ErrNotExist(errors.New("not found"))
|
||||
}
|
||||
delete(s.Sites, domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TryLock implements Storage.TryLock by returning nil values because it
|
||||
// is not a multi-server storage implementation.
|
||||
func (s *InMemoryStorage) TryLock(domain string) (caddytls.Waiter, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Unlock implements Storage.Unlock as a no-op because it is
|
||||
// not a multi-server storage implementation.
|
||||
func (s *InMemoryStorage) Unlock(domain string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadUser implements caddytls.Storage.LoadUser in memory.
|
||||
func (s *InMemoryStorage) LoadUser(email string) (*caddytls.UserData, error) {
|
||||
userData, ok := s.Users[email]
|
||||
if !ok {
|
||||
return nil, caddytls.ErrNotExist(errors.New("not found"))
|
||||
}
|
||||
return userData, nil
|
||||
}
|
||||
|
||||
// StoreUser implements caddytls.Storage.StoreUser in memory.
|
||||
func (s *InMemoryStorage) StoreUser(email string, data *caddytls.UserData) error {
|
||||
copiedData := new(caddytls.UserData)
|
||||
copiedData.Reg = copyBytes(data.Reg)
|
||||
copiedData.Key = copyBytes(data.Key)
|
||||
s.Users[email] = copiedData
|
||||
s.LastUserEmail = email
|
||||
return nil
|
||||
}
|
||||
|
||||
// MostRecentUserEmail implements caddytls.Storage.MostRecentUserEmail in memory.
|
||||
func (s *InMemoryStorage) MostRecentUserEmail() string {
|
||||
return s.LastUserEmail
|
||||
}
|
26
vendor/github.com/mholt/caddy/caddytls/storagetest/memorystorage_test.go
generated
vendored
Normal file
26
vendor/github.com/mholt/caddy/caddytls/storagetest/memorystorage_test.go
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package storagetest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMemoryStorage(t *testing.T) {
|
||||
storage := NewInMemoryStorage()
|
||||
storageTest := &StorageTest{
|
||||
Storage: storage,
|
||||
PostTest: storage.Clear,
|
||||
}
|
||||
storageTest.Test(t, false)
|
||||
}
|
306
vendor/github.com/mholt/caddy/caddytls/storagetest/storagetest.go
generated
vendored
Normal file
306
vendor/github.com/mholt/caddy/caddytls/storagetest/storagetest.go
generated
vendored
Normal file
@@ -0,0 +1,306 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package storagetest provides utilities to assist in testing caddytls.Storage
|
||||
// implementations.
|
||||
package storagetest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
// StorageTest is a test harness that contains tests to execute all exposed
|
||||
// parts of a Storage implementation.
|
||||
type StorageTest struct {
|
||||
// Storage is the implementation to use during tests. This must be
|
||||
// present.
|
||||
caddytls.Storage
|
||||
|
||||
// PreTest, if present, is called before every test. Any error returned
|
||||
// is returned from the test and the test does not continue.
|
||||
PreTest func() error
|
||||
|
||||
// PostTest, if present, is executed after every test via defer which
|
||||
// means it executes even on failure of the test (but not on failure of
|
||||
// PreTest).
|
||||
PostTest func()
|
||||
|
||||
// AfterUserEmailStore, if present, is invoked during
|
||||
// TestMostRecentUserEmail after each storage just in case anything
|
||||
// needs to be mocked.
|
||||
AfterUserEmailStore func(email string) error
|
||||
}
|
||||
|
||||
// TestFunc holds information about a test.
|
||||
type TestFunc struct {
|
||||
// Name is the friendly name of the test.
|
||||
Name string
|
||||
|
||||
// Fn is the function that is invoked for the test.
|
||||
Fn func() error
|
||||
}
|
||||
|
||||
// runPreTest runs the PreTest function if present.
|
||||
func (s *StorageTest) runPreTest() error {
|
||||
if s.PreTest != nil {
|
||||
return s.PreTest()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runPostTest runs the PostTest function if present.
|
||||
func (s *StorageTest) runPostTest() {
|
||||
if s.PostTest != nil {
|
||||
s.PostTest()
|
||||
}
|
||||
}
|
||||
|
||||
// AllFuncs returns all test functions that are part of this harness.
|
||||
func (s *StorageTest) AllFuncs() []TestFunc {
|
||||
return []TestFunc{
|
||||
{"TestSiteInfoExists", s.TestSiteExists},
|
||||
{"TestSite", s.TestSite},
|
||||
{"TestUser", s.TestUser},
|
||||
{"TestMostRecentUserEmail", s.TestMostRecentUserEmail},
|
||||
}
|
||||
}
|
||||
|
||||
// Test executes the entire harness using the testing package. Failures are
|
||||
// reported via T.Fatal. If eagerFail is true, the first failure causes all
|
||||
// testing to stop immediately.
|
||||
func (s *StorageTest) Test(t *testing.T, eagerFail bool) {
|
||||
if errs := s.TestAll(eagerFail); len(errs) > 0 {
|
||||
ifaces := make([]interface{}, len(errs))
|
||||
for i, err := range errs {
|
||||
ifaces[i] = err
|
||||
}
|
||||
t.Fatal(ifaces...)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAll executes the entire harness and returns the results as an array of
|
||||
// errors. If eagerFail is true, the first failure causes all testing to stop
|
||||
// immediately.
|
||||
func (s *StorageTest) TestAll(eagerFail bool) (errs []error) {
|
||||
for _, fn := range s.AllFuncs() {
|
||||
if err := fn.Fn(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%v failed: %v", fn.Name, err))
|
||||
if eagerFail {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var simpleSiteData = &caddytls.SiteData{
|
||||
Cert: []byte("foo"),
|
||||
Key: []byte("bar"),
|
||||
Meta: []byte("baz"),
|
||||
}
|
||||
var simpleSiteDataAlt = &caddytls.SiteData{
|
||||
Cert: []byte("qux"),
|
||||
Key: []byte("quux"),
|
||||
Meta: []byte("corge"),
|
||||
}
|
||||
|
||||
// TestSiteExists tests Storage.SiteExists.
|
||||
func (s *StorageTest) TestSiteExists() error {
|
||||
if err := s.runPreTest(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.runPostTest()
|
||||
|
||||
// Should not exist at first
|
||||
siteExists, err := s.SiteExists("example.com")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if siteExists {
|
||||
return errors.New("Site should not exist")
|
||||
}
|
||||
|
||||
// Should exist after we store it
|
||||
if err := s.StoreSite("example.com", simpleSiteData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
siteExists, err = s.SiteExists("example.com")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !siteExists {
|
||||
return errors.New("Expected site to exist")
|
||||
}
|
||||
|
||||
// Site should no longer exist after we delete it
|
||||
if err := s.DeleteSite("example.com"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
siteExists, err = s.SiteExists("example.com")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if siteExists {
|
||||
return errors.New("Site should not exist after delete")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestSite tests Storage.LoadSite, Storage.StoreSite, and Storage.DeleteSite.
|
||||
func (s *StorageTest) TestSite() error {
|
||||
if err := s.runPreTest(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.runPostTest()
|
||||
|
||||
// Should be a not-found error at first
|
||||
_, err := s.LoadSite("example.com")
|
||||
if _, ok := err.(caddytls.ErrNotExist); !ok {
|
||||
return fmt.Errorf("Expected caddytls.ErrNotExist from load, got %T: %v", err, err)
|
||||
}
|
||||
|
||||
// Delete should also be a not-found error at first
|
||||
err = s.DeleteSite("example.com")
|
||||
if _, ok := err.(caddytls.ErrNotExist); !ok {
|
||||
return fmt.Errorf("Expected ErrNotExist from delete, got: %v", err)
|
||||
}
|
||||
|
||||
// Should store successfully and then load just fine
|
||||
if err := s.StoreSite("example.com", simpleSiteData); err != nil {
|
||||
return err
|
||||
}
|
||||
if siteData, err := s.LoadSite("example.com"); err != nil {
|
||||
return err
|
||||
} else if !bytes.Equal(siteData.Cert, simpleSiteData.Cert) {
|
||||
return errors.New("Unexpected cert returned after store")
|
||||
} else if !bytes.Equal(siteData.Key, simpleSiteData.Key) {
|
||||
return errors.New("Unexpected key returned after store")
|
||||
} else if !bytes.Equal(siteData.Meta, simpleSiteData.Meta) {
|
||||
return errors.New("Unexpected meta returned after store")
|
||||
}
|
||||
|
||||
// Overwrite should work just fine
|
||||
if err := s.StoreSite("example.com", simpleSiteDataAlt); err != nil {
|
||||
return err
|
||||
}
|
||||
if siteData, err := s.LoadSite("example.com"); err != nil {
|
||||
return err
|
||||
} else if !bytes.Equal(siteData.Cert, simpleSiteDataAlt.Cert) {
|
||||
return errors.New("Unexpected cert returned after overwrite")
|
||||
}
|
||||
|
||||
// It should delete fine and then not be there
|
||||
if err := s.DeleteSite("example.com"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.LoadSite("example.com")
|
||||
if _, ok := err.(caddytls.ErrNotExist); !ok {
|
||||
return fmt.Errorf("Expected caddytls.ErrNotExist after delete, got %T: %v", err, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var simpleUserData = &caddytls.UserData{
|
||||
Reg: []byte("foo"),
|
||||
Key: []byte("bar"),
|
||||
}
|
||||
var simpleUserDataAlt = &caddytls.UserData{
|
||||
Reg: []byte("baz"),
|
||||
Key: []byte("qux"),
|
||||
}
|
||||
|
||||
// TestUser tests Storage.LoadUser and Storage.StoreUser.
|
||||
func (s *StorageTest) TestUser() error {
|
||||
if err := s.runPreTest(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.runPostTest()
|
||||
|
||||
// Should be a not-found error at first
|
||||
_, err := s.LoadUser("foo@example.com")
|
||||
if _, ok := err.(caddytls.ErrNotExist); !ok {
|
||||
return fmt.Errorf("Expected caddytls.ErrNotExist from load, got %T: %v", err, err)
|
||||
}
|
||||
|
||||
// Should store successfully and then load just fine
|
||||
if err := s.StoreUser("foo@example.com", simpleUserData); err != nil {
|
||||
return err
|
||||
}
|
||||
if userData, err := s.LoadUser("foo@example.com"); err != nil {
|
||||
return err
|
||||
} else if !bytes.Equal(userData.Reg, simpleUserData.Reg) {
|
||||
return errors.New("Unexpected reg returned after store")
|
||||
} else if !bytes.Equal(userData.Key, simpleUserData.Key) {
|
||||
return errors.New("Unexpected key returned after store")
|
||||
}
|
||||
|
||||
// Overwrite should work just fine
|
||||
if err := s.StoreUser("foo@example.com", simpleUserDataAlt); err != nil {
|
||||
return err
|
||||
}
|
||||
if userData, err := s.LoadUser("foo@example.com"); err != nil {
|
||||
return err
|
||||
} else if !bytes.Equal(userData.Reg, simpleUserDataAlt.Reg) {
|
||||
return errors.New("Unexpected reg returned after overwrite")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestMostRecentUserEmail tests Storage.MostRecentUserEmail.
|
||||
func (s *StorageTest) TestMostRecentUserEmail() error {
|
||||
if err := s.runPreTest(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.runPostTest()
|
||||
|
||||
// Should be empty on first run
|
||||
if e := s.MostRecentUserEmail(); e != "" {
|
||||
return fmt.Errorf("Expected empty most recent user on first run, got: %v", e)
|
||||
}
|
||||
|
||||
// If we store user, then that one should be returned
|
||||
if err := s.StoreUser("foo1@example.com", simpleUserData); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.AfterUserEmailStore != nil {
|
||||
s.AfterUserEmailStore("foo1@example.com")
|
||||
}
|
||||
if e := s.MostRecentUserEmail(); e != "foo1@example.com" {
|
||||
return fmt.Errorf("Unexpected most recent email after first store: %v", e)
|
||||
}
|
||||
|
||||
// If we store another user, then that one should be returned
|
||||
if err := s.StoreUser("foo2@example.com", simpleUserDataAlt); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.AfterUserEmailStore != nil {
|
||||
s.AfterUserEmailStore("foo2@example.com")
|
||||
}
|
||||
if e := s.MostRecentUserEmail(); e != "foo2@example.com" {
|
||||
return fmt.Errorf("Unexpected most recent email after user key: %v", e)
|
||||
}
|
||||
return nil
|
||||
}
|
54
vendor/github.com/mholt/caddy/caddytls/storagetest/storagetest_test.go
generated
vendored
Normal file
54
vendor/github.com/mholt/caddy/caddytls/storagetest/storagetest_test.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package storagetest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
// TestFileStorage tests the file storage set with the test harness in this
|
||||
// package.
|
||||
func TestFileStorage(t *testing.T) {
|
||||
emailCounter := 0
|
||||
storageTest := &StorageTest{
|
||||
Storage: &caddytls.FileStorage{Path: "./testdata"}, // nameLocks isn't made here, but it's okay because the tests don't call TryLock or Unlock
|
||||
PostTest: func() { os.RemoveAll("./testdata") },
|
||||
AfterUserEmailStore: func(email string) error {
|
||||
// We need to change the dir mod time to show a
|
||||
// that certain dirs are newer.
|
||||
emailCounter++
|
||||
fp := filepath.Join("./testdata", "users", email)
|
||||
|
||||
// What we will do is subtract 10 days from today and
|
||||
// then add counter * seconds to make the later
|
||||
// counters newer. We accept that this isn't exactly
|
||||
// how the file storage works because it only changes
|
||||
// timestamps on *newly seen* users, but it achieves
|
||||
// the result that the harness expects.
|
||||
chTime := time.Now().AddDate(0, 0, -10).Add(time.Duration(emailCounter) * time.Second)
|
||||
if err := os.Chtimes(fp, chTime, chTime); err != nil {
|
||||
return fmt.Errorf("Unable to change file time for %v: %v", fp, err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
storageTest.Test(t, false)
|
||||
}
|
310
vendor/github.com/mholt/caddy/caddytls/tls.go
generated
vendored
Normal file
310
vendor/github.com/mholt/caddy/caddytls/tls.go
generated
vendored
Normal file
@@ -0,0 +1,310 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package caddytls facilitates the management of TLS assets and integrates
|
||||
// Let's Encrypt functionality into Caddy with first-class support for
|
||||
// creating and renewing certificates automatically. It also implements
|
||||
// the tls directive.
|
||||
//
|
||||
// This package is meant to be used by Caddy server types. To use the
|
||||
// tls directive, a server type must import this package and call
|
||||
// RegisterConfigGetter(). The server type must make and keep track of
|
||||
// the caddytls.Config structs that this package produces. It must also
|
||||
// add tls to its list of directives. When it comes time to make the
|
||||
// server instances, the server type can call MakeTLSConfig() to convert
|
||||
// a []caddytls.Config to a single tls.Config for use in tls.NewListener().
|
||||
// It is also recommended to call RotateSessionTicketKeys() when
|
||||
// starting a new listener.
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/xenolf/lego/acmev2"
|
||||
)
|
||||
|
||||
// HostQualifies returns true if the hostname alone
|
||||
// appears eligible for automatic HTTPS. For example:
|
||||
// localhost, empty hostname, and IP addresses are
|
||||
// not eligible because we cannot obtain certificates
|
||||
// for those names. Wildcard names are allowed, as long
|
||||
// as they conform to CABF requirements (only one wildcard
|
||||
// label, and it must be the left-most label).
|
||||
func HostQualifies(hostname string) bool {
|
||||
return hostname != "localhost" && // localhost is ineligible
|
||||
|
||||
// hostname must not be empty
|
||||
strings.TrimSpace(hostname) != "" &&
|
||||
|
||||
// only one wildcard label allowed, and it must be left-most
|
||||
(!strings.Contains(hostname, "*") ||
|
||||
(strings.Count(hostname, "*") == 1 &&
|
||||
strings.HasPrefix(hostname, "*."))) &&
|
||||
|
||||
// must not start or end with a dot
|
||||
!strings.HasPrefix(hostname, ".") &&
|
||||
!strings.HasSuffix(hostname, ".") &&
|
||||
|
||||
// cannot be an IP address, see
|
||||
// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
|
||||
net.ParseIP(hostname) == nil
|
||||
}
|
||||
|
||||
// saveCertResource saves the certificate resource to disk. This
|
||||
// includes the certificate file itself, the private key, and the
|
||||
// metadata file.
|
||||
func saveCertResource(storage Storage, cert acme.CertificateResource) error {
|
||||
// Save cert, private key, and metadata
|
||||
siteData := &SiteData{
|
||||
Cert: cert.Certificate,
|
||||
Key: cert.PrivateKey,
|
||||
}
|
||||
var err error
|
||||
siteData.Meta, err = json.MarshalIndent(&cert, "", "\t")
|
||||
if err == nil {
|
||||
err = storage.StoreSite(cert.Domain, siteData)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Revoke revokes the certificate for host via ACME protocol.
|
||||
// It assumes the certificate was obtained from the
|
||||
// CA at DefaultCAUrl.
|
||||
func Revoke(host string) error {
|
||||
client, err := newACMEClient(new(Config), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Revoke(host)
|
||||
}
|
||||
|
||||
// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return
|
||||
// // tlsSNISolver is a type that can solve TLS-SNI challenges using
|
||||
// // an existing listener and our custom, in-memory certificate cache.
|
||||
// type tlsSNISolver struct {
|
||||
// certCache *certificateCache
|
||||
// }
|
||||
|
||||
// // Present adds the challenge certificate to the cache.
|
||||
// func (s tlsSNISolver) Present(domain, token, keyAuth string) error {
|
||||
// cert, acmeDomain, err := acme.TLSSNI01ChallengeCert(keyAuth)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// certHash := hashCertificateChain(cert.Certificate)
|
||||
// s.certCache.Lock()
|
||||
// s.certCache.cache[acmeDomain] = Certificate{
|
||||
// Certificate: cert,
|
||||
// Names: []string{acmeDomain},
|
||||
// Hash: certHash, // perhaps not necesssary
|
||||
// }
|
||||
// s.certCache.Unlock()
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// // CleanUp removes the challenge certificate from the cache.
|
||||
// func (s tlsSNISolver) CleanUp(domain, token, keyAuth string) error {
|
||||
// _, acmeDomain, err := acme.TLSSNI01ChallengeCert(keyAuth)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// s.certCache.Lock()
|
||||
// delete(s.certCache.cache, acmeDomain)
|
||||
// s.certCache.Unlock()
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// distributedHTTPSolver allows the HTTP-01 challenge to be solved by
|
||||
// an instance other than the one which initiated it. This is useful
|
||||
// behind load balancers or in other cluster/fleet configurations.
|
||||
// The only requirement is that this (the initiating) instance share
|
||||
// the $CADDYPATH/acme folder with the instance that will complete
|
||||
// the challenge. Mounting the folder locally should be sufficient.
|
||||
//
|
||||
// Obviously, the instance which completes the challenge must be
|
||||
// serving on the HTTPChallengePort to receive and handle the request.
|
||||
// The HTTP server which receives it must check if a file exists, e.g.:
|
||||
// $CADDYPATH/acme/challenge_tokens/example.com.json, and if so,
|
||||
// decode it and use it to serve up the correct response. Caddy's HTTP
|
||||
// server does this by default.
|
||||
//
|
||||
// So as long as the folder is shared, this will just work. There are
|
||||
// no other requirements. The instances may be on other machines or
|
||||
// even other networks, as long as they share the folder as part of
|
||||
// the local file system.
|
||||
//
|
||||
// This solver works by persisting the token and keyauth information
|
||||
// to disk in the shared folder when the authorization is presented,
|
||||
// and then deletes it when it is cleaned up.
|
||||
type distributedHTTPSolver struct {
|
||||
// The distributed HTTPS solver only works if an instance (either
|
||||
// this one or another one) is already listening and serving on the
|
||||
// HTTPChallengePort. If not -- for example: if this is the only
|
||||
// instance, and it is just starting up and hasn't started serving
|
||||
// yet -- then we still need a listener open with an HTTP server
|
||||
// to handle the challenge request. Set this field to have the
|
||||
// standard HTTPProviderServer open its listener for the duration
|
||||
// of the challenge. Make sure to configure its listen address
|
||||
// correctly.
|
||||
httpProviderServer *acme.HTTPProviderServer
|
||||
}
|
||||
|
||||
type challengeInfo struct {
|
||||
Domain, Token, KeyAuth string
|
||||
}
|
||||
|
||||
// Present adds the challenge certificate to the cache.
|
||||
func (dhs distributedHTTPSolver) Present(domain, token, keyAuth string) error {
|
||||
if dhs.httpProviderServer != nil {
|
||||
err := dhs.httpProviderServer.Present(domain, token, keyAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("presenting with standard HTTP provider server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err := os.MkdirAll(dhs.challengeTokensBasePath(), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
infoBytes, err := json.Marshal(challengeInfo{
|
||||
Domain: domain,
|
||||
Token: token,
|
||||
KeyAuth: keyAuth,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(dhs.challengeTokensPath(domain), infoBytes, 0644)
|
||||
}
|
||||
|
||||
// CleanUp removes the challenge certificate from the cache.
|
||||
func (dhs distributedHTTPSolver) CleanUp(domain, token, keyAuth string) error {
|
||||
if dhs.httpProviderServer != nil {
|
||||
err := dhs.httpProviderServer.CleanUp(domain, token, keyAuth)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Cleaning up standard HTTP provider server: %v", err)
|
||||
}
|
||||
}
|
||||
return os.Remove(dhs.challengeTokensPath(domain))
|
||||
}
|
||||
|
||||
func (dhs distributedHTTPSolver) challengeTokensPath(domain string) string {
|
||||
domainFile := strings.Replace(strings.ToLower(domain), "*", "wildcard_", -1)
|
||||
return filepath.Join(dhs.challengeTokensBasePath(), domainFile+".json")
|
||||
}
|
||||
|
||||
func (dhs distributedHTTPSolver) challengeTokensBasePath() string {
|
||||
return filepath.Join(caddy.AssetsPath(), "acme", "challenge_tokens")
|
||||
}
|
||||
|
||||
// ConfigHolder is any type that has a Config; it presumably is
|
||||
// connected to a hostname and port on which it is serving.
|
||||
type ConfigHolder interface {
|
||||
TLSConfig() *Config
|
||||
Host() string
|
||||
Port() string
|
||||
}
|
||||
|
||||
// QualifiesForManagedTLS returns true if c qualifies for
|
||||
// for managed TLS (but not on-demand TLS specifically).
|
||||
// It does NOT check to see if a cert and key already exist
|
||||
// for the config. If the return value is true, you should
|
||||
// be OK to set c.TLSConfig().Managed to true; then you should
|
||||
// check that value in the future instead, because the process
|
||||
// of setting up the config may make it look like it doesn't
|
||||
// qualify even though it originally did.
|
||||
func QualifiesForManagedTLS(c ConfigHolder) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
tlsConfig := c.TLSConfig()
|
||||
if tlsConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return (!tlsConfig.Manual || tlsConfig.OnDemand) && // user might provide own cert and key
|
||||
|
||||
// if self-signed, we've already generated one to use
|
||||
!tlsConfig.SelfSigned &&
|
||||
|
||||
// user can force-disable managed TLS
|
||||
c.Port() != "80" &&
|
||||
tlsConfig.ACMEEmail != "off" &&
|
||||
|
||||
// we get can't certs for some kinds of hostnames, but
|
||||
// on-demand TLS allows empty hostnames at startup
|
||||
(HostQualifies(c.Host()) || tlsConfig.OnDemand)
|
||||
}
|
||||
|
||||
// ChallengeProvider defines an own type that should be used in Caddy plugins
|
||||
// over acme.ChallengeProvider. Using acme.ChallengeProvider causes version mismatches
|
||||
// with vendored dependencies (see https://github.com/mattfarina/golang-broken-vendor)
|
||||
//
|
||||
// acme.ChallengeProvider is an interface that allows the implementation of custom
|
||||
// challenge providers. For more details, see:
|
||||
// https://godoc.org/github.com/xenolf/lego/acme#ChallengeProvider
|
||||
type ChallengeProvider acme.ChallengeProvider
|
||||
|
||||
// DNSProviderConstructor is a function that takes credentials and
|
||||
// returns a type that can solve the ACME DNS challenges.
|
||||
type DNSProviderConstructor func(credentials ...string) (ChallengeProvider, error)
|
||||
|
||||
// dnsProviders is the list of DNS providers that have been plugged in.
|
||||
var dnsProviders = make(map[string]DNSProviderConstructor)
|
||||
|
||||
// RegisterDNSProvider registers provider by name for solving the ACME DNS challenge.
|
||||
func RegisterDNSProvider(name string, provider DNSProviderConstructor) {
|
||||
dnsProviders[name] = provider
|
||||
caddy.RegisterPlugin("tls.dns."+name, caddy.Plugin{})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// DefaultCAUrl is the default URL to the CA's ACME directory endpoint.
|
||||
// It's very important to set this unless you set it in every Config.
|
||||
DefaultCAUrl string
|
||||
|
||||
// DefaultKeyType is used as the type of key for new certificates
|
||||
// when no other key type is specified.
|
||||
DefaultKeyType = acme.RSA2048
|
||||
|
||||
// DisableHTTPChallenge will disable all HTTP challenges.
|
||||
DisableHTTPChallenge bool
|
||||
|
||||
// DisableTLSSNIChallenge will disable all TLS-SNI challenges.
|
||||
DisableTLSSNIChallenge bool
|
||||
)
|
||||
|
||||
var storageProviders = make(map[string]StorageConstructor)
|
||||
|
||||
// RegisterStorageProvider registers provider by name for storing tls data
|
||||
func RegisterStorageProvider(name string, provider StorageConstructor) {
|
||||
storageProviders[name] = provider
|
||||
caddy.RegisterPlugin("tls.storage."+name, caddy.Plugin{})
|
||||
}
|
184
vendor/github.com/mholt/caddy/caddytls/tls_test.go
generated
vendored
Normal file
184
vendor/github.com/mholt/caddy/caddytls/tls_test.go
generated
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/xenolf/lego/acmev2"
|
||||
)
|
||||
|
||||
func TestHostQualifies(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
host string
|
||||
expect bool
|
||||
}{
|
||||
{"example.com", true},
|
||||
{"sub.example.com", true},
|
||||
{"Sub.Example.COM", true},
|
||||
{"127.0.0.1", false},
|
||||
{"127.0.1.5", false},
|
||||
{"69.123.43.94", false},
|
||||
{"::1", false},
|
||||
{"::", false},
|
||||
{"0.0.0.0", false},
|
||||
{"", false},
|
||||
{" ", false},
|
||||
{"*.example.com", true},
|
||||
{"*.*.example.com", false},
|
||||
{"sub.*.example.com", false},
|
||||
{"*sub.example.com", false},
|
||||
{".com", false},
|
||||
{"example.com.", false},
|
||||
{"localhost", false},
|
||||
{"local", true},
|
||||
{"devsite", true},
|
||||
{"192.168.1.3", false},
|
||||
{"10.0.2.1", false},
|
||||
{"169.112.53.4", false},
|
||||
} {
|
||||
actual := HostQualifies(test.host)
|
||||
if actual != test.expect {
|
||||
t.Errorf("Test %d: Expected HostQualifies(%s)=%v, but got %v",
|
||||
i, test.host, test.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type holder struct {
|
||||
host, port string
|
||||
cfg *Config
|
||||
}
|
||||
|
||||
func (h holder) TLSConfig() *Config { return h.cfg }
|
||||
func (h holder) Host() string { return h.host }
|
||||
func (h holder) Port() string { return h.port }
|
||||
|
||||
func TestQualifiesForManagedTLS(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
cfg ConfigHolder
|
||||
expect bool
|
||||
}{
|
||||
{holder{host: ""}, false},
|
||||
{holder{host: "localhost"}, false},
|
||||
{holder{host: "123.44.3.21"}, false},
|
||||
{holder{host: "example.com"}, false},
|
||||
{holder{host: "", cfg: new(Config)}, false},
|
||||
{holder{host: "localhost", cfg: new(Config)}, false},
|
||||
{holder{host: "123.44.3.21", cfg: new(Config)}, false},
|
||||
{holder{host: "example.com", cfg: new(Config)}, true},
|
||||
{holder{host: "*.example.com", cfg: new(Config)}, true},
|
||||
{holder{host: "*.*.example.com", cfg: new(Config)}, false},
|
||||
{holder{host: "*sub.example.com", cfg: new(Config)}, false},
|
||||
{holder{host: "sub.*.example.com", cfg: new(Config)}, false},
|
||||
{holder{host: "example.com", cfg: &Config{Manual: true}}, false},
|
||||
{holder{host: "example.com", cfg: &Config{ACMEEmail: "off"}}, false},
|
||||
{holder{host: "example.com", cfg: &Config{ACMEEmail: "foo@bar.com"}}, true},
|
||||
{holder{host: "example.com", port: "80"}, false},
|
||||
{holder{host: "example.com", port: "1234", cfg: new(Config)}, true},
|
||||
{holder{host: "example.com", port: "443", cfg: new(Config)}, true},
|
||||
{holder{host: "example.com", port: "80"}, false},
|
||||
} {
|
||||
if got, want := QualifiesForManagedTLS(test.cfg), test.expect; got != want {
|
||||
t.Errorf("Test %d: Expected %v but got %v", i, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveCertResource(t *testing.T) {
|
||||
storage := &FileStorage{Path: "./le_test_save"}
|
||||
defer func() {
|
||||
err := os.RemoveAll(storage.Path)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage.Path, 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(storage, cert)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
siteData, err := storage.LoadSite(domain)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error reading site, got: %v", err)
|
||||
}
|
||||
if string(siteData.Cert) != certContents {
|
||||
t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(siteData.Cert))
|
||||
}
|
||||
if string(siteData.Key) != keyContents {
|
||||
t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(siteData.Key))
|
||||
}
|
||||
if string(siteData.Meta) != metaContents {
|
||||
t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(siteData.Meta))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistingCertAndKey(t *testing.T) {
|
||||
storage := &FileStorage{Path: "./le_test_existing"}
|
||||
defer func() {
|
||||
err := os.RemoveAll(storage.Path)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage.Path, err)
|
||||
}
|
||||
}()
|
||||
|
||||
domain := "example.com"
|
||||
|
||||
siteExists, err := storage.SiteExists(domain)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not determine whether site exists: %v", err)
|
||||
}
|
||||
|
||||
if siteExists {
|
||||
t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain)
|
||||
}
|
||||
|
||||
err = saveCertResource(storage, acme.CertificateResource{
|
||||
Domain: domain,
|
||||
PrivateKey: []byte("key"),
|
||||
Certificate: []byte("cert"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
siteExists, err = storage.SiteExists(domain)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not determine whether site exists: %v", err)
|
||||
}
|
||||
|
||||
if !siteExists {
|
||||
t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain)
|
||||
}
|
||||
}
|
233
vendor/github.com/mholt/caddy/caddytls/user.go
generated
vendored
Normal file
233
vendor/github.com/mholt/caddy/caddytls/user.go
generated
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/xenolf/lego/acmev2"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 within the scope of memory and storage to use
|
||||
// for ACME TLS. If it cannot get an email address, it returns
|
||||
// empty string. (If user is present, 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.
|
||||
// If the user is prompted, a new User will be created and
|
||||
// stored in storage according to the email address they
|
||||
// provided (which might be blank).
|
||||
func getEmail(cfg *Config, userPresent bool) (string, error) {
|
||||
storage, err := cfg.StorageFor(cfg.CAUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// First try memory (command line flag or typed by user previously)
|
||||
leEmail := DefaultEmail
|
||||
|
||||
// Then try to get most recent user email from storage
|
||||
if leEmail == "" {
|
||||
leEmail = storage.MostRecentUserEmail()
|
||||
DefaultEmail = leEmail // save for next time
|
||||
}
|
||||
|
||||
// Looks like there is no email address readily available,
|
||||
// so we will have to ask the user if we can.
|
||||
if leEmail == "" && userPresent {
|
||||
// evidently, no User data was present in storage;
|
||||
// thus we must make a new User so that we can get
|
||||
// the Terms of Service URL via our ACME client, phew!
|
||||
user, err := newUser("")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// get the agreement URL
|
||||
agreementURL := agreementTestURL
|
||||
if agreementURL == "" {
|
||||
// we call acme.NewClient directly because newACMEClient
|
||||
// would require that we already know the user's email
|
||||
caURL := DefaultCAUrl
|
||||
if cfg.CAUrl != "" {
|
||||
caURL = cfg.CAUrl
|
||||
}
|
||||
tempClient, err := acme.NewClient(caURL, user, "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("making ACME client to get ToS URL: %v", err)
|
||||
}
|
||||
agreementURL = tempClient.GetToSURL()
|
||||
}
|
||||
|
||||
// prompt the user for an email address and terms agreement
|
||||
reader := bufio.NewReader(stdin)
|
||||
promptUserAgreement(agreementURL)
|
||||
fmt.Println("Please enter your email address to signify agreement and to be notified")
|
||||
fmt.Println("in case of issues. You can leave it blank, but we don't recommend it.")
|
||||
fmt.Print(" Email address: ")
|
||||
leEmail, err = reader.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return "", fmt.Errorf("reading email address: %v", err)
|
||||
}
|
||||
leEmail = strings.TrimSpace(leEmail)
|
||||
DefaultEmail = leEmail
|
||||
Agreed = true
|
||||
|
||||
// save the new user to preserve this for next time
|
||||
user.Email = leEmail
|
||||
err = saveUser(storage, user)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// lower-casing the email is important for consistency
|
||||
return strings.ToLower(leEmail), nil
|
||||
}
|
||||
|
||||
// getUser loads the user with the given email from disk
|
||||
// using the provided storage. 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(storage Storage, email string) (User, error) {
|
||||
var user User
|
||||
|
||||
// open user reg
|
||||
userData, err := storage.LoadUser(email)
|
||||
if err != nil {
|
||||
if _, ok := err.(ErrNotExist); ok {
|
||||
// create a new user
|
||||
return newUser(email)
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
// load user information
|
||||
err = json.Unmarshal(userData.Reg, &user)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
// load their private key
|
||||
user.key, err = loadPrivateKey(userData.Key)
|
||||
return user, err
|
||||
}
|
||||
|
||||
// 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. You must also pass in the storage
|
||||
// wherein the user should be saved. It should be the storage
|
||||
// for the CA with which user has an account.
|
||||
func saveUser(storage Storage, user User) error {
|
||||
// Save the private key and registration
|
||||
userData := new(UserData)
|
||||
var err error
|
||||
userData.Key, err = savePrivateKey(user.key)
|
||||
if err == nil {
|
||||
userData.Reg, err = json.MarshalIndent(&user, "", "\t")
|
||||
}
|
||||
if err == nil {
|
||||
err = storage.StoreUser(user.Email, userData)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// promptUserAgreement simply outputs the standard user
|
||||
// agreement prompt with the given agreement URL.
|
||||
// It outputs a newline after the message.
|
||||
func promptUserAgreement(agreementURL string) {
|
||||
const userAgreementPrompt = `Your sites will be served over HTTPS automatically using Let's Encrypt.
|
||||
By continuing, you agree to the Let's Encrypt Subscriber Agreement at:`
|
||||
fmt.Printf("\n\n%s\n %s\n", userAgreementPrompt, agreementURL)
|
||||
}
|
||||
|
||||
// askUserAgreement prompts the user to agree to the agreement
|
||||
// at the given agreement URL via stdin. It returns whether the
|
||||
// user agreed or not.
|
||||
func askUserAgreement(agreementURL string) bool {
|
||||
promptUserAgreement(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"
|
||||
}
|
||||
|
||||
// agreementTestURL is set during tests to skip requiring
|
||||
// setting up an entire ACME CA endpoint.
|
||||
var agreementTestURL string
|
||||
|
||||
// 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,
|
||||
// but only for local/storage use, not with the CA.
|
||||
const emptyEmail = "default"
|
221
vendor/github.com/mholt/caddy/caddytls/user_test.go
generated
vendored
Normal file
221
vendor/github.com/mholt/caddy/caddytls/user_test.go
generated
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/xenolf/lego/acmev2"
|
||||
)
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
defer testStorage.clean()
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
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) {
|
||||
defer testStorage.clean()
|
||||
|
||||
email := "me@foobar.com"
|
||||
user, err := newUser(email)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating user: %v", err)
|
||||
}
|
||||
|
||||
err = saveUser(testStorage, user)
|
||||
if err != nil {
|
||||
t.Fatalf("Error saving user: %v", err)
|
||||
}
|
||||
_, err = testStorage.LoadUser(email)
|
||||
if err != nil {
|
||||
t.Errorf("Cannot access user data, error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserDoesNotAlreadyExist(t *testing.T) {
|
||||
defer testStorage.clean()
|
||||
|
||||
user, err := getUser(testStorage, "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) {
|
||||
defer testStorage.clean()
|
||||
|
||||
email := "me@foobar.com"
|
||||
|
||||
// Set up test
|
||||
user, err := newUser(email)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating user: %v", err)
|
||||
}
|
||||
err = saveUser(testStorage, user)
|
||||
if err != nil {
|
||||
t.Fatalf("Error saving user: %v", err)
|
||||
}
|
||||
|
||||
// Expect to load user from disk
|
||||
user2, err := getUser(testStorage, 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) {
|
||||
// ensure storage (via StorageFor) uses the local testdata folder that we delete later
|
||||
origCaddypath := os.Getenv("CADDYPATH")
|
||||
os.Setenv("CADDYPATH", "./testdata")
|
||||
defer os.Setenv("CADDYPATH", origCaddypath)
|
||||
|
||||
agreementTestURL = "(none - testing)"
|
||||
defer func() { agreementTestURL = "" }()
|
||||
|
||||
// let's not clutter up the output
|
||||
origStdout := os.Stdout
|
||||
os.Stdout = nil
|
||||
defer func() { os.Stdout = origStdout }()
|
||||
|
||||
defer testStorage.clean()
|
||||
DefaultEmail = "test2@foo.com"
|
||||
|
||||
// Test1: Use default email from flag (or user previously typing it)
|
||||
actual, err := getEmail(testConfig, true)
|
||||
if err != nil {
|
||||
t.Fatalf("getEmail (1) error: %v", err)
|
||||
}
|
||||
if actual != DefaultEmail {
|
||||
t.Errorf("Did not get correct email from memory; expected '%s' but got '%s'", DefaultEmail, actual)
|
||||
}
|
||||
|
||||
// Test2: 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, err = getEmail(testConfig, true)
|
||||
if err != nil {
|
||||
t.Fatalf("getEmail (2) error: %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// Test3: Get most recent email from before (in storage)
|
||||
DefaultEmail = ""
|
||||
for i, eml := range []string{
|
||||
"TEST4-3@foo.com", // test case insensitivity
|
||||
"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(testStorage, u)
|
||||
if err != nil {
|
||||
t.Fatalf("Error saving user %d: %v", i, err)
|
||||
}
|
||||
|
||||
// Change modified time so they're all different and the test becomes more deterministic
|
||||
f, err := os.Stat(testStorage.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.Hour)) // 1 second isn't always enough space!
|
||||
if err := os.Chtimes(testStorage.user(eml), chTime, chTime); err != nil {
|
||||
t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err)
|
||||
}
|
||||
}
|
||||
actual, err = getEmail(testConfig, true)
|
||||
if err != nil {
|
||||
t.Fatalf("getEmail (3) error: %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
testStorageBase = "./testdata" // ephemeral folder that gets deleted after tests finish
|
||||
testCAHost = "localhost"
|
||||
testConfig = &Config{CAUrl: "http://" + testCAHost + "/directory", StorageProvider: "file"}
|
||||
testStorage = &FileStorage{Path: filepath.Join(testStorageBase, "acme", testCAHost)}
|
||||
)
|
||||
|
||||
func (s *FileStorage) clean() error { return os.RemoveAll(testStorageBase) }
|
Reference in New Issue
Block a user