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
+ }
+}