mirror of
https://github.com/kopia/kopia.git
synced 2026-05-24 22:54:55 -04:00
server: moved serving of static files to internal/server package (#1637)
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -23,7 +20,6 @@
|
||||
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"
|
||||
)
|
||||
@@ -112,6 +108,7 @@ func (c *commandServerStart) setup(svc advancedAppServices, parent commandParent
|
||||
}))
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func (c *commandServerStart) run(ctx context.Context, rep repo.Repository) error {
|
||||
authn, err := c.getAuthenticator(ctx)
|
||||
if err != nil {
|
||||
@@ -123,7 +120,7 @@ func (c *commandServerStart) run(ctx context.Context, rep repo.Repository) error
|
||||
uiPreferencesFile = filepath.Join(filepath.Dir(c.svc.repositoryConfigFileName()), "ui-preferences.json")
|
||||
}
|
||||
|
||||
srv, err := server.New(ctx, server.Options{
|
||||
srv, err := server.New(ctx, &server.Options{
|
||||
ConfigFile: c.svc.repositoryConfigFileName(),
|
||||
ConnectOptions: c.co.toRepoConnectOptions(),
|
||||
RefreshInterval: c.serverStartRefreshInterval,
|
||||
@@ -135,6 +132,7 @@ func (c *commandServerStart) run(ctx context.Context, rep repo.Repository) error
|
||||
LogRequests: c.logServerRequests,
|
||||
PasswordPersist: c.svc.passwordPersistenceStrategy(),
|
||||
UIPreferencesFile: uiPreferencesFile,
|
||||
UITitlePrefix: c.uiTitlePrefix,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to initialize server")
|
||||
@@ -153,10 +151,10 @@ func (c *commandServerStart) run(ctx context.Context, rep repo.Repository) error
|
||||
mux.Handle("/api/", srv.APIHandlers(c.serverStartLegacyRepositoryAPI))
|
||||
|
||||
if c.serverStartHTMLPath != "" {
|
||||
fileServer := srv.RequireUIUserAuth(c.serveIndexFileForKnownUIRoutes(http.Dir(c.serverStartHTMLPath)))
|
||||
fileServer := srv.ServeStaticFiles(http.Dir(c.serverStartHTMLPath))
|
||||
mux.Handle("/", fileServer)
|
||||
} else if c.serverStartUI {
|
||||
mux.Handle("/", srv.RequireUIUserAuth(c.serveIndexFileForKnownUIRoutes(server.AssetFile())))
|
||||
mux.Handle("/", srv.ServeStaticFiles(server.AssetFile()))
|
||||
}
|
||||
|
||||
httpServer := &http.Server{
|
||||
@@ -241,62 +239,6 @@ func stripProtocol(addr string) string {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(addr, "https://"), "http://")
|
||||
}
|
||||
|
||||
func (c *commandServerStart) isKnownUIRoute(path string) bool {
|
||||
return strings.HasPrefix(path, "/snapshots") ||
|
||||
strings.HasPrefix(path, "/policies") ||
|
||||
strings.HasPrefix(path, "/tasks") ||
|
||||
strings.HasPrefix(path, "/repo")
|
||||
}
|
||||
|
||||
func (c *commandServerStart) patchIndexBytes(b []byte) []byte {
|
||||
if c.uiTitlePrefix != "" {
|
||||
b = bytes.ReplaceAll(b, []byte("<title>"), []byte("<title>"+html.EscapeString(c.uiTitlePrefix)))
|
||||
}
|
||||
|
||||
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 := io.ReadAll(rootFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return rd
|
||||
}
|
||||
|
||||
func (c *commandServerStart) serveIndexFileForKnownUIRoutes(fs http.FileSystem) http.Handler {
|
||||
h := http.FileServer(fs)
|
||||
|
||||
// read bytes from 'index.html' and patch based on optional environment variables.
|
||||
indexBytes := c.patchIndexBytes(maybeReadIndexBytes(fs))
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if c.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 {
|
||||
http.ServeContent(w, r, "/", clock.Now(), bytes.NewReader(indexBytes))
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *commandServerStart) getAuthenticator(ctx context.Context) (auth.Authenticator, error) {
|
||||
var authenticators []auth.Authenticator
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -81,7 +84,6 @@ func (s *Server) APIHandlers(legacyAPI bool) http.Handler {
|
||||
|
||||
// snapshots
|
||||
m.HandleFunc("/api/v1/snapshots", s.handleAPI(requireUIUser, s.handleSnapshotList)).Methods(http.MethodGet)
|
||||
|
||||
m.HandleFunc("/api/v1/policy", s.handleAPI(requireUIUser, s.handlePolicyGet)).Methods(http.MethodGet)
|
||||
m.HandleFunc("/api/v1/policy", s.handleAPI(requireUIUser, s.handlePolicyPut)).Methods(http.MethodPut)
|
||||
m.HandleFunc("/api/v1/policy", s.handleAPI(requireUIUser, s.handlePolicyDelete)).Methods(http.MethodDelete)
|
||||
@@ -255,18 +257,6 @@ func (s *Server) handleAPI(isAuthorized isAuthorizedFunc, f apiRequestFunc) http
|
||||
})
|
||||
}
|
||||
|
||||
// RequireUIUserAuth wraps the provided http.Handler to only allow UI user and return 403 otherwise.
|
||||
func (s *Server) RequireUIUserAuth(hf http.Handler) http.Handler {
|
||||
return s.requireAuth(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if !requireUIUser(s, r) {
|
||||
http.Error(rw, `UI Access denied. See https://github.com/kopia/kopia/issues/880#issuecomment-798421751 for more information.`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
hf.ServeHTTP(rw, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAPIPossiblyNotConnected(isAuthorized isAuthorizedFunc, f apiRequestFunc) http.HandlerFunc {
|
||||
return s.requireAuth(func(w http.ResponseWriter, r *http.Request) {
|
||||
// we must pre-read request body before acquiring the lock as it sometimes leads to deadlock
|
||||
@@ -659,24 +649,88 @@ func (s *Server) syncSourcesLocked(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) isKnownUIRoute(path string) bool {
|
||||
return strings.HasPrefix(path, "/snapshots") ||
|
||||
strings.HasPrefix(path, "/policies") ||
|
||||
strings.HasPrefix(path, "/tasks") ||
|
||||
strings.HasPrefix(path, "/repo")
|
||||
}
|
||||
|
||||
func (s *Server) patchIndexBytes(b []byte) []byte {
|
||||
if s.options.UITitlePrefix != "" {
|
||||
b = bytes.ReplaceAll(b, []byte("<title>"), []byte("<title>"+html.EscapeString(s.options.UITitlePrefix)))
|
||||
}
|
||||
|
||||
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 := io.ReadAll(rootFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return rd
|
||||
}
|
||||
|
||||
// ServeStaticFiles returns HTTP handler that serves static files and dynamically patches index.html to embed CSRF token, etc.
|
||||
func (s *Server) ServeStaticFiles(fs http.FileSystem) http.Handler {
|
||||
h := http.FileServer(fs)
|
||||
|
||||
// read bytes from 'index.html'.
|
||||
indexBytes := maybeReadIndexBytes(fs)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.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 !requireUIUser(s, r) {
|
||||
http.Error(w, `UI Access denied. See https://github.com/kopia/kopia/issues/880#issuecomment-798421751 for more information.`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == "/" && indexBytes != nil {
|
||||
http.ServeContent(w, r, "/", clock.Now(), bytes.NewReader(s.patchIndexBytes(indexBytes)))
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Options encompasses all API server options.
|
||||
type Options struct {
|
||||
ConfigFile string
|
||||
ConnectOptions *repo.ConnectOptions
|
||||
RefreshInterval time.Duration
|
||||
MaxConcurrency int
|
||||
Authenticator auth.Authenticator
|
||||
Authorizer auth.Authorizer
|
||||
PasswordPersist passwordpersist.Strategy
|
||||
AuthCookieSigningKey string
|
||||
LogRequests bool
|
||||
UIUser string // name of the user allowed to access the UI
|
||||
UIPreferencesFile string // name of the JSON file storing UI preferences
|
||||
ConfigFile string
|
||||
ConnectOptions *repo.ConnectOptions
|
||||
RefreshInterval time.Duration
|
||||
MaxConcurrency int
|
||||
Authenticator auth.Authenticator
|
||||
Authorizer auth.Authorizer
|
||||
PasswordPersist passwordpersist.Strategy
|
||||
AuthCookieSigningKey string
|
||||
LogRequests bool
|
||||
UIUser string // name of the user allowed to access the UI
|
||||
UIPreferencesFile string // name of the JSON file storing UI preferences
|
||||
DisableCSRFTokenChecks bool
|
||||
UITitlePrefix string
|
||||
}
|
||||
|
||||
// New creates a Server.
|
||||
// The server will manage sources for a given username@hostname.
|
||||
func New(ctx context.Context, options Options) (*Server, error) {
|
||||
func New(ctx context.Context, options *Options) (*Server, error) {
|
||||
if options.Authorizer == nil {
|
||||
return nil, errors.Errorf("missing authorizer")
|
||||
}
|
||||
@@ -692,7 +746,7 @@ func New(ctx context.Context, options Options) (*Server, error) {
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
options: options,
|
||||
options: *options,
|
||||
sourceManagers: map[snapshot.SourceInfo]*sourceManager{},
|
||||
uploadSemaphore: make(chan struct{}, 1),
|
||||
grpcServerState: makeGRPCServerState(options.MaxConcurrency),
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
func startServer(t *testing.T, env *repotesting.Environment, tls bool) *repo.APIServerInfo {
|
||||
ctx := testlogging.Context(t)
|
||||
|
||||
s, err := server.New(ctx, server.Options{
|
||||
s, err := server.New(ctx, &server.Options{
|
||||
ConfigFile: env.ConfigFile(),
|
||||
PasswordPersist: passwordpersist.File,
|
||||
Authorizer: auth.LegacyAuthorizer(),
|
||||
|
||||
Reference in New Issue
Block a user