Currently, the renewal flow will check the cache before renewing to make sure it is actually necessary. This change modifies this flow to update the local state so the cached cert is actually used by the manager. Fixes golang/go#22960 Change-Id: I16668e8098616190938ee52858294b59bc1a5160 Reviewed-on: https://go-review.googlesource.com/89995 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
142 lines
3.6 KiB
Go
142 lines
3.6 KiB
Go
// Copyright 2016 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 (
|
|
"context"
|
|
"crypto"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// 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.
|
|
type domainRenewal struct {
|
|
m *Manager
|
|
domain string
|
|
key crypto.Signer
|
|
|
|
timerMu sync.Mutex
|
|
timer *time.Timer
|
|
}
|
|
|
|
// start starts a cert renewal timer at the time
|
|
// defined by the certificate expiration time exp.
|
|
//
|
|
// If the timer is already started, calling start is a noop.
|
|
func (dr *domainRenewal) start(exp time.Time) {
|
|
dr.timerMu.Lock()
|
|
defer dr.timerMu.Unlock()
|
|
if dr.timer != nil {
|
|
return
|
|
}
|
|
dr.timer = time.AfterFunc(dr.next(exp), dr.renew)
|
|
}
|
|
|
|
// stop stops the cert renewal timer.
|
|
// If the timer is already stopped, calling stop is a noop.
|
|
func (dr *domainRenewal) stop() {
|
|
dr.timerMu.Lock()
|
|
defer dr.timerMu.Unlock()
|
|
if dr.timer == nil {
|
|
return
|
|
}
|
|
dr.timer.Stop()
|
|
dr.timer = nil
|
|
}
|
|
|
|
// renew is called periodically by a timer.
|
|
// The first renew call is kicked off by dr.start.
|
|
func (dr *domainRenewal) renew() {
|
|
dr.timerMu.Lock()
|
|
defer dr.timerMu.Unlock()
|
|
if dr.timer == nil {
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
defer cancel()
|
|
// TODO: rotate dr.key at some point?
|
|
next, err := dr.do(ctx)
|
|
if err != nil {
|
|
next = renewJitter / 2
|
|
next += time.Duration(pseudoRand.int63n(int64(next)))
|
|
}
|
|
dr.timer = time.AfterFunc(next, dr.renew)
|
|
testDidRenewLoop(next, err)
|
|
}
|
|
|
|
// updateState locks and replaces the relevant Manager.state item with the given
|
|
// state. It additionally updates dr.key with the given state's key.
|
|
func (dr *domainRenewal) updateState(state *certState) {
|
|
dr.m.stateMu.Lock()
|
|
defer dr.m.stateMu.Unlock()
|
|
dr.key = state.key
|
|
dr.m.state[dr.domain] = state
|
|
}
|
|
|
|
// do is similar to Manager.createCert but it doesn't lock a Manager.state item.
|
|
// Instead, it requests a new certificate independently and, upon success,
|
|
// replaces dr.m.state item with a new one and updates cache for the given domain.
|
|
//
|
|
// It may lock and update the Manager.state if the expiration date of the currently
|
|
// cached cert is far enough in the future.
|
|
//
|
|
// The returned value is a time interval after which the renewal should occur again.
|
|
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(ctx, dr.domain); err == nil {
|
|
next := dr.next(tlscert.Leaf.NotAfter)
|
|
if next > dr.m.renewBefore()+renewJitter {
|
|
signer, ok := tlscert.PrivateKey.(crypto.Signer)
|
|
if ok {
|
|
state := &certState{
|
|
key: signer,
|
|
cert: tlscert.Certificate,
|
|
leaf: tlscert.Leaf,
|
|
}
|
|
dr.updateState(state)
|
|
return next, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
der, leaf, err := dr.m.authorizedCert(ctx, dr.key, dr.domain)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
state := &certState{
|
|
key: dr.key,
|
|
cert: der,
|
|
leaf: leaf,
|
|
}
|
|
tlscert, err := state.tlscert()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if err := dr.m.cachePut(ctx, dr.domain, tlscert); err != nil {
|
|
return 0, err
|
|
}
|
|
dr.updateState(state)
|
|
return dr.next(leaf.NotAfter), nil
|
|
}
|
|
|
|
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(renewJitter))
|
|
d -= time.Duration(n)
|
|
if d < 0 {
|
|
return 0
|
|
}
|
|
return d
|
|
}
|
|
|
|
var testDidRenewLoop = func(next time.Duration, err error) {}
|