diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index ce36bd926..d884bd516 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -442,10 +442,12 @@ func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Process OPTIONS requests if r.Method == "OPTIONS" { + // Add a generous access-control-allow-origin header for CORS requests + w.Header().Add("Access-Control-Allow-Origin", "*") // Only GET/POST Methods are supported w.Header().Set("Access-Control-Allow-Methods", "GET, POST") - // Only this custom header can be set - w.Header().Set("Access-Control-Allow-Headers", "X-API-Key") + // Only these headers can be set + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-API-Key") // The request is meant to be cached 10 minutes w.Header().Set("Access-Control-Max-Age", "600") diff --git a/cmd/syncthing/gui_csrf.go b/cmd/syncthing/gui_csrf.go index 71a9770be..481d8a36e 100644 --- a/cmd/syncthing/gui_csrf.go +++ b/cmd/syncthing/gui_csrf.go @@ -37,6 +37,9 @@ func csrfMiddleware(unique string, prefix string, cfg config.GUIConfiguration, n return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Allow requests carrying a valid API key if cfg.IsValidAPIKey(r.Header.Get("X-API-Key")) { + // Set the access-control-allow-origin header for CORS requests + // since a valid API key has been provided + w.Header().Add("Access-Control-Allow-Origin", "*") next.ServeHTTP(w, r) return } diff --git a/cmd/syncthing/gui_test.go b/cmd/syncthing/gui_test.go index 239d4328c..1bc26c8d9 100644 --- a/cmd/syncthing/gui_test.go +++ b/cmd/syncthing/gui_test.go @@ -857,3 +857,64 @@ func TestAddressIsLocalhost(t *testing.T) { } } } + +func TestAccessControlAllowOriginHeader(t *testing.T) { + const testAPIKey = "foobarbaz" + cfg := new(mockedConfig) + cfg.gui.APIKey = testAPIKey + baseURL, err := startHTTP(cfg) + if err != nil { + t.Fatal(err) + } + cli := &http.Client{ + Timeout: time.Second, + } + + req, _ := http.NewRequest("GET", baseURL+"/rest/system/status", nil) + req.Header.Set("X-API-Key", testAPIKey) + resp, err := cli.Do(req) + if err != nil { + t.Fatal(err) + } + + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatal("GET on /rest/system/status should succeed, not", resp.Status) + } + if resp.Header.Get("Access-Control-Allow-Origin") != "*" { + t.Fatal("GET on /rest/system/status should return a 'Access-Control-Allow-Origin: *' header") + } +} + +func TestOptionsRequest(t *testing.T) { + const testAPIKey = "foobarbaz" + cfg := new(mockedConfig) + cfg.gui.APIKey = testAPIKey + baseURL, err := startHTTP(cfg) + if err != nil { + t.Fatal(err) + } + cli := &http.Client{ + Timeout: time.Second, + } + + req, _ := http.NewRequest("OPTIONS", baseURL+"/rest/system/status", nil) + resp, err := cli.Do(req) + if err != nil { + t.Fatal(err) + } + + resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + t.Fatal("OPTIONS on /rest/system/status should succeed, not", resp.Status) + } + if resp.Header.Get("Access-Control-Allow-Origin") != "*" { + t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Origin: *' header") + } + if resp.Header.Get("Access-Control-Allow-Methods") != "GET, POST" { + t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Methods: GET, POST' header") + } + if resp.Header.Get("Access-Control-Allow-Headers") != "Content-Type, X-API-Key" { + t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Headers: Content-Type, X-API-KEY' header") + } +}