diff --git a/changelog/unreleased/enhancement-web-cache-control.md b/changelog/unreleased/enhancement-web-cache-control.md new file mode 100644 index 00000000000..ff64ff0b9e7 --- /dev/null +++ b/changelog/unreleased/enhancement-web-cache-control.md @@ -0,0 +1,6 @@ +Enhancement: Re-Enabling web cache control + +We've re-enable browser caching headers (`Expires` and `Last-Modified`) for the web service, this was disabled due to a problem in the fileserver used before. +Since we're now using our own fileserver implementation this works again and is enabled by default. + +https://github.com/owncloud/ocis/pull/3109 diff --git a/changelog/unreleased/enhancement-web-spa-fileserver.md b/changelog/unreleased/enhancement-web-spa-fileserver.md new file mode 100644 index 00000000000..dad95f59eec --- /dev/null +++ b/changelog/unreleased/enhancement-web-spa-fileserver.md @@ -0,0 +1,6 @@ +Enhancement: Add SPA conform fileserver for web + +We've added an SPA conform fileserver to the web service. +It enables web to use vue's history mode and behaves like nginx try_files. + +https://github.com/owncloud/ocis/pull/3109 diff --git a/storage/pkg/command/groups.go b/storage/pkg/command/groups.go index b7195143142..57d1d38998f 100644 --- a/storage/pkg/command/groups.go +++ b/storage/pkg/command/groups.go @@ -32,6 +32,7 @@ func Groups(cfg *config.Config) *cli.Command { tracing.Configure(cfg, logger) gr := run.Group{} ctx, cancel := context.WithCancel(context.Background()) + defer cancel() // pre-create folders if cfg.Reva.Groups.Driver == "json" && cfg.Reva.Groups.JSON != "" { @@ -40,9 +41,8 @@ func Groups(cfg *config.Config) *cli.Command { } } - uuid := uuid.Must(uuid.NewV4()) - pidFile := path.Join(os.TempDir(), "revad-"+c.Command.Name+"-"+uuid.String()+".pid") - defer cancel() + cuuid := uuid.Must(uuid.NewV4()) + pidFile := path.Join(os.TempDir(), "revad-"+c.Command.Name+"-"+cuuid.String()+".pid") rcfg := groupsConfigFromStruct(c, cfg) diff --git a/web/pkg/assets/server.go b/web/pkg/assets/server.go new file mode 100644 index 00000000000..527134330c8 --- /dev/null +++ b/web/pkg/assets/server.go @@ -0,0 +1,80 @@ +package assets + +import ( + "bytes" + "golang.org/x/net/html" + "io" + "mime" + "net/http" + "path" + "path/filepath" +) + +type fileServer struct { + root http.FileSystem +} + +func FileServer(root http.FileSystem) http.Handler { + return &fileServer{root} +} + +func (f *fileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + upath := path.Clean(path.Join("/", r.URL.Path)) + r.URL.Path = upath + + asset, err := f.root.Open(upath) + if err != nil { + r.URL.Path = "/index.html" + f.ServeHTTP(w, r) + return + } + defer asset.Close() + + s, _ := asset.Stat() + if s.IsDir() { + r.URL.Path = "/index.html" + f.ServeHTTP(w, r) + return + } + + w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(s.Name()))) + + buf := new(bytes.Buffer) + + switch s.Name() { + case "index.html", "oidc-callback.html", "oidc-silent-redirect.html": + _ = withBase(buf, asset, "/") + default: + _, _ = buf.ReadFrom(asset) + } + + _, _ = w.Write(buf.Bytes()) +} + +func withBase(w io.Writer, r io.Reader, base string) error { + doc, _ := html.Parse(r) + var parse func(*html.Node) + parse = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "head" { + n.InsertBefore(&html.Node{ + Type: html.ElementNode, + Data: "base", + Attr: []html.Attribute{ + { + Key: "href", + Val: base, + }, + }, + }, n.FirstChild) + + return + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + parse(c) + } + } + parse(doc) + + return html.Render(w, doc) +} diff --git a/web/pkg/service/v0/service.go b/web/pkg/service/v0/service.go index 49b78d14454..d7a92c3ad5f 100644 --- a/web/pkg/service/v0/service.go +++ b/web/pkg/service/v0/service.go @@ -2,10 +2,12 @@ package svc import ( "encoding/json" + "fmt" "io/ioutil" "net/http" "net/url" "os" + "strconv" "strings" "time" @@ -141,28 +143,19 @@ func (p Web) Static(ttl int) http.HandlerFunc { if !strings.HasSuffix(rootWithSlash, "/") { rootWithSlash = rootWithSlash + "/" } - assets := assets.New( - assets.Logger(p.logger), - assets.Config(p.config), - ) - - notFoundFunc := func(w http.ResponseWriter, r *http.Request) { - // TODO: replace the redirect with a not found page containing a link to the Web UI - http.Redirect(w, r, rootWithSlash, http.StatusTemporaryRedirect) - } static := http.StripPrefix( rootWithSlash, - interceptNotFound( - http.FileServer(assets), - notFoundFunc, + assets.FileServer( + assets.New( + assets.Logger(p.logger), + assets.Config(p.config), + ), ), ) - // TODO: investigate broken caching - https://github.com/owncloud/ocis/issues/1094 - // we don't have a last modification date of the static assets, so we use the service start date - //lastModified := time.Now().UTC().Format(http.TimeFormat) - //expires := time.Now().Add(time.Second * time.Duration(ttl)).UTC().Format(http.TimeFormat) + lastModified := time.Now().UTC().Format(http.TimeFormat) + expires := time.Now().Add(time.Second * time.Duration(ttl)).UTC().Format(http.TimeFormat) return func(w http.ResponseWriter, r *http.Request) { if rootWithSlash != "/" && r.URL.Path == p.config.HTTP.Root { @@ -175,49 +168,11 @@ func (p Web) Static(ttl int) http.HandlerFunc { return } - if r.URL.Path != rootWithSlash && strings.HasSuffix(r.URL.Path, "/") { - notFoundFunc(w, r) - return - } - - // TODO: investigate broken caching - https://github.com/owncloud/ocis/issues/1094 - //w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s, must-revalidate", strconv.Itoa(ttl))) - //w.Header().Set("Expires", expires) - //w.Header().Set("Last-Modified", lastModified) - w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") - w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") - w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) + w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s, must-revalidate", strconv.Itoa(ttl))) + w.Header().Set("Expires", expires) + w.Header().Set("Last-Modified", lastModified) w.Header().Set("SameSite", "Strict") static.ServeHTTP(w, r) } } - -func interceptNotFound(h http.Handler, notFoundFunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - notFoundInterceptor := &NotFoundInterceptor{ResponseWriter: w} - h.ServeHTTP(notFoundInterceptor, r) - if notFoundInterceptor.status == http.StatusNotFound { - notFoundFunc(w, r) - } - } -} - -type NotFoundInterceptor struct { - http.ResponseWriter - status int -} - -func (w *NotFoundInterceptor) WriteHeader(status int) { - w.status = status - if status != http.StatusNotFound { - w.ResponseWriter.WriteHeader(status) - } -} - -func (w *NotFoundInterceptor) Write(p []byte) (int, error) { - if w.status != http.StatusNotFound { - return w.ResponseWriter.Write(p) - } - return len(p), nil -}