Fix CORS headers, support OPTIONS requests.
This commit is contained in:
parent
4ddff2c700
commit
b6a4b66899
|
@ -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
|
||||
|
|
|
@ -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"), "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue