mirror of
https://github.com/octoleo/syncthing.git
synced 2025-01-03 07:12:27 +00:00
parent
1c2be84e4e
commit
f0f60ba2e7
1
go.mod
1
go.mod
@ -24,6 +24,7 @@ require (
|
|||||||
github.com/greatroar/blobloom v0.3.0
|
github.com/greatroar/blobloom v0.3.0
|
||||||
github.com/jackpal/gateway v1.0.6
|
github.com/jackpal/gateway v1.0.6
|
||||||
github.com/jackpal/go-nat-pmp v1.0.2
|
github.com/jackpal/go-nat-pmp v1.0.2
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||||
github.com/kr/pretty v0.2.0 // indirect
|
github.com/kr/pretty v0.2.0 // indirect
|
||||||
github.com/lib/pq v1.2.0
|
github.com/lib/pq v1.2.0
|
||||||
|
1
go.sum
1
go.sum
@ -173,6 +173,7 @@ github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0
|
|||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
|
@ -268,7 +268,7 @@ angular.module('syncthing.core')
|
|||||||
$scope.$on(Events.CONFIG_SAVED, function (event, arg) {
|
$scope.$on(Events.CONFIG_SAVED, function (event, arg) {
|
||||||
updateLocalConfig(arg.data);
|
updateLocalConfig(arg.data);
|
||||||
|
|
||||||
$http.get(urlbase + '/system/config/insync').success(function (data) {
|
$http.get(urlbase + '/config/insync').success(function (data) {
|
||||||
$scope.configInSync = data.configInSync;
|
$scope.configInSync = data.configInSync;
|
||||||
}).error($scope.emitHTTPError);
|
}).error($scope.emitHTTPError);
|
||||||
});
|
});
|
||||||
@ -578,12 +578,12 @@ angular.module('syncthing.core')
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshConfig() {
|
function refreshConfig() {
|
||||||
$http.get(urlbase + '/system/config').success(function (data) {
|
$http.get(urlbase + '/config').success(function (data) {
|
||||||
updateLocalConfig(data);
|
updateLocalConfig(data);
|
||||||
console.log("refreshConfig", data);
|
console.log("refreshConfig", data);
|
||||||
}).error($scope.emitHTTPError);
|
}).error($scope.emitHTTPError);
|
||||||
|
|
||||||
$http.get(urlbase + '/system/config/insync').success(function (data) {
|
$http.get(urlbase + '/config/insync').success(function (data) {
|
||||||
$scope.configInSync = data.configInSync;
|
$scope.configInSync = data.configInSync;
|
||||||
}).error($scope.emitHTTPError);
|
}).error($scope.emitHTTPError);
|
||||||
}
|
}
|
||||||
@ -1257,7 +1257,7 @@ angular.module('syncthing.core')
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
$http.post(urlbase + '/system/config', cfg, opts).success(function () {
|
$http.put(urlbase + '/config', cfg, opts).success(function () {
|
||||||
refreshConfig();
|
refreshConfig();
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
|
199
lib/api/api.go
199
lib/api/api.go
@ -31,10 +31,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
metrics "github.com/rcrowley/go-metrics"
|
metrics "github.com/rcrowley/go-metrics"
|
||||||
"github.com/thejerf/suture"
|
"github.com/thejerf/suture"
|
||||||
"github.com/vitrun/qart/qr"
|
"github.com/vitrun/qart/qr"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/build"
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
@ -81,7 +81,6 @@ type service struct {
|
|||||||
connectionsService connections.Service
|
connectionsService connections.Service
|
||||||
fss model.FolderSummaryService
|
fss model.FolderSummaryService
|
||||||
urService *ur.Service
|
urService *ur.Service
|
||||||
systemConfigMut sync.Mutex // serializes posts to /rest/system/config
|
|
||||||
contr Controller
|
contr Controller
|
||||||
noUpgrade bool
|
noUpgrade bool
|
||||||
tlsDefaultCommonName string
|
tlsDefaultCommonName string
|
||||||
@ -123,7 +122,6 @@ func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonNam
|
|||||||
connectionsService: connectionsService,
|
connectionsService: connectionsService,
|
||||||
fss: fss,
|
fss: fss,
|
||||||
urService: urService,
|
urService: urService,
|
||||||
systemConfigMut: sync.NewMutex(),
|
|
||||||
guiErrors: errors,
|
guiErrors: errors,
|
||||||
systemLog: systemLog,
|
systemLog: systemLog,
|
||||||
contr: contr,
|
contr: contr,
|
||||||
@ -243,60 +241,80 @@ func (s *service) serve(ctx context.Context) {
|
|||||||
s.cfg.Subscribe(s)
|
s.cfg.Subscribe(s)
|
||||||
defer s.cfg.Unsubscribe(s)
|
defer s.cfg.Unsubscribe(s)
|
||||||
|
|
||||||
|
restMux := httprouter.New()
|
||||||
|
|
||||||
// The GET handlers
|
// The GET handlers
|
||||||
getRestMux := http.NewServeMux()
|
restMux.HandlerFunc(http.MethodGet, "/rest/db/completion", s.getDBCompletion) // [device] [folder]
|
||||||
getRestMux.HandleFunc("/rest/db/completion", s.getDBCompletion) // [device] [folder]
|
restMux.HandlerFunc(http.MethodGet, "/rest/db/file", s.getDBFile) // folder file
|
||||||
getRestMux.HandleFunc("/rest/db/file", s.getDBFile) // folder file
|
restMux.HandlerFunc(http.MethodGet, "/rest/db/ignores", s.getDBIgnores) // folder
|
||||||
getRestMux.HandleFunc("/rest/db/ignores", s.getDBIgnores) // folder
|
restMux.HandlerFunc(http.MethodGet, "/rest/db/need", s.getDBNeed) // folder [perpage] [page]
|
||||||
getRestMux.HandleFunc("/rest/db/need", s.getDBNeed) // folder [perpage] [page]
|
restMux.HandlerFunc(http.MethodGet, "/rest/db/remoteneed", s.getDBRemoteNeed) // device folder [perpage] [page]
|
||||||
getRestMux.HandleFunc("/rest/db/remoteneed", s.getDBRemoteNeed) // device folder [perpage] [page]
|
restMux.HandlerFunc(http.MethodGet, "/rest/db/localchanged", s.getDBLocalChanged) // folder
|
||||||
getRestMux.HandleFunc("/rest/db/localchanged", s.getDBLocalChanged) // folder
|
restMux.HandlerFunc(http.MethodGet, "/rest/db/status", s.getDBStatus) // folder
|
||||||
getRestMux.HandleFunc("/rest/db/status", s.getDBStatus) // folder
|
restMux.HandlerFunc(http.MethodGet, "/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
|
||||||
getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
|
restMux.HandlerFunc(http.MethodGet, "/rest/folder/versions", s.getFolderVersions) // folder
|
||||||
getRestMux.HandleFunc("/rest/folder/versions", s.getFolderVersions) // folder
|
restMux.HandlerFunc(http.MethodGet, "/rest/folder/errors", s.getFolderErrors) // folder
|
||||||
getRestMux.HandleFunc("/rest/folder/errors", s.getFolderErrors) // folder
|
restMux.HandlerFunc(http.MethodGet, "/rest/folder/pullerrors", s.getFolderErrors) // folder (deprecated)
|
||||||
getRestMux.HandleFunc("/rest/folder/pullerrors", s.getFolderErrors) // folder (deprecated)
|
restMux.HandlerFunc(http.MethodGet, "/rest/events", s.getIndexEvents) // [since] [limit] [timeout] [events]
|
||||||
getRestMux.HandleFunc("/rest/events", s.getIndexEvents) // [since] [limit] [timeout] [events]
|
restMux.HandlerFunc(http.MethodGet, "/rest/events/disk", s.getDiskEvents) // [since] [limit] [timeout]
|
||||||
getRestMux.HandleFunc("/rest/events/disk", s.getDiskEvents) // [since] [limit] [timeout]
|
restMux.HandlerFunc(http.MethodGet, "/rest/stats/device", s.getDeviceStats) // -
|
||||||
getRestMux.HandleFunc("/rest/stats/device", s.getDeviceStats) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/stats/folder", s.getFolderStats) // -
|
||||||
getRestMux.HandleFunc("/rest/stats/folder", s.getFolderStats) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/svc/deviceid", s.getDeviceID) // id
|
||||||
getRestMux.HandleFunc("/rest/svc/deviceid", s.getDeviceID) // id
|
restMux.HandlerFunc(http.MethodGet, "/rest/svc/lang", s.getLang) // -
|
||||||
getRestMux.HandleFunc("/rest/svc/lang", s.getLang) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/svc/report", s.getReport) // -
|
||||||
getRestMux.HandleFunc("/rest/svc/report", s.getReport) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/svc/random/string", s.getRandomString) // [length]
|
||||||
getRestMux.HandleFunc("/rest/svc/random/string", s.getRandomString) // [length]
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/browse", s.getSystemBrowse) // current
|
||||||
getRestMux.HandleFunc("/rest/system/browse", s.getSystemBrowse) // current
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/connections", s.getSystemConnections) // -
|
||||||
getRestMux.HandleFunc("/rest/system/config", s.getSystemConfig) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/discovery", s.getSystemDiscovery) // -
|
||||||
getRestMux.HandleFunc("/rest/system/config/insync", s.getSystemConfigInsync) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/error", s.getSystemError) // -
|
||||||
getRestMux.HandleFunc("/rest/system/connections", s.getSystemConnections) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/ping", s.restPing) // -
|
||||||
getRestMux.HandleFunc("/rest/system/discovery", s.getSystemDiscovery) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/status", s.getSystemStatus) // -
|
||||||
getRestMux.HandleFunc("/rest/system/error", s.getSystemError) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/upgrade", s.getSystemUpgrade) // -
|
||||||
getRestMux.HandleFunc("/rest/system/ping", s.restPing) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/version", s.getSystemVersion) // -
|
||||||
getRestMux.HandleFunc("/rest/system/status", s.getSystemStatus) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/debug", s.getSystemDebug) // -
|
||||||
getRestMux.HandleFunc("/rest/system/upgrade", s.getSystemUpgrade) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/log", s.getSystemLog) // [since]
|
||||||
getRestMux.HandleFunc("/rest/system/version", s.getSystemVersion) // -
|
restMux.HandlerFunc(http.MethodGet, "/rest/system/log.txt", s.getSystemLogTxt) // [since]
|
||||||
getRestMux.HandleFunc("/rest/system/debug", s.getSystemDebug) // -
|
|
||||||
getRestMux.HandleFunc("/rest/system/log", s.getSystemLog) // [since]
|
|
||||||
getRestMux.HandleFunc("/rest/system/log.txt", s.getSystemLogTxt) // [since]
|
|
||||||
|
|
||||||
// The POST handlers
|
// The POST handlers
|
||||||
postRestMux := http.NewServeMux()
|
restMux.HandlerFunc(http.MethodPost, "/rest/db/prio", s.postDBPrio) // folder file [perpage] [page]
|
||||||
postRestMux.HandleFunc("/rest/db/prio", s.postDBPrio) // folder file [perpage] [page]
|
restMux.HandlerFunc(http.MethodPost, "/rest/db/ignores", s.postDBIgnores) // folder
|
||||||
postRestMux.HandleFunc("/rest/db/ignores", s.postDBIgnores) // folder
|
restMux.HandlerFunc(http.MethodPost, "/rest/db/override", s.postDBOverride) // folder
|
||||||
postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder
|
restMux.HandlerFunc(http.MethodPost, "/rest/db/revert", s.postDBRevert) // folder
|
||||||
postRestMux.HandleFunc("/rest/db/revert", s.postDBRevert) // folder
|
restMux.HandlerFunc(http.MethodPost, "/rest/db/scan", s.postDBScan) // folder [sub...] [delay]
|
||||||
postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...] [delay]
|
restMux.HandlerFunc(http.MethodPost, "/rest/folder/versions", s.postFolderVersionsRestore) // folder <body>
|
||||||
postRestMux.HandleFunc("/rest/folder/versions", s.postFolderVersionsRestore) // folder <body>
|
restMux.HandlerFunc(http.MethodPost, "/rest/system/error", s.postSystemError) // <body>
|
||||||
postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // <body>
|
restMux.HandlerFunc(http.MethodPost, "/rest/system/error/clear", s.postSystemErrorClear) // -
|
||||||
postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // <body>
|
restMux.HandlerFunc(http.MethodPost, "/rest/system/ping", s.restPing) // -
|
||||||
postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
|
restMux.HandlerFunc(http.MethodPost, "/rest/system/reset", s.postSystemReset) // [folder]
|
||||||
postRestMux.HandleFunc("/rest/system/ping", s.restPing) // -
|
restMux.HandlerFunc(http.MethodPost, "/rest/system/restart", s.postSystemRestart) // -
|
||||||
postRestMux.HandleFunc("/rest/system/reset", s.postSystemReset) // [folder]
|
restMux.HandlerFunc(http.MethodPost, "/rest/system/shutdown", s.postSystemShutdown) // -
|
||||||
postRestMux.HandleFunc("/rest/system/restart", s.postSystemRestart) // -
|
restMux.HandlerFunc(http.MethodPost, "/rest/system/upgrade", s.postSystemUpgrade) // -
|
||||||
postRestMux.HandleFunc("/rest/system/shutdown", s.postSystemShutdown) // -
|
restMux.HandlerFunc(http.MethodPost, "/rest/system/pause", s.makeDevicePauseHandler(true)) // [device]
|
||||||
postRestMux.HandleFunc("/rest/system/upgrade", s.postSystemUpgrade) // -
|
restMux.HandlerFunc(http.MethodPost, "/rest/system/resume", s.makeDevicePauseHandler(false)) // [device]
|
||||||
postRestMux.HandleFunc("/rest/system/pause", s.makeDevicePauseHandler(true)) // [device]
|
restMux.HandlerFunc(http.MethodPost, "/rest/system/debug", s.postSystemDebug) // [enable] [disable]
|
||||||
postRestMux.HandleFunc("/rest/system/resume", s.makeDevicePauseHandler(false)) // [device]
|
|
||||||
postRestMux.HandleFunc("/rest/system/debug", s.postSystemDebug) // [enable] [disable]
|
// Config endpoints
|
||||||
|
|
||||||
|
configBuilder := &configMuxBuilder{
|
||||||
|
Router: restMux,
|
||||||
|
id: s.id,
|
||||||
|
cfg: s.cfg,
|
||||||
|
mut: sync.NewMutex(),
|
||||||
|
}
|
||||||
|
|
||||||
|
configBuilder.registerConfig("/rest/config/")
|
||||||
|
configBuilder.registerConfigInsync("/rest/config/insync")
|
||||||
|
configBuilder.registerFolders("/rest/config/folders")
|
||||||
|
configBuilder.registerDevices("/rest/config/devices")
|
||||||
|
configBuilder.registerFolder("/rest/config/folders/:id")
|
||||||
|
configBuilder.registerDevice("/rest/config/devices/:id")
|
||||||
|
configBuilder.registerOptions("/rest/config/options")
|
||||||
|
configBuilder.registerLDAP("/rest/config/ldap")
|
||||||
|
configBuilder.registerGUI("/rest/config/gui")
|
||||||
|
|
||||||
|
// Deprecated config endpoints
|
||||||
|
configBuilder.registerConfigDeprecated("/rest/system/config") // POST instead of PUT
|
||||||
|
configBuilder.registerConfigInsync("/rest/system/config/insync")
|
||||||
|
|
||||||
// Debug endpoints, not for general use
|
// Debug endpoints, not for general use
|
||||||
debugMux := http.NewServeMux()
|
debugMux := http.NewServeMux()
|
||||||
@ -305,15 +323,14 @@ func (s *service) serve(ctx context.Context) {
|
|||||||
debugMux.HandleFunc("/rest/debug/cpuprof", s.getCPUProf) // duration
|
debugMux.HandleFunc("/rest/debug/cpuprof", s.getCPUProf) // duration
|
||||||
debugMux.HandleFunc("/rest/debug/heapprof", s.getHeapProf)
|
debugMux.HandleFunc("/rest/debug/heapprof", s.getHeapProf)
|
||||||
debugMux.HandleFunc("/rest/debug/support", s.getSupportBundle)
|
debugMux.HandleFunc("/rest/debug/support", s.getSupportBundle)
|
||||||
getRestMux.Handle("/rest/debug/", s.whenDebugging(debugMux))
|
restMux.Handler(http.MethodGet, "/rest/debug/", s.whenDebugging(debugMux))
|
||||||
|
|
||||||
// A handler that splits requests between the two above and disables
|
// A handler that disables caching
|
||||||
// caching
|
noCacheRestMux := noCacheMiddleware(metricsMiddleware(restMux))
|
||||||
restMux := noCacheMiddleware(metricsMiddleware(getPostHandler(getRestMux, postRestMux)))
|
|
||||||
|
|
||||||
// The main routing handler
|
// The main routing handler
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/rest/", restMux)
|
mux.Handle("/rest/", noCacheRestMux)
|
||||||
mux.HandleFunc("/qr/", s.getQR)
|
mux.HandleFunc("/qr/", s.getQR)
|
||||||
|
|
||||||
// Serve compiled in assets unless an asset directory was set (for development)
|
// Serve compiled in assets unless an asset directory was set (for development)
|
||||||
@ -446,19 +463,6 @@ func (s *service) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPostHandler(get, post http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
get.ServeHTTP(w, r)
|
|
||||||
case "POST":
|
|
||||||
post.ServeHTTP(w, r)
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func debugMiddleware(h http.Handler) http.Handler {
|
func debugMiddleware(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
t0 := time.Now()
|
t0 := time.Now()
|
||||||
@ -837,57 +841,6 @@ func (s *service) getDBFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) getSystemConfig(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sendJSON(w, s.cfg.RawCopy())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
|
||||||
s.systemConfigMut.Lock()
|
|
||||||
defer s.systemConfigMut.Unlock()
|
|
||||||
|
|
||||||
to, err := config.ReadJSON(r.Body, s.id)
|
|
||||||
r.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("Decoding posted config:", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if to.GUI.Password != s.cfg.GUI().Password {
|
|
||||||
if to.GUI.Password != "" && !bcryptExpr.MatchString(to.GUI.Password) {
|
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(to.GUI.Password), 0)
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("bcrypting password:", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
to.GUI.Password = string(hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activate and save. Wait for the configuration to become active before
|
|
||||||
// completing the request.
|
|
||||||
|
|
||||||
if wg, err := s.cfg.Replace(to); err != nil {
|
|
||||||
l.Warnln("Replacing config:", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.cfg.Save(); err != nil {
|
|
||||||
l.Warnln("Saving config:", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sendJSON(w, map[string]bool{"configInSync": !s.cfg.RequiresRestart()})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) postSystemRestart(w http.ResponseWriter, r *http.Request) {
|
func (s *service) postSystemRestart(w http.ResponseWriter, r *http.Request) {
|
||||||
s.flushResponse(`{"ok": "restarting"}`, w)
|
s.flushResponse(`{"ok": "restarting"}`, w)
|
||||||
go s.contr.Restart()
|
go s.contr.Restart()
|
||||||
|
@ -40,8 +40,13 @@ import (
|
|||||||
var (
|
var (
|
||||||
confDir = filepath.Join("testdata", "config")
|
confDir = filepath.Join("testdata", "config")
|
||||||
token = filepath.Join(confDir, "csrftokens.txt")
|
token = filepath.Join(confDir, "csrftokens.txt")
|
||||||
|
dev1 protocol.DeviceID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dev1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
|
||||||
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
orig := locations.GetBaseDir(locations.ConfigBaseDir)
|
orig := locations.GetBaseDir(locations.ConfigBaseDir)
|
||||||
locations.SetBaseDir(locations.ConfigBaseDir, confDir)
|
locations.SetBaseDir(locations.ConfigBaseDir, confDir)
|
||||||
@ -396,6 +401,56 @@ func TestAPIServiceRequests(t *testing.T) {
|
|||||||
Type: "text/plain",
|
Type: "text/plain",
|
||||||
Prefix: "",
|
Prefix: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /rest/config
|
||||||
|
{
|
||||||
|
URL: "/rest/config/folders",
|
||||||
|
Code: 200,
|
||||||
|
Type: "application/json",
|
||||||
|
Prefix: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "/rest/config/folders/missing",
|
||||||
|
Code: 404,
|
||||||
|
Type: "text/plain",
|
||||||
|
Prefix: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "/rest/config/devices",
|
||||||
|
Code: 200,
|
||||||
|
Type: "application/json",
|
||||||
|
Prefix: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "/rest/config/devices/illegalid",
|
||||||
|
Code: 400,
|
||||||
|
Type: "text/plain",
|
||||||
|
Prefix: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "/rest/config/devices/" + protocol.GlobalDeviceID.String(),
|
||||||
|
Code: 404,
|
||||||
|
Type: "text/plain",
|
||||||
|
Prefix: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "/rest/config/options",
|
||||||
|
Code: 200,
|
||||||
|
Type: "application/json",
|
||||||
|
Prefix: "{",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "/rest/config/gui",
|
||||||
|
Code: 200,
|
||||||
|
Type: "application/json",
|
||||||
|
Prefix: "{",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "/rest/config/ldap",
|
||||||
|
Code: 200,
|
||||||
|
Type: "application/json",
|
||||||
|
Prefix: "{",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@ -520,7 +575,7 @@ func TestHTTPLogin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startHTTP(cfg *mockedConfig) (string, *suture.Supervisor, error) {
|
func startHTTP(cfg config.Wrapper) (string, *suture.Supervisor, error) {
|
||||||
m := new(mockedModel)
|
m := new(mockedModel)
|
||||||
assetDir := "../../gui"
|
assetDir := "../../gui"
|
||||||
eventSub := new(mockedEventSub)
|
eventSub := new(mockedEventSub)
|
||||||
@ -552,7 +607,7 @@ func startHTTP(cfg *mockedConfig) (string, *suture.Supervisor, error) {
|
|||||||
return "", nil, fmt.Errorf("weird address from API service: %w", err)
|
return "", nil, fmt.Errorf("weird address from API service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
host, _, _ := net.SplitHostPort(cfg.gui.RawAddress)
|
host, _, _ := net.SplitHostPort(cfg.GUI().RawAddress)
|
||||||
if host == "" || host == "0.0.0.0" {
|
if host == "" || host == "0.0.0.0" {
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
}
|
}
|
||||||
@ -1174,6 +1229,127 @@ func TestShouldRegenerateCertificate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigChanges(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const testAPIKey = "foobarbaz"
|
||||||
|
cfg := config.Configuration{
|
||||||
|
GUI: config.GUIConfiguration{
|
||||||
|
RawAddress: "127.0.0.1:0",
|
||||||
|
RawUseTLS: false,
|
||||||
|
APIKey: testAPIKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tmpFile, err := ioutil.TempFile("", "syncthing-testConfig-")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
w := config.Wrap(tmpFile.Name(), cfg, events.NoopLogger)
|
||||||
|
tmpFile.Close()
|
||||||
|
baseURL, sup, err := startHTTP(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unexpected error from getting base URL:", err)
|
||||||
|
}
|
||||||
|
defer sup.Stop()
|
||||||
|
|
||||||
|
cli := &http.Client{
|
||||||
|
Timeout: time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
do := func(req *http.Request, status int) *http.Response {
|
||||||
|
t.Helper()
|
||||||
|
req.Header.Set("X-API-Key", testAPIKey)
|
||||||
|
resp, err := cli.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != status {
|
||||||
|
t.Errorf("Expected status %v, got %v", status, resp.StatusCode)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
mod := func(method, path string, data interface{}) {
|
||||||
|
t.Helper()
|
||||||
|
bs, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req, _ := http.NewRequest(method, baseURL+path, bytes.NewReader(bs))
|
||||||
|
do(req, http.StatusOK).Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
get := func(path string) *http.Response {
|
||||||
|
t.Helper()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, baseURL+path, nil)
|
||||||
|
return do(req, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
dev1Path := "/rest/config/devices/" + dev1.String()
|
||||||
|
|
||||||
|
// Create device
|
||||||
|
mod(http.MethodPut, "/rest/config/devices", []config.DeviceConfiguration{{DeviceID: dev1}})
|
||||||
|
|
||||||
|
// Check its there
|
||||||
|
get(dev1Path).Body.Close()
|
||||||
|
|
||||||
|
// Modify just a single attribute
|
||||||
|
mod(http.MethodPatch, dev1Path, map[string]bool{"Paused": true})
|
||||||
|
|
||||||
|
// Check that attribute
|
||||||
|
resp := get(dev1Path)
|
||||||
|
var dev config.DeviceConfiguration
|
||||||
|
if err := unmarshalTo(resp.Body, &dev); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !dev.Paused {
|
||||||
|
t.Error("Expected device to be paused")
|
||||||
|
}
|
||||||
|
|
||||||
|
folder2Path := "/rest/config/folders/folder2"
|
||||||
|
|
||||||
|
// Create a folder and add another
|
||||||
|
mod(http.MethodPut, "/rest/config/folders", []config.FolderConfiguration{{ID: "folder1", Path: "folder1"}})
|
||||||
|
mod(http.MethodPut, folder2Path, config.FolderConfiguration{ID: "folder2", Path: "folder2"})
|
||||||
|
|
||||||
|
// Check they are there
|
||||||
|
get("/rest/config/folders/folder1").Body.Close()
|
||||||
|
get(folder2Path).Body.Close()
|
||||||
|
|
||||||
|
// Modify just a single attribute
|
||||||
|
mod(http.MethodPatch, folder2Path, map[string]bool{"Paused": true})
|
||||||
|
|
||||||
|
// Check that attribute
|
||||||
|
resp = get(folder2Path)
|
||||||
|
var folder config.FolderConfiguration
|
||||||
|
if err := unmarshalTo(resp.Body, &folder); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !dev.Paused {
|
||||||
|
t.Error("Expected folder to be paused")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete folder2
|
||||||
|
req, _ := http.NewRequest(http.MethodDelete, baseURL+folder2Path, nil)
|
||||||
|
do(req, http.StatusOK)
|
||||||
|
|
||||||
|
// Check folder1 is still there and folder2 gone
|
||||||
|
get("/rest/config/folders/folder1").Body.Close()
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, baseURL+folder2Path, nil)
|
||||||
|
do(req, http.StatusNotFound)
|
||||||
|
|
||||||
|
mod(http.MethodPatch, "/rest/config/options", map[string]int{"maxSendKbps": 50})
|
||||||
|
resp = get("/rest/config/options")
|
||||||
|
var opts config.OptionsConfiguration
|
||||||
|
if err := unmarshalTo(resp.Body, &opts); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if opts.MaxSendKbps != 50 {
|
||||||
|
t.Error("Exepcted 50 for MaxSendKbps, got", opts.MaxSendKbps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func equalStrings(a, b []string) bool {
|
func equalStrings(a, b []string) bool {
|
||||||
if len(a) != len(b) {
|
if len(a) != len(b) {
|
||||||
return false
|
return false
|
||||||
|
378
lib/api/confighandler.go
Normal file
378
lib/api/confighandler.go
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
// 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"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configMuxBuilder struct {
|
||||||
|
*httprouter.Router
|
||||||
|
id protocol.DeviceID
|
||||||
|
cfg config.Wrapper
|
||||||
|
mut sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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) {
|
||||||
|
c.mut.Lock()
|
||||||
|
defer c.mut.Unlock()
|
||||||
|
var folders []config.FolderConfiguration
|
||||||
|
if err := unmarshalTo(r.Body, &folders); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waiter, err := c.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.mut.Lock()
|
||||||
|
defer c.mut.Unlock()
|
||||||
|
var folder config.FolderConfiguration
|
||||||
|
if err := unmarshalTo(r.Body, &folder); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waiter, err := c.cfg.SetFolder(folder)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.finish(w, waiter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
c.mut.Lock()
|
||||||
|
defer c.mut.Unlock()
|
||||||
|
var devices []config.DeviceConfiguration
|
||||||
|
if err := unmarshalTo(r.Body, &devices); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waiter, err := c.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.mut.Lock()
|
||||||
|
defer c.mut.Unlock()
|
||||||
|
var device config.DeviceConfiguration
|
||||||
|
if err := unmarshalTo(r.Body, &device); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waiter, err := c.cfg.SetDevice(device)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.finish(w, waiter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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, config.FolderConfiguration{})
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
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, config.DeviceConfiguration{})
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Handle(http.MethodDelete, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
|
||||||
|
id, err := protocol.DeviceIDFromString(p.ByName("id"))
|
||||||
|
waiter, err := c.cfg.RemoveDevice(id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.finish(w, waiter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
c.adjustOptions(w, r, config.OptionsConfiguration{})
|
||||||
|
})
|
||||||
|
|
||||||
|
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) {
|
||||||
|
c.adjustLDAP(w, r, config.LDAPConfiguration{})
|
||||||
|
})
|
||||||
|
|
||||||
|
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) {
|
||||||
|
c.adjustGUI(w, r, config.GUIConfiguration{})
|
||||||
|
})
|
||||||
|
|
||||||
|
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) {
|
||||||
|
c.mut.Lock()
|
||||||
|
defer c.mut.Unlock()
|
||||||
|
cfg, 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
|
||||||
|
}
|
||||||
|
if cfg.GUI.Password, err = checkGUIPassword(c.cfg.GUI().Password, cfg.GUI.Password); err != nil {
|
||||||
|
l.Warnln("bcrypting password:", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waiter, err := c.cfg.Replace(cfg)
|
||||||
|
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) {
|
||||||
|
c.mut.Lock()
|
||||||
|
defer c.mut.Unlock()
|
||||||
|
if err := unmarshalTo(r.Body, &folder); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waiter, err := c.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) {
|
||||||
|
c.mut.Lock()
|
||||||
|
defer c.mut.Unlock()
|
||||||
|
if err := unmarshalTo(r.Body, &device); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waiter, err := c.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) {
|
||||||
|
c.mut.Lock()
|
||||||
|
defer c.mut.Unlock()
|
||||||
|
if err := unmarshalTo(r.Body, &opts); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waiter, err := c.cfg.SetOptions(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) {
|
||||||
|
c.mut.Lock()
|
||||||
|
defer c.mut.Unlock()
|
||||||
|
oldPassword := gui.Password
|
||||||
|
err := unmarshalTo(r.Body, &gui)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if gui.Password, err = checkGUIPassword(oldPassword, gui.Password); err != nil {
|
||||||
|
l.Warnln("bcrypting password:", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waiter, err := c.cfg.SetGUI(gui)
|
||||||
|
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) {
|
||||||
|
c.mut.Lock()
|
||||||
|
defer c.mut.Unlock()
|
||||||
|
if err := unmarshalTo(r.Body, &ldap); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waiter, err := c.cfg.SetLDAP(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 checkGUIPassword(oldPassword, newPassword string) (string, error) {
|
||||||
|
if newPassword == oldPassword {
|
||||||
|
return newPassword, nil
|
||||||
|
}
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), 0)
|
||||||
|
return string(hash), 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)
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,10 @@ func (c *mockedConfig) LDAP() config.LDAPConfiguration {
|
|||||||
return config.LDAPConfiguration{}
|
return config.LDAPConfiguration{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *mockedConfig) SetLDAP(config.LDAPConfiguration) (config.Waiter, error) {
|
||||||
|
return noopWaiter{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *mockedConfig) RawCopy() config.Configuration {
|
func (c *mockedConfig) RawCopy() config.Configuration {
|
||||||
cfg := config.Configuration{}
|
cfg := config.Configuration{}
|
||||||
util.SetDefaults(&cfg.Options)
|
util.SetDefaults(&cfg.Options)
|
||||||
@ -54,6 +58,10 @@ func (c *mockedConfig) Devices() map[protocol.DeviceID]config.DeviceConfiguratio
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *mockedConfig) DeviceList() []config.DeviceConfiguration {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *mockedConfig) SetDevice(config.DeviceConfiguration) (config.Waiter, error) {
|
func (c *mockedConfig) SetDevice(config.DeviceConfiguration) (config.Waiter, error) {
|
||||||
return noopWaiter{}, nil
|
return noopWaiter{}, nil
|
||||||
}
|
}
|
||||||
@ -102,6 +110,10 @@ func (c *mockedConfig) SetFolders(folders []config.FolderConfiguration) (config.
|
|||||||
return noopWaiter{}, nil
|
return noopWaiter{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *mockedConfig) RemoveFolder(id string) (config.Waiter, error) {
|
||||||
|
return noopWaiter{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *mockedConfig) Device(id protocol.DeviceID) (config.DeviceConfiguration, bool) {
|
func (c *mockedConfig) Device(id protocol.DeviceID) (config.DeviceConfiguration, bool) {
|
||||||
return config.DeviceConfiguration{}, false
|
return config.DeviceConfiguration{}, false
|
||||||
}
|
}
|
||||||
|
@ -304,7 +304,9 @@ func (cfg *Configuration) clean() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade configuration versions as appropriate
|
// Upgrade configuration versions as appropriate
|
||||||
|
migrationsMut.Lock()
|
||||||
migrations.apply(cfg)
|
migrations.apply(cfg)
|
||||||
|
migrationsMut.Unlock()
|
||||||
|
|
||||||
// Build a list of available devices
|
// Build a list of available devices
|
||||||
existingDevices := make(map[protocol.DeviceID]bool)
|
existingDevices := make(map[protocol.DeviceID]bool)
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/fs"
|
"github.com/syncthing/syncthing/lib/fs"
|
||||||
"github.com/syncthing/syncthing/lib/upgrade"
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
@ -24,30 +25,33 @@ import (
|
|||||||
// config version. The conversion function can be nil in which case we just
|
// config version. The conversion function can be nil in which case we just
|
||||||
// update the config version. The order of migrations doesn't matter here,
|
// update the config version. The order of migrations doesn't matter here,
|
||||||
// put the newest on top for readability.
|
// put the newest on top for readability.
|
||||||
var migrations = migrationSet{
|
var (
|
||||||
{32, migrateToConfigV32},
|
migrations = migrationSet{
|
||||||
{31, migrateToConfigV31},
|
{32, migrateToConfigV32},
|
||||||
{30, migrateToConfigV30},
|
{31, migrateToConfigV31},
|
||||||
{29, migrateToConfigV29},
|
{30, migrateToConfigV30},
|
||||||
{28, migrateToConfigV28},
|
{29, migrateToConfigV29},
|
||||||
{27, migrateToConfigV27},
|
{28, migrateToConfigV28},
|
||||||
{26, nil}, // triggers database update
|
{27, migrateToConfigV27},
|
||||||
{25, migrateToConfigV25},
|
{26, nil}, // triggers database update
|
||||||
{24, migrateToConfigV24},
|
{25, migrateToConfigV25},
|
||||||
{23, migrateToConfigV23},
|
{24, migrateToConfigV24},
|
||||||
{22, migrateToConfigV22},
|
{23, migrateToConfigV23},
|
||||||
{21, migrateToConfigV21},
|
{22, migrateToConfigV22},
|
||||||
{20, migrateToConfigV20},
|
{21, migrateToConfigV21},
|
||||||
{19, nil}, // Triggers a database tweak
|
{20, migrateToConfigV20},
|
||||||
{18, migrateToConfigV18},
|
{19, nil}, // Triggers a database tweak
|
||||||
{17, nil}, // Fsync = true removed
|
{18, migrateToConfigV18},
|
||||||
{16, nil}, // Triggers a database tweak
|
{17, nil}, // Fsync = true removed
|
||||||
{15, migrateToConfigV15},
|
{16, nil}, // Triggers a database tweak
|
||||||
{14, migrateToConfigV14},
|
{15, migrateToConfigV15},
|
||||||
{13, migrateToConfigV13},
|
{14, migrateToConfigV14},
|
||||||
{12, migrateToConfigV12},
|
{13, migrateToConfigV13},
|
||||||
{11, migrateToConfigV11},
|
{12, migrateToConfigV12},
|
||||||
}
|
{11, migrateToConfigV11},
|
||||||
|
}
|
||||||
|
migrationsMut = sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
type migrationSet []migration
|
type migrationSet []migration
|
||||||
|
|
||||||
|
@ -26,7 +26,9 @@ func TestMigrateCrashReporting(t *testing.T) {
|
|||||||
|
|
||||||
for i, tc := range cases {
|
for i, tc := range cases {
|
||||||
cfg := Configuration{Version: 28, Options: tc.opts}
|
cfg := Configuration{Version: 28, Options: tc.opts}
|
||||||
|
migrationsMut.Lock()
|
||||||
migrations.apply(&cfg)
|
migrations.apply(&cfg)
|
||||||
|
migrationsMut.Unlock()
|
||||||
if cfg.Options.CREnabled != tc.enabled {
|
if cfg.Options.CREnabled != tc.enabled {
|
||||||
t.Errorf("%d: unexpected result, CREnabled: %v != %v", i, cfg.Options.CREnabled, tc.enabled)
|
t.Errorf("%d: unexpected result, CREnabled: %v != %v", i, cfg.Options.CREnabled, tc.enabled)
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,7 @@ type Wrapper interface {
|
|||||||
GUI() GUIConfiguration
|
GUI() GUIConfiguration
|
||||||
SetGUI(gui GUIConfiguration) (Waiter, error)
|
SetGUI(gui GUIConfiguration) (Waiter, error)
|
||||||
LDAP() LDAPConfiguration
|
LDAP() LDAPConfiguration
|
||||||
|
SetLDAP(ldap LDAPConfiguration) (Waiter, error)
|
||||||
|
|
||||||
Options() OptionsConfiguration
|
Options() OptionsConfiguration
|
||||||
SetOptions(opts OptionsConfiguration) (Waiter, error)
|
SetOptions(opts OptionsConfiguration) (Waiter, error)
|
||||||
@ -71,11 +72,13 @@ type Wrapper interface {
|
|||||||
Folder(id string) (FolderConfiguration, bool)
|
Folder(id string) (FolderConfiguration, bool)
|
||||||
Folders() map[string]FolderConfiguration
|
Folders() map[string]FolderConfiguration
|
||||||
FolderList() []FolderConfiguration
|
FolderList() []FolderConfiguration
|
||||||
|
RemoveFolder(id string) (Waiter, error)
|
||||||
SetFolder(fld FolderConfiguration) (Waiter, error)
|
SetFolder(fld FolderConfiguration) (Waiter, error)
|
||||||
SetFolders(folders []FolderConfiguration) (Waiter, error)
|
SetFolders(folders []FolderConfiguration) (Waiter, error)
|
||||||
|
|
||||||
Device(id protocol.DeviceID) (DeviceConfiguration, bool)
|
Device(id protocol.DeviceID) (DeviceConfiguration, bool)
|
||||||
Devices() map[protocol.DeviceID]DeviceConfiguration
|
Devices() map[protocol.DeviceID]DeviceConfiguration
|
||||||
|
DeviceList() []DeviceConfiguration
|
||||||
RemoveDevice(id protocol.DeviceID) (Waiter, error)
|
RemoveDevice(id protocol.DeviceID) (Waiter, error)
|
||||||
SetDevice(DeviceConfiguration) (Waiter, error)
|
SetDevice(DeviceConfiguration) (Waiter, error)
|
||||||
SetDevices([]DeviceConfiguration) (Waiter, error)
|
SetDevices([]DeviceConfiguration) (Waiter, error)
|
||||||
@ -230,6 +233,13 @@ func (w *wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
|||||||
return deviceMap
|
return deviceMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeviceList returns a slice of devices.
|
||||||
|
func (w *wrapper) DeviceList() []DeviceConfiguration {
|
||||||
|
w.mut.Lock()
|
||||||
|
defer w.mut.Unlock()
|
||||||
|
return w.cfg.Copy().Devices
|
||||||
|
}
|
||||||
|
|
||||||
// SetDevices adds new devices to the configuration, or overwrites existing
|
// SetDevices adds new devices to the configuration, or overwrites existing
|
||||||
// devices with the same ID.
|
// devices with the same ID.
|
||||||
func (w *wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
|
func (w *wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
|
||||||
@ -327,6 +337,22 @@ func (w *wrapper) SetFolders(folders []FolderConfiguration) (Waiter, error) {
|
|||||||
return w.replaceLocked(newCfg)
|
return w.replaceLocked(newCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveFolder removes the folder from the configuration
|
||||||
|
func (w *wrapper) RemoveFolder(id string) (Waiter, error) {
|
||||||
|
w.mut.Lock()
|
||||||
|
defer w.mut.Unlock()
|
||||||
|
|
||||||
|
newCfg := w.cfg.Copy()
|
||||||
|
for i := range newCfg.Folders {
|
||||||
|
if newCfg.Folders[i].ID == id {
|
||||||
|
newCfg.Folders = append(newCfg.Folders[:i], newCfg.Folders[i+1:]...)
|
||||||
|
return w.replaceLocked(newCfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return noopWaiter{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Options returns the current options configuration object.
|
// Options returns the current options configuration object.
|
||||||
func (w *wrapper) Options() OptionsConfiguration {
|
func (w *wrapper) Options() OptionsConfiguration {
|
||||||
w.mut.Lock()
|
w.mut.Lock()
|
||||||
@ -349,6 +375,14 @@ func (w *wrapper) LDAP() LDAPConfiguration {
|
|||||||
return w.cfg.LDAP.Copy()
|
return w.cfg.LDAP.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) SetLDAP(ldap LDAPConfiguration) (Waiter, error) {
|
||||||
|
w.mut.Lock()
|
||||||
|
defer w.mut.Unlock()
|
||||||
|
newCfg := w.cfg.Copy()
|
||||||
|
newCfg.LDAP = ldap.Copy()
|
||||||
|
return w.replaceLocked(newCfg)
|
||||||
|
}
|
||||||
|
|
||||||
// GUI returns the current GUI configuration object.
|
// GUI returns the current GUI configuration object.
|
||||||
func (w *wrapper) GUI() GUIConfiguration {
|
func (w *wrapper) GUI() GUIConfiguration {
|
||||||
w.mut.Lock()
|
w.mut.Lock()
|
||||||
|
@ -84,7 +84,7 @@ func (n DeviceID) Short() ShortID {
|
|||||||
return ShortID(binary.BigEndian.Uint64(n[:]))
|
return ShortID(binary.BigEndian.Uint64(n[:]))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *DeviceID) MarshalText() ([]byte, error) {
|
func (n DeviceID) MarshalText() ([]byte, error) {
|
||||||
return []byte(n.String()), nil
|
return []byte(n.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user