...

Source file src/golang.org/x/crypto/acme/http_test.go

Documentation: golang.org/x/crypto/acme

     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"
    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:       io.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  
    90  		head, err := decodeJWSHead(r.Body)
    91  		switch {
    92  		case err != nil:
    93  			t.Errorf("decodeJWSHead: %v", err)
    94  		case head.Nonce == "":
    95  			t.Error("head.Nonce is empty")
    96  		case head.Nonce == "nonce1":
    97  			// Return a badNonce error to force the call to retry.
    98  			w.Header().Set("Retry-After", "0")
    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  
   109  	client := &Client{
   110  		Key:          testKey,
   111  		DirectoryURL: ts.URL,
   112  		dir:          &Directory{AuthzURL: ts.URL},
   113  	}
   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 != 3 {
   119  		t.Errorf("total requests count: %d; want 3", count)
   120  	}
   121  }
   122  
   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  
   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  }
   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(`{"newOrder": "sure"}`))
   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  }
   241  
   242  func TestAccountKidLoop(t *testing.T) {
   243  	// if Client.postNoRetry is called with a nil key argument
   244  	// then Client.Key must be set, otherwise we fall into an
   245  	// infinite loop (which also causes a deadlock).
   246  	client := &Client{dir: &Directory{OrderURL: ":)"}}
   247  	_, _, err := client.postNoRetry(context.Background(), nil, "", nil)
   248  	if err == nil {
   249  		t.Fatal("Client.postNoRetry didn't fail with a nil key")
   250  	}
   251  	expected := "acme: Client.Key must be populated to make POST requests"
   252  	if err.Error() != expected {
   253  		t.Fatalf("Unexpected error returned: wanted %q, got %q", expected, err.Error())
   254  	}
   255  }
   256  

View as plain text