1
0

api_test.go 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204
  1. // Copyright (C) 2016 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package api
  7. import (
  8. "bytes"
  9. "compress/gzip"
  10. "encoding/json"
  11. "fmt"
  12. "io"
  13. "io/ioutil"
  14. "net"
  15. "net/http"
  16. "net/http/httptest"
  17. "os"
  18. "path/filepath"
  19. "runtime"
  20. "strconv"
  21. "strings"
  22. "testing"
  23. "time"
  24. "github.com/d4l3k/messagediff"
  25. "github.com/syncthing/syncthing/lib/config"
  26. "github.com/syncthing/syncthing/lib/events"
  27. "github.com/syncthing/syncthing/lib/fs"
  28. "github.com/syncthing/syncthing/lib/locations"
  29. "github.com/syncthing/syncthing/lib/model"
  30. "github.com/syncthing/syncthing/lib/protocol"
  31. "github.com/syncthing/syncthing/lib/sync"
  32. "github.com/syncthing/syncthing/lib/tlsutil"
  33. "github.com/syncthing/syncthing/lib/ur"
  34. "github.com/thejerf/suture"
  35. )
  36. var (
  37. confDir = filepath.Join("testdata", "config")
  38. token = filepath.Join(confDir, "csrftokens.txt")
  39. )
  40. func TestMain(m *testing.M) {
  41. orig := locations.GetBaseDir(locations.ConfigBaseDir)
  42. locations.SetBaseDir(locations.ConfigBaseDir, confDir)
  43. exitCode := m.Run()
  44. locations.SetBaseDir(locations.ConfigBaseDir, orig)
  45. os.Exit(exitCode)
  46. }
  47. func TestCSRFToken(t *testing.T) {
  48. t.Parallel()
  49. max := 250
  50. int := 5
  51. if testing.Short() {
  52. max = 20
  53. int = 2
  54. }
  55. m := newCsrfManager("unique", "prefix", config.GUIConfiguration{}, nil, "")
  56. t1 := m.newToken()
  57. t2 := m.newToken()
  58. t3 := m.newToken()
  59. if !m.validToken(t3) {
  60. t.Fatal("t3 should be valid")
  61. }
  62. for i := 0; i < max; i++ {
  63. if i%int == 0 {
  64. // t1 and t2 should remain valid by virtue of us checking them now
  65. // and then.
  66. if !m.validToken(t1) {
  67. t.Fatal("t1 should be valid at iteration", i)
  68. }
  69. if !m.validToken(t2) {
  70. t.Fatal("t2 should be valid at iteration", i)
  71. }
  72. }
  73. // The newly generated token is always valid
  74. t4 := m.newToken()
  75. if !m.validToken(t4) {
  76. t.Fatal("t4 should be valid at iteration", i)
  77. }
  78. }
  79. if m.validToken(t3) {
  80. t.Fatal("t3 should have expired by now")
  81. }
  82. }
  83. func TestStopAfterBrokenConfig(t *testing.T) {
  84. t.Parallel()
  85. cfg := config.Configuration{
  86. GUI: config.GUIConfiguration{
  87. RawAddress: "127.0.0.1:0",
  88. RawUseTLS: false,
  89. },
  90. }
  91. w := config.Wrap("/dev/null", cfg, events.NoopLogger)
  92. srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, nil, false).(*service)
  93. defer os.Remove(token)
  94. srv.started = make(chan string)
  95. sup := suture.New("test", suture.Spec{
  96. PassThroughPanics: true,
  97. })
  98. sup.Add(srv)
  99. sup.ServeBackground()
  100. <-srv.started
  101. // Service is now running, listening on a random port on localhost. Now we
  102. // request a config change to a completely invalid listen address. The
  103. // commit will fail and the service will be in a broken state.
  104. newCfg := config.Configuration{
  105. GUI: config.GUIConfiguration{
  106. RawAddress: "totally not a valid address",
  107. RawUseTLS: false,
  108. },
  109. }
  110. if err := srv.VerifyConfiguration(cfg, newCfg); err == nil {
  111. t.Fatal("Verify config should have failed")
  112. }
  113. // Nonetheless, it should be fine to Stop() it without panic.
  114. sup.Stop()
  115. }
  116. func TestAssetsDir(t *testing.T) {
  117. t.Parallel()
  118. // For any given request to $FILE, we should return the first found of
  119. // - assetsdir/$THEME/$FILE
  120. // - compiled in asset $THEME/$FILE
  121. // - assetsdir/default/$FILE
  122. // - compiled in asset default/$FILE
  123. // The asset map contains compressed assets, so create a couple of gzip compressed assets here.
  124. buf := new(bytes.Buffer)
  125. gw := gzip.NewWriter(buf)
  126. gw.Write([]byte("default"))
  127. gw.Close()
  128. def := buf.Bytes()
  129. buf = new(bytes.Buffer)
  130. gw = gzip.NewWriter(buf)
  131. gw.Write([]byte("foo"))
  132. gw.Close()
  133. foo := buf.Bytes()
  134. e := &staticsServer{
  135. theme: "foo",
  136. mut: sync.NewRWMutex(),
  137. assetDir: "testdata",
  138. assets: map[string][]byte{
  139. "foo/a": foo, // overridden in foo/a
  140. "foo/b": foo,
  141. "default/a": def, // overridden in default/a (but foo/a takes precedence)
  142. "default/b": def, // overridden in default/b (but foo/b takes precedence)
  143. "default/c": def,
  144. },
  145. }
  146. s := httptest.NewServer(e)
  147. defer s.Close()
  148. // assetsdir/foo/a exists, overrides compiled in
  149. expectURLToContain(t, s.URL+"/a", "overridden-foo")
  150. // foo/b is compiled in, default/b is overridden, return compiled in
  151. expectURLToContain(t, s.URL+"/b", "foo")
  152. // only exists as compiled in default/c so use that
  153. expectURLToContain(t, s.URL+"/c", "default")
  154. // only exists as overridden default/d so use that
  155. expectURLToContain(t, s.URL+"/d", "overridden-default")
  156. }
  157. func expectURLToContain(t *testing.T, url, exp string) {
  158. res, err := http.Get(url)
  159. if err != nil {
  160. t.Error(err)
  161. return
  162. }
  163. if res.StatusCode != 200 {
  164. t.Errorf("Got %s instead of 200 OK", res.Status)
  165. return
  166. }
  167. data, err := ioutil.ReadAll(res.Body)
  168. res.Body.Close()
  169. if err != nil {
  170. t.Error(err)
  171. return
  172. }
  173. if string(data) != exp {
  174. t.Errorf("Got %q instead of %q on %q", data, exp, url)
  175. return
  176. }
  177. }
  178. func TestDirNames(t *testing.T) {
  179. t.Parallel()
  180. names := dirNames("testdata")
  181. expected := []string{"config", "default", "foo", "testfolder"}
  182. if diff, equal := messagediff.PrettyDiff(expected, names); !equal {
  183. t.Errorf("Unexpected dirNames return: %#v\n%s", names, diff)
  184. }
  185. }
  186. type httpTestCase struct {
  187. URL string // URL to check
  188. Code int // Expected result code
  189. Type string // Expected content type
  190. Prefix string // Expected result prefix
  191. Timeout time.Duration // Defaults to a second
  192. }
  193. func TestAPIServiceRequests(t *testing.T) {
  194. t.Parallel()
  195. const testAPIKey = "foobarbaz"
  196. cfg := new(mockedConfig)
  197. cfg.gui.APIKey = testAPIKey
  198. baseURL, sup, err := startHTTP(cfg)
  199. if err != nil {
  200. t.Fatal(err)
  201. }
  202. defer sup.Stop()
  203. cases := []httpTestCase{
  204. // /rest/db
  205. {
  206. URL: "/rest/db/completion?device=" + protocol.LocalDeviceID.String() + "&folder=default",
  207. Code: 200,
  208. Type: "application/json",
  209. Prefix: "{",
  210. },
  211. {
  212. URL: "/rest/db/file?folder=default&file=something",
  213. Code: 404,
  214. },
  215. {
  216. URL: "/rest/db/ignores?folder=default",
  217. Code: 200,
  218. Type: "application/json",
  219. Prefix: "{",
  220. },
  221. {
  222. URL: "/rest/db/need?folder=default",
  223. Code: 200,
  224. Type: "application/json",
  225. Prefix: "{",
  226. },
  227. {
  228. URL: "/rest/db/status?folder=default",
  229. Code: 200,
  230. Type: "application/json",
  231. Prefix: "{",
  232. },
  233. {
  234. URL: "/rest/db/browse?folder=default",
  235. Code: 200,
  236. Type: "application/json",
  237. Prefix: "null",
  238. },
  239. // /rest/stats
  240. {
  241. URL: "/rest/stats/device",
  242. Code: 200,
  243. Type: "application/json",
  244. Prefix: "null",
  245. },
  246. {
  247. URL: "/rest/stats/folder",
  248. Code: 200,
  249. Type: "application/json",
  250. Prefix: "null",
  251. },
  252. // /rest/svc
  253. {
  254. URL: "/rest/svc/deviceid?id=" + protocol.LocalDeviceID.String(),
  255. Code: 200,
  256. Type: "application/json",
  257. Prefix: "{",
  258. },
  259. {
  260. URL: "/rest/svc/lang",
  261. Code: 200,
  262. Type: "application/json",
  263. Prefix: "[",
  264. },
  265. {
  266. URL: "/rest/svc/report",
  267. Code: 200,
  268. Type: "application/json",
  269. Prefix: "{",
  270. Timeout: 5 * time.Second,
  271. },
  272. // /rest/system
  273. {
  274. URL: "/rest/system/browse?current=~",
  275. Code: 200,
  276. Type: "application/json",
  277. Prefix: "[",
  278. },
  279. {
  280. URL: "/rest/system/config",
  281. Code: 200,
  282. Type: "application/json",
  283. Prefix: "{",
  284. },
  285. {
  286. URL: "/rest/system/config/insync",
  287. Code: 200,
  288. Type: "application/json",
  289. Prefix: "{",
  290. },
  291. {
  292. URL: "/rest/system/connections",
  293. Code: 200,
  294. Type: "application/json",
  295. Prefix: "null",
  296. },
  297. {
  298. URL: "/rest/system/discovery",
  299. Code: 200,
  300. Type: "application/json",
  301. Prefix: "{",
  302. },
  303. {
  304. URL: "/rest/system/error?since=0",
  305. Code: 200,
  306. Type: "application/json",
  307. Prefix: "{",
  308. },
  309. {
  310. URL: "/rest/system/ping",
  311. Code: 200,
  312. Type: "application/json",
  313. Prefix: "{",
  314. },
  315. {
  316. URL: "/rest/system/status",
  317. Code: 200,
  318. Type: "application/json",
  319. Prefix: "{",
  320. },
  321. {
  322. URL: "/rest/system/version",
  323. Code: 200,
  324. Type: "application/json",
  325. Prefix: "{",
  326. },
  327. {
  328. URL: "/rest/system/debug",
  329. Code: 200,
  330. Type: "application/json",
  331. Prefix: "{",
  332. },
  333. {
  334. URL: "/rest/system/log?since=0",
  335. Code: 200,
  336. Type: "application/json",
  337. Prefix: "{",
  338. },
  339. {
  340. URL: "/rest/system/log.txt?since=0",
  341. Code: 200,
  342. Type: "text/plain",
  343. Prefix: "",
  344. },
  345. }
  346. for _, tc := range cases {
  347. t.Log("Testing", tc.URL, "...")
  348. testHTTPRequest(t, baseURL, tc, testAPIKey)
  349. }
  350. }
  351. // testHTTPRequest tries the given test case, comparing the result code,
  352. // content type, and result prefix.
  353. func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase, apikey string) {
  354. timeout := time.Second
  355. if tc.Timeout > 0 {
  356. timeout = tc.Timeout
  357. }
  358. cli := &http.Client{
  359. Timeout: timeout,
  360. }
  361. req, err := http.NewRequest("GET", baseURL+tc.URL, nil)
  362. if err != nil {
  363. t.Errorf("Unexpected error requesting %s: %v", tc.URL, err)
  364. return
  365. }
  366. req.Header.Set("X-API-Key", apikey)
  367. resp, err := cli.Do(req)
  368. if err != nil {
  369. t.Errorf("Unexpected error requesting %s: %v", tc.URL, err)
  370. return
  371. }
  372. defer resp.Body.Close()
  373. if resp.StatusCode != tc.Code {
  374. t.Errorf("Get on %s should have returned status code %d, not %s", tc.URL, tc.Code, resp.Status)
  375. return
  376. }
  377. ct := resp.Header.Get("Content-Type")
  378. if !strings.HasPrefix(ct, tc.Type) {
  379. t.Errorf("The content type on %s should be %q, not %q", tc.URL, tc.Type, ct)
  380. return
  381. }
  382. data, err := ioutil.ReadAll(resp.Body)
  383. if err != nil {
  384. t.Errorf("Unexpected error reading %s: %v", tc.URL, err)
  385. return
  386. }
  387. if !bytes.HasPrefix(data, []byte(tc.Prefix)) {
  388. t.Errorf("Returned data from %s does not have prefix %q: %s", tc.URL, tc.Prefix, data)
  389. return
  390. }
  391. }
  392. func TestHTTPLogin(t *testing.T) {
  393. t.Parallel()
  394. cfg := new(mockedConfig)
  395. cfg.gui.User = "üser"
  396. cfg.gui.Password = "$2a$10$IdIZTxTg/dCNuNEGlmLynOjqg4B1FvDKuIV5e0BB3pnWVHNb8.GSq" // bcrypt of "räksmörgås" in UTF-8
  397. baseURL, sup, err := startHTTP(cfg)
  398. if err != nil {
  399. t.Fatal(err)
  400. }
  401. defer sup.Stop()
  402. // Verify rejection when not using authorization
  403. req, _ := http.NewRequest("GET", baseURL, nil)
  404. resp, err := http.DefaultClient.Do(req)
  405. if err != nil {
  406. t.Fatal(err)
  407. }
  408. if resp.StatusCode != http.StatusUnauthorized {
  409. t.Errorf("Unexpected non-401 return code %d for unauthed request", resp.StatusCode)
  410. }
  411. // Verify that incorrect password is rejected
  412. req.SetBasicAuth("üser", "rksmrgs")
  413. resp, err = http.DefaultClient.Do(req)
  414. if err != nil {
  415. t.Fatal(err)
  416. }
  417. if resp.StatusCode != http.StatusUnauthorized {
  418. t.Errorf("Unexpected non-401 return code %d for incorrect password", resp.StatusCode)
  419. }
  420. // Verify that incorrect username is rejected
  421. req.SetBasicAuth("user", "räksmörgås") // string literals in Go source code are in UTF-8
  422. resp, err = http.DefaultClient.Do(req)
  423. if err != nil {
  424. t.Fatal(err)
  425. }
  426. if resp.StatusCode != http.StatusUnauthorized {
  427. t.Errorf("Unexpected non-401 return code %d for incorrect username", resp.StatusCode)
  428. }
  429. // Verify that UTF-8 auth works
  430. req.SetBasicAuth("üser", "räksmörgås") // string literals in Go source code are in UTF-8
  431. resp, err = http.DefaultClient.Do(req)
  432. if err != nil {
  433. t.Fatal(err)
  434. }
  435. if resp.StatusCode != http.StatusOK {
  436. t.Errorf("Unexpected non-200 return code %d for authed request (UTF-8)", resp.StatusCode)
  437. }
  438. // Verify that ISO-8859-1 auth
  439. req.SetBasicAuth("\xfcser", "r\xe4ksm\xf6rg\xe5s") // escaped ISO-8859-1
  440. resp, err = http.DefaultClient.Do(req)
  441. if err != nil {
  442. t.Fatal(err)
  443. }
  444. if resp.StatusCode != http.StatusOK {
  445. t.Errorf("Unexpected non-200 return code %d for authed request (ISO-8859-1)", resp.StatusCode)
  446. }
  447. }
  448. func startHTTP(cfg *mockedConfig) (string, *suture.Supervisor, error) {
  449. m := new(mockedModel)
  450. assetDir := "../../gui"
  451. eventSub := new(mockedEventSub)
  452. diskEventSub := new(mockedEventSub)
  453. discoverer := new(mockedCachingMux)
  454. connections := new(mockedConnections)
  455. errorLog := new(mockedLoggerRecorder)
  456. systemLog := new(mockedLoggerRecorder)
  457. cpu := new(mockedCPUService)
  458. addrChan := make(chan string)
  459. // Instantiate the API service
  460. urService := ur.New(cfg, m, connections, false)
  461. summaryService := model.NewFolderSummaryService(cfg, m, protocol.LocalDeviceID, events.NoopLogger)
  462. svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, summaryService, errorLog, systemLog, cpu, nil, false).(*service)
  463. defer os.Remove(token)
  464. svc.started = addrChan
  465. // Actually start the API service
  466. supervisor := suture.New("API test", suture.Spec{
  467. PassThroughPanics: true,
  468. })
  469. supervisor.Add(svc)
  470. supervisor.ServeBackground()
  471. // Make sure the API service is listening, and get the URL to use.
  472. addr := <-addrChan
  473. tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
  474. if err != nil {
  475. supervisor.Stop()
  476. return "", nil, fmt.Errorf("Weird address from API service: %v", err)
  477. }
  478. host, _, _ := net.SplitHostPort(cfg.gui.RawAddress)
  479. if host == "" || host == "0.0.0.0" {
  480. host = "127.0.0.1"
  481. }
  482. baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)))
  483. return baseURL, supervisor, nil
  484. }
  485. func TestCSRFRequired(t *testing.T) {
  486. t.Parallel()
  487. const testAPIKey = "foobarbaz"
  488. cfg := new(mockedConfig)
  489. cfg.gui.APIKey = testAPIKey
  490. baseURL, sup, err := startHTTP(cfg)
  491. if err != nil {
  492. t.Fatal("Unexpected error from getting base URL:", err)
  493. }
  494. defer sup.Stop()
  495. cli := &http.Client{
  496. Timeout: time.Minute,
  497. }
  498. // Getting the base URL (i.e. "/") should succeed.
  499. resp, err := cli.Get(baseURL)
  500. if err != nil {
  501. t.Fatal("Unexpected error from getting base URL:", err)
  502. }
  503. resp.Body.Close()
  504. if resp.StatusCode != http.StatusOK {
  505. t.Fatal("Getting base URL should succeed, not", resp.Status)
  506. }
  507. // Find the returned CSRF token for future use
  508. var csrfTokenName, csrfTokenValue string
  509. for _, cookie := range resp.Cookies() {
  510. if strings.HasPrefix(cookie.Name, "CSRF-Token") {
  511. csrfTokenName = cookie.Name
  512. csrfTokenValue = cookie.Value
  513. break
  514. }
  515. }
  516. // Calling on /rest without a token should fail
  517. resp, err = cli.Get(baseURL + "/rest/system/config")
  518. if err != nil {
  519. t.Fatal("Unexpected error from getting /rest/system/config:", err)
  520. }
  521. resp.Body.Close()
  522. if resp.StatusCode != http.StatusForbidden {
  523. t.Fatal("Getting /rest/system/config without CSRF token should fail, not", resp.Status)
  524. }
  525. // Calling on /rest with a token should succeed
  526. req, _ := http.NewRequest("GET", baseURL+"/rest/system/config", nil)
  527. req.Header.Set("X-"+csrfTokenName, csrfTokenValue)
  528. resp, err = cli.Do(req)
  529. if err != nil {
  530. t.Fatal("Unexpected error from getting /rest/system/config:", err)
  531. }
  532. resp.Body.Close()
  533. if resp.StatusCode != http.StatusOK {
  534. t.Fatal("Getting /rest/system/config with CSRF token should succeed, not", resp.Status)
  535. }
  536. // Calling on /rest with the API key should succeed
  537. req, _ = http.NewRequest("GET", baseURL+"/rest/system/config", nil)
  538. req.Header.Set("X-API-Key", testAPIKey)
  539. resp, err = cli.Do(req)
  540. if err != nil {
  541. t.Fatal("Unexpected error from getting /rest/system/config:", err)
  542. }
  543. resp.Body.Close()
  544. if resp.StatusCode != http.StatusOK {
  545. t.Fatal("Getting /rest/system/config with API key should succeed, not", resp.Status)
  546. }
  547. }
  548. func TestRandomString(t *testing.T) {
  549. t.Parallel()
  550. const testAPIKey = "foobarbaz"
  551. cfg := new(mockedConfig)
  552. cfg.gui.APIKey = testAPIKey
  553. baseURL, sup, err := startHTTP(cfg)
  554. if err != nil {
  555. t.Fatal(err)
  556. }
  557. defer sup.Stop()
  558. cli := &http.Client{
  559. Timeout: time.Second,
  560. }
  561. // The default should be to return a 32 character random string
  562. for _, url := range []string{"/rest/svc/random/string", "/rest/svc/random/string?length=-1", "/rest/svc/random/string?length=yo"} {
  563. req, _ := http.NewRequest("GET", baseURL+url, nil)
  564. req.Header.Set("X-API-Key", testAPIKey)
  565. resp, err := cli.Do(req)
  566. if err != nil {
  567. t.Fatal(err)
  568. }
  569. var res map[string]string
  570. if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
  571. t.Fatal(err)
  572. }
  573. if len(res["random"]) != 32 {
  574. t.Errorf("Expected 32 random characters, got %q of length %d", res["random"], len(res["random"]))
  575. }
  576. }
  577. // We can ask for a different length if we like
  578. req, _ := http.NewRequest("GET", baseURL+"/rest/svc/random/string?length=27", nil)
  579. req.Header.Set("X-API-Key", testAPIKey)
  580. resp, err := cli.Do(req)
  581. if err != nil {
  582. t.Fatal(err)
  583. }
  584. var res map[string]string
  585. if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
  586. t.Fatal(err)
  587. }
  588. if len(res["random"]) != 27 {
  589. t.Errorf("Expected 27 random characters, got %q of length %d", res["random"], len(res["random"]))
  590. }
  591. }
  592. func TestConfigPostOK(t *testing.T) {
  593. t.Parallel()
  594. cfg := bytes.NewBuffer([]byte(`{
  595. "version": 15,
  596. "folders": [
  597. {
  598. "id": "foo",
  599. "path": "TestConfigPostOK"
  600. }
  601. ]
  602. }`))
  603. resp, err := testConfigPost(cfg)
  604. if err != nil {
  605. t.Fatal(err)
  606. }
  607. if resp.StatusCode != http.StatusOK {
  608. t.Error("Expected 200 OK, not", resp.Status)
  609. }
  610. os.RemoveAll("TestConfigPostOK")
  611. }
  612. func TestConfigPostDupFolder(t *testing.T) {
  613. t.Parallel()
  614. cfg := bytes.NewBuffer([]byte(`{
  615. "version": 15,
  616. "folders": [
  617. {"id": "foo"},
  618. {"id": "foo"}
  619. ]
  620. }`))
  621. resp, err := testConfigPost(cfg)
  622. if err != nil {
  623. t.Fatal(err)
  624. }
  625. if resp.StatusCode != http.StatusBadRequest {
  626. t.Error("Expected 400 Bad Request, not", resp.Status)
  627. }
  628. }
  629. func testConfigPost(data io.Reader) (*http.Response, error) {
  630. const testAPIKey = "foobarbaz"
  631. cfg := new(mockedConfig)
  632. cfg.gui.APIKey = testAPIKey
  633. baseURL, sup, err := startHTTP(cfg)
  634. if err != nil {
  635. return nil, err
  636. }
  637. defer sup.Stop()
  638. cli := &http.Client{
  639. Timeout: time.Second,
  640. }
  641. req, _ := http.NewRequest("POST", baseURL+"/rest/system/config", data)
  642. req.Header.Set("X-API-Key", testAPIKey)
  643. return cli.Do(req)
  644. }
  645. func TestHostCheck(t *testing.T) {
  646. t.Parallel()
  647. // An API service bound to localhost should reject non-localhost host Headers
  648. cfg := new(mockedConfig)
  649. cfg.gui.RawAddress = "127.0.0.1:0"
  650. baseURL, sup, err := startHTTP(cfg)
  651. if err != nil {
  652. t.Fatal(err)
  653. }
  654. defer sup.Stop()
  655. // A normal HTTP get to the localhost-bound service should succeed
  656. resp, err := http.Get(baseURL)
  657. if err != nil {
  658. t.Fatal(err)
  659. }
  660. resp.Body.Close()
  661. if resp.StatusCode != http.StatusOK {
  662. t.Error("Regular HTTP get: expected 200 OK, not", resp.Status)
  663. }
  664. // A request with a suspicious Host header should fail
  665. req, _ := http.NewRequest("GET", baseURL, nil)
  666. req.Host = "example.com"
  667. resp, err = http.DefaultClient.Do(req)
  668. if err != nil {
  669. t.Fatal(err)
  670. }
  671. resp.Body.Close()
  672. if resp.StatusCode != http.StatusForbidden {
  673. t.Error("Suspicious Host header: expected 403 Forbidden, not", resp.Status)
  674. }
  675. // A request with an explicit "localhost:8384" Host header should pass
  676. req, _ = http.NewRequest("GET", baseURL, nil)
  677. req.Host = "localhost:8384"
  678. resp, err = http.DefaultClient.Do(req)
  679. if err != nil {
  680. t.Fatal(err)
  681. }
  682. resp.Body.Close()
  683. if resp.StatusCode != http.StatusOK {
  684. t.Error("Explicit localhost:8384: expected 200 OK, not", resp.Status)
  685. }
  686. // A request with an explicit "localhost" Host header (no port) should pass
  687. req, _ = http.NewRequest("GET", baseURL, nil)
  688. req.Host = "localhost"
  689. resp, err = http.DefaultClient.Do(req)
  690. if err != nil {
  691. t.Fatal(err)
  692. }
  693. resp.Body.Close()
  694. if resp.StatusCode != http.StatusOK {
  695. t.Error("Explicit localhost: expected 200 OK, not", resp.Status)
  696. }
  697. // A server with InsecureSkipHostCheck set behaves differently
  698. cfg = new(mockedConfig)
  699. cfg.gui.RawAddress = "127.0.0.1:0"
  700. cfg.gui.InsecureSkipHostCheck = true
  701. baseURL, sup, err = startHTTP(cfg)
  702. if err != nil {
  703. t.Fatal(err)
  704. }
  705. defer sup.Stop()
  706. // A request with a suspicious Host header should be allowed
  707. req, _ = http.NewRequest("GET", baseURL, nil)
  708. req.Host = "example.com"
  709. resp, err = http.DefaultClient.Do(req)
  710. if err != nil {
  711. t.Fatal(err)
  712. }
  713. resp.Body.Close()
  714. if resp.StatusCode != http.StatusOK {
  715. t.Error("Incorrect host header, check disabled: expected 200 OK, not", resp.Status)
  716. }
  717. // A server bound to a wildcard address also doesn't do the check
  718. cfg = new(mockedConfig)
  719. cfg.gui.RawAddress = "0.0.0.0:0"
  720. cfg.gui.InsecureSkipHostCheck = true
  721. baseURL, sup, err = startHTTP(cfg)
  722. if err != nil {
  723. t.Fatal(err)
  724. }
  725. defer sup.Stop()
  726. // A request with a suspicious Host header should be allowed
  727. req, _ = http.NewRequest("GET", baseURL, nil)
  728. req.Host = "example.com"
  729. resp, err = http.DefaultClient.Do(req)
  730. if err != nil {
  731. t.Fatal(err)
  732. }
  733. resp.Body.Close()
  734. if resp.StatusCode != http.StatusOK {
  735. t.Error("Incorrect host header, wildcard bound: expected 200 OK, not", resp.Status)
  736. }
  737. // This should all work over IPv6 as well
  738. if runningInContainer() {
  739. // Working IPv6 in Docker can't be taken for granted.
  740. return
  741. }
  742. cfg = new(mockedConfig)
  743. cfg.gui.RawAddress = "[::1]:0"
  744. baseURL, sup, err = startHTTP(cfg)
  745. if err != nil {
  746. t.Fatal(err)
  747. }
  748. defer sup.Stop()
  749. // A normal HTTP get to the localhost-bound service should succeed
  750. resp, err = http.Get(baseURL)
  751. if err != nil {
  752. t.Fatal(err)
  753. }
  754. resp.Body.Close()
  755. if resp.StatusCode != http.StatusOK {
  756. t.Error("Regular HTTP get (IPv6): expected 200 OK, not", resp.Status)
  757. }
  758. // A request with a suspicious Host header should fail
  759. req, _ = http.NewRequest("GET", baseURL, nil)
  760. req.Host = "example.com"
  761. resp, err = http.DefaultClient.Do(req)
  762. if err != nil {
  763. t.Fatal(err)
  764. }
  765. resp.Body.Close()
  766. if resp.StatusCode != http.StatusForbidden {
  767. t.Error("Suspicious Host header (IPv6): expected 403 Forbidden, not", resp.Status)
  768. }
  769. // A request with an explicit "localhost:8384" Host header should pass
  770. req, _ = http.NewRequest("GET", baseURL, nil)
  771. req.Host = "localhost:8384"
  772. resp, err = http.DefaultClient.Do(req)
  773. if err != nil {
  774. t.Fatal(err)
  775. }
  776. resp.Body.Close()
  777. if resp.StatusCode != http.StatusOK {
  778. t.Error("Explicit localhost:8384 (IPv6): expected 200 OK, not", resp.Status)
  779. }
  780. }
  781. func TestAddressIsLocalhost(t *testing.T) {
  782. t.Parallel()
  783. testcases := []struct {
  784. address string
  785. result bool
  786. }{
  787. // These are all valid localhost addresses
  788. {"localhost", true},
  789. {"LOCALHOST", true},
  790. {"localhost.", true},
  791. {"::1", true},
  792. {"127.0.0.1", true},
  793. {"127.23.45.56", true},
  794. {"localhost:8080", true},
  795. {"LOCALHOST:8000", true},
  796. {"localhost.:8080", true},
  797. {"[::1]:8080", true},
  798. {"127.0.0.1:8080", true},
  799. {"127.23.45.56:8080", true},
  800. // These are all non-localhost addresses
  801. {"example.com", false},
  802. {"example.com:8080", false},
  803. {"localhost.com", false},
  804. {"localhost.com:8080", false},
  805. {"www.localhost", false},
  806. {"www.localhost:8080", false},
  807. {"192.0.2.10", false},
  808. {"192.0.2.10:8080", false},
  809. {"0.0.0.0", false},
  810. {"0.0.0.0:8080", false},
  811. {"::", false},
  812. {"[::]:8080", false},
  813. {":8080", false},
  814. }
  815. for _, tc := range testcases {
  816. result := addressIsLocalhost(tc.address)
  817. if result != tc.result {
  818. t.Errorf("addressIsLocalhost(%q)=%v, expected %v", tc.address, result, tc.result)
  819. }
  820. }
  821. }
  822. func TestAccessControlAllowOriginHeader(t *testing.T) {
  823. t.Parallel()
  824. const testAPIKey = "foobarbaz"
  825. cfg := new(mockedConfig)
  826. cfg.gui.APIKey = testAPIKey
  827. baseURL, sup, err := startHTTP(cfg)
  828. if err != nil {
  829. t.Fatal(err)
  830. }
  831. defer sup.Stop()
  832. cli := &http.Client{
  833. Timeout: time.Second,
  834. }
  835. req, _ := http.NewRequest("GET", baseURL+"/rest/system/status", nil)
  836. req.Header.Set("X-API-Key", testAPIKey)
  837. resp, err := cli.Do(req)
  838. if err != nil {
  839. t.Fatal(err)
  840. }
  841. resp.Body.Close()
  842. if resp.StatusCode != http.StatusOK {
  843. t.Fatal("GET on /rest/system/status should succeed, not", resp.Status)
  844. }
  845. if resp.Header.Get("Access-Control-Allow-Origin") != "*" {
  846. t.Fatal("GET on /rest/system/status should return a 'Access-Control-Allow-Origin: *' header")
  847. }
  848. }
  849. func TestOptionsRequest(t *testing.T) {
  850. t.Parallel()
  851. const testAPIKey = "foobarbaz"
  852. cfg := new(mockedConfig)
  853. cfg.gui.APIKey = testAPIKey
  854. baseURL, sup, err := startHTTP(cfg)
  855. if err != nil {
  856. t.Fatal(err)
  857. }
  858. defer sup.Stop()
  859. cli := &http.Client{
  860. Timeout: time.Second,
  861. }
  862. req, _ := http.NewRequest("OPTIONS", baseURL+"/rest/system/status", nil)
  863. resp, err := cli.Do(req)
  864. if err != nil {
  865. t.Fatal(err)
  866. }
  867. resp.Body.Close()
  868. if resp.StatusCode != http.StatusNoContent {
  869. t.Fatal("OPTIONS on /rest/system/status should succeed, not", resp.Status)
  870. }
  871. if resp.Header.Get("Access-Control-Allow-Origin") != "*" {
  872. t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Origin: *' header")
  873. }
  874. if resp.Header.Get("Access-Control-Allow-Methods") != "GET, POST" {
  875. t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Methods: GET, POST' header")
  876. }
  877. if resp.Header.Get("Access-Control-Allow-Headers") != "Content-Type, X-API-Key" {
  878. t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Headers: Content-Type, X-API-KEY' header")
  879. }
  880. }
  881. func TestEventMasks(t *testing.T) {
  882. t.Parallel()
  883. cfg := new(mockedConfig)
  884. defSub := new(mockedEventSub)
  885. diskSub := new(mockedEventSub)
  886. svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, nil, false).(*service)
  887. defer os.Remove(token)
  888. if mask := svc.getEventMask(""); mask != DefaultEventMask {
  889. t.Errorf("incorrect default mask %x != %x", int64(mask), int64(DefaultEventMask))
  890. }
  891. expected := events.FolderSummary | events.LocalChangeDetected
  892. if mask := svc.getEventMask("FolderSummary,LocalChangeDetected"); mask != expected {
  893. t.Errorf("incorrect parsed mask %x != %x", int64(mask), int64(expected))
  894. }
  895. expected = 0
  896. if mask := svc.getEventMask("WeirdEvent,something else that doesn't exist"); mask != expected {
  897. t.Errorf("incorrect parsed mask %x != %x", int64(mask), int64(expected))
  898. }
  899. if res := svc.getEventSub(DefaultEventMask); res != defSub {
  900. t.Errorf("should have returned the given default event sub")
  901. }
  902. if res := svc.getEventSub(DiskEventMask); res != diskSub {
  903. t.Errorf("should have returned the given disk event sub")
  904. }
  905. if res := svc.getEventSub(events.LocalIndexUpdated); res == nil || res == defSub || res == diskSub {
  906. t.Errorf("should have returned a valid, non-default event sub")
  907. }
  908. }
  909. func TestBrowse(t *testing.T) {
  910. t.Parallel()
  911. pathSep := string(os.PathSeparator)
  912. tmpDir, err := ioutil.TempDir("", "syncthing")
  913. if err != nil {
  914. t.Fatal(err)
  915. }
  916. defer os.RemoveAll(tmpDir)
  917. if err := os.Mkdir(filepath.Join(tmpDir, "dir"), 0755); err != nil {
  918. t.Fatal(err)
  919. }
  920. if err := ioutil.WriteFile(filepath.Join(tmpDir, "file"), []byte("hello"), 0644); err != nil {
  921. t.Fatal(err)
  922. }
  923. if err := os.Mkdir(filepath.Join(tmpDir, "MiXEDCase"), 0755); err != nil {
  924. t.Fatal(err)
  925. }
  926. // We expect completion to return the full path to the completed
  927. // directory, with an ending slash.
  928. dirPath := filepath.Join(tmpDir, "dir") + pathSep
  929. mixedCaseDirPath := filepath.Join(tmpDir, "MiXEDCase") + pathSep
  930. cases := []struct {
  931. current string
  932. returns []string
  933. }{
  934. // The direcotory without slash is completed to one with slash.
  935. {tmpDir, []string{tmpDir + pathSep}},
  936. // With slash it's completed to its contents.
  937. // Dirs are given pathSeps.
  938. // Files are not returned.
  939. {tmpDir + pathSep, []string{mixedCaseDirPath, dirPath}},
  940. // Globbing is automatic based on prefix.
  941. {tmpDir + pathSep + "d", []string{dirPath}},
  942. {tmpDir + pathSep + "di", []string{dirPath}},
  943. {tmpDir + pathSep + "dir", []string{dirPath}},
  944. {tmpDir + pathSep + "f", nil},
  945. {tmpDir + pathSep + "q", nil},
  946. // Globbing is case-insensitve
  947. {tmpDir + pathSep + "mixed", []string{mixedCaseDirPath}},
  948. }
  949. for _, tc := range cases {
  950. ret := browseFiles(tc.current, fs.FilesystemTypeBasic)
  951. if !equalStrings(ret, tc.returns) {
  952. t.Errorf("browseFiles(%q) => %q, expected %q", tc.current, ret, tc.returns)
  953. }
  954. }
  955. }
  956. func TestPrefixMatch(t *testing.T) {
  957. t.Parallel()
  958. cases := []struct {
  959. s string
  960. prefix string
  961. expected int
  962. }{
  963. {"aaaA", "aaa", matchExact},
  964. {"AAAX", "BBB", noMatch},
  965. {"AAAX", "aAa", matchCaseIns},
  966. {"äÜX", "äü", matchCaseIns},
  967. }
  968. for _, tc := range cases {
  969. ret := checkPrefixMatch(tc.s, tc.prefix)
  970. if ret != tc.expected {
  971. t.Errorf("checkPrefixMatch(%q, %q) => %v, expected %v", tc.s, tc.prefix, ret, tc.expected)
  972. }
  973. }
  974. }
  975. func TestCheckExpiry(t *testing.T) {
  976. dir, err := ioutil.TempDir("", "syncthing-test")
  977. if err != nil {
  978. t.Fatal(err)
  979. }
  980. defer os.RemoveAll(dir)
  981. // Self signed certificates expiring in less than a month are errored so we
  982. // can regenerate in time.
  983. crt, err := tlsutil.NewCertificate(filepath.Join(dir, "crt"), filepath.Join(dir, "key"), "foo.example.com", 29)
  984. if err != nil {
  985. t.Fatal(err)
  986. }
  987. if err := checkExpiry(crt); err == nil {
  988. t.Error("expected expiry error")
  989. }
  990. // Certificates with at least 31 days of life left are fine.
  991. crt, err = tlsutil.NewCertificate(filepath.Join(dir, "crt"), filepath.Join(dir, "key"), "foo.example.com", 31)
  992. if err != nil {
  993. t.Fatal(err)
  994. }
  995. if err := checkExpiry(crt); err != nil {
  996. t.Error("expected no error:", err)
  997. }
  998. if runtime.GOOS == "darwin" {
  999. // Certificates with too long an expiry time are not allowed on macOS
  1000. crt, err = tlsutil.NewCertificate(filepath.Join(dir, "crt"), filepath.Join(dir, "key"), "foo.example.com", 1000)
  1001. if err != nil {
  1002. t.Fatal(err)
  1003. }
  1004. if err := checkExpiry(crt); err == nil {
  1005. t.Error("expected expiry error")
  1006. }
  1007. }
  1008. }
  1009. func equalStrings(a, b []string) bool {
  1010. if len(a) != len(b) {
  1011. return false
  1012. }
  1013. for i := range a {
  1014. if a[i] != b[i] {
  1015. return false
  1016. }
  1017. }
  1018. return true
  1019. }
  1020. // runningInContainer returns true if we are inside Docker or LXC. It might
  1021. // be prone to false negatives if things change in the future, but likely
  1022. // not false positives.
  1023. func runningInContainer() bool {
  1024. if runtime.GOOS != "linux" {
  1025. return false
  1026. }
  1027. bs, err := ioutil.ReadFile("/proc/1/cgroup")
  1028. if err != nil {
  1029. return false
  1030. }
  1031. if bytes.Contains(bs, []byte("/docker/")) {
  1032. return true
  1033. }
  1034. if bytes.Contains(bs, []byte("/lxc/")) {
  1035. return true
  1036. }
  1037. return false
  1038. }