From 6ab513fa05ce2f31ad99eeb9ad235f6309f2be92 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 23 May 2021 21:49:35 +0800 Subject: [PATCH 01/13] Use route to serve assets but not middleware --- modules/public/dynamic.go | 9 ++- modules/public/public.go | 133 +++++++++++--------------------------- modules/public/static.go | 8 +-- routers/routes/install.go | 19 ++---- routers/routes/web.go | 48 +++++++------- 5 files changed, 74 insertions(+), 143 deletions(-) diff --git a/modules/public/dynamic.go b/modules/public/dynamic.go index a57b636369293..a8cf0d867e1c6 100644 --- a/modules/public/dynamic.go +++ b/modules/public/dynamic.go @@ -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) -} - // 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) } + +func fileSystem(dir string) http.FileSystem { + return http.Dir(dir) +} diff --git a/modules/public/public.go b/modules/public/public.go index c68f980352ab4..dfe9abed0d6d9 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -5,13 +5,13 @@ package public import ( - "log" "net/http" - "path" + "os" "path/filepath" "strings" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -24,70 +24,36 @@ type Options struct { Prefix string } -// 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) - } - dir := http.Dir(directory) - return staticFileSystem{&dir} -} - -func (fs staticFileSystem) Open(name string) (http.File, error) { - return fs.dir.Open(name) -} - -// 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) -} - -func (opts *Options) staticHandler(dir string) func(next http.Handler) http.Handler { +// Assets implements the static handler for serving custom or original assets. +func Assets(opts *Options) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { - // Defaults - if len(opts.IndexFile) == 0 { - opts.IndexFile = "index.html" + var custPath = filepath.Join(setting.CustomPath, "public") + if !filepath.IsAbs(custPath) { + custPath = filepath.Join(setting.AppWorkPath, custPath) } - // Normalize the prefix if provided - if opts.Prefix != "" { - // Ensure we have a leading '/' - if opts.Prefix[0] != '/' { - opts.Prefix = "/" + opts.Prefix + + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + // custom files + if opts.staticHandler(http.Dir(custPath), opts.Prefix)(resp, req) { + 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.staticHandler(fileSystem(opts.Directory), opts.Prefix)(resp, req) { + return } + + resp.WriteHeader(404) }) } } +func (opts *Options) staticHandler(fs http.FileSystem, prefix string) func(resp http.ResponseWriter, req *http.Request) bool { + return func(resp http.ResponseWriter, req *http.Request) bool { + return opts.handle(resp, req, fs, prefix) + } +} + // parseAcceptEncoding parse Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5 as compress methods func parseAcceptEncoding(val string) map[string]bool { parts := strings.Split(val, ";") @@ -98,70 +64,45 @@ func parseAcceptEncoding(val string) map[string]bool { return types } -func (opts *Options) handle(w http.ResponseWriter, req *http.Request, opt *Options) bool { +func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, prefix string) 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) { + if prefix != "" { + if !strings.HasPrefix(file, prefix) { return false } - file = file[len(opt.Prefix):] + file = file[len(prefix):] if file != "" && file[0] != '/' { return false } } - f, err := opt.FileSystem.Open(file) + f, err := fs.Open(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(500) + 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(500) + 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(404) + return true } if httpcache.HandleFileETagCache(req, w, fi) { diff --git a/modules/public/static.go b/modules/public/static.go index 36cfdbe44f32e..1f65870e996c2 100644 --- a/modules/public/static.go +++ b/modules/public/static.go @@ -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) { diff --git a/routers/routes/install.go b/routers/routes/install.go index 026d92b13ecec..d743f241f5b05 100644 --- a/routers/routes/install.go +++ b/routers/routes/install.go @@ -93,21 +93,14 @@ func InstallRoutes() *web.Route { })) r.Use(installRecovery()) + r.Use(routers.InstallInit) - 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.Route("/assets/*", "GET, HEAD", corsHandler, public.Assets(&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) r.NotFound(func(w http.ResponseWriter, req *http.Request) { diff --git a/routers/routes/web.go b/routers/routes/web.go index 008c745d6e090..05fc1b3b38519 100644 --- a/routers/routes/web.go +++ b/routers/routes/web.go @@ -113,6 +113,8 @@ func commonMiddlewares() []func(http.Handler) http.Handler { return handlers } +var corsHandler func(http.Handler) http.Handler + // NormalRoutes represents non install routes func NormalRoutes() *web.Route { r := web.NewRoute() @@ -120,6 +122,23 @@ func NormalRoutes() *web.Route { r.Use(middle) } + if setting.CORSConfig.Enabled { + corsHandler = cors.Handler(cors.Options{ + //Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option + AllowedOrigins: setting.CORSConfig.AllowDomain, + //setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option + AllowedMethods: setting.CORSConfig.Methods, + AllowCredentials: setting.CORSConfig.AllowCredentials, + MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), + }) + } else { + corsHandler = func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + next.ServeHTTP(resp, req) + }) + } + } + r.Mount("/", WebRoutes()) r.Mount("/api/v1", apiv1.Routes()) r.Mount("/api/internal", private.Routes()) @@ -143,21 +162,11 @@ func WebRoutes() *web.Route { routes.Use(Recovery()) - // TODO: we should consider if there is a way to mount these using r.Route as at present - // these two handlers mean that every request has to hit these "filesystems" twice - // before finally getting to the router. It allows them to override any matching router below. - routes.Use(public.Custom( - &public.Options{ - SkipLogging: setting.DisableRouterLog, - }, - )) - routes.Use(public.Static( - &public.Options{ - Directory: path.Join(setting.StaticRootPath, "public"), - SkipLogging: setting.DisableRouterLog, - Prefix: "/assets", - }, - )) + routes.Route("/assets/*", "GET, HEAD", corsHandler, public.Assets(&public.Options{ + Directory: path.Join(setting.StaticRootPath, "public"), + SkipLogging: setting.DisableRouterLog, + Prefix: "/assets", + })) // We use r.Route here over r.Use because this prevents requests that are not for avatars having to go through this additional handler routes.Route("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) @@ -349,14 +358,7 @@ func RegisterRoutes(m *web.Route) { }, ignSignInAndCsrf, reqSignIn) m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth) if setting.CORSConfig.Enabled { - m.Post("/login/oauth/access_token", cors.Handler(cors.Options{ - //Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option - AllowedOrigins: setting.CORSConfig.AllowDomain, - //setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option - AllowedMethods: setting.CORSConfig.Methods, - AllowCredentials: setting.CORSConfig.AllowCredentials, - MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), - }), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth) + m.Post("/login/oauth/access_token", corsHandler, bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth) } else { m.Post("/login/oauth/access_token", bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth) } From c2ff37b19b813d6ecbf788fd2328ed4846751db3 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 24 May 2021 09:17:45 +0800 Subject: [PATCH 02/13] Fix build error with bindata tag --- modules/public/dynamic.go | 10 +++++----- modules/public/public.go | 13 +++++-------- modules/public/static.go | 35 ++--------------------------------- routers/routes/install.go | 7 +++---- routers/routes/web.go | 7 +++---- 5 files changed, 18 insertions(+), 54 deletions(-) diff --git a/modules/public/dynamic.go b/modules/public/dynamic.go index a8cf0d867e1c6..0bfe38bc3f3f1 100644 --- a/modules/public/dynamic.go +++ b/modules/public/dynamic.go @@ -13,11 +13,11 @@ import ( "time" ) -// 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) -} - 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) { + http.ServeContent(w, req, fi.Name(), modtime, content) +} diff --git a/modules/public/public.go b/modules/public/public.go index dfe9abed0d6d9..b5390de00c671 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -17,15 +17,12 @@ import ( // Options represents the available options to configure the handler. type Options struct { - Directory string - IndexFile string - SkipLogging bool - FileSystem http.FileSystem - Prefix string + Directory string + Prefix string } -// Assets implements the static handler for serving custom or original assets. -func Assets(opts *Options) func(http.Handler) http.Handler { +// AssetsHandler implements the static handler for serving custom or original assets. +func AssetsHandler(opts *Options) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { var custPath = filepath.Join(setting.CustomPath, "public") if !filepath.IsAbs(custPath) { @@ -109,6 +106,6 @@ func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.Fi return true } - ServeContent(w, req, fi, fi.ModTime(), f) + serveContent(w, req, fi, fi.ModTime(), f) return true } diff --git a/modules/public/static.go b/modules/public/static.go index 1f65870e996c2..d81e4cf07d7e0 100644 --- a/modules/public/static.go +++ b/modules/public/static.go @@ -24,39 +24,8 @@ func fileSystem(dir string) http.FileSystem { return Assets } -func Asset(name string) ([]byte, error) { - f, err := Assets.Open("/" + name) - if err != nil { - return nil, err - } - defer f.Close() - return ioutil.ReadAll(f) -} - -func AssetNames() []string { - realFS := Assets.(vfsgen۰FS) - var results = make([]string, 0, len(realFS)) - for k := range realFS { - results = append(results, k[1:]) - } - return results -} - -func AssetIsDir(name string) (bool, error) { - if f, err := Assets.Open("/" + name); err != nil { - return false, err - } else { - defer f.Close() - if fi, err := f.Stat(); err != nil { - return false, err - } else { - return fi.IsDir(), nil - } - } -} - -// 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 { diff --git a/routers/routes/install.go b/routers/routes/install.go index d743f241f5b05..9d223bba49640 100644 --- a/routers/routes/install.go +++ b/routers/routes/install.go @@ -95,10 +95,9 @@ func InstallRoutes() *web.Route { r.Use(installRecovery()) r.Use(routers.InstallInit) - r.Route("/assets/*", "GET, HEAD", corsHandler, public.Assets(&public.Options{ - Directory: path.Join(setting.StaticRootPath, "public"), - SkipLogging: setting.DisableRouterLog, - Prefix: "/assets", + r.Route("/assets/*", "GET, HEAD", corsHandler, public.AssetsHandler(&public.Options{ + Directory: path.Join(setting.StaticRootPath, "public"), + Prefix: "/assets", })) r.Get("/", routers.Install) diff --git a/routers/routes/web.go b/routers/routes/web.go index 05fc1b3b38519..179e00df5d3d2 100644 --- a/routers/routes/web.go +++ b/routers/routes/web.go @@ -162,10 +162,9 @@ func WebRoutes() *web.Route { routes.Use(Recovery()) - routes.Route("/assets/*", "GET, HEAD", corsHandler, public.Assets(&public.Options{ - Directory: path.Join(setting.StaticRootPath, "public"), - SkipLogging: setting.DisableRouterLog, - Prefix: "/assets", + routes.Route("/assets/*", "GET, HEAD", corsHandler, public.AssetsHandler(&public.Options{ + Directory: path.Join(setting.StaticRootPath, "public"), + Prefix: "/assets", })) // We use r.Route here over r.Use because this prevents requests that are not for avatars having to go through this additional handler From 1baec0bf4bdad2c01724cd98d23ca60e585ac299 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 24 May 2021 09:19:37 +0800 Subject: [PATCH 03/13] convert path to absolute --- modules/public/public.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/public/public.go b/modules/public/public.go index b5390de00c671..362d446af8e57 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -29,6 +29,10 @@ func AssetsHandler(opts *Options) func(http.Handler) http.Handler { custPath = filepath.Join(setting.AppWorkPath, custPath) } + if !filepath.IsAbs(opts.Directory) { + opts.Directory = filepath.Join(setting.AppWorkPath, opts.Directory) + } + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { // custom files if opts.staticHandler(http.Dir(custPath), opts.Prefix)(resp, req) { From a38bfd68c8a6c1d05e9247a95e0a414069a2518d Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 24 May 2021 11:59:38 +0800 Subject: [PATCH 04/13] fix build --- modules/public/static.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/modules/public/static.go b/modules/public/static.go index d81e4cf07d7e0..e8dd42b568f20 100644 --- a/modules/public/static.go +++ b/modules/public/static.go @@ -54,3 +54,34 @@ func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modt http.ServeContent(w, req, fi.Name(), modtime, content) return } + +func Asset(name string) ([]byte, error) { + f, err := Assets.Open("/" + name) + if err != nil { + return nil, err + } + defer f.Close() + return ioutil.ReadAll(f) +} + +func AssetNames() []string { + realFS := Assets.(vfsgen۰FS) + var results = make([]string, 0, len(realFS)) + for k := range realFS { + results = append(results, k[1:]) + } + return results +} + +func AssetIsDir(name string) (bool, error) { + if f, err := Assets.Open("/" + name); err != nil { + return false, err + } else { + defer f.Close() + if fi, err := f.Stat(); err != nil { + return false, err + } else { + return fi.IsDir(), nil + } + } +} From 86b50069d2af1abe1668ad43bdde81f7b83f4c11 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 24 May 2021 12:32:09 +0800 Subject: [PATCH 05/13] reduce function stack --- modules/public/public.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/modules/public/public.go b/modules/public/public.go index 362d446af8e57..e67815ca5d8d1 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -35,12 +35,12 @@ func AssetsHandler(opts *Options) func(http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { // custom files - if opts.staticHandler(http.Dir(custPath), opts.Prefix)(resp, req) { + if opts.handle(resp, req, http.Dir(custPath), opts.Prefix) { return } // internal files - if opts.staticHandler(fileSystem(opts.Directory), opts.Prefix)(resp, req) { + if opts.handle(resp, req, fileSystem(opts.Directory), opts.Prefix) { return } @@ -49,12 +49,6 @@ func AssetsHandler(opts *Options) func(http.Handler) http.Handler { } } -func (opts *Options) staticHandler(fs http.FileSystem, prefix string) func(resp http.ResponseWriter, req *http.Request) bool { - return func(resp http.ResponseWriter, req *http.Request) bool { - return opts.handle(resp, req, fs, prefix) - } -} - // parseAcceptEncoding parse Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5 as compress methods func parseAcceptEncoding(val string) map[string]bool { parts := strings.Split(val, ";") From 389b91e2f3240bebcfeb9469705947ba90a0ab85 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 24 May 2021 12:42:36 +0800 Subject: [PATCH 06/13] Add tests for assets --- integrations/links_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/integrations/links_test.go b/integrations/links_test.go index 3b9c245fc37c1..a983da15283cf 100644 --- a/integrations/links_test.go +++ b/integrations/links_test.go @@ -35,6 +35,11 @@ func TestLinksNoLogin(t *testing.T) { "/user2/repo1", "/user2/repo1/projects", "/user2/repo1/projects/1", + "/assets/img/404.png", + "/assets/img/500.png", + "/assets/js/index.js", + "/assets/css/index.css", + "/assets/serviceworker.js", } for _, link := range links { From d175dba3fb19bc70d9866f044dd945e5a1aae588 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 24 May 2021 18:50:28 +0800 Subject: [PATCH 07/13] Remove test for assets because they are not generated --- integrations/links_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/integrations/links_test.go b/integrations/links_test.go index a983da15283cf..03229e10e1222 100644 --- a/integrations/links_test.go +++ b/integrations/links_test.go @@ -37,9 +37,6 @@ func TestLinksNoLogin(t *testing.T) { "/user2/repo1/projects/1", "/assets/img/404.png", "/assets/img/500.png", - "/assets/js/index.js", - "/assets/css/index.css", - "/assets/serviceworker.js", } for _, link := range links { From 0fba10663a06afc65aa90617114e94546a0c7c1c Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 29 May 2021 12:26:07 +0800 Subject: [PATCH 08/13] Use a http function to serve assets --- modules/public/public.go | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/modules/public/public.go b/modules/public/public.go index e67815ca5d8d1..45bcf6969a6cf 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -22,30 +22,28 @@ type Options struct { } // AssetsHandler implements the static handler for serving custom or original assets. -func AssetsHandler(opts *Options) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - var custPath = filepath.Join(setting.CustomPath, "public") - if !filepath.IsAbs(custPath) { - custPath = filepath.Join(setting.AppWorkPath, custPath) - } +func AssetsHandler(opts *Options) func(resp http.ResponseWriter, req *http.Request) { + 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 !filepath.IsAbs(opts.Directory) { + opts.Directory = filepath.Join(setting.AppWorkPath, opts.Directory) + } - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - // custom files - if opts.handle(resp, req, http.Dir(custPath), opts.Prefix) { - return - } + return func(resp http.ResponseWriter, req *http.Request) { + // custom files + if opts.handle(resp, req, http.Dir(custPath), opts.Prefix) { + return + } - // internal files - if opts.handle(resp, req, fileSystem(opts.Directory), opts.Prefix) { - return - } + // internal files + if opts.handle(resp, req, fileSystem(opts.Directory), opts.Prefix) { + return + } - resp.WriteHeader(404) - }) + resp.WriteHeader(404) } } From b58830f31d01819435add240d48236016586d707 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 29 May 2021 19:10:56 +0800 Subject: [PATCH 09/13] Still use middleware to serve assets then less middleware stack for assets --- modules/public/public.go | 95 ++++++++++++++++++++++++--------------- modules/public/static.go | 2 +- routers/routes/install.go | 10 ++--- routers/routes/web.go | 21 ++++----- 4 files changed, 73 insertions(+), 55 deletions(-) diff --git a/modules/public/public.go b/modules/public/public.go index 45bcf6969a6cf..0abaaf64d1196 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -7,6 +7,7 @@ package public import ( "net/http" "os" + "path" "path/filepath" "strings" @@ -17,12 +18,13 @@ import ( // Options represents the available options to configure the handler. type Options struct { - Directory string - Prefix string + Directory string + Prefix string + CorsHandler func(http.Handler) http.Handler } // AssetsHandler implements the static handler for serving custom or original assets. -func AssetsHandler(opts *Options) func(resp http.ResponseWriter, req *http.Request) { +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) @@ -31,19 +33,55 @@ func AssetsHandler(opts *Options) func(resp http.ResponseWriter, req *http.Reque if !filepath.IsAbs(opts.Directory) { opts.Directory = filepath.Join(setting.AppWorkPath, opts.Directory) } + if opts.Prefix == "" { + opts.Prefix = "/" + } - return func(resp http.ResponseWriter, req *http.Request) { - // custom files - if opts.handle(resp, req, http.Dir(custPath), opts.Prefix) { - return - } - - // internal files - if opts.handle(resp, req, fileSystem(opts.Directory), opts.Prefix) { - return - } - - resp.WriteHeader(404) + 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 + } + + file := req.URL.Path + file = file[len(opts.Prefix):] + if len(file) == 0 { + resp.WriteHeader(http.StatusNotFound) + return + } + if !strings.HasPrefix(file, "/") { + next.ServeHTTP(resp, req) + return + } + + 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 + } + + // custom files + if opts.handle(resp, req, http.Dir(custPath), file) { + return + } + + // internal files + if opts.handle(resp, req, fileSystem(opts.Directory), file) { + return + } + + resp.WriteHeader(http.StatusNotFound) + }) } } @@ -57,29 +95,14 @@ func parseAcceptEncoding(val string) map[string]bool { return types } -func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, prefix string) 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 prefix != "" { - if !strings.HasPrefix(file, prefix) { - return false - } - file = file[len(prefix):] - if file != "" && file[0] != '/' { - return false - } - } - - f, err := fs.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 { if os.IsNotExist(err) { return false } - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) log.Error("[Static] Open %q failed: %v", file, err) return true } @@ -87,14 +110,14 @@ func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.Fi fi, err := f.Stat() if err != nil { - w.WriteHeader(500) + 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() { - w.WriteHeader(404) + w.WriteHeader(http.StatusNotFound) return true } diff --git a/modules/public/static.go b/modules/public/static.go index e8dd42b568f20..9020dcb83b86c 100644 --- a/modules/public/static.go +++ b/modules/public/static.go @@ -41,7 +41,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 } } diff --git a/routers/routes/install.go b/routers/routes/install.go index 9d223bba49640..7e6d5540a1835 100644 --- a/routers/routes/install.go +++ b/routers/routes/install.go @@ -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, @@ -95,11 +100,6 @@ func InstallRoutes() *web.Route { r.Use(installRecovery()) r.Use(routers.InstallInit) - r.Route("/assets/*", "GET, HEAD", corsHandler, public.AssetsHandler(&public.Options{ - Directory: path.Join(setting.StaticRootPath, "public"), - Prefix: "/assets", - })) - r.Get("/", routers.Install) r.Post("/", web.Bind(forms.InstallForm{}), routers.InstallPost) r.NotFound(func(w http.ResponseWriter, req *http.Request) { diff --git a/routers/routes/web.go b/routers/routes/web.go index 179e00df5d3d2..cc65ad6d9fcdb 100644 --- a/routers/routes/web.go +++ b/routers/routes/web.go @@ -133,9 +133,7 @@ func NormalRoutes() *web.Route { }) } else { corsHandler = func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - next.ServeHTTP(resp, req) - }) + return next } } @@ -149,6 +147,12 @@ func NormalRoutes() *web.Route { func WebRoutes() *web.Route { routes := web.NewRoute() + routes.Use(public.AssetsHandler(&public.Options{ + Directory: path.Join(setting.StaticRootPath, "public"), + Prefix: "/assets", + CorsHandler: corsHandler, + })) + routes.Use(session.Sessioner(session.Options{ Provider: setting.SessionConfig.Provider, ProviderConfig: setting.SessionConfig.ProviderConfig, @@ -162,11 +166,6 @@ func WebRoutes() *web.Route { routes.Use(Recovery()) - routes.Route("/assets/*", "GET, HEAD", corsHandler, public.AssetsHandler(&public.Options{ - Directory: path.Join(setting.StaticRootPath, "public"), - Prefix: "/assets", - })) - // We use r.Route here over r.Use because this prevents requests that are not for avatars having to go through this additional handler routes.Route("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) routes.Route("/repo-avatars/*", "GET, HEAD", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars)) @@ -356,11 +355,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth) }, ignSignInAndCsrf, reqSignIn) m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth) - if setting.CORSConfig.Enabled { - m.Post("/login/oauth/access_token", corsHandler, bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth) - } else { - m.Post("/login/oauth/access_token", bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth) - } + m.Post("/login/oauth/access_token", corsHandler, bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth) m.Group("/user/settings", func() { m.Get("", userSetting.Profile) From 83830bf8eea6d8b5751fac05400078d3da2a1a99 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 29 May 2021 19:17:19 +0800 Subject: [PATCH 10/13] Move serveContent to original position --- modules/public/static.go | 62 ++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/modules/public/static.go b/modules/public/static.go index 9020dcb83b86c..827dc2a1e0e8d 100644 --- a/modules/public/static.go +++ b/modules/public/static.go @@ -24,37 +24,6 @@ func fileSystem(dir string) http.FileSystem { return Assets } -// 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 { - rd := bytes.NewReader(cf.GzipBytes()) - w.Header().Set("Content-Encoding", "gzip") - ctype := mime.TypeByExtension(filepath.Ext(fi.Name())) - if ctype == "" { - // read a chunk to decide between utf-8 text and binary - var buf [512]byte - grd, _ := gzip.NewReader(rd) - n, _ := io.ReadFull(grd, buf[:]) - ctype = http.DetectContentType(buf[:n]) - _, 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(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - } - w.Header().Set("Content-Type", ctype) - http.ServeContent(w, req, fi.Name(), modtime, rd) - return - } - } - - http.ServeContent(w, req, fi.Name(), modtime, content) - return -} - func Asset(name string) ([]byte, error) { f, err := Assets.Open("/" + name) if err != nil { @@ -85,3 +54,34 @@ 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) { + encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding")) + if encodings["gzip"] { + if cf, ok := fi.(*vfsgen۰CompressedFileInfo); ok { + rd := bytes.NewReader(cf.GzipBytes()) + w.Header().Set("Content-Encoding", "gzip") + ctype := mime.TypeByExtension(filepath.Ext(fi.Name())) + if ctype == "" { + // read a chunk to decide between utf-8 text and binary + var buf [512]byte + grd, _ := gzip.NewReader(rd) + n, _ := io.ReadFull(grd, buf[:]) + ctype = http.DetectContentType(buf[:n]) + _, 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(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + } + w.Header().Set("Content-Type", ctype) + http.ServeContent(w, req, fi.Name(), modtime, rd) + return + } + } + + http.ServeContent(w, req, fi.Name(), modtime, content) + return +} From 20e21503963e8e913ac6af222bd5ee47f5a3d439 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 29 May 2021 19:20:12 +0800 Subject: [PATCH 11/13] remove unnecessary blank line change --- routers/routes/install.go | 1 - 1 file changed, 1 deletion(-) diff --git a/routers/routes/install.go b/routers/routes/install.go index 7e6d5540a1835..18e74f005fa6d 100644 --- a/routers/routes/install.go +++ b/routers/routes/install.go @@ -99,7 +99,6 @@ func InstallRoutes() *web.Route { r.Use(installRecovery()) r.Use(routers.InstallInit) - r.Get("/", routers.Install) r.Post("/", web.Bind(forms.InstallForm{}), routers.InstallPost) r.NotFound(func(w http.ResponseWriter, req *http.Request) { From c07ef992afffc7f5beba66d055cc4088e24f52de Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 29 May 2021 19:34:00 +0800 Subject: [PATCH 12/13] Fix bug for /assets* requests --- modules/public/public.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/public/public.go b/modules/public/public.go index 0abaaf64d1196..9a07b795111b6 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -36,6 +36,9 @@ func AssetsHandler(opts *Options) func(next http.Handler) http.Handler { if opts.Prefix == "" { opts.Prefix = "/" } + if opts.Prefix != "/" { + opts.Prefix = opts.Prefix + "/" + } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { @@ -54,10 +57,11 @@ func AssetsHandler(opts *Options) func(next http.Handler) http.Handler { resp.WriteHeader(http.StatusNotFound) return } - if !strings.HasPrefix(file, "/") { - next.ServeHTTP(resp, req) + if strings.Contains(file, "\\") { + resp.WriteHeader(http.StatusBadRequest) return } + file = "/" + file var written bool if opts.CorsHandler != nil { From 3c4d22140ac57d2156ce5fc2c08545cc3d2885ec Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 29 May 2021 19:53:13 +0800 Subject: [PATCH 13/13] clean code --- modules/public/public.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/public/public.go b/modules/public/public.go index 9a07b795111b6..a58709d86fc9c 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -29,15 +29,11 @@ func AssetsHandler(opts *Options) func(next http.Handler) http.Handler { if !filepath.IsAbs(custPath) { custPath = filepath.Join(setting.AppWorkPath, custPath) } - if !filepath.IsAbs(opts.Directory) { opts.Directory = filepath.Join(setting.AppWorkPath, opts.Directory) } - if opts.Prefix == "" { - opts.Prefix = "/" - } - if opts.Prefix != "/" { - opts.Prefix = opts.Prefix + "/" + if !strings.HasSuffix(opts.Prefix, "/") { + opts.Prefix += "/" } return func(next http.Handler) http.Handler {