Files
kopia/cli/command_server_start.go
Jarek Kowalski 675bf4e033 Removed manifest manager refresh + server improvements (#835)
* manifest: removed explicit refresh

Instead, content manager is exposing a revision counter that changes
on each mutation or index change. Manifest manager will be invalidated
whenever this is encountered.

* server: refactored initialization API

* server: added unit tests for repository server APIs (HTTP and REST)

* server: ensure we don't upload contents that already exist

This saves bandwidth, since the client can compute hash locally
and ask the server whether the object exists before starting the upload.
2021-02-15 23:55:58 -08:00

263 lines
7.9 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()
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()
serverStartAllowRepoUsers = serverStartCommand.Flag("allow-repository-users", "Allow users defined in the repository to connect").Bool()
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 := getAuthenticatorFunc(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.LegacyAuthorizerForUser,
})
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 := serveIndexFileForKnownUIRoutes(http.Dir(*serverStartHTMLPath))
mux.Handle("/", fileServer)
} else if *serverStartUI {
mux.Handle("/", 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).Warningf("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
}()
}
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 getAuthenticatorFunc(ctx context.Context) (auth.Authenticator, error) {
switch {
case *serverStartHtpasswdFile != "":
f, err := htpasswd.New(*serverStartHtpasswdFile, htpasswd.DefaultSystems, nil)
if err != nil {
return nil, errors.Wrap(err, "error initializing htpasswd")
}
// f.Match happens to match auth.Authenticator
return func(ctx context.Context, rep repo.Repository, username, password string) bool {
return f.Match(username, password)
}, nil
case *serverPassword != "":
return auth.AuthenticateSingleUser(*serverUsername, *serverPassword), nil
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)
return auth.AuthenticateSingleUser(*serverUsername, randomPassword), nil
case *serverStartAllowRepoUsers:
log(ctx).Noticef(`
Server will allow connections from users whose accounts are stored in the repository.
User accounts can be added using 'kopia user add'.
`)
return auth.AuthenticateRepositoryUsers(), nil
default:
if !*serverStartInsecure {
return nil, errors.Errorf("no password option specified, refusing to start server. To start non-authenticated server pass --insecure.")
}
return nil, nil
}
}