Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve assets handler middleware #15961

Merged
merged 15 commits into from
May 30, 2021
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))
zeripath marked this conversation as resolved.
Show resolved Hide resolved
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) {
lunny marked this conversation as resolved.
Show resolved Hide resolved
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
Loading