Fix CORS headers, support OPTIONS requests.

This commit is contained in:
Tom Clegg 2015-09-11 23:13:52 -04:00
parent 4ddff2c700
commit b6a4b66899
2 changed files with 148 additions and 45 deletions

View File

@ -146,11 +146,15 @@ func (mrw BodylessResponseWriter) Write(buf []byte) (int, error) {
//
// * Set a Replay-Nonce header.
//
// * Respond to OPTIONS requests, including CORS preflight requests.
//
// * Respond http.StatusMethodNotAllowed for HTTP methods other than
// those listed.
// those listed.
//
// * Set CORS headers when responding to CORS "actual" requests.
//
// * Never send a body in response to a HEAD request. (Anything
// written by the handler will be discarded if the method is HEAD.)
// written by the handler will be discarded if the method is HEAD.)
func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h func(http.ResponseWriter, *http.Request), methods ...string) {
methodsOK := make(map[string]bool)
for _, m := range methods {
@ -163,7 +167,6 @@ func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h fun
if err == nil {
response.Header().Set("Replay-Nonce", nonce)
}
response.Header().Set("Access-Control-Allow-Origin", "*")
switch request.Method {
case "HEAD":
@ -172,7 +175,8 @@ func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h fun
// sending a body.
response = BodylessResponseWriter{response}
case "OPTIONS":
// TODO, #469
wfe.Options(response, request, methodsOK)
return
}
if _, ok := methodsOK[request.Method]; !ok {
@ -184,6 +188,8 @@ func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h fun
return
}
wfe.setCORSHeaders(response, request, strings.Join(methods, ", "))
// Call the wrapped handler.
h(response, request)
})
@ -1182,6 +1188,55 @@ func (wfe *WebFrontEndImpl) BuildID(response http.ResponseWriter, request *http.
}
}
// Options responds to an HTTP OPTIONS request.
func (wfe *WebFrontEndImpl) Options(response http.ResponseWriter, request *http.Request, methodsOK map[string]bool) {
allowMethods := ""
for method := range methodsOK {
if allowMethods != "" {
allowMethods += ", "
}
allowMethods += method
}
// Every OPTIONS request gets an Allow header with a list of supported methods.
response.Header().Set("Allow", allowMethods)
// CORS preflight requests get additional headers. See
// http://www.w3.org/TR/cors/#resource-preflight-requests
reqMethod := request.Header.Get("Access-Control-Request-Method")
if reqMethod == "" {
reqMethod = "GET"
}
if _, ok := methodsOK[reqMethod]; ok {
wfe.setCORSHeaders(response, request, allowMethods)
}
}
// setCORSHeaders() tells the client that CORS is acceptable for this
// request. If allowMethods == "" the request is assumed to be a CORS
// actual request and no Access-Control-Allow-Methods or -Headers
// headers will be sent.
func (wfe *WebFrontEndImpl) setCORSHeaders(response http.ResponseWriter, request *http.Request, allowMethods string) {
if request.Header.Get("Origin") == "" {
// This is not a CORS request.
return
}
if allowMethods != "" {
// For an OPTIONS request: allow all methods handled at this URL.
response.Header().Set("Access-Control-Allow-Methods", allowMethods)
// Allow all requested headers.
if acrh, ok := request.Header["Access-Control-Request-Headers"]; ok {
for _, h := range acrh {
response.Header().Add("Access-Control-Allow-Headers", h)
}
}
}
response.Header().Set("Access-Control-Allow-Origin", "*")
response.Header().Set("Access-Control-Expose-Headers", "Link, Replay-Nonce")
response.Header().Set("Access-Control-Max-Age", "86400")
}
func (wfe *WebFrontEndImpl) logRequestDetails(logEvent *requestEvent) {
logEvent.ResponseTime = time.Now()
var msg string

View File

@ -421,21 +421,22 @@ func TestHandleFunc(t *testing.T) {
// Plain requests (no CORS)
type testCase struct {
allowed []string
reqMethod string
shouldSucceed bool
allowed []string
reqMethod string
shouldCallStub bool
shouldSucceed bool
}
var lastNonce string
for _, c := range []testCase{
{[]string{"GET", "POST"}, "GET", true},
{[]string{"GET", "POST"}, "POST", true},
{[]string{"GET"}, "", false},
{[]string{"GET"}, "POST", false},
{[]string{"GET"}, "OPTIONS", false}, // TODO, #469
{[]string{"GET"}, "MAKE-COFFEE", false}, // 405, or 418?
{[]string{"GET", "POST"}, "GET", true, true},
{[]string{"GET", "POST"}, "POST", true, true},
{[]string{"GET"}, "", false, false},
{[]string{"GET"}, "POST", false, false},
{[]string{"GET"}, "OPTIONS", false, true},
{[]string{"GET"}, "MAKE-COFFEE", false, false}, // 405, or 418?
} {
runWrappedHandler(&http.Request{Method: c.reqMethod}, c.allowed...)
test.AssertEquals(t, stubCalled, c.shouldSucceed)
test.AssertEquals(t, stubCalled, c.shouldCallStub)
if c.shouldSucceed {
test.AssertEquals(t, rw.Code, http.StatusOK)
} else {
@ -447,6 +448,7 @@ func TestHandleFunc(t *testing.T) {
}
nonce := rw.Header().Get("Replay-Nonce")
test.AssertNotEquals(t, nonce, lastNonce)
test.AssertNotEquals(t, nonce, "")
lastNonce = nonce
}
@ -461,40 +463,86 @@ func TestHandleFunc(t *testing.T) {
test.AssertEquals(t, stubCalled, false)
test.AssertEquals(t, rw.Body.String(), "")
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, POST")
}
func TestStandardHeaders(t *testing.T) {
wfe := setupWFE(t)
mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
// CORS "actual" request for disallowed method
runWrappedHandler(&http.Request{
Method: "POST",
Header: map[string][]string{
"Origin": {"http://example.com"},
},
}, "GET")
test.AssertEquals(t, stubCalled, false)
test.AssertEquals(t, rw.Code, http.StatusMethodNotAllowed)
cases := []struct {
path string
allowed []string
}{
{wfe.NewReg, []string{"POST"}},
{wfe.RegBase, []string{"POST"}},
{wfe.NewAuthz, []string{"POST"}},
{wfe.AuthzBase, []string{"GET"}},
{wfe.ChallengeBase, []string{"GET", "POST"}},
{wfe.NewCert, []string{"POST"}},
{wfe.CertBase, []string{"GET"}},
{wfe.SubscriberAgreementURL, []string{"GET"}},
}
// CORS "actual" request for allowed method
runWrappedHandler(&http.Request{
Method: "GET",
Header: map[string][]string{
"Origin": {"http://example.com"},
},
}, "GET", "POST")
test.AssertEquals(t, stubCalled, true)
test.AssertEquals(t, rw.Code, http.StatusOK)
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "*")
test.AssertEquals(t, sortHeader(rw.Header().Get("Access-Control-Expose-Headers")), "Link, Replay-Nonce")
for _, c := range cases {
responseWriter := httptest.NewRecorder()
mux.ServeHTTP(responseWriter, &http.Request{
Method: "BOGUS",
URL: mustParseURL(c.path),
})
acao := responseWriter.Header().Get("Access-Control-Allow-Origin")
nonce := responseWriter.Header().Get("Replay-Nonce")
allow := responseWriter.Header().Get("Allow")
test.Assert(t, responseWriter.Code == http.StatusMethodNotAllowed, "Bogus method allowed")
test.Assert(t, acao == "*", "Bad CORS header")
test.Assert(t, len(nonce) > 0, "Bad Replay-Nonce header")
test.Assert(t, len(allow) > 0 && allow == strings.Join(c.allowed, ", "), "Bad Allow header")
// CORS preflight request for disallowed method
runWrappedHandler(&http.Request{
Method: "OPTIONS",
Header: map[string][]string{
"Origin": {"http://example.com"},
"Access-Control-Request-Method": {"POST"},
},
}, "GET")
test.AssertEquals(t, stubCalled, false)
test.AssertEquals(t, rw.Code, http.StatusOK)
test.AssertEquals(t, rw.Header().Get("Allow"), "GET")
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "")
// CORS preflight request for allowed method
runWrappedHandler(&http.Request{
Method: "OPTIONS",
Header: map[string][]string{
"Origin": {"http://example.com"},
"Access-Control-Request-Method": {"POST"},
"Access-Control-Request-Headers": {"X-Accept-Header1, X-Accept-Header2", "X-Accept-Header3"},
},
}, "GET", "POST")
test.AssertEquals(t, rw.Code, http.StatusOK)
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "*")
test.AssertEquals(t, rw.Header().Get("Access-Control-Max-Age"), "86400")
test.AssertEquals(t, sortHeader(rw.Header().Get("Access-Control-Allow-Methods")), "GET, POST")
test.AssertDeepEquals(t, rw.Header()["Access-Control-Allow-Headers"], []string{"X-Accept-Header1, X-Accept-Header2", "X-Accept-Header3"})
test.AssertEquals(t, sortHeader(rw.Header().Get("Access-Control-Expose-Headers")), "Link, Replay-Nonce")
// OPTIONS request without an Origin header (i.e., not a CORS
// preflight request)
runWrappedHandler(&http.Request{
Method: "OPTIONS",
Header: map[string][]string{
"Access-Control-Request-Method": {"POST"},
},
}, "GET", "POST")
test.AssertEquals(t, rw.Code, http.StatusOK)
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "")
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, POST")
// CORS preflight request missing optional Request-Method
// header. The "actual" request will be GET.
for _, allowedMethod := range []string{"GET", "POST"} {
runWrappedHandler(&http.Request{
Method: "OPTIONS",
Header: map[string][]string{
"Origin": {"http://example.com"},
},
}, allowedMethod)
test.AssertEquals(t, rw.Code, http.StatusOK)
if allowedMethod == "GET" {
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "*")
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Methods"), "GET")
} else {
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "")
}
}
}