Browse Source

Test script for REST interface

Jakob Borg 11 years ago
parent
commit
77fe8449ba
5 changed files with 268 additions and 10 deletions
  1. 13 5
      cmd/syncthing/gui.go
  2. 1 0
      integration/.gitignore
  3. 2 0
      integration/h1/config.xml
  4. 232 0
      integration/http.go
  5. 20 5
      integration/test.sh

+ 13 - 5
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))
 }
 

+ 1 - 0
integration/.gitignore

@@ -14,3 +14,4 @@ dirs-*
 *.out
 csrftokens.txt
 s4d
+http

+ 2 - 0
integration/h1/config.xml

@@ -25,6 +25,8 @@
     <gui enabled="true" tls="false">
         <address>127.0.0.1:8081</address>
         <apikey>abc123</apikey>
+        <user>testuser</user>
+        <password>testpass</password>
     </gui>
     <options>
         <listenAddress>127.0.0.1:22001</listenAddress>

+ 232 - 0
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)
+}

+ 20 - 5
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