diff --git a/test/all.sh b/test/all.sh index 52ef85359..67d356247 100755 --- a/test/all.sh +++ b/test/all.sh @@ -3,6 +3,5 @@ set -euo pipefail IFS=$'\n\t' go test -tags integration -v -short -./test-http.sh ./test-merge.sh ./test-delupd.sh diff --git a/test/common_test.go b/test/common_test.go index c846cb58a..fa954879e 100644 --- a/test/common_test.go +++ b/test/common_test.go @@ -62,11 +62,11 @@ type syncthingProcess struct { logfd *os.File } -func (p *syncthingProcess) start() (string, error) { +func (p *syncthingProcess) start() error { if p.logfd == nil { logfd, err := os.Create(p.log) if err != nil { - return "", err + return err } p.logfd = logfd } @@ -78,14 +78,15 @@ func (p *syncthingProcess) start() (string, error) { err := cmd.Start() if err != nil { - return "", err + return err } p.cmd = cmd for { - ver, err := p.version() + resp, err := p.get("/") if err == nil { - return ver, nil + resp.Body.Close() + return nil } time.Sleep(250 * time.Millisecond) } diff --git a/test/http.go b/test/http.go deleted file mode 100644 index 70f89beb9..000000000 --- a/test/http.go +++ /dev/null @@ -1,367 +0,0 @@ -// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file). -// -// This program is free software: you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program. If not, see . - -// +build ignore - -package main - -import ( - "bufio" - "bytes" - "flag" - "fmt" - "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/completion?device=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU&folder=default", - "/rest/config", - "/rest/config/sync", - "/rest/connections", - "/rest/errors", - "/rest/events", - "/rest/lang", - "/rest/model?folder=default", - "/rest/need", - "/rest/deviceid?id=I6KAH7666SLLLB5PFXSOAUFJCDZCYAOMLEKCP2GB32BV5RQST3PSROAU", - "/rest/report", - "/rest/system", -} - -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{"TestJSONEndpoints", TestJSONEndpoints}) - tests = append(tests, testing.InternalTest{"TestPOSTNoCSRF", TestPOSTNoCSRF}) - - if len(authUser) > 0 { - // If we expect authentication, verify that it fails with the wrong password and wrong API key - tests = append(tests, testing.InternalTest{"TestJSONEndpointsNoAuth", TestJSONEndpointsNoAuth}) - tests = append(tests, testing.InternalTest{"TestJSONEndpointsIncorrectAuth", TestJSONEndpointsIncorrectAuth}) - } - - if len(csrfToken) > 0 { - // If we have a CSRF token, verify that POST succeeds with it - tests = append(tests, testing.InternalTest{"TestPostWitchCSRF", TestPostWitchCSRF}) - tests = append(tests, testing.InternalTest{"TestGetPostConfigOK", TestGetPostConfigOK}) - tests = append(tests, testing.InternalTest{"TestGetPostConfigFail", TestGetPostConfigFail}) - } - - fmt.Printf("Testing HTTP: CSRF=%v, API=%v, Auth=%v\n", len(csrfToken) > 0, len(apiKey) > 0, len(authUser) > 0) - 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.Error(err) - continue - } - if res.StatusCode != 200 { - t.Errorf("Status %d != 200 for %q", res.StatusCode, p) - continue - } - if ct := res.Header.Get("Content-Type"); ct != "application/json; charset=utf-8" { - t.Errorf("Content-Type %q != \"application/json\" for %q", ct, p) - continue - } - } -} - -func TestPOSTNoCSRF(t *testing.T) { - r, err := http.NewRequest("POST", "http://"+target+"/rest/error/clear", 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 POST", res.StatusCode) - } -} - -func TestPostWitchCSRF(t *testing.T) { - r, err := http.NewRequest("POST", "http://"+target+"/rest/error/clear", nil) - if err != nil { - t.Fatal(err) - } - if len(csrfToken) > 0 { - r.Header.Set("X-CSRF-Token", csrfToken) - } - if len(authUser) > 0 { - r.SetBasicAuth(authUser, authPass) - } - res, err := http.DefaultClient.Do(r) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 200 { - t.Fatalf("Status %d != 200 for POST", res.StatusCode) - } -} - -func TestGetPostConfigOK(t *testing.T) { - // Get config - r, err := http.NewRequest("GET", "http://"+target+"/rest/config", nil) - if err != nil { - t.Fatal(err) - } - if len(csrfToken) > 0 { - r.Header.Set("X-CSRF-Token", csrfToken) - } - if len(authUser) > 0 { - r.SetBasicAuth(authUser, authPass) - } - res, err := http.DefaultClient.Do(r) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 200 { - t.Fatalf("Status %d != 200 for POST", res.StatusCode) - } - bs, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatal(err) - } - res.Body.Close() - - // Post same config back - r, err = http.NewRequest("POST", "http://"+target+"/rest/config", bytes.NewBuffer(bs)) - if err != nil { - t.Fatal(err) - } - if len(csrfToken) > 0 { - r.Header.Set("X-CSRF-Token", csrfToken) - } - if len(authUser) > 0 { - r.SetBasicAuth(authUser, authPass) - } - res, err = http.DefaultClient.Do(r) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 200 { - t.Fatalf("Status %d != 200 for POST", res.StatusCode) - } -} - -func TestGetPostConfigFail(t *testing.T) { - // Get config - r, err := http.NewRequest("GET", "http://"+target+"/rest/config", nil) - if err != nil { - t.Fatal(err) - } - if len(csrfToken) > 0 { - r.Header.Set("X-CSRF-Token", csrfToken) - } - if len(authUser) > 0 { - r.SetBasicAuth(authUser, authPass) - } - res, err := http.DefaultClient.Do(r) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 200 { - t.Fatalf("Status %d != 200 for POST", res.StatusCode) - } - bs, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatal(err) - } - res.Body.Close() - - // Post same config back, with some characters missing to create a syntax error - r, err = http.NewRequest("POST", "http://"+target+"/rest/config", bytes.NewBuffer(bs[2:])) - if err != nil { - t.Fatal(err) - } - if len(csrfToken) > 0 { - r.Header.Set("X-CSRF-Token", csrfToken) - } - if len(authUser) > 0 { - r.SetBasicAuth(authUser, authPass) - } - res, err = http.DefaultClient.Do(r) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 500 { - t.Fatalf("Status %d != 500 for POST", res.StatusCode) - } -} - -func TestJSONEndpointsNoAuth(t *testing.T) { - for _, p := range jsonEndpoints { - r, err := http.NewRequest("GET", "http://"+target+p, nil) - if err != nil { - t.Error(err) - continue - } - if len(csrfToken) > 0 { - r.Header.Set("X-CSRF-Token", csrfToken) - } - res, err := http.DefaultClient.Do(r) - if err != nil { - t.Error(err) - continue - } - if res.StatusCode != 403 && res.StatusCode != 401 { - t.Errorf("Status %d != 403/401 for %q", res.StatusCode, p) - continue - } - } -} - -func TestJSONEndpointsIncorrectAuth(t *testing.T) { - for _, p := range jsonEndpoints { - r, err := http.NewRequest("GET", "http://"+target+p, nil) - if err != nil { - t.Error(err) - continue - } - 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.Error(err) - continue - } - if res.StatusCode != 403 && res.StatusCode != 401 { - t.Errorf("Status %d != 403/401 for %q", res.StatusCode, p) - continue - } - } -} - -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(apiKey) > 0 { - r.Header.Set("X-API-Key", apiKey) - } - return http.DefaultClient.Do(r) -} diff --git a/test/http_test.go b/test/http_test.go new file mode 100644 index 000000000..f436efde7 --- /dev/null +++ b/test/http_test.go @@ -0,0 +1,249 @@ +// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file). +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see . + +// +build integration + +package integration_test + +import ( + "encoding/json" + "net/http" + "strings" + "testing" +) + +var jsonEndpoints = []string{ + "/rest/completion?device=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU&folder=default", + "/rest/config", + "/rest/config/sync", + "/rest/connections", + "/rest/errors", + "/rest/events", + "/rest/lang", + "/rest/model?folder=default", + "/rest/need", + "/rest/deviceid?id=I6KAH7666SLLLB5PFXSOAUFJCDZCYAOMLEKCP2GB32BV5RQST3PSROAU", + "/rest/report", + "/rest/system", +} + +func TestGetIndex(t *testing.T) { + st := syncthingProcess{ + argv: []string{"-home", "h2"}, + port: 8082, + log: "2.out", + } + err := st.start() + if err != nil { + t.Fatal(err) + } + defer st.stop() + + res, err := st.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) + } + if res.Header.Get("Set-Cookie") == "" { + t.Error("No set-cookie header") + } + res.Body.Close() + + res, err = st.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) + } + if res.Header.Get("Set-Cookie") == "" { + t.Error("No set-cookie header") + } + res.Body.Close() +} + +func TestGetIndexAuth(t *testing.T) { + st := syncthingProcess{ + argv: []string{"-home", "h1"}, + port: 8081, + log: "1.out", + } + err := st.start() + if err != nil { + t.Fatal(err) + } + defer st.stop() + + // Without auth should give 401 + + res, err := http.Get("http://127.0.0.1:8081/") + if err != nil { + t.Fatal(err) + } + res.Body.Close() + if res.StatusCode != 401 { + t.Errorf("Status %d != 401", res.StatusCode) + } + + // With wrong username/password should give 401 + + req, err := http.NewRequest("GET", "http://127.0.0.1:8081/", nil) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth("testuser", "wrongpass") + + res, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + if res.StatusCode != 401 { + t.Fatalf("Status %d != 401", res.StatusCode) + } + + // With correct username/password should succeed + + req, err = http.NewRequest("GET", "http://127.0.0.1:8081/", nil) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth("testuser", "testpass") + + res, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + if res.StatusCode != 200 { + t.Fatalf("Status %d != 200", res.StatusCode) + } +} + +func TestGetJSON(t *testing.T) { + st := syncthingProcess{ + argv: []string{"-home", "h2"}, + port: 8082, + log: "2.out", + } + err := st.start() + if err != nil { + t.Fatal(err) + } + defer st.stop() + + for _, path := range jsonEndpoints { + res, err := st.get(path) + if err != nil { + t.Error(err) + } + + if ct := res.Header.Get("Content-Type"); ct != "application/json; charset=utf-8" { + t.Errorf("Incorrect Content-Type %q for %q", ct, path) + } + + var intf interface{} + err = json.NewDecoder(res.Body).Decode(&intf) + res.Body.Close() + + if err != nil { + t.Error(err) + } + } +} + +func TestPOSTWithoutCSRF(t *testing.T) { + st := syncthingProcess{ + argv: []string{"-home", "h2"}, + port: 8082, + log: "2.out", + } + err := st.start() + if err != nil { + t.Fatal(err) + } + defer st.stop() + + // Should fail without CSRF + + req, err := http.NewRequest("POST", "http://127.0.0.1:8082/rest/error/clear", nil) + if err != nil { + t.Fatal(err) + } + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + if res.StatusCode != 403 { + t.Fatalf("Status %d != 403 for POST", res.StatusCode) + } + + // Get CSRF + + req, err = http.NewRequest("GET", "http://127.0.0.1:8082/", nil) + if err != nil { + t.Fatal(err) + } + res, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + hdr := res.Header.Get("Set-Cookie") + if !strings.Contains(hdr, "CSRF-Token") { + t.Error("Missing CSRF-Token in", hdr) + } + + // Should succeed with CSRF + + req, err = http.NewRequest("POST", "http://127.0.0.1:8082/rest/error/clear", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("X-CSRF-Token", hdr[len("CSRF-Token="):]) + res, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + if res.StatusCode != 200 { + t.Fatalf("Status %d != 200 for POST", res.StatusCode) + } + + // Should fail with incorrect CSRF + + req, err = http.NewRequest("POST", "http://127.0.0.1:8082/rest/error/clear", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("X-CSRF-Token", hdr[len("CSRF-Token="):]+"X") + res, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + if res.StatusCode != 403 { + t.Fatalf("Status %d != 403 for POST", res.StatusCode) + } +} diff --git a/test/httpstress_test.go b/test/httpstress_test.go index 004849313..d2fa35f2d 100644 --- a/test/httpstress_test.go +++ b/test/httpstress_test.go @@ -44,11 +44,10 @@ func TestStressHTTP(t *testing.T) { port: 8082, apiKey: apiKey, } - ver, err := sender.start() + err = sender.start() if err != nil { t.Fatal(err) } - log.Println(ver) tc := &tls.Config{InsecureSkipVerify: true} tr := &http.Transport{ diff --git a/test/reconnect_test.go b/test/reconnect_test.go index 837ce17b8..1bd49ae4e 100644 --- a/test/reconnect_test.go +++ b/test/reconnect_test.go @@ -60,11 +60,10 @@ func testRestartDuringTransfer(t *testing.T, restartSender, restartReceiver bool port: 8081, apiKey: apiKey, } - ver, err := sender.start() + err = sender.start() if err != nil { t.Fatal(err) } - log.Println(ver) receiver := syncthingProcess{ // id2 log: "2.out", @@ -72,12 +71,11 @@ func testRestartDuringTransfer(t *testing.T, restartSender, restartReceiver bool port: 8082, apiKey: apiKey, } - ver, err = receiver.start() + err = receiver.start() if err != nil { sender.stop() t.Fatal(err) } - log.Println(ver) var prevComp int for { diff --git a/test/test-http.sh b/test/test-http.sh deleted file mode 100755 index 99fffb99d..000000000 --- a/test/test-http.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -# Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file). -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program. If not, see . - -id1=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU -id2=JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU -id3=373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU - -stop() { - echo Stopping - curl -s -o/dev/null -HX-API-Key:abc123 -X POST http://127.0.0.1:8081/rest/shutdown - curl -s -o/dev/null -HX-API-Key:abc123 -X POST http://127.0.0.1:8082/rest/shutdown - exit $1 -} - -echo Building -go build http.go - -echo Starting -chmod -R +w s1 s2 || true -rm -rf s1 s2 h1/index h2/index -syncthing -home h1 > 1.out 2>&1 & -syncthing -home h2 > 2.out 2>&1 & -sleep 1 - -echo Fetching CSRF tokens -curl -s -o /dev/null http://testuser:testpass@127.0.0.1:8081/index.html -curl -s -o /dev/null http://127.0.0.1:8082/index.html -sleep 1 - -echo Testing -./http -target 127.0.0.1:8081 -user testuser -pass testpass -csrf h1/csrftokens.txt || stop 1 -./http -target 127.0.0.1:8081 -api abc123 || stop 1 -./http -target 127.0.0.1:8082 -csrf h2/csrftokens.txt || stop 1 -./http -target 127.0.0.1:8082 -api abc123 || stop 1 - -stop 0 diff --git a/test/transfer-bench_test.go b/test/transfer-bench_test.go index ee6735c31..65f89b3d3 100644 --- a/test/transfer-bench_test.go +++ b/test/transfer-bench_test.go @@ -49,11 +49,10 @@ func TestBenchmarkTransfer(t *testing.T) { port: 8081, apiKey: apiKey, } - ver, err := sender.start() + err = sender.start() if err != nil { t.Fatal(err) } - log.Println(ver) receiver := syncthingProcess{ // id2 log: "2.out", @@ -61,12 +60,11 @@ func TestBenchmarkTransfer(t *testing.T) { port: 8082, apiKey: apiKey, } - ver, err = receiver.start() + err = receiver.start() if err != nil { sender.stop() t.Fatal(err) } - log.Println(ver) var t0 time.Time loop: