server: moved serving of static files to internal/server package (#1637)

This commit is contained in:
Jarek Kowalski
2022-01-01 13:07:47 -08:00
committed by GitHub
parent 4227de1a4b
commit c66b1c3e76
3 changed files with 86 additions and 90 deletions

View File

@@ -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

View File

@@ -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),

View File

@@ -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(),