Make directory URLs relative to requested URL (#1847)

Prior to this PR the /directory JSON result was built once in Handler() and returned as-is for all requests. Each endpoint URL was fully qualified as an absolute URL using the BaseURL configuration
parameter. This required a configuration change in order to tweak the origin being used for subsequent requests. Returning purely relative URLs (e.g. /acme/new-reg vs http://localhost:4000/acme/new-reg) would break clients that assume absolute paths and we don't want that.

This PR introduces a new behaviour where the /directory JSON is built per-request using the HTTP Host header in place of the BaseURL. Clients will still receive a fully qualified URL in each directory entry but we gain the ability to more easily control the host without requiring config changes. To allow gradual migration via the config file we use the old /directory behaviour when a BaseURL is specified in the configuration file. This will address #1823.

Since the request.URL is not populated (Spare the Path attribute) we can not use request.URL.Scheme for the initial http:// vs https:// prefix when constructing the URLs and instead differentiate between the two cases using the req.TLS attribute. For cases (such as in production) where another service is terminating the initial request and making a subsequent HTTP request to the WFE we support the X-Forwarded-Proto header to ensure we use the original request's protocol when building URLs.

Many unit tests for the WFE assumed that when there is no BaseURL specified and no Host header is sent in the request, that the output will return relative paths. This PR changes that behaviour to always return absolute URLs by defaulting to localhost for the Host when it is not specified via the initial request or the BaseURL config option. This PR changes the expected test output to match this behaviour.
This commit is contained in:
Daniel McCarney 2016-05-27 13:17:19 -04:00 committed by Jacob Hoffman-Andrews
parent 567bc8027a
commit 5ce90a1a72
3 changed files with 202 additions and 117 deletions

View File

@ -349,7 +349,6 @@
},
"common": {
"baseURL": "http://127.0.0.1:4000",
"issuerCert": "test/test-ca.pem",
"dnsResolver": "127.0.0.1:8053",
"dnsTimeout": "10s",

View File

@ -13,6 +13,7 @@ import (
"io/ioutil"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
@ -30,18 +31,18 @@ import (
// Paths are the ACME-spec identified URL path-segments for various methods
const (
DirectoryPath = "/directory"
NewRegPath = "/acme/new-reg"
RegPath = "/acme/reg/"
NewAuthzPath = "/acme/new-authz"
AuthzPath = "/acme/authz/"
ChallengePath = "/acme/challenge/"
NewCertPath = "/acme/new-cert"
CertPath = "/acme/cert/"
RevokeCertPath = "/acme/revoke-cert"
TermsPath = "/terms"
IssuerPath = "/acme/issuer-cert"
BuildIDPath = "/build"
directoryPath = "/directory"
newRegPath = "/acme/new-reg"
regPath = "/acme/reg/"
newAuthzPath = "/acme/new-authz"
authzPath = "/acme/authz/"
challengePath = "/acme/challenge/"
newCertPath = "/acme/new-cert"
certPath = "/acme/cert/"
revokeCertPath = "/acme/revoke-cert"
termsPath = "/terms"
issuerPath = "/acme/issuer-cert"
buildIDPath = "/build"
)
// WebFrontEndImpl provides all the logic for Boulder's web-facing interface,
@ -56,17 +57,7 @@ type WebFrontEndImpl struct {
clk clock.Clock
// URL configuration parameters
BaseURL string
NewReg string
RegBase string
NewAuthz string
AuthzBase string
ChallengeBase string
NewCert string
CertBase string
// JSON encoded endpoint directory
DirectoryJSON []byte
BaseURL string
// Issuer certificate (DER) for /acme/issuer-cert
IssuerCert []byte
@ -192,43 +183,76 @@ func marshalIndent(v interface{}) ([]byte, error) {
return json.MarshalIndent(v, "", " ")
}
// Handler returns an http.Handler that uses various functions for
// various ACME-specified paths.
func (wfe *WebFrontEndImpl) Handler() (http.Handler, error) {
wfe.NewReg = wfe.BaseURL + NewRegPath
wfe.RegBase = wfe.BaseURL + RegPath
wfe.NewAuthz = wfe.BaseURL + NewAuthzPath
wfe.AuthzBase = wfe.BaseURL + AuthzPath
wfe.ChallengeBase = wfe.BaseURL + ChallengePath
wfe.NewCert = wfe.BaseURL + NewCertPath
wfe.CertBase = wfe.BaseURL + CertPath
func (wfe *WebFrontEndImpl) relativeEndpoint(request *http.Request, endpoint string) string {
var result string
proto := "http"
host := request.Host
// Only generate directory once
directory := map[string]string{
"new-reg": wfe.NewReg,
"new-authz": wfe.NewAuthz,
"new-cert": wfe.NewCert,
"revoke-cert": wfe.BaseURL + RevokeCertPath,
// If the request was received via TLS, use `https://` for the protocol
if request.TLS != nil {
proto = "https"
}
directoryJSON, err := marshalIndent(directory)
// Allow upstream proxies to specify the forwarded protocol. Allow this value
// to override our own guess.
if specifiedProto := request.Header.Get("X-Forwarded-Proto"); specifiedProto != "" {
proto = specifiedProto
}
// Default to "localhost" when no request.Host is provided. Otherwise requests
// with an empty `Host` produce results like `http:///acme/new-authz`
if request.Host == "" {
host = "localhost"
}
if wfe.BaseURL != "" {
result = fmt.Sprintf("%s%s", wfe.BaseURL, endpoint)
} else {
resultUrl := url.URL{Scheme: proto, Host: host, Path: endpoint}
result = resultUrl.String()
}
return result
}
func (wfe *WebFrontEndImpl) relativeDirectory(request *http.Request, directory map[string]string) ([]byte, error) {
// Create an empty map sized equal to the provided directory to store the
// relative-ized result
relativeDir := make(map[string]string, len(directory))
// Copy each entry of the provided directory into the new relative map. If
// `wfe.BaseURL` != "", use the old behaviour and prefix each endpoint with
// the `BaseURL`. Otherwise, prefix each endpoint using the request protocol
// & host.
for k, v := range directory {
relativeDir[k] = wfe.relativeEndpoint(request, v)
}
directoryJSON, err := marshalIndent(relativeDir)
// This should never happen since we are just marshalling known strings
if err != nil {
return nil, err
}
wfe.DirectoryJSON = directoryJSON
return directoryJSON, nil
}
// Handler returns an http.Handler that uses various functions for
// various ACME-specified paths.
func (wfe *WebFrontEndImpl) Handler() (http.Handler, error) {
m := http.NewServeMux()
wfe.HandleFunc(m, DirectoryPath, wfe.Directory, "GET")
wfe.HandleFunc(m, NewRegPath, wfe.NewRegistration, "POST")
wfe.HandleFunc(m, NewAuthzPath, wfe.NewAuthorization, "POST")
wfe.HandleFunc(m, NewCertPath, wfe.NewCertificate, "POST")
wfe.HandleFunc(m, RegPath, wfe.Registration, "POST")
wfe.HandleFunc(m, AuthzPath, wfe.Authorization, "GET")
wfe.HandleFunc(m, ChallengePath, wfe.Challenge, "GET", "POST")
wfe.HandleFunc(m, CertPath, wfe.Certificate, "GET")
wfe.HandleFunc(m, RevokeCertPath, wfe.RevokeCertificate, "POST")
wfe.HandleFunc(m, TermsPath, wfe.Terms, "GET")
wfe.HandleFunc(m, IssuerPath, wfe.Issuer, "GET")
wfe.HandleFunc(m, BuildIDPath, wfe.BuildID, "GET")
wfe.HandleFunc(m, directoryPath, wfe.Directory, "GET")
wfe.HandleFunc(m, newRegPath, wfe.NewRegistration, "POST")
wfe.HandleFunc(m, newAuthzPath, wfe.NewAuthorization, "POST")
wfe.HandleFunc(m, newCertPath, wfe.NewCertificate, "POST")
wfe.HandleFunc(m, regPath, wfe.Registration, "POST")
wfe.HandleFunc(m, authzPath, wfe.Authorization, "GET")
wfe.HandleFunc(m, challengePath, wfe.Challenge, "GET", "POST")
wfe.HandleFunc(m, certPath, wfe.Certificate, "GET")
wfe.HandleFunc(m, revokeCertPath, wfe.RevokeCertificate, "POST")
wfe.HandleFunc(m, termsPath, wfe.Terms, "GET")
wfe.HandleFunc(m, issuerPath, wfe.Issuer, "GET")
wfe.HandleFunc(m, buildIDPath, wfe.BuildID, "GET")
// We don't use our special HandleFunc for "/" because it matches everything,
// meaning we can wind up returning 405 when we mean to return 404. See
// https://github.com/letsencrypt/boulder/issues/717
@ -269,7 +293,7 @@ func (wfe *WebFrontEndImpl) Index(ctx context.Context, logEvent *requestEvent, r
JSON directory is available at <a href="%s">%s</a>.
</body>
</html>
`, DirectoryPath, DirectoryPath)))
`, directoryPath, directoryPath)))
addCacheHeader(response, wfe.IndexCacheDuration.Seconds())
}
@ -281,11 +305,27 @@ func addCacheHeader(w http.ResponseWriter, age float64) {
w.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%.f", age))
}
// Directory is an HTTP request handler that simply provides the directory
// object stored in the WFE's DirectoryJSON member.
// Directory is an HTTP request handler that provides the directory
// object stored in the WFE's DirectoryEndpoints member with paths prefixed
// using the `request.Host` of the HTTP request.
func (wfe *WebFrontEndImpl) Directory(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
directoryEndpoints := map[string]string{
"new-reg": newRegPath,
"new-authz": newAuthzPath,
"new-cert": newCertPath,
"revoke-cert": revokeCertPath,
}
response.Header().Set("Content-Type", "application/json")
response.Write(wfe.DirectoryJSON)
relDir, err := wfe.relativeDirectory(request, directoryEndpoints)
if err != nil {
marshalProb := probs.ServerInternal("unable to marshal JSON directory")
wfe.sendError(response, logEvent, marshalProb, nil)
return
}
response.Write(relDir)
}
// The ID is always the last slash-separated token in the path
@ -509,7 +549,7 @@ func (wfe *WebFrontEndImpl) NewRegistration(ctx context.Context, logEvent *reque
}
if existingReg, err := wfe.SA.GetRegistrationByKey(ctx, *key); err == nil {
response.Header().Set("Location", fmt.Sprintf("%s%d", wfe.RegBase, existingReg.ID))
response.Header().Set("Location", wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", regPath, existingReg.ID)))
// TODO(#595): check for missing registration err
wfe.sendError(response, logEvent, probs.Conflict("Registration key is already in use"), err)
return
@ -550,7 +590,7 @@ func (wfe *WebFrontEndImpl) NewRegistration(ctx context.Context, logEvent *reque
// Use an explicitly typed variable. Otherwise `go vet' incorrectly complains
// that reg.ID is a string being passed to %d.
regURL := fmt.Sprintf("%s%d", wfe.RegBase, reg.ID)
regURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", regPath, reg.ID))
responseBody, err := marshalIndent(reg)
if err != nil {
// ServerInternal because we just created this registration, and it
@ -562,7 +602,7 @@ func (wfe *WebFrontEndImpl) NewRegistration(ctx context.Context, logEvent *reque
response.Header().Add("Location", regURL)
response.Header().Set("Content-Type", "application/json")
response.Header().Add("Link", link(wfe.NewAuthz, "next"))
response.Header().Add("Link", link(wfe.relativeEndpoint(request, newAuthzPath), "next"))
if len(wfe.SubscriberAgreementURL) > 0 {
response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service"))
}
@ -605,8 +645,8 @@ func (wfe *WebFrontEndImpl) NewAuthorization(ctx context.Context, logEvent *requ
logEvent.Extra["AuthzID"] = authz.ID
// Make a URL for this authz, then blow away the ID and RegID before serializing
authzURL := wfe.AuthzBase + string(authz.ID)
wfe.prepAuthorizationForDisplay(&authz)
authzURL := wfe.relativeEndpoint(request, authzPath+string(authz.ID))
wfe.prepAuthorizationForDisplay(request, &authz)
responseBody, err := marshalIndent(authz)
if err != nil {
// ServerInternal because we generated the authz, it should be OK
@ -615,7 +655,7 @@ func (wfe *WebFrontEndImpl) NewAuthorization(ctx context.Context, logEvent *requ
}
response.Header().Add("Location", authzURL)
response.Header().Add("Link", link(wfe.NewCert, "next"))
response.Header().Add("Link", link(wfe.relativeEndpoint(request, newCertPath), "next"))
response.Header().Set("Content-Type", "application/json")
response.WriteHeader(http.StatusCreated)
if _, err = response.Write(responseBody); err != nil {
@ -782,11 +822,13 @@ func (wfe *WebFrontEndImpl) NewCertificate(ctx context.Context, logEvent *reques
return
}
serial := parsedCertificate.SerialNumber
certURL := wfe.CertBase + core.SerialToString(serial)
certURL := wfe.relativeEndpoint(request, certPath+core.SerialToString(serial))
relativeIssuerPath := wfe.relativeEndpoint(request, issuerPath)
// TODO Content negotiation
response.Header().Add("Location", certURL)
response.Header().Add("Link", link(wfe.BaseURL+IssuerPath, "up"))
response.Header().Add("Link", link(relativeIssuerPath, "up"))
response.Header().Set("Content-Type", "application/pkix-cert")
response.WriteHeader(http.StatusCreated)
if _, err = response.Write(cert.DER); err != nil {
@ -810,7 +852,7 @@ func (wfe *WebFrontEndImpl) Challenge(
// Challenge URIs are of the form /acme/challenge/<auth id>/<challenge id>.
// Here we parse out the id components. TODO: Use a better tool to parse out
// URL structure: https://github.com/letsencrypt/boulder/issues/437
slug := strings.Split(request.URL.Path[len(ChallengePath):], "/")
slug := strings.Split(request.URL.Path[len(challengePath):], "/")
if len(slug) != 2 {
notFound()
return
@ -866,8 +908,8 @@ func (wfe *WebFrontEndImpl) Challenge(
// fields.
// TODO: Come up with a cleaner way to do this.
// https://github.com/letsencrypt/boulder/issues/761
func (wfe *WebFrontEndImpl) prepChallengeForDisplay(authz core.Authorization, challenge *core.Challenge) {
challenge.URI = fmt.Sprintf("%s%s/%d", wfe.ChallengeBase, authz.ID, challenge.ID)
func (wfe *WebFrontEndImpl) prepChallengeForDisplay(request *http.Request, authz core.Authorization, challenge *core.Challenge) {
challenge.URI = wfe.relativeEndpoint(request, fmt.Sprintf("%s%s/%d", challengePath, authz.ID, challenge.ID))
challenge.AccountKey = nil
// 0 is considered "empty" for the purpose of the JSON omitempty tag.
challenge.ID = 0
@ -876,9 +918,9 @@ func (wfe *WebFrontEndImpl) prepChallengeForDisplay(authz core.Authorization, ch
// prepAuthorizationForDisplay takes a core.Authorization and prepares it for
// display to the client by clearing its ID and RegistrationID fields, and
// preparing all its challenges.
func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(authz *core.Authorization) {
func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(request *http.Request, authz *core.Authorization) {
for i := range authz.Challenges {
wfe.prepChallengeForDisplay(*authz, &authz.Challenges[i])
wfe.prepChallengeForDisplay(request, *authz, &authz.Challenges[i])
}
authz.ID = ""
authz.RegistrationID = 0
@ -892,7 +934,7 @@ func (wfe *WebFrontEndImpl) getChallenge(
challenge *core.Challenge,
logEvent *requestEvent) {
wfe.prepChallengeForDisplay(authz, challenge)
wfe.prepChallengeForDisplay(request, authz, challenge)
jsonReply, err := marshalIndent(challenge)
if err != nil {
@ -903,7 +945,7 @@ func (wfe *WebFrontEndImpl) getChallenge(
return
}
authzURL := wfe.AuthzBase + string(authz.ID)
authzURL := wfe.relativeEndpoint(request, authzPath+string(authz.ID))
response.Header().Add("Location", challenge.URI)
response.Header().Set("Content-Type", "application/json")
response.Header().Add("Link", link(authzURL, "up"))
@ -965,7 +1007,7 @@ func (wfe *WebFrontEndImpl) postChallenge(
// assumption: UpdateAuthorization does not modify order of challenges
challenge := updatedAuthorization.Challenges[challengeIndex]
wfe.prepChallengeForDisplay(authz, &challenge)
wfe.prepChallengeForDisplay(request, authz, &challenge)
jsonReply, err := marshalIndent(challenge)
if err != nil {
// ServerInternal because we made the challenges, they should be OK
@ -974,7 +1016,7 @@ func (wfe *WebFrontEndImpl) postChallenge(
return
}
authzURL := wfe.AuthzBase + string(authz.ID)
authzURL := wfe.relativeEndpoint(request, authzPath+string(authz.ID))
response.Header().Add("Location", challenge.URI)
response.Header().Set("Content-Type", "application/json")
response.Header().Add("Link", link(authzURL, "up"))
@ -1052,7 +1094,7 @@ func (wfe *WebFrontEndImpl) Registration(ctx context.Context, logEvent *requestE
return
}
response.Header().Set("Content-Type", "application/json")
response.Header().Add("Link", link(wfe.NewAuthz, "next"))
response.Header().Add("Link", link(wfe.relativeEndpoint(request, newAuthzPath), "next"))
if len(wfe.SubscriberAgreementURL) > 0 {
response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service"))
}
@ -1086,7 +1128,7 @@ func (wfe *WebFrontEndImpl) Authorization(ctx context.Context, logEvent *request
return
}
wfe.prepAuthorizationForDisplay(&authz)
wfe.prepAuthorizationForDisplay(request, &authz)
jsonReply, err := marshalIndent(authz)
if err != nil {
@ -1095,7 +1137,7 @@ func (wfe *WebFrontEndImpl) Authorization(ctx context.Context, logEvent *request
wfe.sendError(response, logEvent, probs.ServerInternal("Failed to JSON marshal authz"), err)
return
}
response.Header().Add("Link", link(wfe.NewCert, "next"))
response.Header().Add("Link", link(wfe.relativeEndpoint(request, newCertPath), "next"))
response.Header().Set("Content-Type", "application/json")
response.WriteHeader(http.StatusOK)
if _, err = response.Write(jsonReply); err != nil {
@ -1113,13 +1155,13 @@ func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *requestEv
path := request.URL.Path
// Certificate paths consist of the CertBase path, plus exactly sixteen hex
// digits.
if !strings.HasPrefix(path, CertPath) {
logEvent.AddError("this request path should not have gotten to Certificate: %#v is not a prefix of %#v", path, CertPath)
if !strings.HasPrefix(path, certPath) {
logEvent.AddError("this request path should not have gotten to Certificate: %#v is not a prefix of %#v", path, certPath)
wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil)
addNoCacheHeader(response)
return
}
serial := path[len(CertPath):]
serial := path[len(certPath):]
if !core.ValidSerial(serial) {
logEvent.AddError("certificate serial provided was not valid: %s", serial)
wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil)
@ -1145,7 +1187,7 @@ func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *requestEv
// TODO Content negotiation
response.Header().Set("Content-Type", "application/pkix-cert")
response.Header().Add("Link", link(IssuerPath, "up"))
response.Header().Add("Link", link(issuerPath, "up"))
response.WriteHeader(http.StatusOK)
if _, err = response.Write(cert.DER); err != nil {
logEvent.AddError(err.Error())

View File

@ -221,13 +221,6 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock) {
wfe, err := NewWebFrontEndImpl(stats, fc, testKeyPolicy)
test.AssertNotError(t, err, "Unable to create WFE")
wfe.NewReg = wfe.BaseURL + NewRegPath
wfe.RegBase = wfe.BaseURL + RegPath
wfe.NewAuthz = wfe.BaseURL + NewAuthzPath
wfe.AuthzBase = wfe.BaseURL + AuthzPath
wfe.ChallengeBase = wfe.BaseURL + ChallengePath
wfe.NewCert = wfe.BaseURL + NewCertPath
wfe.CertBase = wfe.BaseURL + CertPath
wfe.SubscriberAgreementURL = agreementURL
wfe.log = blog.NewMock()
@ -523,7 +516,7 @@ func TestIndex(t *testing.T) {
})
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
test.AssertNotEquals(t, responseWriter.Body.String(), "404 page not found\n")
test.Assert(t, strings.Contains(responseWriter.Body.String(), DirectoryPath),
test.Assert(t, strings.Contains(responseWriter.Body.String(), directoryPath),
"directory path not found")
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=10")
@ -539,6 +532,10 @@ func TestIndex(t *testing.T) {
}
func TestDirectory(t *testing.T) {
// Note: using `wfe.BaseURL` to test the non-relative /directory behaviour
// This tests to ensure the `Host` in the following `http.Request` is not
// used.by setting `BaseURL` using `localhost`, sending `127.0.0.1` in the Host,
// and expecting `localhost` in the JSON result.
wfe, _ := setupWFE(t)
wfe.BaseURL = "http://localhost:4300"
mux, err := wfe.Handler()
@ -550,12 +547,59 @@ func TestDirectory(t *testing.T) {
mux.ServeHTTP(responseWriter, &http.Request{
Method: "GET",
URL: url,
Host: "127.0.0.1:4300",
})
test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/json")
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
assertJSONEquals(t, responseWriter.Body.String(), `{"new-authz":"http://localhost:4300/acme/new-authz","new-cert":"http://localhost:4300/acme/new-cert","new-reg":"http://localhost:4300/acme/new-reg","revoke-cert":"http://localhost:4300/acme/revoke-cert"}`)
}
func TestRelativeDirectory(t *testing.T) {
wfe, _ := setupWFE(t)
mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
dirTests := []struct {
host string
protoHeader string
result string
}{
// Test '' (No host header) with no proto header
{"", "", `{"new-authz":"http://localhost/acme/new-authz","new-cert":"http://localhost/acme/new-cert","new-reg":"http://localhost/acme/new-reg","revoke-cert":"http://localhost/acme/revoke-cert"}`},
// Test localhost:4300 with no proto header
{"localhost:4300", "", `{"new-authz":"http://localhost:4300/acme/new-authz","new-cert":"http://localhost:4300/acme/new-cert","new-reg":"http://localhost:4300/acme/new-reg","revoke-cert":"http://localhost:4300/acme/revoke-cert"}`},
// Test 127.0.0.1:4300 with no proto header
{"127.0.0.1:4300", "", `{"new-authz":"http://127.0.0.1:4300/acme/new-authz","new-cert":"http://127.0.0.1:4300/acme/new-cert","new-reg":"http://127.0.0.1:4300/acme/new-reg","revoke-cert":"http://127.0.0.1:4300/acme/revoke-cert"}`},
// Test localhost:4300 with HTTP proto header
{"localhost:4300", "http", `{"new-authz":"http://localhost:4300/acme/new-authz","new-cert":"http://localhost:4300/acme/new-cert","new-reg":"http://localhost:4300/acme/new-reg","revoke-cert":"http://localhost:4300/acme/revoke-cert"}`},
// Test localhost:4300 with HTTPS proto header
{"localhost:4300", "https", `{"new-authz":"https://localhost:4300/acme/new-authz","new-cert":"https://localhost:4300/acme/new-cert","new-reg":"https://localhost:4300/acme/new-reg","revoke-cert":"https://localhost:4300/acme/revoke-cert"}`},
}
url, _ := url.Parse("/directory")
for _, tt := range dirTests {
var headers map[string][]string
responseWriter := httptest.NewRecorder()
if tt.protoHeader != "" {
headers = map[string][]string{
"X-Forwarded-Proto": {tt.protoHeader},
}
}
mux.ServeHTTP(responseWriter, &http.Request{
Method: "GET",
Host: tt.host,
URL: url,
Header: headers,
})
test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/json")
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
assertJSONEquals(t, responseWriter.Body.String(), tt.result)
}
}
// TODO: Write additional test cases for:
// - RA returns with a failure
func TestIssueCertificate(t *testing.T) {
@ -590,7 +634,7 @@ func TestIssueCertificate(t *testing.T) {
// GET instead of POST should be rejected
mux.ServeHTTP(responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL(NewCertPath),
URL: mustParseURL(newCertPath),
})
assertJSONEquals(t,
responseWriter.Body.String(),
@ -685,10 +729,10 @@ func TestIssueCertificate(t *testing.T) {
string(cert.Raw))
test.AssertEquals(
t, responseWriter.Header().Get("Location"),
"/acme/cert/0000ff0000000000000e4b4f67d86e818c46")
"http://localhost/acme/cert/0000ff0000000000000e4b4f67d86e818c46")
test.AssertEquals(
t, responseWriter.Header().Get("Link"),
`</acme/issuer-cert>;rel="up"`)
`<http://localhost/acme/issuer-cert>;rel="up"`)
test.AssertEquals(
t, responseWriter.Header().Get("Content-Type"),
"application/pkix-cert")
@ -702,7 +746,7 @@ func TestIssueCertificate(t *testing.T) {
func TestGetChallenge(t *testing.T) {
wfe, _ := setupWFE(t)
challengeURL := "/acme/challenge/valid/23"
challengeURL := "http://localhost/acme/challenge/valid/23"
for _, method := range []string{"GET", "HEAD"} {
resp := httptest.NewRecorder()
@ -722,14 +766,14 @@ func TestGetChallenge(t *testing.T) {
"application/json")
test.AssertEquals(t,
resp.Header().Get("Link"),
`</acme/authz/valid>;rel="up"`)
`<http://localhost/acme/authz/valid>;rel="up"`)
// Body is only relevant for GET. For HEAD, body will
// be discarded by HandleFunc() anyway, so it doesn't
// matter what Challenge() writes to it.
if method == "GET" {
assertJSONEquals(
t, resp.Body.String(),
`{"type":"dns","uri":"/acme/challenge/valid/23"}`)
`{"type":"dns","uri":"http://localhost/acme/challenge/valid/23"}`)
}
}
}
@ -748,7 +792,7 @@ func TestChallenge(t *testing.T) {
`), &key)
test.AssertNotError(t, err, "Could not unmarshal testing key")
challengeURL := "/acme/challenge/valid/23"
challengeURL := "http://localhost/acme/challenge/valid/23"
wfe.Challenge(ctx, newRequestEvent(), responseWriter,
makePostRequestWithPath(challengeURL,
signRequest(t, `{"resource":"challenge"}`, wfe.nonceService)))
@ -759,10 +803,10 @@ func TestChallenge(t *testing.T) {
challengeURL)
test.AssertEquals(
t, responseWriter.Header().Get("Link"),
`</acme/authz/valid>;rel="up"`)
`<http://localhost/acme/authz/valid>;rel="up"`)
assertJSONEquals(
t, responseWriter.Body.String(),
`{"type":"dns","uri":"/acme/challenge/valid/23"}`)
`{"type":"dns","uri":"http://localhost/acme/challenge/valid/23"}`)
// Expired challenges should be inaccessible
challengeURL = "/acme/challenge/expired/23"
@ -818,7 +862,7 @@ func TestNewECDSARegistration(t *testing.T) {
test.AssertEquals(t, reg.Agreement, "http://example.invalid/terms")
test.AssertEquals(t, reg.InitialIP.String(), "1.1.1.1")
test.AssertEquals(t, responseWriter.Header().Get("Location"), "/acme/reg/0")
test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/reg/0")
key, err = jose.LoadPrivateKey([]byte(testE1KeyPrivatePEM))
test.AssertNotError(t, err, "Failed to load key")
@ -835,7 +879,7 @@ func TestNewECDSARegistration(t *testing.T) {
wfe.NewRegistration(ctx, newRequestEvent(), responseWriter, makePostRequest(result.FullSerialize()))
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Registration key is already in use","status":409}`)
test.AssertEquals(t, responseWriter.Header().Get("Location"), "/acme/reg/3")
test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/reg/3")
test.AssertEquals(t, responseWriter.Code, 409)
}
@ -866,7 +910,7 @@ func TestNewRegistration(t *testing.T) {
{
&http.Request{
Method: "GET",
URL: mustParseURL(NewRegPath),
URL: mustParseURL(newRegPath),
},
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`,
},
@ -875,7 +919,7 @@ func TestNewRegistration(t *testing.T) {
{
&http.Request{
Method: "POST",
URL: mustParseURL(NewRegPath),
URL: mustParseURL(newRegPath),
Header: map[string][]string{
"Content-Length": {"0"},
},
@ -885,20 +929,20 @@ func TestNewRegistration(t *testing.T) {
// POST, but body that isn't valid JWS
{
makePostRequestWithPath(NewRegPath, "hi"),
makePostRequestWithPath(newRegPath, "hi"),
`{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`,
},
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
{
makePostRequestWithPath(NewRegPath, fooBody.FullSerialize()),
makePostRequestWithPath(newRegPath, fooBody.FullSerialize()),
`{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`,
},
// Same signed body, but payload modified by one byte, breaking signature.
// should fail JWS verification.
{
makePostRequestWithPath(NewRegPath, `
makePostRequestWithPath(newRegPath, `
{
"header": {
"alg": "RS256",
@ -915,7 +959,7 @@ func TestNewRegistration(t *testing.T) {
`{"type":"urn:acme:error:malformed","detail":"JWS verification error","status":400}`,
},
{
makePostRequestWithPath(NewRegPath, wrongAgreementBody.FullSerialize()),
makePostRequestWithPath(newRegPath, wrongAgreementBody.FullSerialize()),
`{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [` + agreementURL + `]","status":400}`,
},
}
@ -940,14 +984,14 @@ func TestNewRegistration(t *testing.T) {
test.AssertEquals(
t, responseWriter.Header().Get("Location"),
"/acme/reg/0")
"http://localhost/acme/reg/0")
links := responseWriter.Header()["Link"]
test.AssertEquals(t, contains(links, "</acme/new-authz>;rel=\"next\""), true)
test.AssertEquals(t, contains(links, "<http://localhost/acme/new-authz>;rel=\"next\""), true)
test.AssertEquals(t, contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true)
test.AssertEquals(
t, responseWriter.Header().Get("Link"),
`</acme/new-authz>;rel="next"`)
`<http://localhost/acme/new-authz>;rel="next"`)
key, err = jose.LoadPrivateKey([]byte(test1KeyPrivatePEM))
test.AssertNotError(t, err, "Failed to load key")
@ -969,7 +1013,7 @@ func TestNewRegistration(t *testing.T) {
`{"type":"urn:acme:error:malformed","detail":"Registration key is already in use","status":409}`)
test.AssertEquals(
t, responseWriter.Header().Get("Location"),
"/acme/reg/1")
"http://localhost/acme/reg/1")
test.AssertEquals(t, responseWriter.Code, 409)
}
@ -1131,7 +1175,7 @@ func TestAuthorization(t *testing.T) {
// GET instead of POST should be rejected
mux.ServeHTTP(responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL(NewAuthzPath),
URL: mustParseURL(newAuthzPath),
})
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
@ -1185,10 +1229,10 @@ func TestAuthorization(t *testing.T) {
test.AssertEquals(
t, responseWriter.Header().Get("Location"),
"/acme/authz/bkrPh2u0JUf18-rVBZtOOWWb3GuIiliypL-hBM9Ak1Q")
"http://localhost/acme/authz/bkrPh2u0JUf18-rVBZtOOWWb3GuIiliypL-hBM9Ak1Q")
test.AssertEquals(
t, responseWriter.Header().Get("Link"),
`</acme/new-cert>;rel="next"`)
`<http://localhost/acme/new-cert>;rel="next"`)
assertJSONEquals(t, responseWriter.Body.String(), `{"identifier":{"type":"dns","value":"test.com"}}`)
@ -1226,7 +1270,7 @@ func TestRegistration(t *testing.T) {
// Test invalid method
mux.ServeHTTP(responseWriter, &http.Request{
Method: "MAKE-COFFEE",
URL: mustParseURL(RegPath),
URL: mustParseURL(regPath),
Body: makeBody("invalid"),
})
assertJSONEquals(t,
@ -1237,7 +1281,7 @@ func TestRegistration(t *testing.T) {
// Test GET proper entry returns 405
mux.ServeHTTP(responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL(RegPath),
URL: mustParseURL(regPath),
})
assertJSONEquals(t,
responseWriter.Body.String(),
@ -1295,7 +1339,7 @@ func TestRegistration(t *testing.T) {
makePostRequestWithPath("/1", result.FullSerialize()))
test.AssertNotContains(t, responseWriter.Body.String(), "urn:acme:error")
links := responseWriter.Header()["Link"]
test.AssertEquals(t, contains(links, "</acme/new-authz>;rel=\"next\""), true)
test.AssertEquals(t, contains(links, "<http://localhost/acme/new-authz>;rel=\"next\""), true)
test.AssertEquals(t, contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true)
responseWriter.Body.Reset()