// Copyright (C) 2014 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 main import ( "bytes" "crypto/tls" "encoding/json" "fmt" "io" "io/ioutil" "net" "net/http" "net/url" "os" "path/filepath" "reflect" "regexp" "runtime" "runtime/pprof" "sort" "strconv" "strings" "time" metrics "github.com/rcrowley/go-metrics" "github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/connections" "github.com/syncthing/syncthing/lib/db" "github.com/syncthing/syncthing/lib/discover" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/logger" "github.com/syncthing/syncthing/lib/model" "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/rand" "github.com/syncthing/syncthing/lib/stats" "github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/upgrade" "github.com/syncthing/syncthing/lib/versioner" "github.com/vitrun/qart/qr" "golang.org/x/crypto/bcrypt" ) var ( startTime = time.Now() // matches a bcrypt hash and not too much else bcryptExpr = regexp.MustCompile(`^\$2[aby]\$\d+\$.{50,}`) ) const ( defaultEventMask = events.AllEvents &^ events.LocalChangeDetected &^ events.RemoteChangeDetected diskEventMask = events.LocalChangeDetected | events.RemoteChangeDetected eventSubBufferSize = 1000 ) type apiService struct { id protocol.DeviceID cfg configIntf httpsCertFile string httpsKeyFile string statics *staticsServer model modelIntf eventSubs map[events.EventType]events.BufferedSubscription eventSubsMut sync.Mutex discoverer discover.CachingMux connectionsService connectionsIntf fss *folderSummaryService systemConfigMut sync.Mutex // serializes posts to /rest/system/config stop chan struct{} // signals intentional stop configChanged chan struct{} // signals intentional listener close due to config change started chan string // signals startup complete by sending the listener address, for testing only startedOnce chan struct{} // the service has started at least once startupErr error cpu rater guiErrors logger.Recorder systemLog logger.Recorder } type modelIntf interface { GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{} Completion(device protocol.DeviceID, folder string) model.FolderCompletion Override(folder string) Revert(folder string) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated NeedSize(folder string) db.Counts ConnectionStats() map[string]interface{} DeviceStatistics() map[string]stats.DeviceStatistics FolderStatistics() map[string]stats.FolderStatistics CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool) ResetFolder(folder string) Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []model.Availability GetIgnores(folder string) ([]string, []string, error) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) SetIgnores(folder string, content []string) error DelayScan(folder string, next time.Duration) ScanFolder(folder string) error ScanFolders() map[string]error ScanFolderSubdirs(folder string, subs []string) error BringToFront(folder, file string) Connection(deviceID protocol.DeviceID) (connections.Connection, bool) GlobalSize(folder string) db.Counts LocalSize(folder string) db.Counts ReceiveOnlyChangedSize(folder string) db.Counts CurrentSequence(folder string) (int64, bool) RemoteSequence(folder string) (int64, bool) State(folder string) (string, time.Time, error) UsageReportingStats(version int, preview bool) map[string]interface{} FolderErrors(folder string) ([]model.FileError, error) WatchError(folder string) error } type configIntf interface { GUI() config.GUIConfiguration LDAP() config.LDAPConfiguration RawCopy() config.Configuration Options() config.OptionsConfiguration Replace(cfg config.Configuration) (config.Waiter, error) Subscribe(c config.Committer) Folders() map[string]config.FolderConfiguration Devices() map[protocol.DeviceID]config.DeviceConfiguration SetDevice(config.DeviceConfiguration) (config.Waiter, error) SetDevices([]config.DeviceConfiguration) (config.Waiter, error) Save() error ListenAddresses() []string RequiresRestart() bool } type connectionsIntf interface { Status() map[string]interface{} NATType() string } type rater interface { Rate() float64 } func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, defaultSub, diskSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder, cpu rater) *apiService { service := &apiService{ id: id, cfg: cfg, httpsCertFile: httpsCertFile, httpsKeyFile: httpsKeyFile, statics: newStaticsServer(cfg.GUI().Theme, assetDir), model: m, eventSubs: map[events.EventType]events.BufferedSubscription{ defaultEventMask: defaultSub, diskEventMask: diskSub, }, eventSubsMut: sync.NewMutex(), discoverer: discoverer, connectionsService: connectionsService, systemConfigMut: sync.NewMutex(), stop: make(chan struct{}), configChanged: make(chan struct{}), startedOnce: make(chan struct{}), guiErrors: errors, systemLog: systemLog, cpu: cpu, } return service } func (s *apiService) WaitForStart() error { <-s.startedOnce return s.startupErr } func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) { cert, err := tls.LoadX509KeyPair(s.httpsCertFile, s.httpsKeyFile) if err != nil { l.Infoln("Loading HTTPS certificate:", err) l.Infoln("Creating new HTTPS certificate") // When generating the HTTPS certificate, use the system host name per // default. If that isn't available, use the "syncthing" default. var name string name, err = os.Hostname() if err != nil { name = tlsDefaultCommonName } cert, err = tlsutil.NewCertificate(s.httpsCertFile, s.httpsKeyFile, name) } if err != nil { return nil, err } tlsCfg := tlsutil.SecureDefault() tlsCfg.Certificates = []tls.Certificate{cert} if guiCfg.Network() == "unix" { // When listening on a UNIX socket we should unlink before bind, // lest we get a "bind: address already in use". We don't // particularly care if this succeeds or not. os.Remove(guiCfg.Address()) } rawListener, err := net.Listen(guiCfg.Network(), guiCfg.Address()) if err != nil { return nil, err } listener := &tlsutil.DowngradingListener{ Listener: rawListener, TLSConfig: tlsCfg, } return listener, nil } func sendJSON(w http.ResponseWriter, jsonObject interface{}) { w.Header().Set("Content-Type", "application/json; charset=utf-8") // Marshalling might fail, in which case we should return a 500 with the // actual error. bs, err := json.MarshalIndent(jsonObject, "", " ") if err != nil { // This Marshal() can't fail though. bs, _ = json.Marshal(map[string]string{"error": err.Error()}) http.Error(w, string(bs), http.StatusInternalServerError) return } fmt.Fprintf(w, "%s\n", bs) } func (s *apiService) Serve() { listener, err := s.getListener(s.cfg.GUI()) if err != nil { select { case <-s.startedOnce: // We let this be a loud user-visible warning as it may be the only // indication they get that the GUI won't be available. l.Warnln("Starting API/GUI:", err) default: // This is during initialization. A failure here should be fatal // as there will be no way for the user to communicate with us // otherwise anyway. s.startupErr = err close(s.startedOnce) } return } if listener == nil { // Not much we can do here other than exit quickly. The supervisor // will log an error at some point. return } defer listener.Close() // The GET handlers getRestMux := http.NewServeMux() getRestMux.HandleFunc("/rest/db/completion", s.getDBCompletion) // device folder getRestMux.HandleFunc("/rest/db/file", s.getDBFile) // folder file getRestMux.HandleFunc("/rest/db/ignores", s.getDBIgnores) // folder getRestMux.HandleFunc("/rest/db/need", s.getDBNeed) // folder [perpage] [page] getRestMux.HandleFunc("/rest/db/remoteneed", s.getDBRemoteNeed) // device folder [perpage] [page] getRestMux.HandleFunc("/rest/db/localchanged", s.getDBLocalChanged) // folder getRestMux.HandleFunc("/rest/db/status", s.getDBStatus) // folder getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels] getRestMux.HandleFunc("/rest/folder/versions", s.getFolderVersions) // folder getRestMux.HandleFunc("/rest/folder/errors", s.getFolderErrors) // folder getRestMux.HandleFunc("/rest/folder/pullerrors", s.getFolderErrors) // folder (deprecated) getRestMux.HandleFunc("/rest/events", s.getIndexEvents) // [since] [limit] [timeout] [events] getRestMux.HandleFunc("/rest/events/disk", s.getDiskEvents) // [since] [limit] [timeout] getRestMux.HandleFunc("/rest/stats/device", s.getDeviceStats) // - getRestMux.HandleFunc("/rest/stats/folder", s.getFolderStats) // - getRestMux.HandleFunc("/rest/svc/deviceid", s.getDeviceID) // id getRestMux.HandleFunc("/rest/svc/lang", s.getLang) // - getRestMux.HandleFunc("/rest/svc/report", s.getReport) // - getRestMux.HandleFunc("/rest/svc/random/string", s.getRandomString) // [length] getRestMux.HandleFunc("/rest/system/browse", s.getSystemBrowse) // current getRestMux.HandleFunc("/rest/system/config", s.getSystemConfig) // - getRestMux.HandleFunc("/rest/system/config/insync", s.getSystemConfigInsync) // - getRestMux.HandleFunc("/rest/system/connections", s.getSystemConnections) // - getRestMux.HandleFunc("/rest/system/discovery", s.getSystemDiscovery) // - getRestMux.HandleFunc("/rest/system/error", s.getSystemError) // - getRestMux.HandleFunc("/rest/system/ping", s.restPing) // - getRestMux.HandleFunc("/rest/system/status", s.getSystemStatus) // - getRestMux.HandleFunc("/rest/system/upgrade", s.getSystemUpgrade) // - getRestMux.HandleFunc("/rest/system/version", s.getSystemVersion) // - 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 postRestMux := http.NewServeMux() postRestMux.HandleFunc("/rest/db/prio", s.postDBPrio) // folder file [perpage] [page] postRestMux.HandleFunc("/rest/db/ignores", s.postDBIgnores) // folder postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder postRestMux.HandleFunc("/rest/db/revert", s.postDBRevert) // folder postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...] [delay] postRestMux.HandleFunc("/rest/folder/versions", s.postFolderVersionsRestore) // folder postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // - postRestMux.HandleFunc("/rest/system/ping", s.restPing) // - postRestMux.HandleFunc("/rest/system/reset", s.postSystemReset) // [folder] postRestMux.HandleFunc("/rest/system/restart", s.postSystemRestart) // - postRestMux.HandleFunc("/rest/system/shutdown", s.postSystemShutdown) // - postRestMux.HandleFunc("/rest/system/upgrade", s.postSystemUpgrade) // - postRestMux.HandleFunc("/rest/system/pause", s.makeDevicePauseHandler(true)) // [device] postRestMux.HandleFunc("/rest/system/resume", s.makeDevicePauseHandler(false)) // [device] postRestMux.HandleFunc("/rest/system/debug", s.postSystemDebug) // [enable] [disable] // Debug endpoints, not for general use debugMux := http.NewServeMux() debugMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion) debugMux.HandleFunc("/rest/debug/httpmetrics", s.getSystemHTTPMetrics) debugMux.HandleFunc("/rest/debug/cpuprof", s.getCPUProf) // duration debugMux.HandleFunc("/rest/debug/heapprof", s.getHeapProf) debugMux.HandleFunc("/rest/debug/support", s.getSupportBundle) getRestMux.Handle("/rest/debug/", s.whenDebugging(debugMux)) // A handler that splits requests between the two above and disables // caching restMux := noCacheMiddleware(metricsMiddleware(getPostHandler(getRestMux, postRestMux))) // The main routing handler mux := http.NewServeMux() mux.Handle("/rest/", restMux) mux.HandleFunc("/qr/", s.getQR) // Serve compiled in assets unless an asset directory was set (for development) mux.Handle("/", s.statics) // Handle the special meta.js path mux.HandleFunc("/meta.js", s.getJSMetadata) guiCfg := s.cfg.GUI() // Wrap everything in CSRF protection. The /rest prefix should be // protected, other requests will grant cookies. handler := csrfMiddleware(s.id.String()[:5], "/rest", guiCfg, mux) // Add our version and ID as a header to responses handler = withDetailsMiddleware(s.id, handler) // Wrap everything in basic auth, if user/password is set. if guiCfg.IsAuthEnabled() { handler = basicAuthAndSessionMiddleware("sessionid-"+s.id.String()[:5], guiCfg, s.cfg.LDAP(), handler) } // Redirect to HTTPS if we are supposed to if guiCfg.UseTLS() { handler = redirectToHTTPSMiddleware(handler) } // Add the CORS handling handler = corsMiddleware(handler, guiCfg.InsecureAllowFrameLoading) if addressIsLocalhost(guiCfg.Address()) && !guiCfg.InsecureSkipHostCheck { // Verify source host handler = localhostMiddleware(handler) } handler = debugMiddleware(handler) srv := http.Server{ Handler: handler, // ReadTimeout must be longer than SyncthingController $scope.refresh // interval to avoid HTTP keepalive/GUI refresh race. ReadTimeout: 15 * time.Second, } s.fss = newFolderSummaryService(s.cfg, s.model) defer s.fss.Stop() s.fss.ServeBackground() l.Infoln("GUI and API listening on", listener.Addr()) l.Infoln("Access the GUI via the following URL:", guiCfg.URL()) if s.started != nil { // only set when run by the tests s.started <- listener.Addr().String() } // Indicate successful initial startup, to ourselves and to interested // listeners (i.e. the thing that starts the browser). select { case <-s.startedOnce: default: close(s.startedOnce) } // Serve in the background serveError := make(chan error, 1) go func() { serveError <- srv.Serve(listener) }() // Wait for stop, restart or error signals select { case <-s.stop: // Shutting down permanently l.Debugln("shutting down (stop)") case <-s.configChanged: // Soft restart due to configuration change l.Debugln("restarting (config changed)") case <-serveError: // Restart due to listen/serve failure l.Warnln("GUI/API:", err, "(restarting)") } } // Complete implements suture.IsCompletable, which signifies to the supervisor // whether to stop restarting the service. func (s *apiService) Complete() bool { select { case <-s.startedOnce: return s.startupErr != nil case <-s.stop: return true default: } return false } func (s *apiService) Stop() { close(s.stop) } func (s *apiService) String() string { return fmt.Sprintf("apiService@%p", s) } func (s *apiService) VerifyConfiguration(from, to config.Configuration) error { if to.GUI.Network() != "tcp" { return nil } _, err := net.ResolveTCPAddr("tcp", to.GUI.Address()) return err } func (s *apiService) CommitConfiguration(from, to config.Configuration) bool { // No action required when this changes, so mask the fact that it changed at all. from.GUI.Debugging = to.GUI.Debugging if to.GUI == from.GUI { return true } if to.GUI.Theme != from.GUI.Theme { s.statics.setTheme(to.GUI.Theme) } // Tell the serve loop to restart s.configChanged <- struct{}{} 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 { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t0 := time.Now() h.ServeHTTP(w, r) if shouldDebugHTTP() { ms := 1000 * time.Since(t0).Seconds() // The variable `w` is most likely a *http.response, which we can't do // much with since it's a non exported type. We can however peek into // it with reflection to get at the status code and number of bytes // written. var status, written int64 if rw := reflect.Indirect(reflect.ValueOf(w)); rw.IsValid() && rw.Kind() == reflect.Struct { if rf := rw.FieldByName("status"); rf.IsValid() && rf.Kind() == reflect.Int { status = rf.Int() } if rf := rw.FieldByName("written"); rf.IsValid() && rf.Kind() == reflect.Int64 { written = rf.Int() } } httpl.Debugf("http: %s %q: status %d, %d bytes in %.02f ms", r.Method, r.URL.String(), status, written, ms) } }) } func corsMiddleware(next http.Handler, allowFrameLoading bool) http.Handler { // Handle CORS headers and CORS OPTIONS request. // CORS OPTIONS request are typically sent by browser during AJAX preflight // when the browser initiate a POST request. // // As the OPTIONS request is unauthorized, this handler must be the first // of the chain (hence added at the end). // // See https://www.w3.org/TR/cors/ for details. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Process OPTIONS requests if r.Method == "OPTIONS" { // Add a generous access-control-allow-origin header for CORS requests w.Header().Add("Access-Control-Allow-Origin", "*") // Only GET/POST Methods are supported w.Header().Set("Access-Control-Allow-Methods", "GET, POST") // Only these headers can be set w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-API-Key") // The request is meant to be cached 10 minutes w.Header().Set("Access-Control-Max-Age", "600") // Indicate that no content will be returned w.WriteHeader(204) return } // Other security related headers that should be present. // https://www.owasp.org/index.php/Security_Headers if !allowFrameLoading { // We don't want to be rendered in an