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:
parent
ff3129247d
commit
7efa727289
|
|
@ -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 ""
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) }()
|
||||
|
|
|
|||
Loading…
Reference in New Issue