confighandler.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. // Copyright (C) 2020 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. "encoding/json"
  9. "io"
  10. "log/slog"
  11. "net/http"
  12. "github.com/julienschmidt/httprouter"
  13. "github.com/syncthing/syncthing/internal/slogutil"
  14. "github.com/syncthing/syncthing/lib/config"
  15. "github.com/syncthing/syncthing/lib/protocol"
  16. "github.com/syncthing/syncthing/lib/structutil"
  17. )
  18. type configMuxBuilder struct {
  19. *httprouter.Router
  20. id protocol.DeviceID
  21. cfg config.Wrapper
  22. }
  23. func (c *configMuxBuilder) registerConfig(path string) {
  24. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  25. sendJSON(w, c.cfg.RawCopy())
  26. })
  27. c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
  28. c.adjustConfig(w, r)
  29. })
  30. }
  31. func (c *configMuxBuilder) registerConfigDeprecated(path string) {
  32. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  33. sendJSON(w, c.cfg.RawCopy())
  34. })
  35. c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
  36. c.adjustConfig(w, r)
  37. })
  38. }
  39. func (c *configMuxBuilder) registerConfigInsync(path string) {
  40. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  41. sendJSON(w, map[string]bool{"configInSync": !c.cfg.RequiresRestart()})
  42. })
  43. }
  44. func (c *configMuxBuilder) registerConfigRequiresRestart(path string) {
  45. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  46. sendJSON(w, map[string]bool{"requiresRestart": c.cfg.RequiresRestart()})
  47. })
  48. }
  49. func (c *configMuxBuilder) registerFolders(path string) {
  50. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  51. sendJSON(w, c.cfg.FolderList())
  52. })
  53. c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
  54. data, err := unmarshalToRawMessages(r.Body)
  55. if err != nil {
  56. http.Error(w, err.Error(), http.StatusBadRequest)
  57. return
  58. }
  59. folders := make([]config.FolderConfiguration, len(data))
  60. defaultFolder := c.cfg.DefaultFolder()
  61. for i, bs := range data {
  62. folders[i] = defaultFolder.Copy()
  63. if err := json.Unmarshal(bs, &folders[i]); err != nil {
  64. http.Error(w, err.Error(), http.StatusBadRequest)
  65. return
  66. }
  67. }
  68. waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
  69. cfg.SetFolders(folders)
  70. })
  71. if err != nil {
  72. http.Error(w, err.Error(), http.StatusInternalServerError)
  73. return
  74. }
  75. c.finish(w, waiter)
  76. })
  77. c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
  78. c.adjustFolder(w, r, c.cfg.DefaultFolder(), false)
  79. })
  80. }
  81. func (c *configMuxBuilder) registerDevices(path string) {
  82. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  83. sendJSON(w, c.cfg.DeviceList())
  84. })
  85. c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
  86. data, err := unmarshalToRawMessages(r.Body)
  87. if err != nil {
  88. http.Error(w, err.Error(), http.StatusBadRequest)
  89. return
  90. }
  91. devices := make([]config.DeviceConfiguration, len(data))
  92. defaultDevice := c.cfg.DefaultDevice()
  93. for i, bs := range data {
  94. devices[i] = defaultDevice.Copy()
  95. if err := json.Unmarshal(bs, &devices[i]); err != nil {
  96. http.Error(w, err.Error(), http.StatusBadRequest)
  97. return
  98. }
  99. }
  100. waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
  101. cfg.SetDevices(devices)
  102. })
  103. if err != nil {
  104. http.Error(w, err.Error(), http.StatusInternalServerError)
  105. return
  106. }
  107. c.finish(w, waiter)
  108. })
  109. c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
  110. c.adjustDevice(w, r, c.cfg.DefaultDevice(), false)
  111. })
  112. }
  113. func (c *configMuxBuilder) registerFolder(path string) {
  114. c.Handle(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
  115. folder, ok := c.cfg.Folder(p.ByName("id"))
  116. if !ok {
  117. http.Error(w, "No folder with given ID", http.StatusNotFound)
  118. return
  119. }
  120. sendJSON(w, folder)
  121. })
  122. c.Handle(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
  123. c.adjustFolder(w, r, c.cfg.DefaultFolder(), false)
  124. })
  125. c.Handle(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
  126. folder, ok := c.cfg.Folder(p.ByName("id"))
  127. if !ok {
  128. http.Error(w, "No folder with given ID", http.StatusNotFound)
  129. return
  130. }
  131. c.adjustFolder(w, r, folder, false)
  132. })
  133. c.Handle(http.MethodDelete, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
  134. waiter, err := c.cfg.RemoveFolder(p.ByName("id"))
  135. if err != nil {
  136. http.Error(w, err.Error(), http.StatusBadRequest)
  137. return
  138. }
  139. c.finish(w, waiter)
  140. })
  141. }
  142. func (c *configMuxBuilder) registerDevice(path string) {
  143. deviceFromParams := func(w http.ResponseWriter, p httprouter.Params) (config.DeviceConfiguration, bool) {
  144. id, err := protocol.DeviceIDFromString(p.ByName("id"))
  145. if err != nil {
  146. http.Error(w, err.Error(), http.StatusBadRequest)
  147. return config.DeviceConfiguration{}, false
  148. }
  149. device, ok := c.cfg.Device(id)
  150. if !ok {
  151. http.Error(w, "No device with given ID", http.StatusNotFound)
  152. return config.DeviceConfiguration{}, false
  153. }
  154. return device, true
  155. }
  156. c.Handle(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
  157. if device, ok := deviceFromParams(w, p); ok {
  158. sendJSON(w, device)
  159. }
  160. })
  161. c.Handle(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
  162. c.adjustDevice(w, r, c.cfg.DefaultDevice(), false)
  163. })
  164. c.Handle(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
  165. if device, ok := deviceFromParams(w, p); ok {
  166. c.adjustDevice(w, r, device, false)
  167. }
  168. })
  169. c.Handle(http.MethodDelete, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
  170. id, err := protocol.DeviceIDFromString(p.ByName("id"))
  171. if err != nil {
  172. http.Error(w, err.Error(), http.StatusBadRequest)
  173. return
  174. }
  175. waiter, err := c.cfg.RemoveDevice(id)
  176. if err != nil {
  177. http.Error(w, err.Error(), http.StatusBadRequest)
  178. return
  179. }
  180. c.finish(w, waiter)
  181. })
  182. }
  183. func (c *configMuxBuilder) registerDefaultFolder(path string) {
  184. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  185. sendJSON(w, c.cfg.DefaultFolder())
  186. })
  187. c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
  188. var cfg config.FolderConfiguration
  189. structutil.SetDefaults(&cfg)
  190. c.adjustFolder(w, r, cfg, true)
  191. })
  192. c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
  193. c.adjustFolder(w, r, c.cfg.DefaultFolder(), true)
  194. })
  195. }
  196. func (c *configMuxBuilder) registerDefaultDevice(path string) {
  197. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  198. sendJSON(w, c.cfg.DefaultDevice())
  199. })
  200. c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
  201. var cfg config.DeviceConfiguration
  202. structutil.SetDefaults(&cfg)
  203. c.adjustDevice(w, r, cfg, true)
  204. })
  205. c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
  206. c.adjustDevice(w, r, c.cfg.DefaultDevice(), true)
  207. })
  208. }
  209. func (c *configMuxBuilder) registerDefaultIgnores(path string) {
  210. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  211. sendJSON(w, c.cfg.DefaultIgnores())
  212. })
  213. c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
  214. var ignores config.Ignores
  215. if err := unmarshalTo(r.Body, &ignores); err != nil {
  216. http.Error(w, err.Error(), http.StatusBadRequest)
  217. return
  218. }
  219. waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
  220. cfg.Defaults.Ignores = ignores
  221. })
  222. if err != nil {
  223. http.Error(w, err.Error(), http.StatusInternalServerError)
  224. return
  225. }
  226. c.finish(w, waiter)
  227. })
  228. }
  229. func (c *configMuxBuilder) registerOptions(path string) {
  230. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  231. sendJSON(w, c.cfg.Options())
  232. })
  233. c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
  234. var cfg config.OptionsConfiguration
  235. structutil.SetDefaults(&cfg)
  236. c.adjustOptions(w, r, cfg)
  237. })
  238. c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
  239. c.adjustOptions(w, r, c.cfg.Options())
  240. })
  241. }
  242. func (c *configMuxBuilder) registerLDAP(path string) {
  243. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  244. sendJSON(w, c.cfg.LDAP())
  245. })
  246. c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
  247. var cfg config.LDAPConfiguration
  248. structutil.SetDefaults(&cfg)
  249. c.adjustLDAP(w, r, cfg)
  250. })
  251. c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
  252. c.adjustLDAP(w, r, c.cfg.LDAP())
  253. })
  254. }
  255. func (c *configMuxBuilder) registerGUI(path string) {
  256. c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
  257. sendJSON(w, c.cfg.GUI())
  258. })
  259. c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
  260. var cfg config.GUIConfiguration
  261. structutil.SetDefaults(&cfg)
  262. c.adjustGUI(w, r, cfg)
  263. })
  264. c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
  265. c.adjustGUI(w, r, c.cfg.GUI())
  266. })
  267. }
  268. func (c *configMuxBuilder) adjustConfig(w http.ResponseWriter, r *http.Request) {
  269. to, err := config.ReadJSON(r.Body, c.id)
  270. r.Body.Close()
  271. if err != nil {
  272. slog.Error("Failed to decode posted config", slogutil.Error(err))
  273. http.Error(w, err.Error(), http.StatusBadRequest)
  274. return
  275. }
  276. var errMsg string
  277. var status int
  278. waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
  279. if err := c.postAdjustGui(&cfg.GUI, &to.GUI); err != nil {
  280. errMsg = err.Error()
  281. status = http.StatusInternalServerError
  282. return
  283. }
  284. *cfg = to
  285. })
  286. if errMsg != "" {
  287. http.Error(w, errMsg, status)
  288. } else if err != nil {
  289. http.Error(w, err.Error(), http.StatusInternalServerError)
  290. return
  291. }
  292. c.finish(w, waiter)
  293. }
  294. func (c *configMuxBuilder) adjustFolder(w http.ResponseWriter, r *http.Request, folder config.FolderConfiguration, defaults bool) {
  295. if err := unmarshalTo(r.Body, &folder); err != nil {
  296. http.Error(w, err.Error(), http.StatusBadRequest)
  297. return
  298. }
  299. waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
  300. if defaults {
  301. cfg.Defaults.Folder = folder
  302. } else {
  303. cfg.SetFolder(folder)
  304. }
  305. })
  306. if err != nil {
  307. http.Error(w, err.Error(), http.StatusInternalServerError)
  308. return
  309. }
  310. c.finish(w, waiter)
  311. }
  312. func (c *configMuxBuilder) adjustDevice(w http.ResponseWriter, r *http.Request, device config.DeviceConfiguration, defaults bool) {
  313. if err := unmarshalTo(r.Body, &device); err != nil {
  314. http.Error(w, err.Error(), http.StatusBadRequest)
  315. return
  316. }
  317. waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
  318. if defaults {
  319. cfg.Defaults.Device = device
  320. } else {
  321. cfg.SetDevice(device)
  322. }
  323. })
  324. if err != nil {
  325. http.Error(w, err.Error(), http.StatusInternalServerError)
  326. return
  327. }
  328. c.finish(w, waiter)
  329. }
  330. func (c *configMuxBuilder) adjustOptions(w http.ResponseWriter, r *http.Request, opts config.OptionsConfiguration) {
  331. if err := unmarshalTo(r.Body, &opts); err != nil {
  332. http.Error(w, err.Error(), http.StatusBadRequest)
  333. return
  334. }
  335. waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
  336. cfg.Options = opts
  337. })
  338. if err != nil {
  339. http.Error(w, err.Error(), http.StatusInternalServerError)
  340. return
  341. }
  342. c.finish(w, waiter)
  343. }
  344. func (c *configMuxBuilder) adjustGUI(w http.ResponseWriter, r *http.Request, gui config.GUIConfiguration) {
  345. err := unmarshalTo(r.Body, &gui)
  346. if err != nil {
  347. http.Error(w, err.Error(), http.StatusBadRequest)
  348. return
  349. }
  350. var errMsg string
  351. var status int
  352. waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
  353. if err := c.postAdjustGui(&cfg.GUI, &gui); err != nil {
  354. errMsg = err.Error()
  355. status = http.StatusInternalServerError
  356. return
  357. }
  358. cfg.GUI = gui
  359. })
  360. if errMsg != "" {
  361. http.Error(w, errMsg, status)
  362. } else if err != nil {
  363. http.Error(w, err.Error(), http.StatusInternalServerError)
  364. return
  365. }
  366. c.finish(w, waiter)
  367. }
  368. func (c *configMuxBuilder) postAdjustGui(from *config.GUIConfiguration, to *config.GUIConfiguration) error {
  369. if to.Password != from.Password {
  370. if err := to.SetPassword(to.Password); err != nil {
  371. slog.Error("Failed to hash password", slogutil.Error(err))
  372. return err
  373. }
  374. }
  375. return nil
  376. }
  377. func (c *configMuxBuilder) adjustLDAP(w http.ResponseWriter, r *http.Request, ldap config.LDAPConfiguration) {
  378. if err := unmarshalTo(r.Body, &ldap); err != nil {
  379. http.Error(w, err.Error(), http.StatusBadRequest)
  380. return
  381. }
  382. waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
  383. cfg.LDAP = ldap
  384. })
  385. if err != nil {
  386. http.Error(w, err.Error(), http.StatusInternalServerError)
  387. return
  388. }
  389. c.finish(w, waiter)
  390. }
  391. // Unmarshals the content of the given body and stores it in to (i.e. to must be a pointer).
  392. func unmarshalTo(body io.ReadCloser, to interface{}) error {
  393. bs, err := io.ReadAll(body)
  394. body.Close()
  395. if err != nil {
  396. return err
  397. }
  398. return json.Unmarshal(bs, to)
  399. }
  400. func unmarshalToRawMessages(body io.ReadCloser) ([]json.RawMessage, error) {
  401. var data []json.RawMessage
  402. err := unmarshalTo(body, &data)
  403. return data, err
  404. }
  405. func (c *configMuxBuilder) finish(w http.ResponseWriter, waiter config.Waiter) {
  406. waiter.Wait()
  407. if err := c.cfg.Save(); err != nil {
  408. slog.Error("Failed to save config", slogutil.Error(err))
  409. http.Error(w, err.Error(), http.StatusInternalServerError)
  410. }
  411. }