acme: try to fetch nonce from directory first

The change should reduce resource quota consumed by the client overall.

Instead of sending HEAD to an ACME resource URL to get a new nonce,
the Client will now try to fetch it from the Directory URL first
and only then from the ACME resource URL if the former fails.

This builds up on an abandoned https://golang.org/cl/34623,
only this time with a fallback to the original behaviour.

Change-Id: I6e75c0e524c4bc751f3a651b290c0ac2493e0628
Reviewed-on: https://go-review.googlesource.com/c/162057
Run-TryBot: Alex Vaghin <ddos@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Alex Vaghin 2019-02-12 18:56:05 +01:00
Родитель 74369b46fc
Коммит a4c6cb3142
3 изменённых файлов: 92 добавлений и 12 удалений

Просмотреть файл

@ -128,11 +128,7 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) {
return *c.dir, nil return *c.dir, nil
} }
dirURL := c.DirectoryURL res, err := c.get(ctx, c.directoryURL(), wantStatus(http.StatusOK))
if dirURL == "" {
dirURL = LetsEncryptURL
}
res, err := c.get(ctx, dirURL, wantStatus(http.StatusOK))
if err != nil { if err != nil {
return Directory{}, err return Directory{}, err
} }
@ -165,6 +161,13 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) {
return *c.dir, nil return *c.dir, nil
} }
func (c *Client) directoryURL() string {
if c.DirectoryURL != "" {
return c.DirectoryURL
}
return LetsEncryptURL
}
// CreateCert requests a new certificate using the Certificate Signing Request csr encoded in DER format. // CreateCert requests a new certificate using the Certificate Signing Request csr encoded in DER format.
// The exp argument indicates the desired certificate validity duration. CA may issue a certificate // The exp argument indicates the desired certificate validity duration. CA may issue a certificate
// with a different duration. // with a different duration.
@ -711,12 +714,18 @@ func (c *Client) doReg(ctx context.Context, url string, typ string, acct *Accoun
} }
// popNonce returns a nonce value previously stored with c.addNonce // popNonce returns a nonce value previously stored with c.addNonce
// or fetches a fresh one from the given URL. // or fetches a fresh one from a URL by issuing a HEAD request.
// It first tries c.directoryURL() and then the provided url if the former fails.
func (c *Client) popNonce(ctx context.Context, url string) (string, error) { func (c *Client) popNonce(ctx context.Context, url string) (string, error) {
c.noncesMu.Lock() c.noncesMu.Lock()
defer c.noncesMu.Unlock() defer c.noncesMu.Unlock()
if len(c.nonces) == 0 { if len(c.nonces) == 0 {
return c.fetchNonce(ctx, url) dirURL := c.directoryURL()
v, err := c.fetchNonce(ctx, dirURL)
if err != nil && url != dirURL {
v, err = c.fetchNonce(ctx, url)
}
return v, err
} }
var nonce string var nonce string
for nonce = range c.nonces { for nonce = range c.nonces {

Просмотреть файл

@ -75,6 +75,7 @@ func TestDiscover(t *testing.T) {
) )
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Header().Set("Replay-Nonce", "testnonce")
fmt.Fprintf(w, `{ fmt.Fprintf(w, `{
"new-reg": %q, "new-reg": %q,
"new-authz": %q, "new-authz": %q,
@ -100,6 +101,9 @@ func TestDiscover(t *testing.T) {
if dir.RevokeURL != revoke { if dir.RevokeURL != revoke {
t.Errorf("dir.RevokeURL = %q; want %q", dir.RevokeURL, revoke) t.Errorf("dir.RevokeURL = %q; want %q", dir.RevokeURL, revoke)
} }
if _, exist := c.nonces["testnonce"]; !exist {
t.Errorf("c.nonces = %q; want 'testnonce' in the map", c.nonces)
}
} }
func TestRegister(t *testing.T) { func TestRegister(t *testing.T) {
@ -147,7 +151,11 @@ func TestRegister(t *testing.T) {
return false return false
} }
c := Client{Key: testKeyEC, dir: &Directory{RegURL: ts.URL}} c := Client{
Key: testKeyEC,
DirectoryURL: ts.URL,
dir: &Directory{RegURL: ts.URL},
}
a := &Account{Contact: contacts} a := &Account{Contact: contacts}
var err error var err error
if a, err = c.Register(context.Background(), a, prompt); err != nil { if a, err = c.Register(context.Background(), a, prompt); err != nil {
@ -351,7 +359,11 @@ func TestAuthorize(t *testing.T) {
auth *Authorization auth *Authorization
err error err error
) )
cl := Client{Key: testKeyEC, dir: &Directory{AuthzURL: ts.URL}} cl := Client{
Key: testKeyEC,
DirectoryURL: ts.URL,
dir: &Directory{AuthzURL: ts.URL},
}
switch test.typ { switch test.typ {
case "dns": case "dns":
auth, err = cl.Authorize(context.Background(), test.value) auth, err = cl.Authorize(context.Background(), test.value)
@ -422,7 +434,11 @@ func TestAuthorizeValid(t *testing.T) {
w.Write([]byte(`{"status":"valid"}`)) w.Write([]byte(`{"status":"valid"}`))
})) }))
defer ts.Close() defer ts.Close()
client := Client{Key: testKey, dir: &Directory{AuthzURL: ts.URL}} client := Client{
Key: testKey,
DirectoryURL: ts.URL,
dir: &Directory{AuthzURL: ts.URL},
}
_, err := client.Authorize(context.Background(), "example.com") _, err := client.Authorize(context.Background(), "example.com")
if err != nil { if err != nil {
t.Errorf("err = %v", err) t.Errorf("err = %v", err)
@ -1037,6 +1053,53 @@ func TestNonce_fetchError(t *testing.T) {
} }
} }
func TestNonce_popWhenEmpty(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "HEAD" {
t.Errorf("r.Method = %q; want HEAD", r.Method)
}
switch r.URL.Path {
case "/dir-with-nonce":
w.Header().Set("Replay-Nonce", "dirnonce")
case "/new-nonce":
w.Header().Set("Replay-Nonce", "newnonce")
case "/dir-no-nonce", "/empty":
// No nonce in the header.
default:
t.Errorf("Unknown URL: %s", r.URL)
}
}))
defer ts.Close()
ctx := context.Background()
tt := []struct {
dirURL, popURL, nonce string
wantOK bool
}{
{ts.URL + "/dir-with-nonce", ts.URL + "/new-nonce", "dirnonce", true},
{ts.URL + "/dir-no-nonce", ts.URL + "/new-nonce", "newnonce", true},
{ts.URL + "/dir-no-nonce", ts.URL + "/empty", "", false},
}
for _, test := range tt {
t.Run(fmt.Sprintf("nonce:%s wantOK:%v", test.nonce, test.wantOK), func(t *testing.T) {
c := Client{DirectoryURL: test.dirURL}
v, err := c.popNonce(ctx, test.popURL)
if !test.wantOK {
if err == nil {
t.Fatalf("c.popNonce(%q) returned nil error", test.popURL)
}
return
}
if err != nil {
t.Fatalf("c.popNonce(%q): %v", test.popURL, err)
}
if v != test.nonce {
t.Errorf("c.popNonce(%q) = %q; want %q", test.popURL, v, test.nonce)
}
})
}
}
func TestNonce_postJWS(t *testing.T) { func TestNonce_postJWS(t *testing.T) {
var count int var count int
seen := make(map[string]bool) seen := make(map[string]bool)
@ -1070,7 +1133,11 @@ func TestNonce_postJWS(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
client := Client{Key: testKey, dir: &Directory{AuthzURL: ts.URL}} client := Client{
Key: testKey,
DirectoryURL: ts.URL, // nonces are fetched from here first
dir: &Directory{AuthzURL: ts.URL},
}
if _, err := client.Authorize(context.Background(), "example.com"); err != nil { if _, err := client.Authorize(context.Background(), "example.com"); err != nil {
t.Errorf("client.Authorize 1: %v", err) t.Errorf("client.Authorize 1: %v", err)
} }

Просмотреть файл

@ -106,7 +106,11 @@ func TestPostWithRetries(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
client := &Client{Key: testKey, dir: &Directory{AuthzURL: ts.URL}} client := &Client{
Key: testKey,
DirectoryURL: ts.URL,
dir: &Directory{AuthzURL: ts.URL},
}
// This call will fail with badNonce, causing a retry // This call will fail with badNonce, causing a retry
if _, err := client.Authorize(context.Background(), "example.com"); err != nil { if _, err := client.Authorize(context.Background(), "example.com"); err != nil {
t.Errorf("client.Authorize 1: %v", err) t.Errorf("client.Authorize 1: %v", err)