Alex Vaghin | df8d471 | 2018-04-26 21:26:21 +0200 | [diff] [blame] | 1 | // Copyright 2018 The Go Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style |
| 3 | // license that can be found in the LICENSE file. |
| 4 | |
| 5 | package acme |
| 6 | |
| 7 | import ( |
| 8 | "context" |
| 9 | "fmt" |
| 10 | "io/ioutil" |
| 11 | "net/http" |
| 12 | "net/http/httptest" |
| 13 | "reflect" |
| 14 | "strings" |
| 15 | "testing" |
| 16 | "time" |
| 17 | ) |
| 18 | |
| 19 | func TestDefaultBackoff(t *testing.T) { |
| 20 | tt := []struct { |
| 21 | nretry int |
| 22 | retryAfter string // Retry-After header |
| 23 | out time.Duration // expected min; max = min + jitter |
| 24 | }{ |
| 25 | {-1, "", time.Second}, // verify the lower bound is 1 |
| 26 | {0, "", time.Second}, // verify the lower bound is 1 |
| 27 | {100, "", 10 * time.Second}, // verify the ceiling |
| 28 | {1, "3600", time.Hour}, // verify the header value is used |
| 29 | {1, "", 1 * time.Second}, |
| 30 | {2, "", 2 * time.Second}, |
| 31 | {3, "", 4 * time.Second}, |
| 32 | {4, "", 8 * time.Second}, |
| 33 | } |
| 34 | for i, test := range tt { |
| 35 | r := httptest.NewRequest("GET", "/", nil) |
| 36 | resp := &http.Response{Header: http.Header{}} |
| 37 | if test.retryAfter != "" { |
| 38 | resp.Header.Set("Retry-After", test.retryAfter) |
| 39 | } |
| 40 | d := defaultBackoff(test.nretry, r, resp) |
| 41 | max := test.out + time.Second // + max jitter |
| 42 | if d < test.out || max < d { |
| 43 | t.Errorf("%d: defaultBackoff(%v) = %v; want between %v and %v", i, test.nretry, d, test.out, max) |
| 44 | } |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | func TestErrorResponse(t *testing.T) { |
| 49 | s := `{ |
| 50 | "status": 400, |
| 51 | "type": "urn:acme:error:xxx", |
| 52 | "detail": "text" |
| 53 | }` |
| 54 | res := &http.Response{ |
| 55 | StatusCode: 400, |
| 56 | Status: "400 Bad Request", |
| 57 | Body: ioutil.NopCloser(strings.NewReader(s)), |
| 58 | Header: http.Header{"X-Foo": {"bar"}}, |
| 59 | } |
| 60 | err := responseError(res) |
| 61 | v, ok := err.(*Error) |
| 62 | if !ok { |
| 63 | t.Fatalf("err = %+v (%T); want *Error type", err, err) |
| 64 | } |
| 65 | if v.StatusCode != 400 { |
| 66 | t.Errorf("v.StatusCode = %v; want 400", v.StatusCode) |
| 67 | } |
| 68 | if v.ProblemType != "urn:acme:error:xxx" { |
| 69 | t.Errorf("v.ProblemType = %q; want urn:acme:error:xxx", v.ProblemType) |
| 70 | } |
| 71 | if v.Detail != "text" { |
| 72 | t.Errorf("v.Detail = %q; want text", v.Detail) |
| 73 | } |
| 74 | if !reflect.DeepEqual(v.Header, res.Header) { |
| 75 | t.Errorf("v.Header = %+v; want %+v", v.Header, res.Header) |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | func TestPostWithRetries(t *testing.T) { |
| 80 | var count int |
| 81 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 82 | count++ |
| 83 | w.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", count)) |
| 84 | if r.Method == "HEAD" { |
| 85 | // We expect the client to do 2 head requests to fetch |
| 86 | // nonces, one to start and another after getting badNonce |
| 87 | return |
| 88 | } |
| 89 | |
Alex Vaghin | a832865 | 2019-08-28 23:16:55 +0200 | [diff] [blame] | 90 | head, err := decodeJWSHead(r.Body) |
Alex Vaghin | a49355c | 2018-06-20 11:14:27 +0200 | [diff] [blame] | 91 | switch { |
| 92 | case err != nil: |
Alex Vaghin | df8d471 | 2018-04-26 21:26:21 +0200 | [diff] [blame] | 93 | t.Errorf("decodeJWSHead: %v", err) |
Alex Vaghin | a49355c | 2018-06-20 11:14:27 +0200 | [diff] [blame] | 94 | case head.Nonce == "": |
Alex Vaghin | df8d471 | 2018-04-26 21:26:21 +0200 | [diff] [blame] | 95 | t.Error("head.Nonce is empty") |
Alex Vaghin | a49355c | 2018-06-20 11:14:27 +0200 | [diff] [blame] | 96 | case head.Nonce == "nonce1": |
| 97 | // Return a badNonce error to force the call to retry. |
| 98 | w.Header().Set("Retry-After", "0") |
Alex Vaghin | df8d471 | 2018-04-26 21:26:21 +0200 | [diff] [blame] | 99 | w.WriteHeader(http.StatusBadRequest) |
| 100 | w.Write([]byte(`{"type":"urn:ietf:params:acme:error:badNonce"}`)) |
| 101 | return |
| 102 | } |
| 103 | // Make client.Authorize happy; we're not testing its result. |
| 104 | w.WriteHeader(http.StatusCreated) |
| 105 | w.Write([]byte(`{"status":"valid"}`)) |
| 106 | })) |
| 107 | defer ts.Close() |
| 108 | |
Alex Vaghin | a4c6cb3 | 2019-02-12 18:56:05 +0100 | [diff] [blame] | 109 | client := &Client{ |
| 110 | Key: testKey, |
| 111 | DirectoryURL: ts.URL, |
| 112 | dir: &Directory{AuthzURL: ts.URL}, |
| 113 | } |
Alex Vaghin | df8d471 | 2018-04-26 21:26:21 +0200 | [diff] [blame] | 114 | // This call will fail with badNonce, causing a retry |
| 115 | if _, err := client.Authorize(context.Background(), "example.com"); err != nil { |
| 116 | t.Errorf("client.Authorize 1: %v", err) |
| 117 | } |
| 118 | if count != 4 { |
| 119 | t.Errorf("total requests count: %d; want 4", count) |
| 120 | } |
| 121 | } |
| 122 | |
Alex Vaghin | a49355c | 2018-06-20 11:14:27 +0200 | [diff] [blame] | 123 | func TestRetryErrorType(t *testing.T) { |
| 124 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 125 | w.Header().Set("Replay-Nonce", "nonce") |
| 126 | w.WriteHeader(http.StatusTooManyRequests) |
| 127 | w.Write([]byte(`{"type":"rateLimited"}`)) |
| 128 | })) |
| 129 | defer ts.Close() |
| 130 | |
| 131 | client := &Client{ |
| 132 | Key: testKey, |
| 133 | RetryBackoff: func(n int, r *http.Request, res *http.Response) time.Duration { |
| 134 | // Do no retries. |
| 135 | return 0 |
| 136 | }, |
| 137 | dir: &Directory{AuthzURL: ts.URL}, |
| 138 | } |
| 139 | |
| 140 | t.Run("post", func(t *testing.T) { |
| 141 | testRetryErrorType(t, func() error { |
| 142 | _, err := client.Authorize(context.Background(), "example.com") |
| 143 | return err |
| 144 | }) |
| 145 | }) |
| 146 | t.Run("get", func(t *testing.T) { |
| 147 | testRetryErrorType(t, func() error { |
| 148 | _, err := client.GetAuthorization(context.Background(), ts.URL) |
| 149 | return err |
| 150 | }) |
| 151 | }) |
| 152 | } |
| 153 | |
| 154 | func testRetryErrorType(t *testing.T, callClient func() error) { |
| 155 | t.Helper() |
| 156 | err := callClient() |
| 157 | if err == nil { |
| 158 | t.Fatal("client.Authorize returned nil error") |
| 159 | } |
| 160 | acmeErr, ok := err.(*Error) |
| 161 | if !ok { |
| 162 | t.Fatalf("err is %v (%T); want *Error", err, err) |
| 163 | } |
| 164 | if acmeErr.StatusCode != http.StatusTooManyRequests { |
| 165 | t.Errorf("acmeErr.StatusCode = %d; want %d", acmeErr.StatusCode, http.StatusTooManyRequests) |
| 166 | } |
| 167 | if acmeErr.ProblemType != "rateLimited" { |
| 168 | t.Errorf("acmeErr.ProblemType = %q; want 'rateLimited'", acmeErr.ProblemType) |
| 169 | } |
| 170 | } |
| 171 | |
Alex Vaghin | df8d471 | 2018-04-26 21:26:21 +0200 | [diff] [blame] | 172 | func TestRetryBackoffArgs(t *testing.T) { |
| 173 | const resCode = http.StatusInternalServerError |
| 174 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 175 | w.Header().Set("Replay-Nonce", "test-nonce") |
| 176 | w.WriteHeader(resCode) |
| 177 | })) |
| 178 | defer ts.Close() |
| 179 | |
| 180 | // Canceled in backoff. |
| 181 | ctx, cancel := context.WithCancel(context.Background()) |
| 182 | |
| 183 | var nretry int |
| 184 | backoff := func(n int, r *http.Request, res *http.Response) time.Duration { |
| 185 | nretry++ |
| 186 | if n != nretry { |
| 187 | t.Errorf("n = %d; want %d", n, nretry) |
| 188 | } |
| 189 | if nretry == 3 { |
| 190 | cancel() |
| 191 | } |
| 192 | |
| 193 | if r == nil { |
| 194 | t.Error("r is nil") |
| 195 | } |
| 196 | if res.StatusCode != resCode { |
| 197 | t.Errorf("res.StatusCode = %d; want %d", res.StatusCode, resCode) |
| 198 | } |
| 199 | return time.Millisecond |
| 200 | } |
| 201 | |
| 202 | client := &Client{ |
| 203 | Key: testKey, |
| 204 | RetryBackoff: backoff, |
| 205 | dir: &Directory{AuthzURL: ts.URL}, |
| 206 | } |
| 207 | if _, err := client.Authorize(ctx, "example.com"); err == nil { |
| 208 | t.Error("err is nil") |
| 209 | } |
| 210 | if nretry != 3 { |
| 211 | t.Errorf("nretry = %d; want 3", nretry) |
| 212 | } |
| 213 | } |
Filippo Valsorda | cc06ce4 | 2019-06-20 17:50:31 -0400 | [diff] [blame] | 214 | |
| 215 | func TestUserAgent(t *testing.T) { |
| 216 | for _, custom := range []string{"", "CUSTOM_UA"} { |
| 217 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 218 | t.Log(r.UserAgent()) |
| 219 | if s := "golang.org/x/crypto/acme"; !strings.Contains(r.UserAgent(), s) { |
| 220 | t.Errorf("expected User-Agent to contain %q, got %q", s, r.UserAgent()) |
| 221 | } |
| 222 | if !strings.Contains(r.UserAgent(), custom) { |
| 223 | t.Errorf("expected User-Agent to contain %q, got %q", custom, r.UserAgent()) |
| 224 | } |
| 225 | |
| 226 | w.WriteHeader(http.StatusOK) |
| 227 | w.Write([]byte(`{}`)) |
| 228 | })) |
| 229 | defer ts.Close() |
| 230 | |
| 231 | client := &Client{ |
| 232 | Key: testKey, |
| 233 | DirectoryURL: ts.URL, |
| 234 | UserAgent: custom, |
| 235 | } |
| 236 | if _, err := client.Discover(context.Background()); err != nil { |
| 237 | t.Errorf("client.Discover: %v", err) |
| 238 | } |
| 239 | } |
| 240 | } |