all: Implement suture v4-api (#6947)

This commit is contained in:
Simon Frei
2020-11-17 13:19:04 +01:00
committed by GitHub
parent e8fc465ea8
commit 9524b51708
65 changed files with 617 additions and 693 deletions
+38 -21
View File
@@ -33,7 +33,7 @@ import (
"github.com/julienschmidt/httprouter"
metrics "github.com/rcrowley/go-metrics"
"github.com/thejerf/suture"
"github.com/thejerf/suture/v4"
"github.com/vitrun/qart/qr"
"github.com/syncthing/syncthing/lib/build"
@@ -82,7 +82,6 @@ type service struct {
connectionsService connections.Service
fss model.FolderSummaryService
urService *ur.Service
contr Controller
noUpgrade bool
tlsDefaultCommonName string
configChanged chan struct{} // signals intentional listener close due to config change
@@ -90,25 +89,20 @@ type service struct {
startedOnce chan struct{} // the service has started successfully at least once
startupErr error
listenerAddr net.Addr
exitChan chan *util.FatalErr
guiErrors logger.Recorder
systemLog logger.Recorder
}
type Controller interface {
ExitUpgrading()
Restart()
Shutdown()
}
type Service interface {
suture.Service
config.Committer
WaitForStart() error
}
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, contr Controller, noUpgrade bool) Service {
s := &service{
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, noUpgrade bool) Service {
return &service{
id: id,
cfg: cfg,
statics: newStaticsServer(cfg.GUI().Theme, assetDir, cfg.Options().FeatureFlag(featureFlagUntrusted)),
@@ -125,14 +119,12 @@ func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonNam
urService: urService,
guiErrors: errors,
systemLog: systemLog,
contr: contr,
noUpgrade: noUpgrade,
tlsDefaultCommonName: tlsDefaultCommonName,
configChanged: make(chan struct{}),
startedOnce: make(chan struct{}),
exitChan: make(chan *util.FatalErr, 1),
}
s.Service = util.AsService(s.serve, s.String())
return s
}
func (s *service) WaitForStart() error {
@@ -211,7 +203,7 @@ func sendJSON(w http.ResponseWriter, jsonObject interface{}) {
fmt.Fprintf(w, "%s\n", bs)
}
func (s *service) serve(ctx context.Context) {
func (s *service) Serve(ctx context.Context) error {
listener, err := s.getListener(s.cfg.GUI())
if err != nil {
select {
@@ -227,13 +219,13 @@ func (s *service) serve(ctx context.Context) {
s.startupErr = err
close(s.startedOnce)
}
return
return err
}
if listener == nil {
// Not much we can do here other than exit quickly. The supervisor
// will log an error at some point.
return
return nil
}
s.listenerAddr = listener.Addr()
@@ -410,6 +402,7 @@ func (s *service) serve(ctx context.Context) {
// Wait for stop, restart or error signals
err = nil
select {
case <-ctx.Done():
// Shutting down permanently
@@ -417,11 +410,14 @@ func (s *service) serve(ctx context.Context) {
case <-s.configChanged:
// Soft restart due to configuration change
l.Debugln("restarting (config changed)")
case <-serveError:
case err = <-s.exitChan:
case err = <-serveError:
// Restart due to listen/serve failure
l.Warnln("GUI/API:", err, "(restarting)")
}
srv.Close()
return err
}
// Complete implements suture.IsCompletable, which signifies to the supervisor
@@ -470,6 +466,14 @@ func (s *service) CommitConfiguration(from, to config.Configuration) bool {
return true
}
func (s *service) fatal(err *util.FatalErr) {
// s.exitChan is 1-buffered and whoever is first gets handled.
select {
case s.exitChan <- err:
default:
}
}
func debugMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t0 := time.Now()
@@ -874,7 +878,11 @@ func (s *service) getDebugFile(w http.ResponseWriter, r *http.Request) {
func (s *service) postSystemRestart(w http.ResponseWriter, r *http.Request) {
s.flushResponse(`{"ok": "restarting"}`, w)
go s.contr.Restart()
s.fatal(&util.FatalErr{
Err: errors.New("restart initiated by rest API"),
Status: util.ExitRestart,
})
}
func (s *service) postSystemReset(w http.ResponseWriter, r *http.Request) {
@@ -900,12 +908,18 @@ func (s *service) postSystemReset(w http.ResponseWriter, r *http.Request) {
s.flushResponse(`{"ok": "resetting folder `+folder+`"}`, w)
}
go s.contr.Restart()
s.fatal(&util.FatalErr{
Err: errors.New("restart after db reset initiated by rest API"),
Status: util.ExitRestart,
})
}
func (s *service) postSystemShutdown(w http.ResponseWriter, r *http.Request) {
s.flushResponse(`{"ok": "shutting down"}`, w)
go s.contr.Shutdown()
s.fatal(&util.FatalErr{
Err: errors.New("shutdown initiated by rest API"),
Status: util.ExitSuccess,
})
}
func (s *service) flushResponse(resp string, w http.ResponseWriter) {
@@ -1340,7 +1354,10 @@ func (s *service) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
}
s.flushResponse(`{"ok": "restarting"}`, w)
s.contr.ExitUpgrading()
s.fatal(&util.FatalErr{
Err: errors.New("exit after upgrade initiated by rest API"),
Status: util.ExitUpgrade,
})
}
}
+40 -40
View File
@@ -9,6 +9,7 @@ package api
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
@@ -34,7 +35,8 @@ import (
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/ur"
"github.com/thejerf/suture"
"github.com/syncthing/syncthing/lib/util"
"github.com/thejerf/suture/v4"
)
var (
@@ -113,15 +115,14 @@ func TestStopAfterBrokenConfig(t *testing.T) {
}
w := config.Wrap("/dev/null", cfg, events.NoopLogger)
srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, false).(*service)
srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, false).(*service)
defer os.Remove(token)
srv.started = make(chan string)
sup := suture.New("test", suture.Spec{
PassThroughPanics: true,
})
sup := suture.New("test", util.Spec())
sup.Add(srv)
sup.ServeBackground()
ctx, cancel := context.WithCancel(context.Background())
sup.ServeBackground(ctx)
<-srv.started
@@ -139,9 +140,7 @@ func TestStopAfterBrokenConfig(t *testing.T) {
t.Fatal("Verify config should have failed")
}
// Nonetheless, it should be fine to Stop() it without panic.
sup.Stop()
cancel()
}
func TestAssetsDir(t *testing.T) {
@@ -250,11 +249,11 @@ func TestAPIServiceRequests(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, sup, err := startHTTP(cfg)
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer sup.Stop()
defer cancel()
cases := []httpTestCase{
// /rest/db
@@ -519,11 +518,11 @@ func TestHTTPLogin(t *testing.T) {
cfg := new(mockedConfig)
cfg.gui.User = "üser"
cfg.gui.Password = "$2a$10$IdIZTxTg/dCNuNEGlmLynOjqg4B1FvDKuIV5e0BB3pnWVHNb8.GSq" // bcrypt of "räksmörgås" in UTF-8
baseURL, sup, err := startHTTP(cfg)
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer sup.Stop()
defer cancel()
// Verify rejection when not using authorization
@@ -581,7 +580,7 @@ func TestHTTPLogin(t *testing.T) {
}
}
func startHTTP(cfg config.Wrapper) (string, *suture.Supervisor, error) {
func startHTTP(cfg config.Wrapper) (string, context.CancelFunc, error) {
m := new(mockedModel)
assetDir := "../../gui"
eventSub := new(mockedEventSub)
@@ -594,7 +593,7 @@ func startHTTP(cfg config.Wrapper) (string, *suture.Supervisor, error) {
// Instantiate the API service
urService := ur.New(cfg, m, connections, false)
svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, &mockedFolderSummaryService{}, errorLog, systemLog, nil, false).(*service)
svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, &mockedFolderSummaryService{}, errorLog, systemLog, false).(*service)
defer os.Remove(token)
svc.started = addrChan
@@ -603,14 +602,15 @@ func startHTTP(cfg config.Wrapper) (string, *suture.Supervisor, error) {
PassThroughPanics: true,
})
supervisor.Add(svc)
supervisor.ServeBackground()
ctx, cancel := context.WithCancel(context.Background())
supervisor.ServeBackground(ctx)
// Make sure the API service is listening, and get the URL to use.
addr := <-addrChan
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
supervisor.Stop()
return "", nil, fmt.Errorf("weird address from API service: %w", err)
cancel()
return "", cancel, fmt.Errorf("weird address from API service: %w", err)
}
host, _, _ := net.SplitHostPort(cfg.GUI().RawAddress)
@@ -619,7 +619,7 @@ func startHTTP(cfg config.Wrapper) (string, *suture.Supervisor, error) {
}
baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)))
return baseURL, supervisor, nil
return baseURL, cancel, nil
}
func TestCSRFRequired(t *testing.T) {
@@ -628,11 +628,11 @@ func TestCSRFRequired(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, sup, err := startHTTP(cfg)
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal("Unexpected error from getting base URL:", err)
}
defer sup.Stop()
defer cancel()
cli := &http.Client{
Timeout: time.Minute,
@@ -704,11 +704,11 @@ func TestRandomString(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, sup, err := startHTTP(cfg)
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer sup.Stop()
defer cancel()
cli := &http.Client{
Timeout: time.Second,
}
@@ -797,11 +797,11 @@ func testConfigPost(data io.Reader) (*http.Response, error) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, sup, err := startHTTP(cfg)
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
return nil, err
}
defer sup.Stop()
defer cancel()
cli := &http.Client{
Timeout: time.Second,
}
@@ -818,11 +818,11 @@ func TestHostCheck(t *testing.T) {
cfg := new(mockedConfig)
cfg.gui.RawAddress = "127.0.0.1:0"
baseURL, sup, err := startHTTP(cfg)
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer sup.Stop()
defer cancel()
// A normal HTTP get to the localhost-bound service should succeed
@@ -879,11 +879,11 @@ func TestHostCheck(t *testing.T) {
cfg = new(mockedConfig)
cfg.gui.RawAddress = "127.0.0.1:0"
cfg.gui.InsecureSkipHostCheck = true
baseURL, sup, err = startHTTP(cfg)
baseURL, cancel, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer sup.Stop()
defer cancel()
// A request with a suspicious Host header should be allowed
@@ -903,11 +903,11 @@ func TestHostCheck(t *testing.T) {
cfg = new(mockedConfig)
cfg.gui.RawAddress = "0.0.0.0:0"
cfg.gui.InsecureSkipHostCheck = true
baseURL, sup, err = startHTTP(cfg)
baseURL, cancel, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer sup.Stop()
defer cancel()
// A request with a suspicious Host header should be allowed
@@ -931,11 +931,11 @@ func TestHostCheck(t *testing.T) {
cfg = new(mockedConfig)
cfg.gui.RawAddress = "[::1]:0"
baseURL, sup, err = startHTTP(cfg)
baseURL, cancel, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer sup.Stop()
defer cancel()
// A normal HTTP get to the localhost-bound service should succeed
@@ -1026,11 +1026,11 @@ func TestAccessControlAllowOriginHeader(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, sup, err := startHTTP(cfg)
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer sup.Stop()
defer cancel()
cli := &http.Client{
Timeout: time.Second,
}
@@ -1057,11 +1057,11 @@ func TestOptionsRequest(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, sup, err := startHTTP(cfg)
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer sup.Stop()
defer cancel()
cli := &http.Client{
Timeout: time.Second,
}
@@ -1093,7 +1093,7 @@ func TestEventMasks(t *testing.T) {
cfg := new(mockedConfig)
defSub := new(mockedEventSub)
diskSub := new(mockedEventSub)
svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, false).(*service)
svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, false).(*service)
defer os.Remove(token)
if mask := svc.getEventMask(""); mask != DefaultEventMask {
@@ -1253,11 +1253,11 @@ func TestConfigChanges(t *testing.T) {
defer os.Remove(tmpFile.Name())
w := config.Wrap(tmpFile.Name(), cfg, events.NoopLogger)
tmpFile.Close()
baseURL, sup, err := startHTTP(w)
baseURL, cancel, err := startHTTP(w)
if err != nil {
t.Fatal("Unexpected error from getting base URL:", err)
}
defer sup.Stop()
defer cancel()
cli := &http.Client{
Timeout: time.Second,
+3 -3
View File
@@ -7,6 +7,8 @@
package api
import (
"context"
"github.com/syncthing/syncthing/lib/connections"
)
@@ -24,9 +26,7 @@ func (m *mockedConnections) NATType() string {
return ""
}
func (m *mockedConnections) Serve() {}
func (m *mockedConnections) Stop() {}
func (m *mockedConnections) Serve(ctx context.Context) error { return nil }
func (m *mockedConnections) ExternalAddresses() []string { return nil }
+1 -4
View File
@@ -17,13 +17,10 @@ type mockedCachingMux struct{}
// from suture.Service
func (m *mockedCachingMux) Serve() {
func (m *mockedCachingMux) Serve(ctx context.Context) error {
select {}
}
func (m *mockedCachingMux) Stop() {
}
// from events.Finder
func (m *mockedCachingMux) Lookup(ctx context.Context, deviceID protocol.DeviceID) (direct []string, err error) {
+3 -5
View File
@@ -7,6 +7,7 @@
package api
import (
"context"
"net"
"time"
@@ -124,8 +125,7 @@ func (m *mockedModel) WatchError(folder string) error {
return nil
}
func (m *mockedModel) Serve() {}
func (m *mockedModel) Stop() {}
func (m *mockedModel) Serve(ctx context.Context) error { return nil }
func (m *mockedModel) Index(deviceID protocol.DeviceID, folder string, files []protocol.FileInfo) error {
return nil
@@ -167,9 +167,7 @@ func (m *mockedModel) DBSnapshot(_ string) (*db.Snapshot, error) {
type mockedFolderSummaryService struct{}
func (m *mockedFolderSummaryService) Serve() {}
func (m *mockedFolderSummaryService) Stop() {}
func (m *mockedFolderSummaryService) Serve(context.Context) error { return nil }
func (m *mockedFolderSummaryService) Summary(folder string) (map[string]interface{}, error) {
return map[string]interface{}{"mocked": true}, nil