load-generator: bootstrap URLs from ACME server directory. (#4137)

Removing hard-coded paths and using the server directory to bootstrap endpoint URLs improves RFC 8555 compatibility.

This branch also updates the `github.com/letsencrypt/challtestsrv` vendored dep to the latest release. There are no upstream unit tests to run.

Updates https://github.com/letsencrypt/boulder/issues/4086 - there are still a few Pebble compatibility issues to work out. I started on what became a near total rewrite of the load-generator and decided it was best to pull out some smaller PRs and re-evaluate. I'm optimistic that stashing little bits of a Go testing/boulder focused ACME client in `test/load-generator` will one day help https://github.com/letsencrypt/boulder/issues/4127
This commit is contained in:
Daniel McCarney 2019-04-02 13:23:38 -04:00 committed by Roland Bracewell Shoemaker
parent ff3129247d
commit 7efa727289
7 changed files with 524 additions and 46 deletions

View File

@ -0,0 +1,248 @@
// Package acme provides ACME client functionality tailored to the needs of the
// load-generator. It is not a general purpose ACME client library.
package acme
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"time"
)
const (
// NewNonceEndpoint is the directory key for the newNonce endpoint.
NewNonceEndpoint Endpoint = "newNonce"
// NewAccountEndpoint is the directory key for the newAccount endpoint.
NewAccountEndpoint Endpoint = "newAccount"
// NewOrderEndpoint is the directory key for the newOrder endpoint.
NewOrderEndpoint Endpoint = "newOrder"
// RevokeCertEndpoint is the directory key for the revokeCert endpoint.
RevokeCertEndpoint Endpoint = "revokeCert"
// KeyChangeEndpoint is the directory key for the keyChange endpoint.
KeyChangeEndpoint Endpoint = "keyChange"
)
var (
// ErrEmptyDirectory is returned if NewDirectory is provided and empty directory URL.
ErrEmptyDirectory = errors.New("directoryURL must not be empty")
// ErrInvalidDirectoryURL is returned if NewDirectory is provided an invalid directory URL.
ErrInvalidDirectoryURL = errors.New("directoryURL is not a valid URL")
// ErrInvalidDirectoryHTTPCode is returned if NewDirectory is provided a directory URL
// that returns something other than HTTP Status OK to a GET request.
ErrInvalidDirectoryHTTPCode = errors.New("GET request to directoryURL did not result in HTTP Status 200")
// ErrInvalidDirectoryJSON is returned if NewDirectory is provided a directory URL
// that returns invalid JSON.
ErrInvalidDirectoryJSON = errors.New("GET request to directoryURL returned invalid JSON")
// ErrInvalidDirectoryMeta is returned if NewDirectory is provided a directory
// URL that returns a directory resource with an invalid or missing "meta" key.
ErrInvalidDirectoryMeta = errors.New(`server's directory resource had invalid or missing "meta" key`)
// ErrInvalidTermsOfSerivce is returned if NewDirectory is provided
// a directory URL that returns a directory resource with an invalid or
// missing "termsOfService" key in the "meta" map.
ErrInvalidTermsOfService = errors.New(`server's directory resource had invalid or missing "meta.termsOfService" key`)
// RequiredEndpoints is a slice of Endpoint keys that must be present in the
// ACME server's directory. The load-generator uses each of these endpoints
// and expects to be able to find a URL for each in the server's directory
// resource.
RequiredEndpoints = []Endpoint{
NewNonceEndpoint, NewAccountEndpoint,
NewOrderEndpoint, RevokeCertEndpoint,
}
)
// Endpoint represents a string key used for looking up an endpoint URL in an ACME
// server directory resource.
//
// E.g. NewOrderEndpoint -> "newOrder" -> "https://acme.example.com/acme/v1/new-order-plz"
//
// See "ACME Resource Types" registry - RFC 8555 Section 9.7.5.
type Endpoint string
// ErrMissingEndpoint is an error returned if NewDirectory is provided an ACME
// server directory URL that is missing a key for a required endpoint in the
// response JSON. See also RequiredEndpoints.
type ErrMissingEndpoint struct {
endpoint Endpoint
}
// Error returns the error message for an ErrMissingEndpoint error.
func (e ErrMissingEndpoint) Error() string {
return fmt.Sprintf(
"directoryURL JSON was missing required key for %q endpoint",
e.endpoint,
)
}
// ErrInvalidEndpointURL is an error returned if NewDirectory is provided an
// ACME server directory URL that has an invalid URL for a required endpoint.
// See also RequiredEndpoints.
type ErrInvalidEndpointURL struct {
endpoint Endpoint
value string
}
// Error returns the error message for an ErrInvalidEndpointURL error.
func (e ErrInvalidEndpointURL) Error() string {
return fmt.Sprintf(
"directoryURL JSON had invalid URL value (%q) for %q endpoint",
e.value, e.endpoint)
}
// Directory is a type for holding URLs extracted from the ACME server's
// Directory resource.
//
// See RFC 8555 Section 7.1.1 "Directory".
//
// Its public API is read-only and therefore it is safe for concurrent access.
type Directory struct {
// TermsOfService is the URL identifying the current terms of service found in
// the ACME server's directory resource's "meta" field.
TermsOfService string
// endpointURLs is a map from endpoint name to URL.
endpointURLs map[Endpoint]string
}
// getRawDirectory validates the provided directoryURL and makes a GET request
// to fetch the raw bytes of the server's directory resource. If the URL is
// invalid, if there is an error getting the directory bytes, or if the HTTP
// response code is not 200 an error is returned.
func getRawDirectory(directoryURL string) ([]byte, error) {
if directoryURL == "" {
return nil, ErrEmptyDirectory
}
if _, err := url.Parse(directoryURL); err != nil {
return nil, ErrInvalidDirectoryURL
}
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
TLSClientConfig: &tls.Config{
// Bypassing CDN or testing against Pebble instances can cause
// validation failures. For a **test-only** tool its acceptable to skip
// cert verification of the ACME server's HTTPs certificate.
InsecureSkipVerify: true,
},
MaxIdleConns: 1,
IdleConnTimeout: 15 * time.Second,
},
Timeout: 10 * time.Second,
}
resp, err := httpClient.Get(directoryURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, ErrInvalidDirectoryHTTPCode
}
rawDirectory, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return rawDirectory, nil
}
// termsOfService reads the termsOfService key from the meta key of the raw
// directory resource.
func termsOfService(rawDirectory map[string]interface{}) (string, error) {
var directoryMeta map[string]interface{}
if rawDirectoryMeta, ok := rawDirectory["meta"]; !ok {
return "", ErrInvalidDirectoryMeta
} else if directoryMetaMap, ok := rawDirectoryMeta.(map[string]interface{}); !ok {
return "", ErrInvalidDirectoryMeta
} else {
directoryMeta = directoryMetaMap
}
rawToSURL, ok := directoryMeta["termsOfService"]
if !ok {
return "", ErrInvalidTermsOfService
}
tosURL, ok := rawToSURL.(string)
if !ok {
return "", ErrInvalidTermsOfService
}
return tosURL, nil
}
// NewDirectory creates a Directory populated from the ACME directory resource
// returned by a GET request to the provided directoryURL. It also checks that
// the fetched directory contains each of the RequiredEndpoints.
func NewDirectory(directoryURL string) (*Directory, error) {
// Fetch the raw directory JSON
dirContents, err := getRawDirectory(directoryURL)
if err != nil {
return nil, err
}
// Unmarshal the directory
var dirResource map[string]interface{}
if err := json.Unmarshal(dirContents, &dirResource); err != nil {
return nil, ErrInvalidDirectoryJSON
}
// serverURL tries to find a valid url.URL for the provided endpoint in
// the unmarshaled directory resource.
serverURL := func(name Endpoint) (*url.URL, error) {
if rawURL, ok := dirResource[string(name)]; !ok {
return nil, ErrMissingEndpoint{endpoint: name}
} else if urlString, ok := rawURL.(string); !ok {
return nil, ErrInvalidEndpointURL{endpoint: name, value: urlString}
} else if url, err := url.Parse(urlString); err != nil {
return nil, ErrInvalidEndpointURL{endpoint: name, value: urlString}
} else {
return url, nil
}
}
// Create an empty directory to populate
directory := &Directory{
endpointURLs: make(map[Endpoint]string),
}
// Every required endpoint must have a valid URL populated from the directory
for _, endpointName := range RequiredEndpoints {
url, err := serverURL(endpointName)
if err != nil {
return nil, err
}
directory.endpointURLs[endpointName] = url.String()
}
// Populate the terms-of-service
tos, err := termsOfService(dirResource)
if err != nil {
return nil, err
}
directory.TermsOfService = tos
return directory, nil
}
// EndpointURL returns the string representation of the ACME server's URL for
// the provided endpoint. If the Endpoint is not known an empty string is
// returned.
func (d *Directory) EndpointURL(ep Endpoint) string {
if url, ok := d.endpointURLs[ep]; ok {
return url
}
return ""
}

View File

@ -0,0 +1,207 @@
package acme
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/letsencrypt/boulder/test"
)
// Path constants for test cases and mockDirectoryServer handlers.
const (
wrongStatusCodePath = "/dir-wrong-status"
invalidJSONPath = "/dir-bad-json"
missingEndpointPath = "/dir-missing-endpoint"
invalidEndpointURLPath = "/dir-invalid-endpoint"
validDirectoryPath = "/dir-valid"
invalidMetaDirectoryPath = "/dir-valid-meta-invalid"
invalidMetaDirectoryToSPath = "/dir-valid-meta-valid-tos-invalid"
)
// mockDirectoryServer is an httptest.Server that returns mock data for ACME
// directory GET requests based on the requested path.
type mockDirectoryServer struct {
*httptest.Server
}
// newMockDirectoryServer creates a mockDirectoryServer that returns mock data
// based on the requested path. The returned server will not be started
// automatically.
func newMockDirectoryServer() *mockDirectoryServer {
m := http.NewServeMux()
m.HandleFunc(wrongStatusCodePath, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnavailableForLegalReasons)
})
m.HandleFunc(invalidJSONPath, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{`)
})
m.HandleFunc(missingEndpointPath, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{}`)
})
m.HandleFunc(invalidEndpointURLPath, func(w http.ResponseWriter, r *http.Request) {
invalidURLDir := fmt.Sprintf(`{
"newAccount": "",
"newNonce": %q,
"newOrder": "",
"revokeCert": ""
}`, "http://"+string([]byte{0x7F}))
fmt.Fprint(w, invalidURLDir)
})
m.HandleFunc(invalidMetaDirectoryPath, func(w http.ResponseWriter, r *http.Request) {
noMetaDir := `{
"keyChange": "https://localhost:14000/rollover-account-key",
"newAccount": "https://localhost:14000/sign-me-up",
"newNonce": "https://localhost:14000/nonce-plz",
"newOrder": "https://localhost:14000/order-plz",
"revokeCert": "https://localhost:14000/revoke-cert"
}`
fmt.Fprint(w, noMetaDir)
})
m.HandleFunc(invalidMetaDirectoryToSPath, func(w http.ResponseWriter, r *http.Request) {
noToSDir := `{
"keyChange": "https://localhost:14000/rollover-account-key",
"meta": {
"chaos": "reigns"
},
"newAccount": "https://localhost:14000/sign-me-up",
"newNonce": "https://localhost:14000/nonce-plz",
"newOrder": "https://localhost:14000/order-plz",
"revokeCert": "https://localhost:14000/revoke-cert"
}`
fmt.Fprint(w, noToSDir)
})
m.HandleFunc(validDirectoryPath, func(w http.ResponseWriter, r *http.Request) {
validDir := `{
"keyChange": "https://localhost:14000/rollover-account-key",
"meta": {
"termsOfService": "data:text/plain,Do%20what%20thou%20wilt"
},
"newAccount": "https://localhost:14000/sign-me-up",
"newNonce": "https://localhost:14000/nonce-plz",
"newOrder": "https://localhost:14000/order-plz",
"revokeCert": "https://localhost:14000/revoke-cert"
}`
fmt.Fprint(w, validDir)
})
srv := &mockDirectoryServer{
Server: httptest.NewUnstartedServer(m),
}
return srv
}
// TestNew tests that creating a new Client and populating the endpoint map
// works correctly.
func TestNew(t *testing.T) {
unreachableDirectoryURL := "http://localhost:1987"
srv := newMockDirectoryServer()
srv.Start()
defer srv.Close()
srvUrl, _ := url.Parse(srv.URL)
_, port, _ := net.SplitHostPort(srvUrl.Host)
testURL := func(path string) string {
return fmt.Sprintf("http://localhost:%s%s", port, path)
}
wrongStatusCodeURL := testURL(wrongStatusCodePath)
invalidJSONURL := testURL(invalidJSONPath)
missingEndpointURL := testURL(missingEndpointPath)
missingEndpointErr := ErrMissingEndpoint{
endpoint: NewNonceEndpoint,
}
invalidEndpointURL := testURL(invalidEndpointURLPath)
invalidEndpointErr := ErrInvalidEndpointURL{
endpoint: NewNonceEndpoint,
value: "http://" + string([]byte{0x7F}),
}
invalidDirectoryMetaURL := testURL(invalidMetaDirectoryPath)
invalidDirectoryToSURL := testURL(invalidMetaDirectoryToSPath)
validDirectoryURL := testURL(validDirectoryPath)
testCases := []struct {
Name string
DirectoryURL string
ExpectedError string
}{
{
Name: "empty directory URL",
ExpectedError: ErrEmptyDirectory.Error(),
},
{
Name: "invalid directory URL",
DirectoryURL: "http://" + string([]byte{0x1, 0x7F}),
ExpectedError: ErrInvalidDirectoryURL.Error(),
},
{
Name: "unreachable directory URL",
DirectoryURL: unreachableDirectoryURL,
ExpectedError: "Get http://localhost:1987: dial tcp 127.0.0.1:1987: connect: connection refused",
},
{
Name: "wrong directory HTTP status code",
DirectoryURL: wrongStatusCodeURL,
ExpectedError: ErrInvalidDirectoryHTTPCode.Error(),
},
{
Name: "invalid directory JSON",
DirectoryURL: invalidJSONURL,
ExpectedError: ErrInvalidDirectoryJSON.Error(),
},
{
Name: "directory JSON missing required endpoint",
DirectoryURL: missingEndpointURL,
ExpectedError: missingEndpointErr.Error(),
},
{
Name: "directory JSON with invalid endpoint URL",
DirectoryURL: invalidEndpointURL,
ExpectedError: invalidEndpointErr.Error(),
},
{
Name: "directory JSON missing meta key",
DirectoryURL: invalidDirectoryMetaURL,
ExpectedError: ErrInvalidDirectoryMeta.Error(),
},
{
Name: "directory JSON missing meta TermsOfService key",
DirectoryURL: invalidDirectoryToSURL,
ExpectedError: ErrInvalidTermsOfService.Error(),
},
{
Name: "valid directory",
DirectoryURL: validDirectoryURL,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
_, err := NewDirectory(tc.DirectoryURL)
if err == nil && tc.ExpectedError != "" {
t.Errorf("expected error %q got nil", tc.ExpectedError)
} else if err != nil {
test.AssertEquals(t, err.Error(), tc.ExpectedError)
}
})
}
}

View File

@ -16,11 +16,11 @@ import (
"io/ioutil"
mrand "math/rand"
"net/http"
"strings"
"time"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/test/load-generator/acme"
"gopkg.in/square/go-jose.v2"
)
@ -37,17 +37,6 @@ var (
}
)
// API path constants
const (
newAcctPath = "/acme/new-acct"
newOrderPath = "/acme/new-order"
newRegPath = "/acme/new-reg"
newAuthzPath = "/acme/new-authz"
challengePath = "/acme/challenge"
newCertPath = "/acme/new-cert"
revokeCertPath = "/acme/revoke-cert"
)
// It's awkward to work with core.Order or corepb.Order when the API returns
// a different object than either of these types can represent without
// converting field values. The WFE uses an unexported `orderJSON` type for the
@ -123,7 +112,8 @@ func newAccount(s *State, ctx *context) error {
// Sign the new account registration body using a JWS with an embedded JWK
// because we do not have a key ID from the server yet.
jws, err := ctx.signEmbeddedV2Request(reqBodyStr, fmt.Sprintf("%s%s", s.apiBase, newAcctPath))
newAccountURL := s.directory.EndpointURL(acme.NewAccountEndpoint)
jws, err := ctx.signEmbeddedV2Request(reqBodyStr, newAccountURL)
if err != nil {
return err
}
@ -131,15 +121,15 @@ func newAccount(s *State, ctx *context) error {
// POST the account creation request to the server
nStarted := time.Now()
resp, err := s.post(fmt.Sprintf("%s%s", s.apiBase, newAcctPath), bodyBuf, ctx.ns)
resp, err := s.post(newAccountURL, bodyBuf, ctx.ns)
nFinished := time.Now()
nState := "error"
defer func() {
s.callLatency.Add(
fmt.Sprintf("POST %s", newAcctPath), nStarted, nFinished, nState)
fmt.Sprintf("POST %s", acme.NewAccountEndpoint), nStarted, nFinished, nState)
}()
if err != nil {
return fmt.Errorf("%s, post failed: %s", newAcctPath, err)
return fmt.Errorf("%s, post failed: %s", newAccountURL, err)
}
defer resp.Body.Close()
@ -147,16 +137,16 @@ func newAccount(s *State, ctx *context) error {
if resp.StatusCode != http.StatusCreated {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("%s, bad response: %s", newAcctPath, body)
return fmt.Errorf("%s, bad response: %s", newAccountURL, body)
}
return fmt.Errorf("%s, bad response status %d: %s", newAcctPath, resp.StatusCode, body)
return fmt.Errorf("%s, bad response status %d: %s", newAccountURL, resp.StatusCode, body)
}
// Populate the context account's key ID with the Location header returned by
// the server
locHeader := resp.Header.Get("Location")
if locHeader == "" {
return fmt.Errorf("%s, bad response - no Location header with account ID", newAcctPath)
return fmt.Errorf("%s, bad response - no Location header with account ID", newAccountURL)
}
ctx.acct.id = locHeader
@ -205,8 +195,8 @@ func newOrder(s *State, ctx *context) error {
}
// Sign the new order request with the context account's key/key ID
url := fmt.Sprintf("%s%s", s.apiBase, newOrderPath)
jws, err := ctx.signKeyIDV2Request(initOrderStr, url)
newOrderURL := s.directory.EndpointURL(acme.NewOrderEndpoint)
jws, err := ctx.signKeyIDV2Request(initOrderStr, newOrderURL)
if err != nil {
return err
}
@ -214,25 +204,25 @@ func newOrder(s *State, ctx *context) error {
// POST the new-order endpoint
nStarted := time.Now()
resp, err := s.post(url, bodyBuf, ctx.ns)
resp, err := s.post(newOrderURL, bodyBuf, ctx.ns)
nFinished := time.Now()
nState := "error"
defer func() {
s.callLatency.Add(
fmt.Sprintf("POST %s", newOrderPath), nStarted, nFinished, nState)
fmt.Sprintf("POST %s", acme.NewOrderEndpoint), nStarted, nFinished, nState)
}()
if err != nil {
return fmt.Errorf("%s, post failed: %s", newOrderPath, err)
return fmt.Errorf("%s, post failed: %s", newOrderURL, err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("%s, bad response: %s", newOrderPath, body)
return fmt.Errorf("%s, bad response: %s", newOrderURL, body)
}
// We expect that the result is a created order
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("%s, bad response status %d: %s", newOrderPath, resp.StatusCode, body)
return fmt.Errorf("%s, bad response status %d: %s", newOrderURL, resp.StatusCode, body)
}
// Unmarshal the Order object
@ -245,7 +235,7 @@ func newOrder(s *State, ctx *context) error {
// Populate the URL of the order from the Location header
orderURL := resp.Header.Get("Location")
if orderURL == "" {
return fmt.Errorf("%s, bad response - no Location header with order ID", newOrderPath)
return fmt.Errorf("%s, bad response - no Location header with order ID", newOrderURL)
}
orderJSON.URL = orderURL
@ -294,10 +284,9 @@ func getAuthorization(s *State, url string) (*core.Authorization, error) {
if err != nil {
return nil, fmt.Errorf("%s response: %s", url, body)
}
// The Authorization ID is not set in the response so we populate it based on
// the URL
paths := strings.Split(url, "/")
authz.ID = paths[len(paths)-1]
// The Authorization ID is not set in the response so we populate it using the
// URL
authz.ID = url
aState = "good"
return &authz, nil
}
@ -389,7 +378,7 @@ func completeAuthorization(authz *core.Authorization, s *State, ctx *context) er
// correct authorization state an error is returned. If no error is returned
// then the authorization is valid and ready.
func pollAuthorization(authz *core.Authorization, s *State, ctx *context) error {
authzURL := fmt.Sprintf("%s/acme/authz/%s", s.apiBase, authz.ID)
authzURL := authz.ID
for i := 0; i < 3; i++ {
// Fetch the authz by its URL
authz, err := getAuthorization(s, authzURL)
@ -461,12 +450,12 @@ func getOrder(s *State, url string) (*OrderJSON, error) {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("%s, bad response: %s", newOrderPath, body)
return nil, fmt.Errorf("%s, bad response: %s", url, body)
}
// We expect a HTTP status OK response
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s, bad response status %d: %s", newOrderPath, resp.StatusCode, body)
return nil, fmt.Errorf("%s, bad response status %d: %s", url, resp.StatusCode, body)
}
// Unmarshal the Order object from the response body

View File

@ -10,7 +10,7 @@
"runtime": "10s",
"rateDelta": "5/1m"
},
"apiBase": "http://boulder:4001",
"directoryURL": "http://boulder:4001/directory",
"domainBase": "com",
"httpOneAddr": "localhost:5002",
"regKeySize": 2048,

View File

@ -0,0 +1,22 @@
{
"plan": {
"actions": [
"newAccount",
"newOrder",
"fulfillOrder",
"finalizeOrder"
],
"rate": 1,
"runtime": "1s",
"rateDelta": "1/1m"
},
"directoryURL": "https://localhost:14000/dir",
"domainBase": "com",
"httpOneAddr": "localhost:5002",
"regKeySize": 2048,
"certKeySize": 2048,
"regEmail": "loadtesting@letsencrypt.org",
"maxRegs": 20,
"maxNamesPerCert": 20,
"dontSaveState": true
}

View File

@ -23,7 +23,7 @@ type Config struct {
}
ExternalState string // path to file to load/save registrations etc to/from
DontSaveState bool // don't save changes to external state
APIBase string // ACME API address to send requests to
DirectoryURL string // ACME server directory URL
DomainBase string // base domain name to create authorizations for
HTTPOneAddr string // address to listen for http-01 validation requests on
RealIP string // value of the Real-IP header to use when bypassing CDN
@ -42,6 +42,11 @@ func main() {
deltaArg := flag.String("delta", "", "")
flag.Parse()
if *configPath == "" {
fmt.Fprintf(os.Stderr, "-config argument must not be empty\n")
os.Exit(1)
}
configBytes, err := ioutil.ReadFile(*configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read load-generator config file %q: %s\n", *configPath, err)
@ -68,7 +73,7 @@ func main() {
}
s, err := New(
config.APIBase,
config.DirectoryURL,
config.CertKeySize,
config.DomainBase,
config.RealIP,

View File

@ -27,6 +27,7 @@ import (
"gopkg.in/square/go-jose.v2"
"github.com/letsencrypt/boulder/test/load-generator/acme"
"github.com/letsencrypt/challtestsrv"
)
@ -166,7 +167,6 @@ type respCode struct {
// State holds *all* the stuff
type State struct {
apiBase string
domainBase string
email string
maxRegs int
@ -183,7 +183,9 @@ type State struct {
challSrv *challtestsrv.ChallSrv
callLatency latencyWriter
client *http.Client
directory *acme.Directory
httpClient *http.Client
getTotal int64
postTotal int64
@ -265,7 +267,7 @@ func (s *State) Restore(filename string) error {
// New returns a pointer to a new State struct or an error
func New(
apiBase string,
directoryURL string,
keySize int,
domainBase string,
realIP string,
@ -277,7 +279,11 @@ func New(
if err != nil {
return nil, err
}
client := &http.Client{
directory, err := acme.NewDirectory(directoryURL)
if err != nil {
return nil, err
}
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
@ -297,8 +303,8 @@ func New(
return nil, err
}
s := &State{
client: client,
apiBase: apiBase,
httpClient: httpClient,
directory: directory,
certKey: certKey,
domainBase: domainBase,
callLatency: latencyFile,
@ -466,7 +472,7 @@ func (s *State) post(endpoint string, payload []byte, ns *nonceSource) (*http.Re
req.Header.Add("User-Agent", userAgent)
req.Header.Add("Content-Type", "application/jose+json")
atomic.AddInt64(&s.postTotal, 1)
resp, err := s.client.Do(req)
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, err
}
@ -485,7 +491,7 @@ func (s *State) get(path string) (*http.Response, error) {
req.Header.Add("X-Real-IP", s.realIP)
req.Header.Add("User-Agent", userAgent)
atomic.AddInt64(&s.getTotal, 1)
resp, err := s.client.Get(path)
resp, err := s.httpClient.Get(path)
if err != nil {
return nil, err
}
@ -510,8 +516,9 @@ type nonceSource struct {
}
func (ns *nonceSource) getNonce() (string, error) {
directoryURL := ns.s.directory.EndpointURL(acme.NewNonceEndpoint)
started := time.Now()
resp, err := ns.s.client.Head(fmt.Sprintf("%s/directory", ns.s.apiBase))
resp, err := ns.s.httpClient.Head(directoryURL)
finished := time.Now()
state := "good"
defer func() { ns.s.callLatency.Add("HEAD /directory", started, finished, state) }()