Files
syncthing/lib/api/confighandler.go
T
André Colomb dec6f80d2b lib/config: Move the bcrypt password hashing to GUIConfiguration (#8028)
What hash is used to store the password should ideally be an
implementation detail, so that every user of the GUIConfiguration
object automatically agrees on how to handle it.  That is currently
distribututed over the confighandler.go and api_auth.go files, plus
tests.

Add the SetHasedPassword() / CompareHashedPassword() API to keep the
hashing method encapsulated.  Add a separate test for it and adjust
other users and tests.  Remove all deprecated imports of the bcrypt
package.
2021-11-08 13:32:04 +01:00

431 lines
12 KiB
Go

// Copyright (C) 2020 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package api
import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/util"
)
type configMuxBuilder struct {
*httprouter.Router
id protocol.DeviceID
cfg config.Wrapper
}
func (c *configMuxBuilder) registerConfig(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.RawCopy())
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustConfig(w, r)
})
}
func (c *configMuxBuilder) registerConfigDeprecated(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.RawCopy())
})
c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustConfig(w, r)
})
}
func (c *configMuxBuilder) registerConfigInsync(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, map[string]bool{"configInSync": !c.cfg.RequiresRestart()})
})
}
func (c *configMuxBuilder) registerConfigRequiresRestart(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, map[string]bool{"requiresRestart": c.cfg.RequiresRestart()})
})
}
func (c *configMuxBuilder) registerFolders(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.FolderList())
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
data, err := unmarshalToRawMessages(r.Body)
folders := make([]config.FolderConfiguration, len(data))
defaultFolder := c.cfg.DefaultFolder()
for i, bs := range data {
folders[i] = defaultFolder.Copy()
if err := json.Unmarshal(bs, &folders[i]); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetFolders(folders)
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.finish(w, waiter)
})
c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustFolder(w, r, c.cfg.DefaultFolder(), false)
})
}
func (c *configMuxBuilder) registerDevices(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.DeviceList())
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
data, err := unmarshalToRawMessages(r.Body)
devices := make([]config.DeviceConfiguration, len(data))
defaultDevice := c.cfg.DefaultDevice()
for i, bs := range data {
devices[i] = defaultDevice.Copy()
if err := json.Unmarshal(bs, &devices[i]); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetDevices(devices)
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.finish(w, waiter)
})
c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustDevice(w, r, c.cfg.DefaultDevice(), false)
})
}
func (c *configMuxBuilder) registerFolder(path string) {
c.Handle(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
folder, ok := c.cfg.Folder(p.ByName("id"))
if !ok {
http.Error(w, "No folder with given ID", http.StatusNotFound)
return
}
sendJSON(w, folder)
})
c.Handle(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
c.adjustFolder(w, r, c.cfg.DefaultFolder(), false)
})
c.Handle(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
folder, ok := c.cfg.Folder(p.ByName("id"))
if !ok {
http.Error(w, "No folder with given ID", http.StatusNotFound)
return
}
c.adjustFolder(w, r, folder, false)
})
c.Handle(http.MethodDelete, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
waiter, err := c.cfg.RemoveFolder(p.ByName("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
c.finish(w, waiter)
})
}
func (c *configMuxBuilder) registerDevice(path string) {
deviceFromParams := func(w http.ResponseWriter, p httprouter.Params) (config.DeviceConfiguration, bool) {
id, err := protocol.DeviceIDFromString(p.ByName("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return config.DeviceConfiguration{}, false
}
device, ok := c.cfg.Device(id)
if !ok {
http.Error(w, "No device with given ID", http.StatusNotFound)
return config.DeviceConfiguration{}, false
}
return device, true
}
c.Handle(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
if device, ok := deviceFromParams(w, p); ok {
sendJSON(w, device)
}
})
c.Handle(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
c.adjustDevice(w, r, c.cfg.DefaultDevice(), false)
})
c.Handle(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
if device, ok := deviceFromParams(w, p); ok {
c.adjustDevice(w, r, device, false)
}
})
c.Handle(http.MethodDelete, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
id, err := protocol.DeviceIDFromString(p.ByName("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.RemoveDevice(id)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
c.finish(w, waiter)
})
}
func (c *configMuxBuilder) registerDefaultFolder(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.DefaultFolder())
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
var cfg config.FolderConfiguration
util.SetDefaults(&cfg)
c.adjustFolder(w, r, cfg, true)
})
c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustFolder(w, r, c.cfg.DefaultFolder(), true)
})
}
func (c *configMuxBuilder) registerDefaultDevice(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.DefaultDevice())
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
var cfg config.DeviceConfiguration
util.SetDefaults(&cfg)
c.adjustDevice(w, r, cfg, true)
})
c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustDevice(w, r, c.cfg.DefaultDevice(), true)
})
}
func (c *configMuxBuilder) registerOptions(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.Options())
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
var cfg config.OptionsConfiguration
util.SetDefaults(&cfg)
c.adjustOptions(w, r, cfg)
})
c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustOptions(w, r, c.cfg.Options())
})
}
func (c *configMuxBuilder) registerLDAP(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.LDAP())
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
var cfg config.LDAPConfiguration
util.SetDefaults(&cfg)
c.adjustLDAP(w, r, cfg)
})
c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustLDAP(w, r, c.cfg.LDAP())
})
}
func (c *configMuxBuilder) registerGUI(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.GUI())
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
var cfg config.GUIConfiguration
util.SetDefaults(&cfg)
c.adjustGUI(w, r, cfg)
})
c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustGUI(w, r, c.cfg.GUI())
})
}
func (c *configMuxBuilder) adjustConfig(w http.ResponseWriter, r *http.Request) {
to, err := config.ReadJSON(r.Body, c.id)
r.Body.Close()
if err != nil {
l.Warnln("Decoding posted config:", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var errMsg string
var status int
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
if to.GUI.Password != cfg.GUI.Password {
if err := to.GUI.HashAndSetPassword(to.GUI.Password); err != nil {
l.Warnln("hashing password:", err)
errMsg = err.Error()
status = http.StatusInternalServerError
return
}
}
*cfg = to
})
if errMsg != "" {
http.Error(w, errMsg, status)
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.finish(w, waiter)
}
func (c *configMuxBuilder) adjustFolder(w http.ResponseWriter, r *http.Request, folder config.FolderConfiguration, defaults bool) {
if err := unmarshalTo(r.Body, &folder); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
if defaults {
cfg.Defaults.Folder = folder
} else {
cfg.SetFolder(folder)
}
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.finish(w, waiter)
}
func (c *configMuxBuilder) adjustDevice(w http.ResponseWriter, r *http.Request, device config.DeviceConfiguration, defaults bool) {
if err := unmarshalTo(r.Body, &device); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
if defaults {
cfg.Defaults.Device = device
} else {
cfg.SetDevice(device)
}
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.finish(w, waiter)
}
func (c *configMuxBuilder) adjustOptions(w http.ResponseWriter, r *http.Request, opts config.OptionsConfiguration) {
if err := unmarshalTo(r.Body, &opts); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.Options = opts
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.finish(w, waiter)
}
func (c *configMuxBuilder) adjustGUI(w http.ResponseWriter, r *http.Request, gui config.GUIConfiguration) {
oldPassword := gui.Password
err := unmarshalTo(r.Body, &gui)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var errMsg string
var status int
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
if gui.Password != oldPassword {
if err := gui.HashAndSetPassword(gui.Password); err != nil {
l.Warnln("hashing password:", err)
errMsg = err.Error()
status = http.StatusInternalServerError
return
}
}
cfg.GUI = gui
})
if errMsg != "" {
http.Error(w, errMsg, status)
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.finish(w, waiter)
}
func (c *configMuxBuilder) adjustLDAP(w http.ResponseWriter, r *http.Request, ldap config.LDAPConfiguration) {
if err := unmarshalTo(r.Body, &ldap); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.LDAP = ldap
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.finish(w, waiter)
}
// Unmarshals the content of the given body and stores it in to (i.e. to must be a pointer).
func unmarshalTo(body io.ReadCloser, to interface{}) error {
bs, err := ioutil.ReadAll(body)
body.Close()
if err != nil {
return err
}
return json.Unmarshal(bs, to)
}
func unmarshalToRawMessages(body io.ReadCloser) ([]json.RawMessage, error) {
var data []json.RawMessage
err := unmarshalTo(body, &data)
return data, err
}
func (c *configMuxBuilder) finish(w http.ResponseWriter, waiter config.Waiter) {
waiter.Wait()
if err := c.cfg.Save(); err != nil {
l.Warnln("Saving config:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}