api_test.go 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702
  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. "context"
  11. "encoding/json"
  12. "fmt"
  13. "io"
  14. "net"
  15. "net/http"
  16. "net/http/httptest"
  17. "os"
  18. "path/filepath"
  19. "strconv"
  20. "strings"
  21. "testing"
  22. "time"
  23. "github.com/d4l3k/messagediff"
  24. "github.com/syncthing/syncthing/lib/assets"
  25. "github.com/syncthing/syncthing/lib/build"
  26. "github.com/syncthing/syncthing/lib/config"
  27. connmocks "github.com/syncthing/syncthing/lib/connections/mocks"
  28. "github.com/syncthing/syncthing/lib/db"
  29. "github.com/syncthing/syncthing/lib/db/backend"
  30. discovermocks "github.com/syncthing/syncthing/lib/discover/mocks"
  31. "github.com/syncthing/syncthing/lib/events"
  32. eventmocks "github.com/syncthing/syncthing/lib/events/mocks"
  33. "github.com/syncthing/syncthing/lib/fs"
  34. "github.com/syncthing/syncthing/lib/locations"
  35. "github.com/syncthing/syncthing/lib/logger"
  36. loggermocks "github.com/syncthing/syncthing/lib/logger/mocks"
  37. "github.com/syncthing/syncthing/lib/model"
  38. modelmocks "github.com/syncthing/syncthing/lib/model/mocks"
  39. "github.com/syncthing/syncthing/lib/protocol"
  40. "github.com/syncthing/syncthing/lib/rand"
  41. "github.com/syncthing/syncthing/lib/svcutil"
  42. "github.com/syncthing/syncthing/lib/sync"
  43. "github.com/syncthing/syncthing/lib/tlsutil"
  44. "github.com/syncthing/syncthing/lib/ur"
  45. "github.com/thejerf/suture/v4"
  46. "golang.org/x/exp/slices"
  47. )
  48. var (
  49. confDir = filepath.Join("testdata", "config")
  50. token = filepath.Join(confDir, "csrftokens.txt")
  51. dev1 protocol.DeviceID
  52. apiCfg = newMockedConfig()
  53. testAPIKey = "foobarbaz"
  54. )
  55. func init() {
  56. dev1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
  57. apiCfg.GUIReturns(config.GUIConfiguration{APIKey: testAPIKey, RawAddress: "127.0.0.1:0"})
  58. }
  59. func TestMain(m *testing.M) {
  60. orig := locations.GetBaseDir(locations.ConfigBaseDir)
  61. locations.SetBaseDir(locations.ConfigBaseDir, confDir)
  62. exitCode := m.Run()
  63. locations.SetBaseDir(locations.ConfigBaseDir, orig)
  64. os.Exit(exitCode)
  65. }
  66. func TestStopAfterBrokenConfig(t *testing.T) {
  67. t.Parallel()
  68. cfg := config.Configuration{
  69. GUI: config.GUIConfiguration{
  70. RawAddress: "127.0.0.1:0",
  71. RawUseTLS: false,
  72. },
  73. }
  74. w := config.Wrap("/dev/null", cfg, protocol.LocalDeviceID, events.NoopLogger)
  75. mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
  76. kdb := db.NewMiscDataNamespace(mdb)
  77. srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, false, kdb).(*service)
  78. defer os.Remove(token)
  79. srv.started = make(chan string)
  80. sup := suture.New("test", svcutil.SpecWithDebugLogger(l))
  81. sup.Add(srv)
  82. ctx, cancel := context.WithCancel(context.Background())
  83. sup.ServeBackground(ctx)
  84. <-srv.started
  85. // Service is now running, listening on a random port on localhost. Now we
  86. // request a config change to a completely invalid listen address. The
  87. // commit will fail and the service will be in a broken state.
  88. newCfg := config.Configuration{
  89. GUI: config.GUIConfiguration{
  90. RawAddress: "totally not a valid address",
  91. RawUseTLS: false,
  92. },
  93. }
  94. if err := srv.VerifyConfiguration(cfg, newCfg); err == nil {
  95. t.Fatal("Verify config should have failed")
  96. }
  97. cancel()
  98. }
  99. func TestAssetsDir(t *testing.T) {
  100. t.Parallel()
  101. // For any given request to $FILE, we should return the first found of
  102. // - assetsdir/$THEME/$FILE
  103. // - compiled in asset $THEME/$FILE
  104. // - assetsdir/default/$FILE
  105. // - compiled in asset default/$FILE
  106. // The asset map contains compressed assets, so create a couple of gzip compressed assets here.
  107. buf := new(bytes.Buffer)
  108. gw := gzip.NewWriter(buf)
  109. gw.Write([]byte("default"))
  110. gw.Close()
  111. def := assets.Asset{
  112. Content: buf.String(),
  113. Gzipped: true,
  114. }
  115. buf = new(bytes.Buffer)
  116. gw = gzip.NewWriter(buf)
  117. gw.Write([]byte("foo"))
  118. gw.Close()
  119. foo := assets.Asset{
  120. Content: buf.String(),
  121. Gzipped: true,
  122. }
  123. e := &staticsServer{
  124. theme: "foo",
  125. mut: sync.NewRWMutex(),
  126. assetDir: "testdata",
  127. assets: map[string]assets.Asset{
  128. "foo/a": foo, // overridden in foo/a
  129. "foo/b": foo,
  130. "default/a": def, // overridden in default/a (but foo/a takes precedence)
  131. "default/b": def, // overridden in default/b (but foo/b takes precedence)
  132. "default/c": def,
  133. },
  134. }
  135. s := httptest.NewServer(e)
  136. defer s.Close()
  137. // assetsdir/foo/a exists, overrides compiled in
  138. expectURLToContain(t, s.URL+"/a", "overridden-foo")
  139. // foo/b is compiled in, default/b is overridden, return compiled in
  140. expectURLToContain(t, s.URL+"/b", "foo")
  141. // only exists as compiled in default/c so use that
  142. expectURLToContain(t, s.URL+"/c", "default")
  143. // only exists as overridden default/d so use that
  144. expectURLToContain(t, s.URL+"/d", "overridden-default")
  145. }
  146. func expectURLToContain(t *testing.T, url, exp string) {
  147. res, err := http.Get(url)
  148. if err != nil {
  149. t.Error(err)
  150. return
  151. }
  152. if res.StatusCode != 200 {
  153. t.Errorf("Got %s instead of 200 OK", res.Status)
  154. return
  155. }
  156. data, err := io.ReadAll(res.Body)
  157. res.Body.Close()
  158. if err != nil {
  159. t.Error(err)
  160. return
  161. }
  162. if string(data) != exp {
  163. t.Errorf("Got %q instead of %q on %q", data, exp, url)
  164. return
  165. }
  166. }
  167. func TestDirNames(t *testing.T) {
  168. t.Parallel()
  169. names := dirNames("testdata")
  170. expected := []string{"config", "default", "foo", "testfolder"}
  171. if diff, equal := messagediff.PrettyDiff(expected, names); !equal {
  172. t.Errorf("Unexpected dirNames return: %#v\n%s", names, diff)
  173. }
  174. }
  175. type httpTestCase struct {
  176. URL string // URL to check
  177. Code int // Expected result code
  178. Type string // Expected content type
  179. Prefix string // Expected result prefix
  180. Timeout time.Duration // Defaults to a second
  181. }
  182. func TestAPIServiceRequests(t *testing.T) {
  183. t.Parallel()
  184. baseURL, cancel, err := startHTTP(apiCfg)
  185. if err != nil {
  186. t.Fatal(err)
  187. }
  188. t.Cleanup(cancel)
  189. cases := []httpTestCase{
  190. // /rest/db
  191. {
  192. URL: "/rest/db/completion?device=" + protocol.LocalDeviceID.String() + "&folder=default",
  193. Code: 200,
  194. Type: "application/json",
  195. Prefix: "{",
  196. },
  197. {
  198. URL: "/rest/db/file?folder=default&file=something",
  199. Code: 404,
  200. },
  201. {
  202. URL: "/rest/db/ignores?folder=default",
  203. Code: 200,
  204. Type: "application/json",
  205. Prefix: "{",
  206. },
  207. {
  208. URL: "/rest/db/need?folder=default",
  209. Code: 200,
  210. Type: "application/json",
  211. Prefix: "{",
  212. },
  213. {
  214. URL: "/rest/db/status?folder=default",
  215. Code: 200,
  216. Type: "application/json",
  217. Prefix: "{",
  218. },
  219. {
  220. URL: "/rest/db/browse?folder=default",
  221. Code: 200,
  222. Type: "application/json",
  223. Prefix: "null",
  224. },
  225. {
  226. URL: "/rest/db/status?folder=default",
  227. Code: 200,
  228. Type: "application/json",
  229. Prefix: "",
  230. },
  231. // /rest/stats
  232. {
  233. URL: "/rest/stats/device",
  234. Code: 200,
  235. Type: "application/json",
  236. Prefix: "null",
  237. },
  238. {
  239. URL: "/rest/stats/folder",
  240. Code: 200,
  241. Type: "application/json",
  242. Prefix: "null",
  243. },
  244. // /rest/svc
  245. {
  246. URL: "/rest/svc/deviceid?id=" + protocol.LocalDeviceID.String(),
  247. Code: 200,
  248. Type: "application/json",
  249. Prefix: "{",
  250. },
  251. {
  252. URL: "/rest/svc/lang",
  253. Code: 200,
  254. Type: "application/json",
  255. Prefix: "[",
  256. },
  257. {
  258. URL: "/rest/svc/report",
  259. Code: 200,
  260. Type: "application/json",
  261. Prefix: "{",
  262. Timeout: 5 * time.Second,
  263. },
  264. // /rest/system
  265. {
  266. URL: "/rest/system/browse?current=~",
  267. Code: 200,
  268. Type: "application/json",
  269. Prefix: "[",
  270. },
  271. {
  272. URL: "/rest/system/config",
  273. Code: 200,
  274. Type: "application/json",
  275. Prefix: "{",
  276. },
  277. {
  278. URL: "/rest/system/config/insync",
  279. Code: 200,
  280. Type: "application/json",
  281. Prefix: "{",
  282. },
  283. {
  284. URL: "/rest/system/connections",
  285. Code: 200,
  286. Type: "application/json",
  287. Prefix: "null",
  288. },
  289. {
  290. URL: "/rest/system/discovery",
  291. Code: 200,
  292. Type: "application/json",
  293. Prefix: "{",
  294. },
  295. {
  296. URL: "/rest/system/error?since=0",
  297. Code: 200,
  298. Type: "application/json",
  299. Prefix: "{",
  300. },
  301. {
  302. URL: "/rest/system/ping",
  303. Code: 200,
  304. Type: "application/json",
  305. Prefix: "{",
  306. },
  307. {
  308. URL: "/rest/system/status",
  309. Code: 200,
  310. Type: "application/json",
  311. Prefix: "{",
  312. },
  313. {
  314. URL: "/rest/system/version",
  315. Code: 200,
  316. Type: "application/json",
  317. Prefix: "{",
  318. },
  319. {
  320. URL: "/rest/system/debug",
  321. Code: 200,
  322. Type: "application/json",
  323. Prefix: "{",
  324. },
  325. {
  326. URL: "/rest/system/log?since=0",
  327. Code: 200,
  328. Type: "application/json",
  329. Prefix: "{",
  330. },
  331. {
  332. URL: "/rest/system/log.txt?since=0",
  333. Code: 200,
  334. Type: "text/plain",
  335. Prefix: "",
  336. },
  337. // /rest/config
  338. {
  339. URL: "/rest/config",
  340. Code: 200,
  341. Type: "application/json",
  342. Prefix: "",
  343. },
  344. {
  345. URL: "/rest/config/folders",
  346. Code: 200,
  347. Type: "application/json",
  348. Prefix: "",
  349. },
  350. {
  351. URL: "/rest/config/folders/missing",
  352. Code: 404,
  353. Type: "text/plain",
  354. Prefix: "",
  355. },
  356. {
  357. URL: "/rest/config/devices",
  358. Code: 200,
  359. Type: "application/json",
  360. Prefix: "",
  361. },
  362. {
  363. URL: "/rest/config/devices/illegalid",
  364. Code: 400,
  365. Type: "text/plain",
  366. Prefix: "",
  367. },
  368. {
  369. URL: "/rest/config/devices/" + protocol.GlobalDeviceID.String(),
  370. Code: 404,
  371. Type: "text/plain",
  372. Prefix: "",
  373. },
  374. {
  375. URL: "/rest/config/options",
  376. Code: 200,
  377. Type: "application/json",
  378. Prefix: "{",
  379. },
  380. {
  381. URL: "/rest/config/gui",
  382. Code: 200,
  383. Type: "application/json",
  384. Prefix: "{",
  385. },
  386. {
  387. URL: "/rest/config/ldap",
  388. Code: 200,
  389. Type: "application/json",
  390. Prefix: "{",
  391. },
  392. }
  393. for _, tc := range cases {
  394. tc := tc
  395. t.Run(cases[0].URL, func(t *testing.T) {
  396. t.Parallel()
  397. testHTTPRequest(t, baseURL, tc, testAPIKey)
  398. })
  399. }
  400. }
  401. // testHTTPRequest tries the given test case, comparing the result code,
  402. // content type, and result prefix.
  403. func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase, apikey string) {
  404. // Should not be parallelized, as that just causes timeouts eventually with more test-cases
  405. timeout := time.Second
  406. if tc.Timeout > 0 {
  407. timeout = tc.Timeout
  408. }
  409. cli := &http.Client{
  410. Timeout: timeout,
  411. }
  412. req, err := http.NewRequest("GET", baseURL+tc.URL, nil)
  413. if err != nil {
  414. t.Errorf("Unexpected error requesting %s: %v", tc.URL, err)
  415. return
  416. }
  417. req.Header.Set("X-API-Key", apikey)
  418. resp, err := cli.Do(req)
  419. if err != nil {
  420. t.Errorf("Unexpected error requesting %s: %v", tc.URL, err)
  421. return
  422. }
  423. defer resp.Body.Close()
  424. if resp.StatusCode != tc.Code {
  425. t.Errorf("Get on %s should have returned status code %d, not %s", tc.URL, tc.Code, resp.Status)
  426. return
  427. }
  428. ct := resp.Header.Get("Content-Type")
  429. if !strings.HasPrefix(ct, tc.Type) {
  430. t.Errorf("The content type on %s should be %q, not %q", tc.URL, tc.Type, ct)
  431. return
  432. }
  433. data, err := io.ReadAll(resp.Body)
  434. if err != nil {
  435. t.Errorf("Unexpected error reading %s: %v", tc.URL, err)
  436. return
  437. }
  438. if !bytes.HasPrefix(data, []byte(tc.Prefix)) {
  439. t.Errorf("Returned data from %s does not have prefix %q: %s", tc.URL, tc.Prefix, data)
  440. return
  441. }
  442. }
  443. func hasSessionCookie(cookies []*http.Cookie) bool {
  444. for _, cookie := range cookies {
  445. if cookie.MaxAge >= 0 && strings.HasPrefix(cookie.Name, "sessionid") {
  446. return true
  447. }
  448. }
  449. return false
  450. }
  451. func httpGet(url string, basicAuthUsername string, basicAuthPassword string, xapikeyHeader string, authorizationBearer string, cookies []*http.Cookie, t *testing.T) *http.Response {
  452. req, err := http.NewRequest("GET", url, nil)
  453. for _, cookie := range cookies {
  454. req.AddCookie(cookie)
  455. }
  456. if err != nil {
  457. t.Fatal(err)
  458. }
  459. if basicAuthUsername != "" || basicAuthPassword != "" {
  460. req.SetBasicAuth(basicAuthUsername, basicAuthPassword)
  461. }
  462. if xapikeyHeader != "" {
  463. req.Header.Set("X-API-Key", xapikeyHeader)
  464. }
  465. if authorizationBearer != "" {
  466. req.Header.Set("Authorization", "Bearer "+authorizationBearer)
  467. }
  468. resp, err := http.DefaultClient.Do(req)
  469. if err != nil {
  470. t.Fatal(err)
  471. }
  472. return resp
  473. }
  474. func httpPost(url string, body map[string]string, t *testing.T) *http.Response {
  475. bodyBytes, err := json.Marshal(body)
  476. if err != nil {
  477. t.Fatal(err)
  478. }
  479. req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
  480. if err != nil {
  481. t.Fatal(err)
  482. }
  483. resp, err := http.DefaultClient.Do(req)
  484. if err != nil {
  485. t.Fatal(err)
  486. }
  487. return resp
  488. }
  489. func TestHTTPLogin(t *testing.T) {
  490. t.Parallel()
  491. httpGetBasicAuth := func(url string, username string, password string) *http.Response {
  492. return httpGet(url, username, password, "", "", nil, t)
  493. }
  494. httpGetXapikey := func(url string, xapikeyHeader string) *http.Response {
  495. return httpGet(url, "", "", xapikeyHeader, "", nil, t)
  496. }
  497. httpGetAuthorizationBearer := func(url string, bearer string) *http.Response {
  498. return httpGet(url, "", "", "", bearer, nil, t)
  499. }
  500. testWith := func(sendBasicAuthPrompt bool, expectedOkStatus int, expectedFailStatus int, path string) {
  501. cfg := newMockedConfig()
  502. cfg.GUIReturns(config.GUIConfiguration{
  503. User: "üser",
  504. Password: "$2a$10$IdIZTxTg/dCNuNEGlmLynOjqg4B1FvDKuIV5e0BB3pnWVHNb8.GSq", // bcrypt of "räksmörgås" in UTF-8
  505. RawAddress: "127.0.0.1:0",
  506. APIKey: testAPIKey,
  507. SendBasicAuthPrompt: sendBasicAuthPrompt,
  508. })
  509. baseURL, cancel, err := startHTTP(cfg)
  510. if err != nil {
  511. t.Fatal(err)
  512. }
  513. t.Cleanup(cancel)
  514. url := baseURL + path
  515. t.Run(fmt.Sprintf("%d path", expectedOkStatus), func(t *testing.T) {
  516. t.Run("no auth is rejected", func(t *testing.T) {
  517. t.Parallel()
  518. resp := httpGetBasicAuth(url, "", "")
  519. if resp.StatusCode != expectedFailStatus {
  520. t.Errorf("Unexpected non-%d return code %d for unauthed request", expectedFailStatus, resp.StatusCode)
  521. }
  522. if hasSessionCookie(resp.Cookies()) {
  523. t.Errorf("Unexpected session cookie for unauthed request")
  524. }
  525. })
  526. t.Run("incorrect password is rejected", func(t *testing.T) {
  527. t.Parallel()
  528. resp := httpGetBasicAuth(url, "üser", "rksmrgs")
  529. if resp.StatusCode != expectedFailStatus {
  530. t.Errorf("Unexpected non-%d return code %d for incorrect password", expectedFailStatus, resp.StatusCode)
  531. }
  532. if hasSessionCookie(resp.Cookies()) {
  533. t.Errorf("Unexpected session cookie for incorrect password")
  534. }
  535. })
  536. t.Run("incorrect username is rejected", func(t *testing.T) {
  537. t.Parallel()
  538. resp := httpGetBasicAuth(url, "user", "räksmörgås") // string literals in Go source code are in UTF-8
  539. if resp.StatusCode != expectedFailStatus {
  540. t.Errorf("Unexpected non-%d return code %d for incorrect username", expectedFailStatus, resp.StatusCode)
  541. }
  542. if hasSessionCookie(resp.Cookies()) {
  543. t.Errorf("Unexpected session cookie for incorrect username")
  544. }
  545. })
  546. t.Run("UTF-8 auth works", func(t *testing.T) {
  547. t.Parallel()
  548. resp := httpGetBasicAuth(url, "üser", "räksmörgås") // string literals in Go source code are in UTF-8
  549. if resp.StatusCode != expectedOkStatus {
  550. t.Errorf("Unexpected non-%d return code %d for authed request (UTF-8)", expectedOkStatus, resp.StatusCode)
  551. }
  552. if !hasSessionCookie(resp.Cookies()) {
  553. t.Errorf("Expected session cookie for authed request (UTF-8)")
  554. }
  555. })
  556. t.Run("ISO-8859-1 auth works", func(t *testing.T) {
  557. t.Parallel()
  558. resp := httpGetBasicAuth(url, "\xfcser", "r\xe4ksm\xf6rg\xe5s") // escaped ISO-8859-1
  559. if resp.StatusCode != expectedOkStatus {
  560. t.Errorf("Unexpected non-%d return code %d for authed request (ISO-8859-1)", expectedOkStatus, resp.StatusCode)
  561. }
  562. if !hasSessionCookie(resp.Cookies()) {
  563. t.Errorf("Expected session cookie for authed request (ISO-8859-1)")
  564. }
  565. })
  566. t.Run("bad X-API-Key is rejected", func(t *testing.T) {
  567. t.Parallel()
  568. resp := httpGetXapikey(url, testAPIKey+"X")
  569. if resp.StatusCode != expectedFailStatus {
  570. t.Errorf("Unexpected non-%d return code %d for bad API key", expectedFailStatus, resp.StatusCode)
  571. }
  572. if hasSessionCookie(resp.Cookies()) {
  573. t.Errorf("Unexpected session cookie for bad API key")
  574. }
  575. })
  576. t.Run("good X-API-Key is accepted", func(t *testing.T) {
  577. t.Parallel()
  578. resp := httpGetXapikey(url, testAPIKey)
  579. if resp.StatusCode != expectedOkStatus {
  580. t.Errorf("Unexpected non-%d return code %d for API key", expectedOkStatus, resp.StatusCode)
  581. }
  582. if hasSessionCookie(resp.Cookies()) {
  583. t.Errorf("Unexpected session cookie for API key")
  584. }
  585. })
  586. t.Run("bad Bearer is rejected", func(t *testing.T) {
  587. t.Parallel()
  588. resp := httpGetAuthorizationBearer(url, testAPIKey+"X")
  589. if resp.StatusCode != expectedFailStatus {
  590. t.Errorf("Unexpected non-%d return code %d for bad Authorization: Bearer", expectedFailStatus, resp.StatusCode)
  591. }
  592. if hasSessionCookie(resp.Cookies()) {
  593. t.Errorf("Unexpected session cookie for bad Authorization: Bearer")
  594. }
  595. })
  596. t.Run("good Bearer is accepted", func(t *testing.T) {
  597. t.Parallel()
  598. resp := httpGetAuthorizationBearer(url, testAPIKey)
  599. if resp.StatusCode != expectedOkStatus {
  600. t.Errorf("Unexpected non-%d return code %d for Authorization: Bearer", expectedOkStatus, resp.StatusCode)
  601. }
  602. if hasSessionCookie(resp.Cookies()) {
  603. t.Errorf("Unexpected session cookie for bad Authorization: Bearer")
  604. }
  605. })
  606. })
  607. }
  608. testWith(true, http.StatusOK, http.StatusOK, "/")
  609. testWith(true, http.StatusOK, http.StatusUnauthorized, "/meta.js")
  610. testWith(true, http.StatusNotFound, http.StatusUnauthorized, "/any-path/that/does/nooooooot/match-any/noauth-pattern")
  611. testWith(false, http.StatusOK, http.StatusOK, "/")
  612. testWith(false, http.StatusOK, http.StatusForbidden, "/meta.js")
  613. testWith(false, http.StatusNotFound, http.StatusForbidden, "/any-path/that/does/nooooooot/match-any/noauth-pattern")
  614. }
  615. func TestHtmlFormLogin(t *testing.T) {
  616. t.Parallel()
  617. cfg := newMockedConfig()
  618. cfg.GUIReturns(config.GUIConfiguration{
  619. User: "üser",
  620. Password: "$2a$10$IdIZTxTg/dCNuNEGlmLynOjqg4B1FvDKuIV5e0BB3pnWVHNb8.GSq", // bcrypt of "räksmörgås" in UTF-8
  621. SendBasicAuthPrompt: false,
  622. })
  623. baseURL, cancel, err := startHTTP(cfg)
  624. if err != nil {
  625. t.Fatal(err)
  626. }
  627. t.Cleanup(cancel)
  628. loginUrl := baseURL + "/rest/noauth/auth/password"
  629. resourceUrl := baseURL + "/meta.js"
  630. resourceUrl404 := baseURL + "/any-path/that/does/nooooooot/match-any/noauth-pattern"
  631. performLogin := func(username string, password string) *http.Response {
  632. return httpPost(loginUrl, map[string]string{"username": username, "password": password}, t)
  633. }
  634. performResourceRequest := func(url string, cookies []*http.Cookie) *http.Response {
  635. return httpGet(url, "", "", "", "", cookies, t)
  636. }
  637. testNoAuthPath := func(noAuthPath string) {
  638. t.Run("auth is not needed for "+noAuthPath, func(t *testing.T) {
  639. t.Parallel()
  640. resp := httpGet(baseURL+noAuthPath, "", "", "", "", nil, t)
  641. if resp.StatusCode != http.StatusOK {
  642. t.Errorf("Unexpected non-200 return code %d at %s", resp.StatusCode, noAuthPath)
  643. }
  644. if hasSessionCookie(resp.Cookies()) {
  645. t.Errorf("Unexpected session cookie at " + noAuthPath)
  646. }
  647. })
  648. }
  649. testNoAuthPath("/index.html")
  650. testNoAuthPath("/rest/svc/lang")
  651. t.Run("incorrect password is rejected with 403", func(t *testing.T) {
  652. t.Parallel()
  653. resp := performLogin("üser", "rksmrgs") // string literals in Go source code are in UTF-8
  654. if resp.StatusCode != http.StatusForbidden {
  655. t.Errorf("Unexpected non-403 return code %d for incorrect password", resp.StatusCode)
  656. }
  657. if hasSessionCookie(resp.Cookies()) {
  658. t.Errorf("Unexpected session cookie for incorrect password")
  659. }
  660. resp = performResourceRequest(resourceUrl, resp.Cookies())
  661. if resp.StatusCode != http.StatusForbidden {
  662. t.Errorf("Unexpected non-403 return code %d for incorrect password", resp.StatusCode)
  663. }
  664. })
  665. t.Run("incorrect username is rejected with 403", func(t *testing.T) {
  666. t.Parallel()
  667. resp := performLogin("user", "räksmörgås") // string literals in Go source code are in UTF-8
  668. if resp.StatusCode != http.StatusForbidden {
  669. t.Errorf("Unexpected non-403 return code %d for incorrect username", resp.StatusCode)
  670. }
  671. if hasSessionCookie(resp.Cookies()) {
  672. t.Errorf("Unexpected session cookie for incorrect username")
  673. }
  674. resp = performResourceRequest(resourceUrl, resp.Cookies())
  675. if resp.StatusCode != http.StatusForbidden {
  676. t.Errorf("Unexpected non-403 return code %d for incorrect username", resp.StatusCode)
  677. }
  678. })
  679. t.Run("UTF-8 auth works", func(t *testing.T) {
  680. t.Parallel()
  681. // JSON is always UTF-8, so ISO-8859-1 case is not applicable
  682. resp := performLogin("üser", "räksmörgås") // string literals in Go source code are in UTF-8
  683. if resp.StatusCode != http.StatusNoContent {
  684. t.Errorf("Unexpected non-204 return code %d for authed request (UTF-8)", resp.StatusCode)
  685. }
  686. resp = performResourceRequest(resourceUrl, resp.Cookies())
  687. if resp.StatusCode != http.StatusOK {
  688. t.Errorf("Unexpected non-200 return code %d for authed request (UTF-8)", resp.StatusCode)
  689. }
  690. })
  691. t.Run("form login is not applicable to other URLs", func(t *testing.T) {
  692. t.Parallel()
  693. resp := httpPost(baseURL+"/meta.js", map[string]string{"username": "üser", "password": "räksmörgås"}, t)
  694. if resp.StatusCode != http.StatusForbidden {
  695. t.Errorf("Unexpected non-403 return code %d for incorrect form login URL", resp.StatusCode)
  696. }
  697. if hasSessionCookie(resp.Cookies()) {
  698. t.Errorf("Unexpected session cookie for incorrect form login URL")
  699. }
  700. })
  701. t.Run("invalid URL returns 403 before auth and 404 after auth", func(t *testing.T) {
  702. t.Parallel()
  703. resp := performResourceRequest(resourceUrl404, nil)
  704. if resp.StatusCode != http.StatusForbidden {
  705. t.Errorf("Unexpected non-403 return code %d for unauthed request", resp.StatusCode)
  706. }
  707. resp = performLogin("üser", "räksmörgås")
  708. if resp.StatusCode != http.StatusNoContent {
  709. t.Errorf("Unexpected non-204 return code %d for authed request", resp.StatusCode)
  710. }
  711. resp = performResourceRequest(resourceUrl404, resp.Cookies())
  712. if resp.StatusCode != http.StatusNotFound {
  713. t.Errorf("Unexpected non-404 return code %d for authed request", resp.StatusCode)
  714. }
  715. })
  716. }
  717. func TestApiCache(t *testing.T) {
  718. t.Parallel()
  719. cfg := newMockedConfig()
  720. cfg.GUIReturns(config.GUIConfiguration{
  721. RawAddress: "127.0.0.1:0",
  722. APIKey: testAPIKey,
  723. })
  724. baseURL, cancel, err := startHTTP(cfg)
  725. if err != nil {
  726. t.Fatal(err)
  727. }
  728. t.Cleanup(cancel)
  729. httpGet := func(url string, bearer string) *http.Response {
  730. return httpGet(url, "", "", "", bearer, nil, t)
  731. }
  732. t.Run("meta.js has no-cache headers", func(t *testing.T) {
  733. t.Parallel()
  734. url := baseURL + "/meta.js"
  735. resp := httpGet(url, testAPIKey)
  736. if resp.Header.Get("Cache-Control") != "max-age=0, no-cache, no-store" {
  737. t.Errorf("Expected no-cache headers at %s", url)
  738. }
  739. })
  740. t.Run("/rest/ has no-cache headers", func(t *testing.T) {
  741. t.Parallel()
  742. url := baseURL + "/rest/system/version"
  743. resp := httpGet(url, testAPIKey)
  744. if resp.Header.Get("Cache-Control") != "max-age=0, no-cache, no-store" {
  745. t.Errorf("Expected no-cache headers at %s", url)
  746. }
  747. })
  748. }
  749. func startHTTP(cfg config.Wrapper) (string, context.CancelFunc, error) {
  750. m := new(modelmocks.Model)
  751. assetDir := "../../gui"
  752. eventSub := new(eventmocks.BufferedSubscription)
  753. diskEventSub := new(eventmocks.BufferedSubscription)
  754. discoverer := new(discovermocks.Manager)
  755. connections := new(connmocks.Service)
  756. errorLog := new(loggermocks.Recorder)
  757. systemLog := new(loggermocks.Recorder)
  758. for _, l := range []*loggermocks.Recorder{errorLog, systemLog} {
  759. l.SinceReturns([]logger.Line{
  760. {
  761. When: time.Now(),
  762. Message: "Test message",
  763. },
  764. })
  765. }
  766. addrChan := make(chan string)
  767. mockedSummary := &modelmocks.FolderSummaryService{}
  768. mockedSummary.SummaryReturns(new(model.FolderSummary), nil)
  769. // Instantiate the API service
  770. urService := ur.New(cfg, m, connections, false)
  771. mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
  772. kdb := db.NewMiscDataNamespace(mdb)
  773. svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, mockedSummary, errorLog, systemLog, false, kdb).(*service)
  774. defer os.Remove(token)
  775. svc.started = addrChan
  776. // Actually start the API service
  777. supervisor := suture.New("API test", suture.Spec{
  778. PassThroughPanics: true,
  779. })
  780. supervisor.Add(svc)
  781. ctx, cancel := context.WithCancel(context.Background())
  782. supervisor.ServeBackground(ctx)
  783. // Make sure the API service is listening, and get the URL to use.
  784. addr := <-addrChan
  785. tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
  786. if err != nil {
  787. cancel()
  788. return "", cancel, fmt.Errorf("weird address from API service: %w", err)
  789. }
  790. host, _, _ := net.SplitHostPort(cfg.GUI().RawAddress)
  791. if host == "" || host == "0.0.0.0" {
  792. host = "127.0.0.1"
  793. }
  794. baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)))
  795. return baseURL, cancel, nil
  796. }
  797. func TestCSRFRequired(t *testing.T) {
  798. t.Parallel()
  799. baseURL, cancel, err := startHTTP(apiCfg)
  800. if err != nil {
  801. t.Fatal("Unexpected error from getting base URL:", err)
  802. }
  803. t.Cleanup(cancel)
  804. cli := &http.Client{
  805. Timeout: time.Minute,
  806. }
  807. // Getting the base URL (i.e. "/") should succeed.
  808. resp, err := cli.Get(baseURL)
  809. if err != nil {
  810. t.Fatal("Unexpected error from getting base URL:", err)
  811. }
  812. resp.Body.Close()
  813. if resp.StatusCode != http.StatusOK {
  814. t.Fatal("Getting base URL should succeed, not", resp.Status)
  815. }
  816. // Find the returned CSRF token for future use
  817. var csrfTokenName, csrfTokenValue string
  818. for _, cookie := range resp.Cookies() {
  819. if strings.HasPrefix(cookie.Name, "CSRF-Token") {
  820. csrfTokenName = cookie.Name
  821. csrfTokenValue = cookie.Value
  822. break
  823. }
  824. }
  825. if csrfTokenValue == "" {
  826. t.Fatal("Failed to initialize CSRF test: no CSRF cookie returned from " + baseURL)
  827. }
  828. t.Run("/rest without a token should fail", func(t *testing.T) {
  829. t.Parallel()
  830. resp, err := cli.Get(baseURL + "/rest/system/config")
  831. if err != nil {
  832. t.Fatal("Unexpected error from getting /rest/system/config:", err)
  833. }
  834. resp.Body.Close()
  835. if resp.StatusCode != http.StatusForbidden {
  836. t.Fatal("Getting /rest/system/config without CSRF token should fail, not", resp.Status)
  837. }
  838. })
  839. t.Run("/rest with a token should succeed", func(t *testing.T) {
  840. t.Parallel()
  841. req, _ := http.NewRequest("GET", baseURL+"/rest/system/config", nil)
  842. req.Header.Set("X-"+csrfTokenName, csrfTokenValue)
  843. resp, err := cli.Do(req)
  844. if err != nil {
  845. t.Fatal("Unexpected error from getting /rest/system/config:", err)
  846. }
  847. resp.Body.Close()
  848. if resp.StatusCode != http.StatusOK {
  849. t.Fatal("Getting /rest/system/config with CSRF token should succeed, not", resp.Status)
  850. }
  851. })
  852. t.Run("/rest with an incorrect API key should fail, X-API-Key version", func(t *testing.T) {
  853. t.Parallel()
  854. req, _ := http.NewRequest("GET", baseURL+"/rest/system/config", nil)
  855. req.Header.Set("X-API-Key", testAPIKey+"X")
  856. resp, err := cli.Do(req)
  857. if err != nil {
  858. t.Fatal("Unexpected error from getting /rest/system/config:", err)
  859. }
  860. resp.Body.Close()
  861. if resp.StatusCode != http.StatusForbidden {
  862. t.Fatal("Getting /rest/system/config with incorrect API token should fail, not", resp.Status)
  863. }
  864. })
  865. t.Run("/rest with an incorrect API key should fail, Bearer auth version", func(t *testing.T) {
  866. t.Parallel()
  867. req, _ := http.NewRequest("GET", baseURL+"/rest/system/config", nil)
  868. req.Header.Set("Authorization", "Bearer "+testAPIKey+"X")
  869. resp, err := cli.Do(req)
  870. if err != nil {
  871. t.Fatal("Unexpected error from getting /rest/system/config:", err)
  872. }
  873. resp.Body.Close()
  874. if resp.StatusCode != http.StatusForbidden {
  875. t.Fatal("Getting /rest/system/config with incorrect API token should fail, not", resp.Status)
  876. }
  877. })
  878. t.Run("/rest with the API key should succeed", func(t *testing.T) {
  879. t.Parallel()
  880. req, _ := http.NewRequest("GET", baseURL+"/rest/system/config", nil)
  881. req.Header.Set("X-API-Key", testAPIKey)
  882. resp, err := cli.Do(req)
  883. if err != nil {
  884. t.Fatal("Unexpected error from getting /rest/system/config:", err)
  885. }
  886. resp.Body.Close()
  887. if resp.StatusCode != http.StatusOK {
  888. t.Fatal("Getting /rest/system/config with API key should succeed, not", resp.Status)
  889. }
  890. })
  891. t.Run("/rest with the API key as a bearer token should succeed", func(t *testing.T) {
  892. t.Parallel()
  893. req, _ := http.NewRequest("GET", baseURL+"/rest/system/config", nil)
  894. req.Header.Set("Authorization", "Bearer "+testAPIKey)
  895. resp, err := cli.Do(req)
  896. if err != nil {
  897. t.Fatal("Unexpected error from getting /rest/system/config:", err)
  898. }
  899. resp.Body.Close()
  900. if resp.StatusCode != http.StatusOK {
  901. t.Fatal("Getting /rest/system/config with API key should succeed, not", resp.Status)
  902. }
  903. })
  904. }
  905. func TestRandomString(t *testing.T) {
  906. t.Parallel()
  907. baseURL, cancel, err := startHTTP(apiCfg)
  908. if err != nil {
  909. t.Fatal(err)
  910. }
  911. defer cancel()
  912. cli := &http.Client{
  913. Timeout: time.Second,
  914. }
  915. // The default should be to return a 32 character random string
  916. for _, url := range []string{"/rest/svc/random/string", "/rest/svc/random/string?length=-1", "/rest/svc/random/string?length=yo"} {
  917. req, _ := http.NewRequest("GET", baseURL+url, nil)
  918. req.Header.Set("X-API-Key", testAPIKey)
  919. resp, err := cli.Do(req)
  920. if err != nil {
  921. t.Fatal(err)
  922. }
  923. var res map[string]string
  924. if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
  925. t.Fatal(err)
  926. }
  927. if len(res["random"]) != 32 {
  928. t.Errorf("Expected 32 random characters, got %q of length %d", res["random"], len(res["random"]))
  929. }
  930. }
  931. // We can ask for a different length if we like
  932. req, _ := http.NewRequest("GET", baseURL+"/rest/svc/random/string?length=27", nil)
  933. req.Header.Set("X-API-Key", testAPIKey)
  934. resp, err := cli.Do(req)
  935. if err != nil {
  936. t.Fatal(err)
  937. }
  938. var res map[string]string
  939. if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
  940. t.Fatal(err)
  941. }
  942. if len(res["random"]) != 27 {
  943. t.Errorf("Expected 27 random characters, got %q of length %d", res["random"], len(res["random"]))
  944. }
  945. }
  946. func TestConfigPostOK(t *testing.T) {
  947. t.Parallel()
  948. cfg := bytes.NewBuffer([]byte(`{
  949. "version": 15,
  950. "folders": [
  951. {
  952. "id": "foo",
  953. "path": "TestConfigPostOK"
  954. }
  955. ]
  956. }`))
  957. resp, err := testConfigPost(cfg)
  958. if err != nil {
  959. t.Fatal(err)
  960. }
  961. if resp.StatusCode != http.StatusOK {
  962. t.Error("Expected 200 OK, not", resp.Status)
  963. }
  964. os.RemoveAll("TestConfigPostOK")
  965. }
  966. func TestConfigPostDupFolder(t *testing.T) {
  967. t.Parallel()
  968. cfg := bytes.NewBuffer([]byte(`{
  969. "version": 15,
  970. "folders": [
  971. {"id": "foo"},
  972. {"id": "foo"}
  973. ]
  974. }`))
  975. resp, err := testConfigPost(cfg)
  976. if err != nil {
  977. t.Fatal(err)
  978. }
  979. if resp.StatusCode != http.StatusBadRequest {
  980. t.Error("Expected 400 Bad Request, not", resp.Status)
  981. }
  982. }
  983. func testConfigPost(data io.Reader) (*http.Response, error) {
  984. baseURL, cancel, err := startHTTP(apiCfg)
  985. if err != nil {
  986. return nil, err
  987. }
  988. defer cancel()
  989. cli := &http.Client{
  990. Timeout: time.Second,
  991. }
  992. req, _ := http.NewRequest("POST", baseURL+"/rest/system/config", data)
  993. req.Header.Set("X-API-Key", testAPIKey)
  994. return cli.Do(req)
  995. }
  996. func TestHostCheck(t *testing.T) {
  997. t.Parallel()
  998. // An API service bound to localhost should reject non-localhost host Headers
  999. cfg := newMockedConfig()
  1000. cfg.GUIReturns(config.GUIConfiguration{RawAddress: "127.0.0.1:0"})
  1001. baseURL, cancel, err := startHTTP(cfg)
  1002. if err != nil {
  1003. t.Fatal(err)
  1004. }
  1005. defer cancel()
  1006. // A normal HTTP get to the localhost-bound service should succeed
  1007. resp, err := http.Get(baseURL)
  1008. if err != nil {
  1009. t.Fatal(err)
  1010. }
  1011. resp.Body.Close()
  1012. if resp.StatusCode != http.StatusOK {
  1013. t.Error("Regular HTTP get: expected 200 OK, not", resp.Status)
  1014. }
  1015. // A request with a suspicious Host header should fail
  1016. req, _ := http.NewRequest("GET", baseURL, nil)
  1017. req.Host = "example.com"
  1018. resp, err = http.DefaultClient.Do(req)
  1019. if err != nil {
  1020. t.Fatal(err)
  1021. }
  1022. resp.Body.Close()
  1023. if resp.StatusCode != http.StatusForbidden {
  1024. t.Error("Suspicious Host header: expected 403 Forbidden, not", resp.Status)
  1025. }
  1026. // A request with an explicit "localhost:8384" Host header should pass
  1027. req, _ = http.NewRequest("GET", baseURL, nil)
  1028. req.Host = "localhost:8384"
  1029. resp, err = http.DefaultClient.Do(req)
  1030. if err != nil {
  1031. t.Fatal(err)
  1032. }
  1033. resp.Body.Close()
  1034. if resp.StatusCode != http.StatusOK {
  1035. t.Error("Explicit localhost:8384: expected 200 OK, not", resp.Status)
  1036. }
  1037. // A request with an explicit "localhost" Host header (no port) should pass
  1038. req, _ = http.NewRequest("GET", baseURL, nil)
  1039. req.Host = "localhost"
  1040. resp, err = http.DefaultClient.Do(req)
  1041. if err != nil {
  1042. t.Fatal(err)
  1043. }
  1044. resp.Body.Close()
  1045. if resp.StatusCode != http.StatusOK {
  1046. t.Error("Explicit localhost: expected 200 OK, not", resp.Status)
  1047. }
  1048. // A server with InsecureSkipHostCheck set behaves differently
  1049. cfg = newMockedConfig()
  1050. cfg.GUIReturns(config.GUIConfiguration{
  1051. RawAddress: "127.0.0.1:0",
  1052. InsecureSkipHostCheck: true,
  1053. })
  1054. baseURL, cancel, err = startHTTP(cfg)
  1055. if err != nil {
  1056. t.Fatal(err)
  1057. }
  1058. defer cancel()
  1059. // A request with a suspicious Host header should be allowed
  1060. req, _ = http.NewRequest("GET", baseURL, nil)
  1061. req.Host = "example.com"
  1062. resp, err = http.DefaultClient.Do(req)
  1063. if err != nil {
  1064. t.Fatal(err)
  1065. }
  1066. resp.Body.Close()
  1067. if resp.StatusCode != http.StatusOK {
  1068. t.Error("Incorrect host header, check disabled: expected 200 OK, not", resp.Status)
  1069. }
  1070. if !testing.Short() {
  1071. // A server bound to a wildcard address also doesn't do the check
  1072. cfg = newMockedConfig()
  1073. cfg.GUIReturns(config.GUIConfiguration{
  1074. RawAddress: "0.0.0.0:0",
  1075. })
  1076. baseURL, cancel, err = startHTTP(cfg)
  1077. if err != nil {
  1078. t.Fatal(err)
  1079. }
  1080. defer cancel()
  1081. // A request with a suspicious Host header should be allowed
  1082. req, _ = http.NewRequest("GET", baseURL, nil)
  1083. req.Host = "example.com"
  1084. resp, err = http.DefaultClient.Do(req)
  1085. if err != nil {
  1086. t.Fatal(err)
  1087. }
  1088. resp.Body.Close()
  1089. if resp.StatusCode != http.StatusOK {
  1090. t.Error("Incorrect host header, wildcard bound: expected 200 OK, not", resp.Status)
  1091. }
  1092. }
  1093. // This should all work over IPv6 as well
  1094. if runningInContainer() {
  1095. // Working IPv6 in Docker can't be taken for granted.
  1096. return
  1097. }
  1098. cfg = newMockedConfig()
  1099. cfg.GUIReturns(config.GUIConfiguration{
  1100. RawAddress: "[::1]:0",
  1101. })
  1102. baseURL, cancel, err = startHTTP(cfg)
  1103. if err != nil {
  1104. t.Fatal(err)
  1105. }
  1106. defer cancel()
  1107. // A normal HTTP get to the localhost-bound service should succeed
  1108. resp, err = http.Get(baseURL)
  1109. if err != nil {
  1110. t.Fatal(err)
  1111. }
  1112. resp.Body.Close()
  1113. if resp.StatusCode != http.StatusOK {
  1114. t.Error("Regular HTTP get (IPv6): expected 200 OK, not", resp.Status)
  1115. }
  1116. // A request with a suspicious Host header should fail
  1117. req, _ = http.NewRequest("GET", baseURL, nil)
  1118. req.Host = "example.com"
  1119. resp, err = http.DefaultClient.Do(req)
  1120. if err != nil {
  1121. t.Fatal(err)
  1122. }
  1123. resp.Body.Close()
  1124. if resp.StatusCode != http.StatusForbidden {
  1125. t.Error("Suspicious Host header (IPv6): expected 403 Forbidden, not", resp.Status)
  1126. }
  1127. // A request with an explicit "localhost:8384" Host header should pass
  1128. req, _ = http.NewRequest("GET", baseURL, nil)
  1129. req.Host = "localhost:8384"
  1130. resp, err = http.DefaultClient.Do(req)
  1131. if err != nil {
  1132. t.Fatal(err)
  1133. }
  1134. resp.Body.Close()
  1135. if resp.StatusCode != http.StatusOK {
  1136. t.Error("Explicit localhost:8384 (IPv6): expected 200 OK, not", resp.Status)
  1137. }
  1138. }
  1139. func TestAddressIsLocalhost(t *testing.T) {
  1140. t.Parallel()
  1141. testcases := []struct {
  1142. address string
  1143. result bool
  1144. }{
  1145. // These are all valid localhost addresses
  1146. {"localhost", true},
  1147. {"LOCALHOST", true},
  1148. {"localhost.", true},
  1149. {"::1", true},
  1150. {"127.0.0.1", true},
  1151. {"127.23.45.56", true},
  1152. {"localhost:8080", true},
  1153. {"LOCALHOST:8000", true},
  1154. {"localhost.:8080", true},
  1155. {"[::1]:8080", true},
  1156. {"127.0.0.1:8080", true},
  1157. {"127.23.45.56:8080", true},
  1158. {"www.localhost", true},
  1159. {"www.localhost:8080", true},
  1160. // These are all non-localhost addresses
  1161. {"example.com", false},
  1162. {"example.com:8080", false},
  1163. {"localhost.com", false},
  1164. {"localhost.com:8080", false},
  1165. {"192.0.2.10", false},
  1166. {"192.0.2.10:8080", false},
  1167. {"0.0.0.0", false},
  1168. {"0.0.0.0:8080", false},
  1169. {"::", false},
  1170. {"[::]:8080", false},
  1171. {":8080", false},
  1172. }
  1173. for _, tc := range testcases {
  1174. result := addressIsLocalhost(tc.address)
  1175. if result != tc.result {
  1176. t.Errorf("addressIsLocalhost(%q)=%v, expected %v", tc.address, result, tc.result)
  1177. }
  1178. }
  1179. }
  1180. func TestAccessControlAllowOriginHeader(t *testing.T) {
  1181. t.Parallel()
  1182. baseURL, cancel, err := startHTTP(apiCfg)
  1183. if err != nil {
  1184. t.Fatal(err)
  1185. }
  1186. defer cancel()
  1187. cli := &http.Client{
  1188. Timeout: time.Second,
  1189. }
  1190. req, _ := http.NewRequest("GET", baseURL+"/rest/system/status", nil)
  1191. req.Header.Set("X-API-Key", testAPIKey)
  1192. resp, err := cli.Do(req)
  1193. if err != nil {
  1194. t.Fatal(err)
  1195. }
  1196. resp.Body.Close()
  1197. if resp.StatusCode != http.StatusOK {
  1198. t.Fatal("GET on /rest/system/status should succeed, not", resp.Status)
  1199. }
  1200. if resp.Header.Get("Access-Control-Allow-Origin") != "*" {
  1201. t.Fatal("GET on /rest/system/status should return a 'Access-Control-Allow-Origin: *' header")
  1202. }
  1203. }
  1204. func TestOptionsRequest(t *testing.T) {
  1205. t.Parallel()
  1206. baseURL, cancel, err := startHTTP(apiCfg)
  1207. if err != nil {
  1208. t.Fatal(err)
  1209. }
  1210. defer cancel()
  1211. cli := &http.Client{
  1212. Timeout: time.Second,
  1213. }
  1214. req, _ := http.NewRequest("OPTIONS", baseURL+"/rest/system/status", nil)
  1215. resp, err := cli.Do(req)
  1216. if err != nil {
  1217. t.Fatal(err)
  1218. }
  1219. resp.Body.Close()
  1220. if resp.StatusCode != http.StatusNoContent {
  1221. t.Fatal("OPTIONS on /rest/system/status should succeed, not", resp.Status)
  1222. }
  1223. if resp.Header.Get("Access-Control-Allow-Origin") != "*" {
  1224. t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Origin: *' header")
  1225. }
  1226. if resp.Header.Get("Access-Control-Allow-Methods") != "GET, POST, PUT, PATCH, DELETE, OPTIONS" {
  1227. t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS' header")
  1228. }
  1229. if resp.Header.Get("Access-Control-Allow-Headers") != "Content-Type, X-API-Key" {
  1230. t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Headers: Content-Type, X-API-KEY' header")
  1231. }
  1232. }
  1233. func TestEventMasks(t *testing.T) {
  1234. t.Parallel()
  1235. cfg := newMockedConfig()
  1236. defSub := new(eventmocks.BufferedSubscription)
  1237. diskSub := new(eventmocks.BufferedSubscription)
  1238. mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
  1239. kdb := db.NewMiscDataNamespace(mdb)
  1240. svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, false, kdb).(*service)
  1241. defer os.Remove(token)
  1242. if mask := svc.getEventMask(""); mask != DefaultEventMask {
  1243. t.Errorf("incorrect default mask %x != %x", int64(mask), int64(DefaultEventMask))
  1244. }
  1245. expected := events.FolderSummary | events.LocalChangeDetected
  1246. if mask := svc.getEventMask("FolderSummary,LocalChangeDetected"); mask != expected {
  1247. t.Errorf("incorrect parsed mask %x != %x", int64(mask), int64(expected))
  1248. }
  1249. expected = 0
  1250. if mask := svc.getEventMask("WeirdEvent,something else that doesn't exist"); mask != expected {
  1251. t.Errorf("incorrect parsed mask %x != %x", int64(mask), int64(expected))
  1252. }
  1253. if res := svc.getEventSub(DefaultEventMask); res != defSub {
  1254. t.Errorf("should have returned the given default event sub")
  1255. }
  1256. if res := svc.getEventSub(DiskEventMask); res != diskSub {
  1257. t.Errorf("should have returned the given disk event sub")
  1258. }
  1259. if res := svc.getEventSub(events.LocalIndexUpdated); res == nil || res == defSub || res == diskSub {
  1260. t.Errorf("should have returned a valid, non-default event sub")
  1261. }
  1262. }
  1263. func TestBrowse(t *testing.T) {
  1264. t.Parallel()
  1265. pathSep := string(os.PathSeparator)
  1266. ffs := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?nostfolder=true")
  1267. _ = ffs.Mkdir("dir", 0o755)
  1268. _ = fs.WriteFile(ffs, "file", []byte("hello"), 0o644)
  1269. _ = ffs.Mkdir("MiXEDCase", 0o755)
  1270. // We expect completion to return the full path to the completed
  1271. // directory, with an ending slash.
  1272. dirPath := "dir" + pathSep
  1273. mixedCaseDirPath := "MiXEDCase" + pathSep
  1274. cases := []struct {
  1275. current string
  1276. returns []string
  1277. }{
  1278. // The directory without slash is completed to one with slash.
  1279. {"dir", []string{"dir" + pathSep}},
  1280. // With slash it's completed to its contents.
  1281. // Dirs are given pathSeps.
  1282. // Files are not returned.
  1283. {"", []string{mixedCaseDirPath, dirPath}},
  1284. // Globbing is automatic based on prefix.
  1285. {"d", []string{dirPath}},
  1286. {"di", []string{dirPath}},
  1287. {"dir", []string{dirPath}},
  1288. {"f", nil},
  1289. {"q", nil},
  1290. // Globbing is case-insensitive
  1291. {"mixed", []string{mixedCaseDirPath}},
  1292. }
  1293. for _, tc := range cases {
  1294. ret := browseFiles(ffs, tc.current)
  1295. if !slices.Equal(ret, tc.returns) {
  1296. t.Errorf("browseFiles(%q) => %q, expected %q", tc.current, ret, tc.returns)
  1297. }
  1298. }
  1299. }
  1300. func TestPrefixMatch(t *testing.T) {
  1301. t.Parallel()
  1302. cases := []struct {
  1303. s string
  1304. prefix string
  1305. expected int
  1306. }{
  1307. {"aaaA", "aaa", matchExact},
  1308. {"AAAX", "BBB", noMatch},
  1309. {"AAAX", "aAa", matchCaseIns},
  1310. {"äÜX", "äü", matchCaseIns},
  1311. }
  1312. for _, tc := range cases {
  1313. ret := checkPrefixMatch(tc.s, tc.prefix)
  1314. if ret != tc.expected {
  1315. t.Errorf("checkPrefixMatch(%q, %q) => %v, expected %v", tc.s, tc.prefix, ret, tc.expected)
  1316. }
  1317. }
  1318. }
  1319. func TestShouldRegenerateCertificate(t *testing.T) {
  1320. // Self signed certificates expiring in less than a month are errored so we
  1321. // can regenerate in time.
  1322. crt, err := tlsutil.NewCertificateInMemory("foo.example.com", 29)
  1323. if err != nil {
  1324. t.Fatal(err)
  1325. }
  1326. if err := shouldRegenerateCertificate(crt); err == nil {
  1327. t.Error("expected expiry error")
  1328. }
  1329. // Certificates with at least 31 days of life left are fine.
  1330. crt, err = tlsutil.NewCertificateInMemory("foo.example.com", 31)
  1331. if err != nil {
  1332. t.Fatal(err)
  1333. }
  1334. if err := shouldRegenerateCertificate(crt); err != nil {
  1335. t.Error("expected no error:", err)
  1336. }
  1337. if build.IsDarwin {
  1338. // Certificates with too long an expiry time are not allowed on macOS
  1339. crt, err = tlsutil.NewCertificateInMemory("foo.example.com", 1000)
  1340. if err != nil {
  1341. t.Fatal(err)
  1342. }
  1343. if err := shouldRegenerateCertificate(crt); err == nil {
  1344. t.Error("expected expiry error")
  1345. }
  1346. }
  1347. }
  1348. func TestConfigChanges(t *testing.T) {
  1349. t.Parallel()
  1350. const testAPIKey = "foobarbaz"
  1351. cfg := config.Configuration{
  1352. GUI: config.GUIConfiguration{
  1353. RawAddress: "127.0.0.1:0",
  1354. RawUseTLS: false,
  1355. APIKey: testAPIKey,
  1356. },
  1357. }
  1358. tmpFile, err := os.CreateTemp("", "syncthing-testConfig-")
  1359. if err != nil {
  1360. panic(err)
  1361. }
  1362. defer os.Remove(tmpFile.Name())
  1363. w := config.Wrap(tmpFile.Name(), cfg, protocol.LocalDeviceID, events.NoopLogger)
  1364. tmpFile.Close()
  1365. cfgCtx, cfgCancel := context.WithCancel(context.Background())
  1366. go w.Serve(cfgCtx)
  1367. defer cfgCancel()
  1368. baseURL, cancel, err := startHTTP(w)
  1369. if err != nil {
  1370. t.Fatal("Unexpected error from getting base URL:", err)
  1371. }
  1372. defer cancel()
  1373. cli := &http.Client{
  1374. Timeout: time.Minute,
  1375. }
  1376. do := func(req *http.Request, status int) *http.Response {
  1377. t.Helper()
  1378. req.Header.Set("X-API-Key", testAPIKey)
  1379. resp, err := cli.Do(req)
  1380. if err != nil {
  1381. t.Fatal(err)
  1382. }
  1383. if resp.StatusCode != status {
  1384. t.Errorf("Expected status %v, got %v", status, resp.StatusCode)
  1385. }
  1386. return resp
  1387. }
  1388. mod := func(method, path string, data interface{}) {
  1389. t.Helper()
  1390. bs, err := json.Marshal(data)
  1391. if err != nil {
  1392. t.Fatal(err)
  1393. }
  1394. req, _ := http.NewRequest(method, baseURL+path, bytes.NewReader(bs))
  1395. do(req, http.StatusOK).Body.Close()
  1396. }
  1397. get := func(path string) *http.Response {
  1398. t.Helper()
  1399. req, _ := http.NewRequest(http.MethodGet, baseURL+path, nil)
  1400. return do(req, http.StatusOK)
  1401. }
  1402. dev1Path := "/rest/config/devices/" + dev1.String()
  1403. // Create device
  1404. mod(http.MethodPut, "/rest/config/devices", []config.DeviceConfiguration{{DeviceID: dev1}})
  1405. // Check its there
  1406. get(dev1Path).Body.Close()
  1407. // Modify just a single attribute
  1408. mod(http.MethodPatch, dev1Path, map[string]bool{"Paused": true})
  1409. // Check that attribute
  1410. resp := get(dev1Path)
  1411. var dev config.DeviceConfiguration
  1412. if err := unmarshalTo(resp.Body, &dev); err != nil {
  1413. t.Fatal(err)
  1414. }
  1415. if !dev.Paused {
  1416. t.Error("Expected device to be paused")
  1417. }
  1418. folder2Path := "/rest/config/folders/folder2"
  1419. // Create a folder and add another
  1420. mod(http.MethodPut, "/rest/config/folders", []config.FolderConfiguration{{ID: "folder1", Path: "folder1"}})
  1421. mod(http.MethodPut, folder2Path, config.FolderConfiguration{ID: "folder2", Path: "folder2"})
  1422. // Check they are there
  1423. get("/rest/config/folders/folder1").Body.Close()
  1424. get(folder2Path).Body.Close()
  1425. // Modify just a single attribute
  1426. mod(http.MethodPatch, folder2Path, map[string]bool{"Paused": true})
  1427. // Check that attribute
  1428. resp = get(folder2Path)
  1429. var folder config.FolderConfiguration
  1430. if err := unmarshalTo(resp.Body, &folder); err != nil {
  1431. t.Fatal(err)
  1432. }
  1433. if !dev.Paused {
  1434. t.Error("Expected folder to be paused")
  1435. }
  1436. // Delete folder2
  1437. req, _ := http.NewRequest(http.MethodDelete, baseURL+folder2Path, nil)
  1438. do(req, http.StatusOK)
  1439. // Check folder1 is still there and folder2 gone
  1440. get("/rest/config/folders/folder1").Body.Close()
  1441. req, _ = http.NewRequest(http.MethodGet, baseURL+folder2Path, nil)
  1442. do(req, http.StatusNotFound)
  1443. mod(http.MethodPatch, "/rest/config/options", map[string]int{"maxSendKbps": 50})
  1444. resp = get("/rest/config/options")
  1445. var opts config.OptionsConfiguration
  1446. if err := unmarshalTo(resp.Body, &opts); err != nil {
  1447. t.Fatal(err)
  1448. }
  1449. if opts.MaxSendKbps != 50 {
  1450. t.Error("Expected 50 for MaxSendKbps, got", opts.MaxSendKbps)
  1451. }
  1452. }
  1453. func TestSanitizedHostname(t *testing.T) {
  1454. cases := []struct {
  1455. in, out string
  1456. }{
  1457. {"foo.BAR-baz", "foo.bar-baz"},
  1458. {"~.~-Min 1:a Räksmörgås-dator 😀😎 ~.~-", "min1araksmorgas-dator"},
  1459. {"Vicenç-PC", "vicenc-pc"},
  1460. {"~.~-~.~-", ""},
  1461. {"", ""},
  1462. }
  1463. for _, tc := range cases {
  1464. res, err := sanitizedHostname(tc.in)
  1465. if tc.out == "" && err == nil {
  1466. t.Errorf("%q should cause error", tc.in)
  1467. } else if res != tc.out {
  1468. t.Errorf("%q => %q, expected %q", tc.in, res, tc.out)
  1469. }
  1470. }
  1471. }
  1472. // runningInContainer returns true if we are inside Docker or LXC. It might
  1473. // be prone to false negatives if things change in the future, but likely
  1474. // not false positives.
  1475. func runningInContainer() bool {
  1476. if !build.IsLinux {
  1477. return false
  1478. }
  1479. bs, err := os.ReadFile("/proc/1/cgroup")
  1480. if err != nil {
  1481. return false
  1482. }
  1483. if bytes.Contains(bs, []byte("/docker/")) {
  1484. return true
  1485. }
  1486. if bytes.Contains(bs, []byte("/lxc/")) {
  1487. return true
  1488. }
  1489. return false
  1490. }