From cc06ce4a13d484c0101a9e92913248488a75786d Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Thu, 20 Jun 2019 17:50:31 -0400 Subject: [PATCH] acme: send User-Agent and add Client.UserAgent This is useful to CAs, to identify and reach out to problematic clients. Fixes golang/go#24496 Change-Id: I944fc8178c8fa8acaf3854e9c125d3af0364a4fb Reviewed-on: https://go-review.googlesource.com/c/crypto/+/183267 Reviewed-by: Brad Fitzpatrick --- acme/acme.go | 7 +++++++ acme/autocert/autocert.go | 3 +++ acme/http.go | 18 ++++++++++++++++++ acme/http_test.go | 27 +++++++++++++++++++++++++++ acme/version_go112.go | 27 +++++++++++++++++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 acme/version_go112.go diff --git a/acme/acme.go b/acme/acme.go index 00ee955..fa365b7 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -109,6 +109,13 @@ type Client struct { // The jitter is a random value up to 1 second. RetryBackoff func(n int, r *http.Request, resp *http.Response) time.Duration + // UserAgent is prepended to the User-Agent header sent to the ACME server, + // which by default is this package's name and version. + // + // Reusable libraries and tools in particular should set this value to be + // identifiable by the server, in case they are causing issues. + UserAgent string + dirMu sync.Mutex // guards writes to dir dir *Directory // cached result of Client's Discover method diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go index e562609..70ab355 100644 --- a/acme/autocert/autocert.go +++ b/acme/autocert/autocert.go @@ -980,6 +980,9 @@ func (m *Manager) acmeClient(ctx context.Context) (*acme.Client, error) { return nil, err } } + if client.UserAgent == "" { + client.UserAgent = "autocert" + } var contact []string if m.Email != "" { contact = []string{"mailto:" + m.Email} diff --git a/acme/http.go b/acme/http.go index a43ce6a..600d579 100644 --- a/acme/http.go +++ b/acme/http.go @@ -219,6 +219,7 @@ func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string, // doNoRetry issues a request req, replacing its context (if any) with ctx. func (c *Client) doNoRetry(ctx context.Context, req *http.Request) (*http.Response, error) { + req.Header.Set("User-Agent", c.userAgent()) res, err := c.httpClient().Do(req.WithContext(ctx)) if err != nil { select { @@ -243,6 +244,23 @@ func (c *Client) httpClient() *http.Client { return http.DefaultClient } +// packageVersion is the version of the module that contains this package, for +// sending as part of the User-Agent header. It's set in version_go112.go. +var packageVersion string + +// userAgent returns the User-Agent header value. It includes the package name, +// the module version (if available), and the c.UserAgent value (if set). +func (c *Client) userAgent() string { + ua := "golang.org/x/crypto/acme" + if packageVersion != "" { + ua += "@" + packageVersion + } + if c.UserAgent != "" { + ua = c.UserAgent + " " + ua + } + return ua +} + // isBadNonce reports whether err is an ACME "badnonce" error. func isBadNonce(err error) bool { // According to the spec badNonce is urn:ietf:params:acme:error:badNonce. diff --git a/acme/http_test.go b/acme/http_test.go index 2748995..a5dc35f 100644 --- a/acme/http_test.go +++ b/acme/http_test.go @@ -211,3 +211,30 @@ func TestRetryBackoffArgs(t *testing.T) { t.Errorf("nretry = %d; want 3", nretry) } } + +func TestUserAgent(t *testing.T) { + for _, custom := range []string{"", "CUSTOM_UA"} { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Log(r.UserAgent()) + if s := "golang.org/x/crypto/acme"; !strings.Contains(r.UserAgent(), s) { + t.Errorf("expected User-Agent to contain %q, got %q", s, r.UserAgent()) + } + if !strings.Contains(r.UserAgent(), custom) { + t.Errorf("expected User-Agent to contain %q, got %q", custom, r.UserAgent()) + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{}`)) + })) + defer ts.Close() + + client := &Client{ + Key: testKey, + DirectoryURL: ts.URL, + UserAgent: custom, + } + if _, err := client.Discover(context.Background()); err != nil { + t.Errorf("client.Discover: %v", err) + } + } +} diff --git a/acme/version_go112.go b/acme/version_go112.go new file mode 100644 index 0000000..b58f245 --- /dev/null +++ b/acme/version_go112.go @@ -0,0 +1,27 @@ +// Copyright 2019 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. + +// +build go1.12 + +package acme + +import "runtime/debug" + +func init() { + // Set packageVersion if the binary was built in modules mode and x/crypto + // was not replaced with a different module. + info, ok := debug.ReadBuildInfo() + if !ok { + return + } + for _, m := range info.Deps { + if m.Path != "golang.org/x/crypto" { + continue + } + if m.Replace == nil { + packageVersion = m.Version + } + break + } +}