Skip to content

Commit

Permalink
Improve assets handler middleware (#15961)
Browse files Browse the repository at this point in the history
* Use route to serve assets but not middleware

* Fix build error with bindata tag

* convert path to absolute

* fix build

* reduce function stack

* Add tests for assets

* Remove test for assets because they are not generated

* Use a http function to serve assets

* Still use middleware to serve assets then less middleware stack for assets

* Move serveContent to original position

* remove unnecessary blank line change

* Fix bug for /assets* requests

* clean code

Co-authored-by: zeripath <art27@cantab.net>
  • Loading branch information
lunny and zeripath committed May 30, 2021
1 parent d79c8bc commit effad26
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 166 deletions.
2 changes: 2 additions & 0 deletions integrations/links_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func TestLinksNoLogin(t *testing.T) {
"/user2/repo1",
"/user2/repo1/projects",
"/user2/repo1/projects/1",
"/assets/img/404.png",
"/assets/img/500.png",
}

for _, link := range links {
Expand Down
9 changes: 4 additions & 5 deletions modules/public/dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ import (
"time"
)

// Static implements the static handler for serving assets.
func Static(opts *Options) func(next http.Handler) http.Handler {
return opts.staticHandler(opts.Directory)
func fileSystem(dir string) http.FileSystem {
return http.Dir(dir)
}

// ServeContent serve http content
func ServeContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
// serveContent serve http content
func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
http.ServeContent(w, req, fi.Name(), modtime, content)
}
177 changes: 67 additions & 110 deletions modules/public/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,85 +5,82 @@
package public

import (
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"

"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)

// Options represents the available options to configure the handler.
type Options struct {
Directory string
IndexFile string
SkipLogging bool
FileSystem http.FileSystem
Prefix string
CorsHandler func(http.Handler) http.Handler
}

// KnownPublicEntries list all direct children in the `public` directory
var KnownPublicEntries = []string{
"css",
"fonts",
"img",
"js",
"serviceworker.js",
"vendor",
}

// Custom implements the static handler for serving custom assets.
func Custom(opts *Options) func(next http.Handler) http.Handler {
return opts.staticHandler(path.Join(setting.CustomPath, "public"))
}

// staticFileSystem implements http.FileSystem interface.
type staticFileSystem struct {
dir *http.Dir
}

func newStaticFileSystem(directory string) staticFileSystem {
if !filepath.IsAbs(directory) {
directory = filepath.Join(setting.AppWorkPath, directory)
// AssetsHandler implements the static handler for serving custom or original assets.
func AssetsHandler(opts *Options) func(next http.Handler) http.Handler {
var custPath = filepath.Join(setting.CustomPath, "public")
if !filepath.IsAbs(custPath) {
custPath = filepath.Join(setting.AppWorkPath, custPath)
}
if !filepath.IsAbs(opts.Directory) {
opts.Directory = filepath.Join(setting.AppWorkPath, opts.Directory)
}
if !strings.HasSuffix(opts.Prefix, "/") {
opts.Prefix += "/"
}
dir := http.Dir(directory)
return staticFileSystem{&dir}
}

func (fs staticFileSystem) Open(name string) (http.File, error) {
return fs.dir.Open(name)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
if !strings.HasPrefix(req.URL.Path, opts.Prefix) {
next.ServeHTTP(resp, req)
return
}
if req.Method != "GET" && req.Method != "HEAD" {
resp.WriteHeader(http.StatusNotFound)
return
}

// StaticHandler sets up a new middleware for serving static files in the
func StaticHandler(dir string, opts *Options) func(next http.Handler) http.Handler {
return opts.staticHandler(dir)
}
file := req.URL.Path
file = file[len(opts.Prefix):]
if len(file) == 0 {
resp.WriteHeader(http.StatusNotFound)
return
}
if strings.Contains(file, "\\") {
resp.WriteHeader(http.StatusBadRequest)
return
}
file = "/" + file

var written bool
if opts.CorsHandler != nil {
written = true
opts.CorsHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
written = false
})).ServeHTTP(resp, req)
}
if written {
return
}

func (opts *Options) staticHandler(dir string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
// Defaults
if len(opts.IndexFile) == 0 {
opts.IndexFile = "index.html"
}
// Normalize the prefix if provided
if opts.Prefix != "" {
// Ensure we have a leading '/'
if opts.Prefix[0] != '/' {
opts.Prefix = "/" + opts.Prefix
// custom files
if opts.handle(resp, req, http.Dir(custPath), file) {
return
}
// Remove any trailing '/'
opts.Prefix = strings.TrimRight(opts.Prefix, "/")
}
if opts.FileSystem == nil {
opts.FileSystem = newStaticFileSystem(dir)
}

return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if !opts.handle(w, req, opts) {
next.ServeHTTP(w, req)
// internal files
if opts.handle(resp, req, fileSystem(opts.Directory), file) {
return
}

resp.WriteHeader(http.StatusNotFound)
})
}
}
Expand All @@ -98,76 +95,36 @@ func parseAcceptEncoding(val string) map[string]bool {
return types
}

func (opts *Options) handle(w http.ResponseWriter, req *http.Request, opt *Options) bool {
if req.Method != "GET" && req.Method != "HEAD" {
return false
}

file := req.URL.Path
// if we have a prefix, filter requests by stripping the prefix
if opt.Prefix != "" {
if !strings.HasPrefix(file, opt.Prefix) {
return false
}
file = file[len(opt.Prefix):]
if file != "" && file[0] != '/' {
return false
}
}

f, err := opt.FileSystem.Open(file)
func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool {
// use clean to keep the file is a valid path with no . or ..
f, err := fs.Open(path.Clean(file))
if err != nil {
// 404 requests to any known entries in `public`
if path.Base(opts.Directory) == "public" {
parts := strings.Split(file, "/")
if len(parts) < 2 {
return false
}
for _, entry := range KnownPublicEntries {
if entry == parts[1] {
w.WriteHeader(404)
return true
}
}
if os.IsNotExist(err) {
return false
}
return false
w.WriteHeader(http.StatusInternalServerError)
log.Error("[Static] Open %q failed: %v", file, err)
return true
}
defer f.Close()

fi, err := f.Stat()
if err != nil {
log.Printf("[Static] %q exists, but fails to open: %v", file, err)
w.WriteHeader(http.StatusInternalServerError)
log.Error("[Static] %q exists, but fails to open: %v", file, err)
return true
}

// Try to serve index file
if fi.IsDir() {
// Redirect if missing trailing slash.
if !strings.HasSuffix(req.URL.Path, "/") {
http.Redirect(w, req, path.Clean(req.URL.Path+"/"), http.StatusFound)
return true
}

f, err = opt.FileSystem.Open(file)
if err != nil {
return false // Discard error.
}
defer f.Close()

fi, err = f.Stat()
if err != nil || fi.IsDir() {
return false
}
}

if !opt.SkipLogging {
log.Println("[Static] Serving " + file)
w.WriteHeader(http.StatusNotFound)
return true
}

if httpcache.HandleFileETagCache(req, w, fi) {
return true
}

ServeContent(w, req, fi, fi.ModTime(), f)
serveContent(w, req, fi, fi.ModTime(), f)
return true
}
14 changes: 5 additions & 9 deletions modules/public/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,8 @@ import (
"code.gitea.io/gitea/modules/log"
)

// Static implements the static handler for serving assets.
func Static(opts *Options) func(next http.Handler) http.Handler {
opts.FileSystem = Assets
// we don't need to pass the directory, because the directory var is only
// used when in the options there is no FileSystem.
return opts.staticHandler("")
func fileSystem(dir string) http.FileSystem {
return Assets
}

func Asset(name string) ([]byte, error) {
Expand Down Expand Up @@ -59,8 +55,8 @@ func AssetIsDir(name string) (bool, error) {
}
}

// ServeContent serve http content
func ServeContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
// serveContent serve http content
func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding"))
if encodings["gzip"] {
if cf, ok := fi.(*vfsgen۰CompressedFileInfo); ok {
Expand All @@ -76,7 +72,7 @@ func ServeContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modt
_, err := rd.Seek(0, io.SeekStart) // rewind to output whole file
if err != nil {
log.Error("rd.Seek error: %v", err)
http.Error(w, http.StatusText(500), 500)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
Expand Down
19 changes: 5 additions & 14 deletions routers/routes/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ func InstallRoutes() *web.Route {
r.Use(middle)
}

r.Use(public.AssetsHandler(&public.Options{
Directory: path.Join(setting.StaticRootPath, "public"),
Prefix: "/assets",
}))

r.Use(session.Sessioner(session.Options{
Provider: setting.SessionConfig.Provider,
ProviderConfig: setting.SessionConfig.ProviderConfig,
Expand All @@ -93,20 +98,6 @@ func InstallRoutes() *web.Route {
}))

r.Use(installRecovery())

r.Use(public.Custom(
&public.Options{
SkipLogging: setting.DisableRouterLog,
},
))
r.Use(public.Static(
&public.Options{
Directory: path.Join(setting.StaticRootPath, "public"),
SkipLogging: setting.DisableRouterLog,
Prefix: "/assets",
},
))

r.Use(routers.InstallInit)
r.Get("/", routers.Install)
r.Post("/", web.Bind(forms.InstallForm{}), routers.InstallPost)
Expand Down

0 comments on commit effad26

Please sign in to comment.