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:
parent
567bc8027a
commit
5ce90a1a72
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue