diff --git a/cmd/syncthing/cmdutil/options_common.go b/cmd/syncthing/cmdutil/options_common.go new file mode 100644 index 000000000..34485e6a5 --- /dev/null +++ b/cmd/syncthing/cmdutil/options_common.go @@ -0,0 +1,15 @@ +// Copyright (C) 2021 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 cmdutil + +// CommonOptions are reused among several subcommands +type CommonOptions struct { + buildCommonOptions + ConfDir string `name:"config" placeholder:"PATH" help:"Set configuration directory (config and keys)"` + HomeDir string `name:"home" placeholder:"PATH" help:"Set configuration and data directory"` + NoDefaultFolder bool `env:"STNODEFAULTFOLDER" help:"Don't create the \"default\" folder on first startup"` +} diff --git a/cmd/syncthing/options_others.go b/cmd/syncthing/cmdutil/options_others.go similarity index 86% rename from cmd/syncthing/options_others.go rename to cmd/syncthing/cmdutil/options_others.go index b1f4764ca..e299ba48c 100644 --- a/cmd/syncthing/options_others.go +++ b/cmd/syncthing/cmdutil/options_others.go @@ -7,8 +7,8 @@ //go:build !windows // +build !windows -package main +package cmdutil -type buildServeOptions struct { +type buildCommonOptions struct { HideConsole bool `hidden:""` } diff --git a/cmd/syncthing/options_windows.go b/cmd/syncthing/cmdutil/options_windows.go similarity index 86% rename from cmd/syncthing/options_windows.go rename to cmd/syncthing/cmdutil/options_windows.go index 38d26a22a..99fb50d5b 100644 --- a/cmd/syncthing/options_windows.go +++ b/cmd/syncthing/cmdutil/options_windows.go @@ -4,8 +4,8 @@ // 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 +package cmdutil -type buildServeOptions struct { +type buildCommonOptions struct { HideConsole bool `name:"no-console" help:"Hide console window"` } diff --git a/cmd/syncthing/generate/generate.go b/cmd/syncthing/generate/generate.go new file mode 100644 index 000000000..8c5bd1323 --- /dev/null +++ b/cmd/syncthing/generate/generate.go @@ -0,0 +1,144 @@ +// Copyright (C) 2021 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 generate implements the `syncthing generate` subcommand. +package generate + +import ( + "bufio" + "context" + "crypto/tls" + "fmt" + "log" + "os" + + "github.com/syncthing/syncthing/cmd/syncthing/cmdutil" + "github.com/syncthing/syncthing/lib/config" + "github.com/syncthing/syncthing/lib/events" + "github.com/syncthing/syncthing/lib/fs" + "github.com/syncthing/syncthing/lib/locations" + "github.com/syncthing/syncthing/lib/osutil" + "github.com/syncthing/syncthing/lib/protocol" + "github.com/syncthing/syncthing/lib/syncthing" +) + +type CLI struct { + cmdutil.CommonOptions + GUIUser string `placeholder:"STRING" help:"Specify new GUI authentication user name"` + GUIPassword string `placeholder:"STRING" help:"Specify new GUI authentication password (use - to read from standard input)"` +} + +func (c *CLI) Run() error { + log.SetFlags(0) + + if c.HideConsole { + osutil.HideConsole() + } + + if c.HomeDir != "" { + if c.ConfDir != "" { + return fmt.Errorf("--home must not be used together with --config") + } + c.ConfDir = c.HomeDir + } + if c.ConfDir == "" { + c.ConfDir = locations.GetBaseDir(locations.ConfigBaseDir) + } + + // Support reading the password from a pipe or similar + if c.GUIPassword == "-" { + reader := bufio.NewReader(os.Stdin) + password, _, err := reader.ReadLine() + if err != nil { + return fmt.Errorf("Failed reading GUI password: %w", err) + } + c.GUIPassword = string(password) + } + + if err := Generate(c.ConfDir, c.GUIUser, c.GUIPassword, c.NoDefaultFolder); err != nil { + return fmt.Errorf("Failed to generate config and keys: %w", err) + } + return nil +} + +func Generate(confDir, guiUser, guiPassword string, noDefaultFolder bool) error { + dir, err := fs.ExpandTilde(confDir) + if err != nil { + return err + } + + if err := syncthing.EnsureDir(dir, 0700); err != nil { + return err + } + locations.SetBaseDir(locations.ConfigBaseDir, dir) + + var myID protocol.DeviceID + certFile, keyFile := locations.Get(locations.CertFile), locations.Get(locations.KeyFile) + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err == nil { + log.Println("WARNING: Key exists; will not overwrite.") + } else { + cert, err = syncthing.GenerateCertificate(certFile, keyFile) + if err != nil { + return fmt.Errorf("create certificate: %w", err) + } + } + myID = protocol.NewDeviceID(cert.Certificate[0]) + log.Println("Device ID:", myID) + + cfgFile := locations.Get(locations.ConfigFile) + var cfg config.Wrapper + if _, err := os.Stat(cfgFile); err == nil { + if guiUser == "" && guiPassword == "" { + log.Println("WARNING: Config exists; will not overwrite.") + return nil + } + + if cfg, _, err = config.Load(cfgFile, myID, events.NoopLogger); err != nil { + return fmt.Errorf("load config: %w", err) + } + } else { + if cfg, err = syncthing.DefaultConfig(cfgFile, myID, events.NoopLogger, noDefaultFolder); err != nil { + return fmt.Errorf("create config: %w", err) + } + } + + ctx, cancel := context.WithCancel(context.Background()) + go cfg.Serve(ctx) + defer cancel() + + var updateErr error + waiter, err := cfg.Modify(func(cfg *config.Configuration) { + updateErr = updateGUIAuthentication(&cfg.GUI, guiUser, guiPassword) + }) + if err != nil { + return fmt.Errorf("modify config: %w", err) + } + + waiter.Wait() + if updateErr != nil { + return updateErr + } + if err := cfg.Save(); err != nil { + return fmt.Errorf("save config: %w", err) + } + return nil +} + +func updateGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) error { + if guiUser != "" && guiCfg.User != guiUser { + guiCfg.User = guiUser + log.Println("Updated GUI authentication user name:", guiUser) + } + + if guiPassword != "" && guiCfg.Password != guiPassword { + if err := guiCfg.HashAndSetPassword(guiPassword); err != nil { + return fmt.Errorf("Failed to set GUI authentication password: %w", err) + } + log.Println("Updated GUI authentication password.") + } + return nil +} diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 21a8fbe6b..127fcc54f 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -36,6 +36,7 @@ import ( "github.com/syncthing/syncthing/cmd/syncthing/cli" "github.com/syncthing/syncthing/cmd/syncthing/cmdutil" "github.com/syncthing/syncthing/cmd/syncthing/decrypt" + "github.com/syncthing/syncthing/cmd/syncthing/generate" "github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/db" @@ -132,32 +133,30 @@ var ( // commands and options here are top level commands to syncthing. // Cli is just a placeholder for the help text (see main). var entrypoint struct { - Serve serveOptions `cmd:"" help:"Run Syncthing"` - Decrypt decrypt.CLI `cmd:"" help:"Decrypt or verify an encrypted folder"` - Cli struct{} `cmd:"" help:"Command line interface for Syncthing"` + Serve serveOptions `cmd:"" help:"Run Syncthing"` + Generate generate.CLI `cmd:"" help:"Generate key and config, then exit"` + Decrypt decrypt.CLI `cmd:"" help:"Decrypt or verify an encrypted folder"` + Cli struct{} `cmd:"" help:"Command line interface for Syncthing"` } // serveOptions are the options for the `syncthing serve` command. type serveOptions struct { - buildServeOptions + cmdutil.CommonOptions AllowNewerConfig bool `help:"Allow loading newer than current config version"` Audit bool `help:"Write events to audit file"` AuditFile string `name:"auditfile" placeholder:"PATH" help:"Specify audit file (use \"-\" for stdout, \"--\" for stderr)"` BrowserOnly bool `help:"Open GUI in browser"` - ConfDir string `name:"config" placeholder:"PATH" help:"Set configuration directory (config and keys)"` DataDir string `name:"data" placeholder:"PATH" help:"Set data directory (database and logs)"` DeviceID bool `help:"Show the device ID"` - GenerateDir string `name:"generate" placeholder:"PATH" help:"Generate key and config in specified dir, then exit"` + GenerateDir string `name:"generate" placeholder:"PATH" help:"Generate key and config in specified dir, then exit"` //DEPRECATED: replaced by subcommand! GUIAddress string `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"` GUIAPIKey string `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"` - HomeDir string `name:"home" placeholder:"PATH" help:"Set configuration and data directory"` LogFile string `name:"logfile" default:"${logFile}" placeholder:"PATH" help:"Log file name (see below)"` LogFlags int `name:"logflags" default:"${logFlags}" placeholder:"BITS" help:"Select information in log line prefix (see below)"` LogMaxFiles int `placeholder:"N" default:"${logMaxFiles}" name:"log-max-old-files" help:"Number of old files to keep (zero to keep only current)"` LogMaxSize int `placeholder:"BYTES" default:"${logMaxSize}" help:"Maximum size of any file (zero to disable log rotation)"` NoBrowser bool `help:"Do not start browser"` NoRestart bool `env:"STNORESTART" help:"Do not restart Syncthing when exiting due to API/GUI command, upgrade, or crash"` - NoDefaultFolder bool `env:"STNODEFAULTFOLDER" help:"Don't create the \"default\" folder on first startup"` NoUpgrade bool `env:"STNOUPGRADE" help:"Disable automatic upgrades"` Paths bool `help:"Show configuration paths"` Paused bool `help:"Start with all devices and folders paused"` @@ -339,7 +338,7 @@ func (options serveOptions) Run() error { } if options.GenerateDir != "" { - if err := generate(options.GenerateDir, options.NoDefaultFolder); err != nil { + if err := generate.Generate(options.GenerateDir, "", "", options.NoDefaultFolder); err != nil { l.Warnln("Failed to generate config and keys:", err) os.Exit(svcutil.ExitError.AsInt()) } @@ -347,7 +346,7 @@ func (options serveOptions) Run() error { } // Ensure that our home directory exists. - if err := ensureDir(locations.GetBaseDir(locations.ConfigBaseDir), 0700); err != nil { + if err := syncthing.EnsureDir(locations.GetBaseDir(locations.ConfigBaseDir), 0700); err != nil { l.Warnln("Failure on home directory:", err) os.Exit(svcutil.ExitError.AsInt()) } @@ -413,8 +412,8 @@ func openGUI(myID protocol.DeviceID) error { if err != nil { return err } - if cfg.GUI().Enabled { - if err := openURL(cfg.GUI().URL()); err != nil { + if guiCfg := cfg.GUI(); guiCfg.Enabled { + if err := openURL(guiCfg.URL()); err != nil { return err } } else { @@ -423,46 +422,6 @@ func openGUI(myID protocol.DeviceID) error { return nil } -func generate(generateDir string, noDefaultFolder bool) error { - dir, err := fs.ExpandTilde(generateDir) - if err != nil { - return err - } - - if err := ensureDir(dir, 0700); err != nil { - return err - } - - var myID protocol.DeviceID - certFile, keyFile := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem") - cert, err := tls.LoadX509KeyPair(certFile, keyFile) - if err == nil { - l.Warnln("Key exists; will not overwrite.") - } else { - cert, err = syncthing.GenerateCertificate(certFile, keyFile) - if err != nil { - return errors.Wrap(err, "create certificate") - } - } - myID = protocol.NewDeviceID(cert.Certificate[0]) - l.Infoln("Device ID:", myID) - - cfgFile := filepath.Join(dir, "config.xml") - if _, err := os.Stat(cfgFile); err == nil { - l.Warnln("Config exists; will not overwrite.") - return nil - } - cfg, err := syncthing.DefaultConfig(cfgFile, myID, events.NoopLogger, noDefaultFolder) - if err != nil { - return err - } - err = cfg.Save() - if err != nil { - return errors.Wrap(err, "save config") - } - return nil -} - func debugFacilities() string { facilities := l.Facilities() @@ -800,29 +759,6 @@ func resetDB() error { return os.RemoveAll(locations.Get(locations.Database)) } -func ensureDir(dir string, mode fs.FileMode) error { - fs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir) - err := fs.MkdirAll(".", mode) - if err != nil { - return err - } - - if fi, err := fs.Stat("."); err == nil { - // Apprently the stat may fail even though the mkdirall passed. If it - // does, we'll just assume things are in order and let other things - // fail (like loading or creating the config...). - currentMode := fi.Mode() & 0777 - if currentMode != mode { - err := fs.Chmod(".", mode) - // This can fail on crappy filesystems, nothing we can do about it. - if err != nil { - l.Warnln(err) - } - } - } - return nil -} - func standbyMonitor(app *syncthing.App, cfg config.Wrapper) { restartDelay := 60 * time.Second now := time.Now() diff --git a/lib/syncthing/utils.go b/lib/syncthing/utils.go index 468e94a04..eecb56394 100644 --- a/lib/syncthing/utils.go +++ b/lib/syncthing/utils.go @@ -24,6 +24,29 @@ import ( "github.com/syncthing/syncthing/lib/tlsutil" ) +func EnsureDir(dir string, mode fs.FileMode) error { + fs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir) + err := fs.MkdirAll(".", mode) + if err != nil { + return err + } + + if fi, err := fs.Stat("."); err == nil { + // Apprently the stat may fail even though the mkdirall passed. If it + // does, we'll just assume things are in order and let other things + // fail (like loading or creating the config...). + currentMode := fi.Mode() & 0777 + if currentMode != mode { + err := fs.Chmod(".", mode) + // This can fail on crappy filesystems, nothing we can do about it. + if err != nil { + l.Warnln(err) + } + } + } + return nil +} + func LoadOrGenerateCertificate(certFile, keyFile string) (tls.Certificate, error) { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil {