diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 8a57d5325..5d70ed580 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -152,7 +152,7 @@ func restGetModelVersion(m *model.Model, w http.ResponseWriter, r *http.Request) res["version"] = m.Version(repo) - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(res) } @@ -182,7 +182,7 @@ func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) { res["state"] = m.State(repo) res["version"] = m.Version(repo) - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(res) } @@ -198,13 +198,13 @@ func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) { files := m.NeedFilesRepo(repo) - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(files) } func restGetConnections(m *model.Model, w http.ResponseWriter) { var res = m.ConnectionStats() - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(res) } @@ -213,6 +213,7 @@ func restGetConfig(w http.ResponseWriter) { if encCfg.GUI.Password != "" { encCfg.GUI.Password = unchangedPassword } + w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(encCfg) } @@ -289,21 +290,25 @@ func restPostConfig(req *http.Request, m *model.Model) { } func restGetConfigInSync(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync}) } func restPostRestart(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") flushResponse(`{"ok": "restarting"}`, w) go restart() } func restPostReset(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") flushResponse(`{"ok": "resetting repos"}`, w) resetRepositories() go restart() } func restPostShutdown(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") flushResponse(`{"ok": "shutting down"}`, w) go shutdown() } @@ -338,11 +343,12 @@ func restGetSystem(w http.ResponseWriter) { cpuUsageLock.RUnlock() res["cpuPercent"] = cpusum / 10 - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(res) } func restGetErrors(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") guiErrorsMut.Lock() json.NewEncoder(w).Encode(guiErrors) guiErrorsMut.Unlock() @@ -379,10 +385,12 @@ func restPostDiscoveryHint(r *http.Request) { } func restGetDiscovery(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(discoverer.All()) } func restGetReport(w http.ResponseWriter, m *model.Model) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(reportData(m)) } diff --git a/integration/.gitignore b/integration/.gitignore index 0b56d613b..1a589db24 100644 --- a/integration/.gitignore +++ b/integration/.gitignore @@ -14,3 +14,4 @@ dirs-* *.out csrftokens.txt s4d +http diff --git a/integration/h1/config.xml b/integration/h1/config.xml index 1506af089..e864a7234 100644 --- a/integration/h1/config.xml +++ b/integration/h1/config.xml @@ -25,6 +25,8 @@
127.0.0.1:8081
abc123 + testuser + testpass
127.0.0.1:22001 diff --git a/integration/http.go b/integration/http.go new file mode 100644 index 000000000..56e0516d1 --- /dev/null +++ b/integration/http.go @@ -0,0 +1,232 @@ +// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved. +// Use of this source code is governed by an MIT-style license that can be +// found in the LICENSE file. + +// +build ignore + +package main + +import ( + "bufio" + "flag" + "io/ioutil" + "log" + "net/http" + "os" + "regexp" + "testing" +) + +var ( + target string + authUser string + authPass string + csrfToken string + csrfFile string + apiKey string +) + +var jsonEndpoints = []string{ + "/rest/model?repo=default", + "/rest/model/version?repo=default", + "/rest/need", + "/rest/connections", + "/rest/config", + "/rest/config/sync", + "/rest/system", + "/rest/errors", + // "/rest/discovery", + "/rest/report", +} + +func main() { + flag.StringVar(&target, "target", "localhost:8080", "Test target") + flag.StringVar(&authUser, "user", "", "Username") + flag.StringVar(&authPass, "pass", "", "Password") + flag.StringVar(&csrfFile, "csrf", "", "CSRF token file") + flag.StringVar(&apiKey, "api", "", "API key") + flag.Parse() + + if len(csrfFile) > 0 { + fd, err := os.Open(csrfFile) + if err != nil { + log.Fatal(err) + } + s := bufio.NewScanner(fd) + for s.Scan() { + csrfToken = s.Text() + } + fd.Close() + } + + var tests []testing.InternalTest + tests = append(tests, testing.InternalTest{"TestGetIndex", TestGetIndex}) + tests = append(tests, testing.InternalTest{"TestGetVersion", TestGetVersion}) + tests = append(tests, testing.InternalTest{"TestGetVersionNoCSRF", TestGetVersion}) + tests = append(tests, testing.InternalTest{"TestJSONEndpoints", TestJSONEndpoints}) + if len(authUser) > 0 || len(apiKey) > 0 { + tests = append(tests, testing.InternalTest{"TestJSONEndpointsNoAuth", TestJSONEndpointsNoAuth}) + tests = append(tests, testing.InternalTest{"TestJSONEndpointsIncorrectAuth", TestJSONEndpointsIncorrectAuth}) + } + if len(csrfToken) > 0 { + tests = append(tests, testing.InternalTest{"TestJSONEndpointsNoCSRF", TestJSONEndpointsNoCSRF}) + } + + testing.Main(matcher, tests, nil, nil) +} + +func matcher(s0, s1 string) (bool, error) { + return true, nil +} + +func TestGetIndex(t *testing.T) { + res, err := get("/index.html") + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 200 { + t.Errorf("Status %d != 200", res.StatusCode) + } + if res.ContentLength < 1024 { + t.Errorf("Length %d < 1024", res.ContentLength) + } + res.Body.Close() + + res, err = get("/") + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 200 { + t.Errorf("Status %d != 200", res.StatusCode) + } + if res.ContentLength < 1024 { + t.Errorf("Length %d < 1024", res.ContentLength) + } + res.Body.Close() +} + +func TestGetVersion(t *testing.T) { + res, err := get("/rest/version") + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 200 { + t.Fatalf("Status %d != 200", res.StatusCode) + } + ver, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + + if !regexp.MustCompile(`v\d+\.\d+\.\d+`).Match(ver) { + t.Errorf("Invalid version %q", ver) + } +} + +func TestGetVersionNoCSRF(t *testing.T) { + r, err := http.NewRequest("GET", "http://"+target+"/rest/version", nil) + if err != nil { + t.Fatal(err) + } + if len(authUser) > 0 { + r.SetBasicAuth(authUser, authPass) + } + res, err := http.DefaultClient.Do(r) + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 403 { + t.Fatalf("Status %d != 403", res.StatusCode) + } +} + +func TestJSONEndpoints(t *testing.T) { + for _, p := range jsonEndpoints { + res, err := get(p) + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 200 { + t.Errorf("Status %d != 200 for %q", res.StatusCode, p) + } + if ct := res.Header.Get("Content-Type"); ct != "application/json; charset=utf-8" { + t.Errorf("Content-Type %q != \"application/json\" for %q", ct, p) + } + } +} + +func TestJSONEndpointsNoCSRF(t *testing.T) { + for _, p := range jsonEndpoints { + r, err := http.NewRequest("GET", "http://"+target+p, nil) + if err != nil { + t.Fatal(err) + } + if len(authUser) > 0 { + r.SetBasicAuth(authUser, authPass) + } + res, err := http.DefaultClient.Do(r) + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 403 && res.StatusCode != 401 { + t.Fatalf("Status %d != 403/401 for %q", res.StatusCode, p) + } + } +} + +func TestJSONEndpointsNoAuth(t *testing.T) { + for _, p := range jsonEndpoints { + r, err := http.NewRequest("GET", "http://"+target+p, nil) + if err != nil { + t.Fatal(err) + } + if len(csrfToken) > 0 { + r.Header.Set("X-CSRF-Token", csrfToken) + } + res, err := http.DefaultClient.Do(r) + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 403 && res.StatusCode != 401 { + t.Fatalf("Status %d != 403/401 for %q", res.StatusCode, p) + } + } +} + +func TestJSONEndpointsIncorrectAuth(t *testing.T) { + for _, p := range jsonEndpoints { + r, err := http.NewRequest("GET", "http://"+target+p, nil) + if err != nil { + t.Fatal(err) + } + if len(csrfToken) > 0 { + r.Header.Set("X-CSRF-Token", csrfToken) + } + r.SetBasicAuth("wronguser", "wrongpass") + res, err := http.DefaultClient.Do(r) + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 403 && res.StatusCode != 401 { + t.Fatalf("Status %d != 403/401 for %q", res.StatusCode, p) + } + } +} + +func get(path string) (*http.Response, error) { + r, err := http.NewRequest("GET", "http://"+target+path, nil) + if err != nil { + return nil, err + } + if len(authUser) > 0 { + r.SetBasicAuth(authUser, authPass) + } + if len(csrfToken) > 0 { + r.Header.Set("X-CSRF-Token", csrfToken) + } + if len(apiKey) > 0 { + r.Header.Set("X-API-Key", apiKey) + } + return http.DefaultClient.Do(r) +} diff --git a/integration/test.sh b/integration/test.sh index 5f9abc535..1f3613cfd 100755 --- a/integration/test.sh +++ b/integration/test.sh @@ -13,12 +13,30 @@ id3=373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA go build genfiles.go go build md5r.go go build json.go +go build http.go start() { echo "Starting..." for i in 1 2 3 4 ; do STPROFILER=":909$i" syncthing -home "h$i" > "$i.out" 2>&1 & done + + # Test REST API + sleep 2 + curl -s -o /dev/null http://testuser:testpass@localhost:8081/index.html + curl -s -o /dev/null http://localhost:8082/index.html + sleep 1 + ./http -target localhost:8081 -user testuser -pass testpass -csrf h1/csrftokens.txt || stop 1 + ./http -target localhost:8081 -api abc123 || stop 1 + ./http -target localhost:8082 -csrf h2/csrftokens.txt || stop 1 + ./http -target localhost:8082 -api abc123 || stop 1 +} + +stop() { + for i in 1 2 3 4 ; do + curl -HX-API-Key:abc123 -X POST "http://localhost:808$i/rest/shutdown" + done + exit $1 } clean() { @@ -83,8 +101,7 @@ testConvergence() { fi done if [[ $ok != 7 ]] ; then - pkill syncthing - exit 1 + stop 1 fi } @@ -157,6 +174,4 @@ for ((t = 1; t <= $iterations; t++)) ; do testConvergence done -for i in 1 2 3 4 ; do - curl -HX-API-Key:abc123 -X POST "http://localhost:808$i/rest/shutdown" -done +stop 0