add HTTPStatus to ProblemDetails

For #1161
This commit is contained in:
Jeff Hodges 2015-11-24 20:19:09 -08:00
parent 2114f5d5cc
commit 1dc5e53c5a
5 changed files with 55 additions and 49 deletions

View File

@ -20,8 +20,9 @@ type ProblemType string
// ProblemDetails objects represent problem documents
// https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
type ProblemDetails struct {
Type ProblemType `json:"type,omitempty"`
Detail string `json:"detail,omitempty"`
Type ProblemType `json:"type,omitempty"`
Detail string `json:"detail,omitempty"`
HTTPStatus int `json:"status,omitempty"`
}
func (pd *ProblemDetails) Error() string {

View File

@ -210,8 +210,9 @@ func (rpc *AmqpRPCServer) Handle(method string, handler func([]byte) ([]byte, er
// rpcError is a JSON wrapper for error as it cannot be un/marshalled
// due to type interface{}.
type rpcError struct {
Value string `json:"value"`
Type string `json:"type,omitempty"`
Value string `json:"value"`
Type string `json:"type,omitempty"`
HTTPStatus int `json:"status,omitempty"`
}
// Wraps a error in a rpcError so it can be marshalled to
@ -249,7 +250,7 @@ func wrapError(err error) *rpcError {
case *probs.ProblemDetails:
wrapped.Type = string(terr.Type)
wrapped.Value = terr.Detail
wrapped.HTTPStatus = terr.HTTPStatus
}
return wrapped
}
@ -287,8 +288,9 @@ func unwrapError(rpcError *rpcError) error {
default:
if strings.HasPrefix(rpcError.Type, "urn:") {
return &probs.ProblemDetails{
Type: probs.ProblemType(rpcError.Type),
Detail: rpcError.Value,
Type: probs.ProblemType(rpcError.Type),
Detail: rpcError.Value,
HTTPStatus: rpcError.HTTPStatus,
}
}
return errors.New(rpcError.Value)
@ -320,7 +322,7 @@ func (r rpcResponse) debugString() string {
if r.Error == nil {
return ret
}
return fmt.Sprintf("%s, RPCERR: %s", ret, r.Error)
return fmt.Sprintf("%s, RPCERR: %v", ret, r.Error)
}
// AmqpChannel sets a AMQP connection up using SSL if configuration is provided

View File

@ -45,12 +45,14 @@ func TestWrapError(t *testing.T) {
}{
{
&probs.ProblemDetails{
Type: probs.ConnectionProblem,
Detail: "whoops",
Type: probs.ConnectionProblem,
Detail: "whoops",
HTTPStatus: 417,
},
&probs.ProblemDetails{
Type: probs.ConnectionProblem,
Detail: "whoops",
Type: probs.ConnectionProblem,
Detail: "whoops",
HTTPStatus: 417,
},
},
{

View File

@ -487,7 +487,7 @@ func (wfe *WebFrontEndImpl) verifyPOST(logEvent *requestEvent, request *http.Req
// Notify the client of an error condition and log it for audit purposes.
func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *requestEvent, msg string, detail error, code int) {
problem := probs.ProblemDetails{Detail: msg}
problem := probs.ProblemDetails{Detail: msg, HTTPStatus: code}
switch code {
case http.StatusPreconditionFailed:
fallthrough
@ -507,6 +507,7 @@ func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *re
problem.Type = probs.RateLimitedProblem
case statusBadNonce:
problem.Type = probs.BadNonceProblem
problem.HTTPStatus = http.StatusBadRequest
code = http.StatusBadRequest
default: // Either http.StatusInternalServerError or an unexpected code
problem.Type = probs.ServerInternalProblem

View File

@ -302,7 +302,7 @@ func TestHandleFunc(t *testing.T) {
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), sortHeader(strings.Join(addHeadIfGet(c.allowed), ", ")))
test.AssertEquals(t,
rw.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
}
nonce := rw.Header().Get("Replay-Nonce")
test.AssertNotEquals(t, nonce, lastNonce)
@ -313,7 +313,7 @@ func TestHandleFunc(t *testing.T) {
// Disallowed method returns error JSON in body
runWrappedHandler(&http.Request{Method: "PUT"}, "GET", "POST")
test.AssertEquals(t, rw.Header().Get("Content-Type"), "application/problem+json")
test.AssertEquals(t, rw.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
test.AssertEquals(t, rw.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, HEAD, POST")
// Disallowed method special case: response to HEAD has got no body
@ -559,7 +559,7 @@ func TestIssueCertificate(t *testing.T) {
})
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
// POST, but no body.
responseWriter.Body.Reset()
@ -571,14 +571,14 @@ func TestIssueCertificate(t *testing.T) {
})
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: No body on POST"}`)
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: No body on POST","status":400}`)
// POST, but body that isn't valid JWS
responseWriter.Body.Reset()
wfe.NewCertificate(newRequestEvent(), responseWriter, makePostRequest("hi"))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS"}`)
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS","status":400}`)
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
responseWriter.Body.Reset()
@ -586,7 +586,7 @@ func TestIssueCertificate(t *testing.T) {
makePostRequest(signRequest(t, "foo", wfe.nonceService)))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload did not parse as JSON"}`)
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload did not parse as JSON","status":400}`)
// Valid, signed JWS body, payload is '{}'
responseWriter.Body.Reset()
@ -595,7 +595,7 @@ func TestIssueCertificate(t *testing.T) {
signRequest(t, "{}", wfe.nonceService)))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload does not specify a resource"}`)
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload does not specify a resource","status":400}`)
// Valid, signed JWS body, payload is '{"resource":"new-cert"}'
responseWriter.Body.Reset()
@ -603,7 +603,7 @@ func TestIssueCertificate(t *testing.T) {
makePostRequest(signRequest(t, `{"resource":"new-cert"}`, wfe.nonceService)))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Error unmarshaling certificate request"}`)
`{"type":"urn:acme:error:malformed","detail":"Error unmarshaling certificate request","status":400}`)
// Valid, signed JWS body, payload has a invalid signature on CSR and no authorizations:
// alias b64url="base64 -w0 | sed -e 's,+,-,g' -e 's,/,_,g'"
@ -618,7 +618,7 @@ func TestIssueCertificate(t *testing.T) {
}`, wfe.nonceService)))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:unauthorized","detail":"Error creating new cert :: Invalid signature on CSR"}`)
`{"type":"urn:acme:error:unauthorized","detail":"Error creating new cert :: Invalid signature on CSR","status":403}`)
// Valid, signed JWS body, payload has a valid CSR but no authorizations:
// openssl req -outform der -new -nodes -key wfe/test/178.key -subj /CN=meep.com | b64url
@ -631,7 +631,7 @@ func TestIssueCertificate(t *testing.T) {
}`, wfe.nonceService)))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:unauthorized","detail":"Error creating new cert :: Authorizations for these names not found or expired: meep.com"}`)
`{"type":"urn:acme:error:unauthorized","detail":"Error creating new cert :: Authorizations for these names not found or expired: meep.com","status":403}`)
assertCsrLogged(t, mockLog)
mockLog.Clear()
@ -737,7 +737,7 @@ func TestChallenge(t *testing.T) {
signRequest(t, `{"resource":"challenge"}`, wfe.nonceService)))
test.AssertEquals(t, responseWriter.Code, http.StatusNotFound)
test.AssertEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Expired authorization"}`)
`{"type":"urn:acme:error:malformed","detail":"Expired authorization","status":404}`)
}
func TestBadNonce(t *testing.T) {
@ -755,7 +755,7 @@ func TestBadNonce(t *testing.T) {
test.AssertNotError(t, err, "Failed to sign body")
wfe.NewRegistration(newRequestEvent(), responseWriter,
makePostRequest(result.FullSerialize()))
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:badNonce","detail":"Unable to read/verify body :: JWS has no anti-replay nonce"}`)
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:badNonce","detail":"Unable to read/verify body :: JWS has no anti-replay nonce","status":400}`)
}
func TestNewRegistration(t *testing.T) {
@ -787,7 +787,7 @@ func TestNewRegistration(t *testing.T) {
Method: "GET",
URL: mustParseURL(NewRegPath),
},
`{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`,
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`,
},
// POST, but no body.
@ -799,19 +799,19 @@ func TestNewRegistration(t *testing.T) {
"Content-Length": []string{"0"},
},
},
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: No body on POST"}`,
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: No body on POST","status":400}`,
},
// POST, but body that isn't valid JWS
{
makePostRequestWithPath(NewRegPath, "hi"),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS"}`,
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS","status":400}`,
},
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
{
makePostRequestWithPath(NewRegPath, fooBody.FullSerialize()),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload did not parse as JSON"}`,
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload did not parse as JSON","status":400}`,
},
// Same signed body, but payload modified by one byte, breaking signature.
@ -831,11 +831,11 @@ func TestNewRegistration(t *testing.T) {
"signature": "RjUQ679fxJgeAJlxqgvDP_sfGZnJ-1RgWF2qmcbnBWljs6h1qp63pLnJOl13u81bP_bCSjaWkelGG8Ymx_X-aQ"
}
`),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: JWS verification error"}`,
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: JWS verification error","status":400}`,
},
{
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 + `]"}`,
`{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [` + agreementURL + `]","status":400}`,
},
}
for _, rt := range regErrTests {
@ -885,7 +885,7 @@ func TestNewRegistration(t *testing.T) {
makePostRequest(result.FullSerialize()))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Registration key is already in use"}`)
`{"type":"urn:acme:error:malformed","detail":"Registration key is already in use","status":409}`)
test.AssertEquals(
t, responseWriter.Header().Get("Location"),
"/acme/reg/1")
@ -996,7 +996,7 @@ func TestRevokeCertificateWrongKey(t *testing.T) {
makePostRequest(result.FullSerialize()))
test.AssertEquals(t, responseWriter.Code, 403)
test.AssertEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:unauthorized","detail":"Revocation request must be signed by private key of cert to be revoked, or by the account key of the account that issued it."}`)
`{"type":"urn:acme:error:unauthorized","detail":"Revocation request must be signed by private key of cert to be revoked, or by the account key of the account that issued it.","status":403}`)
}
// Valid revocation request for already-revoked cert
@ -1037,7 +1037,7 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
makePostRequest(result.FullSerialize()))
test.AssertEquals(t, responseWriter.Code, 409)
test.AssertEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Certificate already revoked"}`)
`{"type":"urn:acme:error:malformed","detail":"Certificate already revoked","status":409}`)
}
func TestAuthorization(t *testing.T) {
@ -1052,7 +1052,7 @@ func TestAuthorization(t *testing.T) {
Method: "GET",
URL: mustParseURL(NewAuthzPath),
})
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
// POST, but no body.
responseWriter.Body.Reset()
@ -1062,12 +1062,12 @@ func TestAuthorization(t *testing.T) {
"Content-Length": []string{"0"},
},
})
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: No body on POST"}`)
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: No body on POST","status":400}`)
// POST, but body that isn't valid JWS
responseWriter.Body.Reset()
wfe.NewAuthorization(newRequestEvent(), responseWriter, makePostRequest("hi"))
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS"}`)
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS","status":400}`)
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
responseWriter.Body.Reset()
@ -1075,7 +1075,7 @@ func TestAuthorization(t *testing.T) {
makePostRequest(signRequest(t, "foo", wfe.nonceService)))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload did not parse as JSON"}`)
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload did not parse as JSON","status":400}`)
// Same signed body, but payload modified by one byte, breaking signature.
// should fail JWS verification.
@ -1096,7 +1096,7 @@ func TestAuthorization(t *testing.T) {
`))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: JWS verification error"}`)
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: JWS verification error","status":400}`)
responseWriter.Body.Reset()
wfe.NewAuthorization(newRequestEvent(), responseWriter,
@ -1124,7 +1124,7 @@ func TestAuthorization(t *testing.T) {
})
test.AssertEquals(t, responseWriter.Code, http.StatusNotFound)
test.AssertEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Expired authorization"}`)
`{"type":"urn:acme:error:malformed","detail":"Expired authorization","status":404}`)
}
func contains(s []string, e string) bool {
@ -1150,7 +1150,7 @@ func TestRegistration(t *testing.T) {
})
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
responseWriter.Body.Reset()
// Test GET proper entry returns 405
@ -1160,14 +1160,14 @@ func TestRegistration(t *testing.T) {
})
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
responseWriter.Body.Reset()
// Test POST invalid JSON
wfe.Registration(newRequestEvent(), responseWriter, makePostRequestWithPath("/2", "invalid"))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS"}`)
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS","status":400}`)
responseWriter.Body.Reset()
key, err := jose.LoadPrivateKey([]byte(test2KeyPrivatePEM))
@ -1185,7 +1185,7 @@ func TestRegistration(t *testing.T) {
makePostRequestWithPath("/2", result.FullSerialize()))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:unauthorized","detail":"No registration exists matching provided key"}`)
`{"type":"urn:acme:error:unauthorized","detail":"No registration exists matching provided key","status":403}`)
responseWriter.Body.Reset()
key, err = jose.LoadPrivateKey([]byte(test1KeyPrivatePEM))
@ -1204,7 +1204,7 @@ func TestRegistration(t *testing.T) {
makePostRequestWithPath("/1", result.FullSerialize()))
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [`+agreementURL+`]"}`)
`{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [`+agreementURL+`]","status":400}`)
responseWriter.Body.Reset()
// Test POST valid JSON with registration up in the mock (with correct agreement URL)
@ -1290,7 +1290,7 @@ func TestGetCertificate(t *testing.T) {
mux.ServeHTTP(responseWriter, req)
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found","status":404}`)
reqlogs = mockLog.GetAllMatching(`Terminated request`)
test.AssertEquals(t, len(reqlogs), 1)
@ -1303,7 +1303,7 @@ func TestGetCertificate(t *testing.T) {
mux.ServeHTTP(responseWriter, req)
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found","status":404}`)
// Invalid serial, no cache
responseWriter = httptest.NewRecorder()
@ -1311,7 +1311,7 @@ func TestGetCertificate(t *testing.T) {
mux.ServeHTTP(responseWriter, req)
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found","status":404}`)
}
func assertCsrLogged(t *testing.T, mockLog *mocks.SyslogWriter) {
@ -1397,7 +1397,7 @@ func TestBadKeyCSR(t *testing.T) {
test.AssertEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Invalid key in certificate request :: Key too small: 512"}`)
`{"type":"urn:acme:error:malformed","detail":"Invalid key in certificate request :: Key too small: 512","status":400}`)
}
func TestStatusCodeFromError(t *testing.T) {