Files
kopia/cli/command_server_start.go
Jarek Kowalski f4347886b8 logging: simplified log levels (#954)
Removed Warning, Notify and Fatal:

* `Warning` => `Error` or `Info`
* `Notify` => `Info`
* `Fatal` was never used.

Note that --log-level=warning is still supported for backwards
compatibility, but it is the same as --log-level=error.

Co-authored-by: Julio López <julio+gh@kasten.io>
2021-04-09 07:27:35 -07:00

277 lines
8.6 KiB
Go

package cli
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"html"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"contrib.go.opencensus.io/exporter/prometheus"
"github.com/pkg/errors"
prom "github.com/prometheus/client_golang/prometheus"
htpasswd "github.com/tg123/go-htpasswd"
"github.com/kopia/kopia/internal/auth"
"github.com/kopia/kopia/internal/clock"
"github.com/kopia/kopia/internal/server"
"github.com/kopia/kopia/repo"
)
var (
serverStartCommand = serverCommands.Command("start", "Start Kopia server").Default()
serverStartHTMLPath = serverStartCommand.Flag("html", "Server the provided HTML at the root URL").ExistingDir()
serverStartUI = serverStartCommand.Flag("ui", "Start the server with HTML UI").Default("true").Bool()
serverStartLegacyRepositoryAPI = serverStartCommand.Flag("legacy-api", "Start the legacy server API").Default("true").Bool()
serverStartGRPC = serverStartCommand.Flag("grpc", "Start the GRPC server").Default("true").Bool()
serverStartRefreshInterval = serverStartCommand.Flag("refresh-interval", "Frequency for refreshing repository status").Default("300s").Duration()
serverStartInsecure = serverStartCommand.Flag("insecure", "Allow insecure configurations (do not use in production)").Hidden().Bool()
serverStartMaxConcurrency = serverStartCommand.Flag("max-concurrency", "Maximum number of server goroutines").Default("0").Int()
serverStartWithoutPassword = serverStartCommand.Flag("without-password", "Start the server without a password").Hidden().Bool()
serverStartRandomPassword = serverStartCommand.Flag("random-password", "Generate random password and print to stderr").Hidden().Bool()
serverStartHtpasswdFile = serverStartCommand.Flag("htpasswd-file", "Path to htpasswd file that contains allowed user@hostname entries").Hidden().ExistingFile()
serverAuthCookieSingingKey = serverStartCommand.Flag("auth-cookie-signing-key", "Force particular auth cookie signing key").Envar("KOPIA_AUTH_COOKIE_SIGNING_KEY").Hidden().String()
serverStartShutdownWhenStdinClosed = serverStartCommand.Flag("shutdown-on-stdin", "Shut down the server when stdin handle has closed.").Hidden().Bool()
)
func init() {
setupConnectOptions(serverStartCommand)
serverStartCommand.Action(maybeRepositoryAction(runServer, repositoryAccessMode{
mustBeConnected: false,
disableMaintenance: true, // server closes the repository so maintenance can't run.
}))
}
func runServer(ctx context.Context, rep repo.Repository) error {
authn, err := getAuthenticator(ctx)
if err != nil {
return errors.Wrap(err, "unable to initialize authentication")
}
srv, err := server.New(ctx, server.Options{
ConfigFile: repositoryConfigFileName(),
ConnectOptions: connectOptions(),
RefreshInterval: *serverStartRefreshInterval,
MaxConcurrency: *serverStartMaxConcurrency,
Authenticator: authn,
Authorizer: auth.DefaultAuthorizer(),
AuthCookieSigningKey: *serverAuthCookieSingingKey,
UIUser: *serverUsername,
})
if err != nil {
return errors.Wrap(err, "unable to initialize server")
}
if err = maybeAutoUpgradeRepository(ctx, rep); err != nil {
return errors.Wrap(err, "error upgrading repository")
}
if err = srv.SetRepository(ctx, rep); err != nil {
return errors.Wrap(err, "error connecting to repository")
}
mux := http.NewServeMux()
mux.Handle("/api/", srv.APIHandlers(*serverStartLegacyRepositoryAPI))
if *serverStartHTMLPath != "" {
fileServer := srv.RequireUIUserAuth(serveIndexFileForKnownUIRoutes(http.Dir(*serverStartHTMLPath)))
mux.Handle("/", fileServer)
} else if *serverStartUI {
mux.Handle("/", srv.RequireUIUserAuth(serveIndexFileForKnownUIRoutes(server.AssetFile())))
}
httpServer := &http.Server{Addr: stripProtocol(*serverAddress)}
srv.OnShutdown = httpServer.Shutdown
onCtrlC(func() {
log(ctx).Infof("Shutting down...")
if err = httpServer.Shutdown(ctx); err != nil {
log(ctx).Debugf("unable to shut down: %v", err)
}
})
// init prometheus after adding interceptors that require credentials, so that this
// handler can be called without auth
if err = initPrometheus(mux); err != nil {
return errors.Wrap(err, "error initializing Prometheus")
}
var handler http.Handler = mux
if *serverStartGRPC {
handler = srv.GRPCRouterHandler(handler)
}
httpServer.Handler = handler
if *serverStartShutdownWhenStdinClosed {
log(ctx).Infof("Server will close when stdin is closed...")
go func() {
// consume all stdin and close the server when it closes
ioutil.ReadAll(os.Stdin) //nolint:errcheck
log(ctx).Infof("Shutting down server...")
httpServer.Shutdown(ctx) //nolint:errcheck
}()
}
onExternalConfigReloadRequest(func() {
if rerr := srv.Refresh(ctx); rerr != nil {
log(ctx).Errorf("refresh failed: %v", rerr)
}
})
err = startServerWithOptionalTLS(ctx, httpServer)
if !errors.Is(err, http.ErrServerClosed) {
return err
}
return srv.SetRepository(ctx, nil)
}
func initPrometheus(mux *http.ServeMux) error {
reg := prom.NewRegistry()
if err := reg.Register(prom.NewProcessCollector(prom.ProcessCollectorOpts{})); err != nil {
return errors.Wrap(err, "error registering process collector")
}
if err := reg.Register(prom.NewGoCollector()); err != nil {
return errors.Wrap(err, "error registering go collector")
}
pe, err := prometheus.NewExporter(prometheus.Options{
Registry: reg,
})
if err != nil {
return errors.Wrap(err, "unable to initialize prometheus exporter")
}
mux.Handle("/metrics", pe)
return nil
}
func stripProtocol(addr string) string {
return strings.TrimPrefix(strings.TrimPrefix(addr, "https://"), "http://")
}
func isKnownUIRoute(path string) bool {
return strings.HasPrefix(path, "/snapshots") ||
strings.HasPrefix(path, "/policies") ||
strings.HasPrefix(path, "/tasks") ||
strings.HasPrefix(path, "/repo")
}
func patchIndexBytes(b []byte) []byte {
if prefix := os.Getenv("KOPIA_UI_TITLE_PREFIX"); prefix != "" {
b = bytes.ReplaceAll(b, []byte("<title>"), []byte("<title>"+html.EscapeString(prefix)))
}
return b
}
func maybeReadIndexBytes(fs http.FileSystem) []byte {
rootFile, err := fs.Open("index.html")
if err != nil {
return nil
}
defer rootFile.Close() //nolint:errcheck
rd, err := ioutil.ReadAll(rootFile)
if err != nil {
return nil
}
return rd
}
func serveIndexFileForKnownUIRoutes(fs http.FileSystem) http.Handler {
h := http.FileServer(fs)
// read bytes from 'index.html' and patch based on optional environment variables.
indexBytes := patchIndexBytes(maybeReadIndexBytes(fs))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isKnownUIRoute(r.URL.Path) {
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = "/"
r = r2
}
if r.URL.Path == "/" && indexBytes != nil {
fmt.Println("serving patched index")
http.ServeContent(w, r, "/", clock.Now(), bytes.NewReader(indexBytes))
return
}
h.ServeHTTP(w, r)
})
}
func getAuthenticator(ctx context.Context) (auth.Authenticator, error) {
var authenticators []auth.Authenticator
// handle passwords (UI and remote) from htpasswd file.
if *serverStartHtpasswdFile != "" {
f, err := htpasswd.New(*serverStartHtpasswdFile, htpasswd.DefaultSystems, nil)
if err != nil {
return nil, errors.Wrap(err, "error initializing htpasswd")
}
authenticators = append(authenticators, auth.AuthenticateHtpasswdFile(f))
}
// handle UI password (--without-password, --password or --random-password)
switch {
case *serverStartWithoutPassword:
if !*serverStartInsecure {
return nil, errors.Errorf("--without-password specified without --insecure, refusing to start server.")
}
return nil, nil
case *serverPassword != "":
authenticators = append(authenticators, auth.AuthenticateSingleUser(*serverUsername, *serverPassword))
case *serverStartRandomPassword:
// generate very long random one-time password
b := make([]byte, 32)
io.ReadFull(rand.Reader, b) //nolint:errcheck
randomPassword := hex.EncodeToString(b)
// print it to the stderr bypassing any log file so that the user or calling process can connect
fmt.Fprintln(os.Stderr, "SERVER PASSWORD:", randomPassword)
authenticators = append(authenticators, auth.AuthenticateSingleUser(*serverUsername, randomPassword))
}
log(ctx).Infof(`
Server will allow connections from users whose accounts are stored in the repository.
User accounts can be added using 'kopia server user add'.
`)
// handle user accounts stored in the repository
authenticators = append(authenticators, auth.AuthenticateRepositoryUsers())
return auth.CombineAuthenticators(authenticators...), nil
}