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 <bradfitz@golang.org>
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 @@
 	// 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 @@
 			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 @@
 
 // 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 @@
 	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 @@
 		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
+	}
+}