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