This commit is contained in:
Manfred Touron
2017-05-18 18:54:23 +02:00
parent dc386661ca
commit 5448f25fd6
645 changed files with 55908 additions and 33297 deletions

View File

@@ -10,6 +10,7 @@ package autocert
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
@@ -30,9 +31,14 @@ import (
"time"
"golang.org/x/crypto/acme"
"golang.org/x/net/context"
)
// createCertRetryAfter is how much time to wait before removing a failed state
// entry due to an unsuccessful createCert call.
// This is a variable instead of a const for testing.
// TODO: Consider making it configurable or an exp backoff?
var createCertRetryAfter = time.Minute
// pseudoRand is safe for concurrent use.
var pseudoRand *lockedMathRand
@@ -41,8 +47,9 @@ func init() {
pseudoRand = &lockedMathRand{rnd: mathrand.New(src)}
}
// AcceptTOS always returns true to indicate the acceptance of a CA Terms of Service
// during account registration.
// AcceptTOS is a Manager.Prompt function that always returns true to
// indicate acceptance of the CA's Terms of Service during account
// registration.
func AcceptTOS(tosURL string) bool { return true }
// HostPolicy specifies which host names the Manager is allowed to respond to.
@@ -76,18 +83,6 @@ func defaultHostPolicy(context.Context, string) error {
// It obtains and refreshes certificates automatically,
// as well as providing them to a TLS server via tls.Config.
//
// A simple usage example:
//
// m := autocert.Manager{
// Prompt: autocert.AcceptTOS,
// HostPolicy: autocert.HostWhitelist("example.org"),
// }
// s := &http.Server{
// Addr: ":https",
// TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
// }
// s.ListenAndServeTLS("", "")
//
// To preserve issued certificates and improve overall performance,
// use a cache implementation of Cache. For instance, DirCache.
type Manager struct {
@@ -123,7 +118,7 @@ type Manager struct {
// RenewBefore optionally specifies how early certificates should
// be renewed before they expire.
//
// If zero, they're renewed 1 week before expiration.
// If zero, they're renewed 30 days before expiration.
RenewBefore time.Duration
// Client is used to perform low-level operations, such as account registration
@@ -173,10 +168,23 @@ type Manager struct {
// The error is propagated back to the caller of GetCertificate and is user-visible.
// This does not affect cached certs. See HostPolicy field description for more details.
func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if m.Prompt == nil {
return nil, errors.New("acme/autocert: Manager.Prompt not set")
}
name := hello.ServerName
if name == "" {
return nil, errors.New("acme/autocert: missing server name")
}
if !strings.Contains(strings.Trim(name, "."), ".") {
return nil, errors.New("acme/autocert: server name component count invalid")
}
if strings.ContainsAny(name, `/\`) {
return nil, errors.New("acme/autocert: server name contains invalid character")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
// check whether this is a token cert requested for TLS-SNI challenge
if strings.HasSuffix(name, ".acme.invalid") {
@@ -185,7 +193,7 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
if cert := m.tokenCert[name]; cert != nil {
return cert, nil
}
if cert, err := m.cacheGet(name); err == nil {
if cert, err := m.cacheGet(ctx, name); err == nil {
return cert, nil
}
// TODO: cache error results?
@@ -194,7 +202,7 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
// regular domain
name = strings.TrimSuffix(name, ".") // golang.org/issue/18114
cert, err := m.cert(name)
cert, err := m.cert(ctx, name)
if err == nil {
return cert, nil
}
@@ -203,7 +211,6 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
}
// first-time
ctx := context.Background() // TODO: use a deadline?
if err := m.hostPolicy()(ctx, name); err != nil {
return nil, err
}
@@ -211,14 +218,14 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
if err != nil {
return nil, err
}
m.cachePut(name, cert)
m.cachePut(ctx, name, cert)
return cert, nil
}
// cert returns an existing certificate either from m.state or cache.
// If a certificate is found in cache but not in m.state, the latter will be filled
// with the cached value.
func (m *Manager) cert(name string) (*tls.Certificate, error) {
func (m *Manager) cert(ctx context.Context, name string) (*tls.Certificate, error) {
m.stateMu.Lock()
if s, ok := m.state[name]; ok {
m.stateMu.Unlock()
@@ -227,7 +234,7 @@ func (m *Manager) cert(name string) (*tls.Certificate, error) {
return s.tlscert()
}
defer m.stateMu.Unlock()
cert, err := m.cacheGet(name)
cert, err := m.cacheGet(ctx, name)
if err != nil {
return nil, err
}
@@ -249,12 +256,11 @@ func (m *Manager) cert(name string) (*tls.Certificate, error) {
}
// cacheGet always returns a valid certificate, or an error otherwise.
func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
// If a cached certficate exists but is not valid, ErrCacheMiss is returned.
func (m *Manager) cacheGet(ctx context.Context, domain string) (*tls.Certificate, error) {
if m.Cache == nil {
return nil, ErrCacheMiss
}
// TODO: might want to define a cache timeout on m
ctx := context.Background()
data, err := m.Cache.Get(ctx, domain)
if err != nil {
return nil, err
@@ -263,7 +269,7 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
// private
priv, pub := pem.Decode(data)
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
return nil, errors.New("acme/autocert: no private key found in cache")
return nil, ErrCacheMiss
}
privKey, err := parsePrivateKey(priv.Bytes)
if err != nil {
@@ -281,13 +287,14 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
pubDER = append(pubDER, b.Bytes)
}
if len(pub) > 0 {
return nil, errors.New("acme/autocert: invalid public key")
// Leftover content not consumed by pem.Decode. Corrupt. Ignore.
return nil, ErrCacheMiss
}
// verify and create TLS cert
leaf, err := validCert(domain, pubDER, privKey)
if err != nil {
return nil, err
return nil, ErrCacheMiss
}
tlscert := &tls.Certificate{
Certificate: pubDER,
@@ -297,7 +304,7 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
return tlscert, nil
}
func (m *Manager) cachePut(domain string, tlscert *tls.Certificate) error {
func (m *Manager) cachePut(ctx context.Context, domain string, tlscert *tls.Certificate) error {
if m.Cache == nil {
return nil
}
@@ -329,8 +336,6 @@ func (m *Manager) cachePut(domain string, tlscert *tls.Certificate) error {
}
}
// TODO: might want to define a cache timeout on m
ctx := context.Background()
return m.Cache.Put(ctx, domain, buf.Bytes())
}
@@ -370,6 +375,23 @@ func (m *Manager) createCert(ctx context.Context, domain string) (*tls.Certifica
der, leaf, err := m.authorizedCert(ctx, state.key, domain)
if err != nil {
// Remove the failed state after some time,
// making the manager call createCert again on the following TLS hello.
time.AfterFunc(createCertRetryAfter, func() {
defer testDidRemoveState(domain)
m.stateMu.Lock()
defer m.stateMu.Unlock()
// Verify the state hasn't changed and it's still invalid
// before deleting.
s, ok := m.state[domain]
if !ok {
return
}
if _, err := validCert(domain, s.cert, s.key); err == nil {
return
}
delete(m.state, domain)
})
return nil, err
}
state.cert = der
@@ -418,7 +440,6 @@ func (m *Manager) certState(domain string) (*certState, error) {
// authorizedCert starts domain ownership verification process and requests a new cert upon success.
// The key argument is the certificate private key.
func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, domain string) (der [][]byte, leaf *x509.Certificate, err error) {
// TODO: make m.verify retry or retry m.verify calls here
if err := m.verify(ctx, domain); err != nil {
return nil, nil, err
}
@@ -494,7 +515,7 @@ func (m *Manager) verify(ctx context.Context, domain string) error {
if err != nil {
return err
}
m.putTokenCert(name, &cert)
m.putTokenCert(ctx, name, &cert)
defer func() {
// verification has ended at this point
// don't need token cert anymore
@@ -512,14 +533,14 @@ func (m *Manager) verify(ctx context.Context, domain string) error {
// putTokenCert stores the cert under the named key in both m.tokenCert map
// and m.Cache.
func (m *Manager) putTokenCert(name string, cert *tls.Certificate) {
func (m *Manager) putTokenCert(ctx context.Context, name string, cert *tls.Certificate) {
m.tokenCertMu.Lock()
defer m.tokenCertMu.Unlock()
if m.tokenCert == nil {
m.tokenCert = make(map[string]*tls.Certificate)
}
m.tokenCert[name] = cert
m.cachePut(name, cert)
m.cachePut(ctx, name, cert)
}
// deleteTokenCert removes the token certificate for the specified domain name
@@ -644,10 +665,10 @@ func (m *Manager) hostPolicy() HostPolicy {
}
func (m *Manager) renewBefore() time.Duration {
if m.RenewBefore > maxRandRenew {
if m.RenewBefore > renewJitter {
return m.RenewBefore
}
return 7 * 24 * time.Hour // 1 week
return 720 * time.Hour // 30 days
}
// certState is ready when its mutex is unlocked for reading.
@@ -789,5 +810,10 @@ func (r *lockedMathRand) int63n(max int64) int64 {
return n
}
// for easier testing
var timeNow = time.Now
// For easier testing.
var (
timeNow = time.Now
// Called when a state is removed.
testDidRemoveState = func(domain string) {}
)

View File

@@ -5,6 +5,7 @@
package autocert
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
@@ -27,7 +28,6 @@ import (
"time"
"golang.org/x/crypto/acme"
"golang.org/x/net/context"
)
var discoTmpl = template.Must(template.New("disco").Parse(`{
@@ -150,7 +150,7 @@ func TestGetCertificate_ForceRSA(t *testing.T) {
hello := &tls.ClientHelloInfo{ServerName: "example.org"}
testGetCertificate(t, man, "example.org", hello)
cert, err := man.cacheGet("example.org")
cert, err := man.cacheGet(context.Background(), "example.org")
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}
@@ -159,9 +159,110 @@ func TestGetCertificate_ForceRSA(t *testing.T) {
}
}
// tests man.GetCertificate flow using the provided hello argument.
func TestGetCertificate_nilPrompt(t *testing.T) {
man := &Manager{}
defer man.stopRenew()
url, finish := startACMEServerStub(t, man, "example.org")
defer finish()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
man.Client = &acme.Client{
Key: key,
DirectoryURL: url,
}
hello := &tls.ClientHelloInfo{ServerName: "example.org"}
if _, err := man.GetCertificate(hello); err == nil {
t.Error("got certificate for example.org; wanted error")
}
}
func TestGetCertificate_expiredCache(t *testing.T) {
// Make an expired cert and cache it.
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "example.org"},
NotAfter: time.Now(),
}
pub, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &pk.PublicKey, pk)
if err != nil {
t.Fatal(err)
}
tlscert := &tls.Certificate{
Certificate: [][]byte{pub},
PrivateKey: pk,
}
man := &Manager{Prompt: AcceptTOS, Cache: newMemCache()}
defer man.stopRenew()
if err := man.cachePut(context.Background(), "example.org", tlscert); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
// The expired cached cert should trigger a new cert issuance
// and return without an error.
hello := &tls.ClientHelloInfo{ServerName: "example.org"}
testGetCertificate(t, man, "example.org", hello)
}
func TestGetCertificate_failedAttempt(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
}))
defer ts.Close()
const example = "example.org"
d := createCertRetryAfter
f := testDidRemoveState
defer func() {
createCertRetryAfter = d
testDidRemoveState = f
}()
createCertRetryAfter = 0
done := make(chan struct{})
testDidRemoveState = func(domain string) {
if domain != example {
t.Errorf("testDidRemoveState: domain = %q; want %q", domain, example)
}
close(done)
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
man := &Manager{
Prompt: AcceptTOS,
Client: &acme.Client{
Key: key,
DirectoryURL: ts.URL,
},
}
defer man.stopRenew()
hello := &tls.ClientHelloInfo{ServerName: example}
if _, err := man.GetCertificate(hello); err == nil {
t.Error("GetCertificate: err is nil")
}
select {
case <-time.After(5 * time.Second):
t.Errorf("took too long to remove the %q state", example)
case <-done:
man.stateMu.Lock()
defer man.stateMu.Unlock()
if v, exist := man.state[example]; exist {
t.Errorf("state exists for %q: %+v", example, v)
}
}
}
// startACMEServerStub runs an ACME server
// The domain argument is the expected domain name of a certificate request.
func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.ClientHelloInfo) {
func startACMEServerStub(t *testing.T, man *Manager, domain string) (url string, finish func()) {
// echo token-02 | shasum -a 256
// then divide result in 2 parts separated by dot
tokenCertName := "4e8eb87631187e9ff2153b56b13a4dec.13a35d002e485d60ff37354b32f665d9.token.acme.invalid"
@@ -187,7 +288,7 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
// discovery
case "/":
if err := discoTmpl.Execute(w, ca.URL); err != nil {
t.Fatalf("discoTmpl: %v", err)
t.Errorf("discoTmpl: %v", err)
}
// client key registration
case "/new-reg":
@@ -197,7 +298,7 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
w.Header().Set("location", ca.URL+"/authz/1")
w.WriteHeader(http.StatusCreated)
if err := authzTmpl.Execute(w, ca.URL); err != nil {
t.Fatalf("authzTmpl: %v", err)
t.Errorf("authzTmpl: %v", err)
}
// accept tls-sni-02 challenge
case "/challenge/2":
@@ -215,14 +316,14 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
csr, err := x509.ParseCertificateRequest(b)
if err != nil {
t.Fatalf("new-cert: CSR: %v", err)
t.Errorf("new-cert: CSR: %v", err)
}
if csr.Subject.CommonName != domain {
t.Errorf("CommonName in CSR = %q; want %q", csr.Subject.CommonName, domain)
}
der, err := dummyCert(csr.PublicKey, domain)
if err != nil {
t.Fatalf("new-cert: dummyCert: %v", err)
t.Errorf("new-cert: dummyCert: %v", err)
}
chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL)
w.Header().Set("link", chainUp)
@@ -232,14 +333,51 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
case "/ca-cert":
der, err := dummyCert(nil, "ca")
if err != nil {
t.Fatalf("ca-cert: dummyCert: %v", err)
t.Errorf("ca-cert: dummyCert: %v", err)
}
w.Write(der)
default:
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
}
}))
defer ca.Close()
finish = func() {
ca.Close()
// make sure token cert was removed
cancel := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
tick := time.NewTicker(100 * time.Millisecond)
defer tick.Stop()
for {
hello := &tls.ClientHelloInfo{ServerName: tokenCertName}
if _, err := man.GetCertificate(hello); err != nil {
return
}
select {
case <-tick.C:
case <-cancel:
return
}
}
}()
select {
case <-done:
case <-time.After(5 * time.Second):
close(cancel)
t.Error("token cert was not removed")
<-done
}
}
return ca.URL, finish
}
// tests man.GetCertificate flow using the provided hello argument.
// The domain argument is the expected domain name of a certificate request.
func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.ClientHelloInfo) {
url, finish := startACMEServerStub(t, man, domain)
defer finish()
// use EC key to run faster on 386
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@@ -248,7 +386,7 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
}
man.Client = &acme.Client{
Key: key,
DirectoryURL: ca.URL,
DirectoryURL: url,
}
// simulate tls.Config.GetCertificate
@@ -279,23 +417,6 @@ func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.Cl
t.Errorf("cert.DNSNames = %v; want %q", cert.DNSNames, domain)
}
// make sure token cert was removed
done = make(chan struct{})
go func() {
for {
hello := &tls.ClientHelloInfo{ServerName: tokenCertName}
if _, err := man.GetCertificate(hello); err != nil {
break
}
time.Sleep(100 * time.Millisecond)
}
close(done)
}()
select {
case <-time.After(5 * time.Second):
t.Error("token cert was not removed")
case <-done:
}
}
func TestAccountKeyCache(t *testing.T) {
@@ -335,10 +456,11 @@ func TestCache(t *testing.T) {
man := &Manager{Cache: newMemCache()}
defer man.stopRenew()
if err := man.cachePut("example.org", tlscert); err != nil {
ctx := context.Background()
if err := man.cachePut(ctx, "example.org", tlscert); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
res, err := man.cacheGet("example.org")
res, err := man.cacheGet(ctx, "example.org")
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}
@@ -438,3 +560,47 @@ func TestValidCert(t *testing.T) {
}
}
}
type cacheGetFunc func(ctx context.Context, key string) ([]byte, error)
func (f cacheGetFunc) Get(ctx context.Context, key string) ([]byte, error) {
return f(ctx, key)
}
func (f cacheGetFunc) Put(ctx context.Context, key string, data []byte) error {
return fmt.Errorf("unsupported Put of %q = %q", key, data)
}
func (f cacheGetFunc) Delete(ctx context.Context, key string) error {
return fmt.Errorf("unsupported Delete of %q", key)
}
func TestManagerGetCertificateBogusSNI(t *testing.T) {
m := Manager{
Prompt: AcceptTOS,
Cache: cacheGetFunc(func(ctx context.Context, key string) ([]byte, error) {
return nil, fmt.Errorf("cache.Get of %s", key)
}),
}
tests := []struct {
name string
wantErr string
}{
{"foo.com", "cache.Get of foo.com"},
{"foo.com.", "cache.Get of foo.com"},
{`a\b.com`, "acme/autocert: server name contains invalid character"},
{`a/b.com`, "acme/autocert: server name contains invalid character"},
{"", "acme/autocert: missing server name"},
{"foo", "acme/autocert: server name component count invalid"},
{".foo", "acme/autocert: server name component count invalid"},
{"foo.", "acme/autocert: server name component count invalid"},
{"fo.o", "cache.Get of fo.o"},
}
for _, tt := range tests {
_, err := m.GetCertificate(&tls.ClientHelloInfo{ServerName: tt.name})
got := fmt.Sprint(err)
if got != tt.wantErr {
t.Errorf("GetCertificate(SNI = %q) = %q; want %q", tt.name, got, tt.wantErr)
}
}
}

View File

@@ -5,12 +5,11 @@
package autocert
import (
"context"
"errors"
"io/ioutil"
"os"
"path/filepath"
"golang.org/x/net/context"
)
// ErrCacheMiss is returned when a certificate is not found in cache.
@@ -78,12 +77,13 @@ func (d DirCache) Put(ctx context.Context, name string, data []byte) error {
if tmp, err = d.writeTempFile(name, data); err != nil {
return
}
// prevent overwriting the file if the context was cancelled
if ctx.Err() != nil {
return // no need to set err
select {
case <-ctx.Done():
// Don't overwrite the file if the context was canceled.
default:
newName := filepath.Join(string(d), name)
err = os.Rename(tmp, newName)
}
name = filepath.Join(string(d), name)
err = os.Rename(tmp, name)
}()
select {
case <-ctx.Done():

View File

@@ -5,13 +5,12 @@
package autocert
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"golang.org/x/net/context"
)
// make sure DirCache satisfies Cache interface

View File

@@ -0,0 +1,34 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package autocert_test
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"golang.org/x/crypto/acme/autocert"
)
func ExampleNewListener() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, TLS user! Your config: %+v", r.TLS)
})
log.Fatal(http.Serve(autocert.NewListener("example.com"), mux))
}
func ExampleManager() {
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.org"),
}
s := &http.Server{
Addr: ":https",
TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
}
s.ListenAndServeTLS("", "")
}

153
vendor/golang.org/x/crypto/acme/autocert/listener.go generated vendored Normal file
View File

@@ -0,0 +1,153 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package autocert
import (
"crypto/tls"
"log"
"net"
"os"
"path/filepath"
"runtime"
"time"
)
// NewListener returns a net.Listener that listens on the standard TLS
// port (443) on all interfaces and returns *tls.Conn connections with
// LetsEncrypt certificates for the provided domain or domains.
//
// It enables one-line HTTPS servers:
//
// log.Fatal(http.Serve(autocert.NewListener("example.com"), handler))
//
// NewListener is a convenience function for a common configuration.
// More complex or custom configurations can use the autocert.Manager
// type instead.
//
// Use of this function implies acceptance of the LetsEncrypt Terms of
// Service. If domains is not empty, the provided domains are passed
// to HostWhitelist. If domains is empty, the listener will do
// LetsEncrypt challenges for any requested domain, which is not
// recommended.
//
// Certificates are cached in a "golang-autocert" directory under an
// operating system-specific cache or temp directory. This may not
// be suitable for servers spanning multiple machines.
//
// The returned Listener also enables TCP keep-alives on the accepted
// connections. The returned *tls.Conn are returned before their TLS
// handshake has completed.
func NewListener(domains ...string) net.Listener {
m := &Manager{
Prompt: AcceptTOS,
}
if len(domains) > 0 {
m.HostPolicy = HostWhitelist(domains...)
}
dir := cacheDir()
if err := os.MkdirAll(dir, 0700); err != nil {
log.Printf("warning: autocert.NewListener not using a cache: %v", err)
} else {
m.Cache = DirCache(dir)
}
return m.Listener()
}
// Listener listens on the standard TLS port (443) on all interfaces
// and returns a net.Listener returning *tls.Conn connections.
//
// The returned Listener also enables TCP keep-alives on the accepted
// connections. The returned *tls.Conn are returned before their TLS
// handshake has completed.
//
// Unlike NewListener, it is the caller's responsibility to initialize
// the Manager m's Prompt, Cache, HostPolicy, and other desired options.
func (m *Manager) Listener() net.Listener {
ln := &listener{
m: m,
conf: &tls.Config{
GetCertificate: m.GetCertificate, // bonus: panic on nil m
},
}
ln.tcpListener, ln.tcpListenErr = net.Listen("tcp", ":443")
return ln
}
type listener struct {
m *Manager
conf *tls.Config
tcpListener net.Listener
tcpListenErr error
}
func (ln *listener) Accept() (net.Conn, error) {
if ln.tcpListenErr != nil {
return nil, ln.tcpListenErr
}
conn, err := ln.tcpListener.Accept()
if err != nil {
return nil, err
}
tcpConn := conn.(*net.TCPConn)
// Because Listener is a convenience function, help out with
// this too. This is not possible for the caller to set once
// we return a *tcp.Conn wrapping an inaccessible net.Conn.
// If callers don't want this, they can do things the manual
// way and tweak as needed. But this is what net/http does
// itself, so copy that. If net/http changes, we can change
// here too.
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(3 * time.Minute)
return tls.Server(tcpConn, ln.conf), nil
}
func (ln *listener) Addr() net.Addr {
if ln.tcpListener != nil {
return ln.tcpListener.Addr()
}
// net.Listen failed. Return something non-nil in case callers
// call Addr before Accept:
return &net.TCPAddr{IP: net.IP{0, 0, 0, 0}, Port: 443}
}
func (ln *listener) Close() error {
if ln.tcpListenErr != nil {
return ln.tcpListenErr
}
return ln.tcpListener.Close()
}
func homeDir() string {
if runtime.GOOS == "windows" {
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
}
if h := os.Getenv("HOME"); h != "" {
return h
}
return "/"
}
func cacheDir() string {
const base = "golang-autocert"
switch runtime.GOOS {
case "darwin":
return filepath.Join(homeDir(), "Library", "Caches", base)
case "windows":
for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} {
if v := os.Getenv(ev); v != "" {
return filepath.Join(v, base)
}
}
// Worst case:
return filepath.Join(homeDir(), base)
}
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
return filepath.Join(xdg, base)
}
return filepath.Join(homeDir(), ".cache", base)
}

View File

@@ -5,15 +5,14 @@
package autocert
import (
"context"
"crypto"
"sync"
"time"
"golang.org/x/net/context"
)
// maxRandRenew is a maximum deviation from Manager.RenewBefore.
const maxRandRenew = time.Hour
// renewJitter is the maximum deviation from Manager.RenewBefore.
const renewJitter = time.Hour
// domainRenewal tracks the state used by the periodic timers
// renewing a single domain's cert.
@@ -65,7 +64,7 @@ func (dr *domainRenewal) renew() {
// TODO: rotate dr.key at some point?
next, err := dr.do(ctx)
if err != nil {
next = maxRandRenew / 2
next = renewJitter / 2
next += time.Duration(pseudoRand.int63n(int64(next)))
}
dr.timer = time.AfterFunc(next, dr.renew)
@@ -83,9 +82,9 @@ func (dr *domainRenewal) renew() {
func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
// a race is likely unavoidable in a distributed environment
// but we try nonetheless
if tlscert, err := dr.m.cacheGet(dr.domain); err == nil {
if tlscert, err := dr.m.cacheGet(ctx, dr.domain); err == nil {
next := dr.next(tlscert.Leaf.NotAfter)
if next > dr.m.renewBefore()+maxRandRenew {
if next > dr.m.renewBefore()+renewJitter {
return next, nil
}
}
@@ -103,7 +102,7 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
if err != nil {
return 0, err
}
dr.m.cachePut(dr.domain, tlscert)
dr.m.cachePut(ctx, dr.domain, tlscert)
dr.m.stateMu.Lock()
defer dr.m.stateMu.Unlock()
// m.state is guaranteed to be non-nil at this point
@@ -114,7 +113,7 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
func (dr *domainRenewal) next(expiry time.Time) time.Duration {
d := expiry.Sub(timeNow()) - dr.m.renewBefore()
// add a bit of randomness to renew deadline
n := pseudoRand.int63n(int64(maxRandRenew))
n := pseudoRand.int63n(int64(renewJitter))
d -= time.Duration(n)
if d < 0 {
return 0

View File

@@ -5,6 +5,7 @@
package autocert
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
@@ -31,7 +32,7 @@ func TestRenewalNext(t *testing.T) {
expiry time.Time
min, max time.Duration
}{
{now.Add(90 * 24 * time.Hour), 83*24*time.Hour - maxRandRenew, 83 * 24 * time.Hour},
{now.Add(90 * 24 * time.Hour), 83*24*time.Hour - renewJitter, 83 * 24 * time.Hour},
{now.Add(time.Hour), 0, 1},
{now, 0, 1},
{now.Add(-time.Hour), 0, 1},
@@ -127,7 +128,7 @@ func TestRenewFromCache(t *testing.T) {
t.Fatal(err)
}
tlscert := &tls.Certificate{PrivateKey: key, Certificate: [][]byte{cert}}
if err := man.cachePut(domain, tlscert); err != nil {
if err := man.cachePut(context.Background(), domain, tlscert); err != nil {
t.Fatal(err)
}
@@ -151,7 +152,7 @@ func TestRenewFromCache(t *testing.T) {
// ensure the new cert is cached
after := time.Now().Add(future)
tlscert, err := man.cacheGet(domain)
tlscert, err := man.cacheGet(context.Background(), domain)
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}