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

Serve pre-defined files in "public", add "security.txt", add CORS header for ".well-known" #25974

Merged
merged 11 commits into from Jul 21, 2023
10 changes: 10 additions & 0 deletions cmd/web.go
Expand Up @@ -172,6 +172,16 @@ func serveInstalled(ctx *cli.Context) error {
}
}

legacyPublicAssetFiles := []string{"img", "css", "js"}
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
for _, fn := range legacyPublicAssetFiles {
if _, err := os.Stat(filepath.Join(setting.CustomPath, "public", fn)); err == nil {
log.Error("Found legacy public asset %q in CustomPath. Please move it to %s/public/assets/%s", fn, setting.CustomPath, fn)
}
}
if _, err := os.Stat(filepath.Join(setting.CustomPath, "robots.txt")); err == nil {
log.Error(`Found legacy public asset "robots.txt" in CustomPath. Please move it to %s/public/robots.txt`, setting.CustomPath)
}

routers.InitWebInstalled(graceful.GetManager().HammerContext())

// We check that AppDataPath exists here (it should have been created during installation)
Expand Down
6 changes: 5 additions & 1 deletion docs/content/doc/administration/customizing-gitea.en-us.md
Expand Up @@ -56,7 +56,11 @@ is set under the "Configuration" tab on the site administration page.

To make Gitea serve custom public files (like pages and images), use the folder
`$GITEA_CUSTOM/public/` as the webroot. Symbolic links will be followed.
At the moment, only files in the `public/assets/` folder are served.
At the moment, only the following files are served:

- `public/robots.txt`
- files in the `public/.well-known/` folder
- files in the `public/assets/` folder

For example, a file `image.png` stored in `$GITEA_CUSTOM/public/assets/`, can be accessed with
the url `http://gitea.domain.tld/assets/image.png`.
Expand Down
34 changes: 11 additions & 23 deletions modules/public/public.go
Expand Up @@ -28,27 +28,15 @@ func AssetFS() *assetfs.LayeredFS {
return assetfs.Layered(CustomAssets(), BuiltinAssets())
}

// AssetsHandlerFunc implements the static handler for serving custom or original assets.
func AssetsHandlerFunc(prefix string) http.HandlerFunc {
// FileHandlerFunc implements the static handler for serving files in "public" assets
func FileHandlerFunc() http.HandlerFunc {
assetFS := AssetFS()
prefix = strings.TrimSuffix(prefix, "/") + "/"
return func(resp http.ResponseWriter, req *http.Request) {
subPath := req.URL.Path
if !strings.HasPrefix(subPath, prefix) {
return
}
subPath = strings.TrimPrefix(subPath, prefix)

if req.Method != "GET" && req.Method != "HEAD" {
resp.WriteHeader(http.StatusNotFound)
return
}

if handleRequest(resp, req, assetFS, subPath) {
return
}

resp.WriteHeader(http.StatusNotFound)
handleRequest(resp, req, assetFS, req.URL.Path)
}
}

Expand All @@ -71,34 +59,34 @@ func setWellKnownContentType(w http.ResponseWriter, file string) {
}
}

func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool {
func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) {
// actually, fs (http.FileSystem) is designed to be a safe interface, relative paths won't bypass its parent directory, it's also fine to do a clean here
f, err := fs.Open(util.PathJoinRelX("assets", file))
f, err := fs.Open(util.PathJoinRelX(file))
if err != nil {
if os.IsNotExist(err) {
return false
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusInternalServerError)
log.Error("[Static] Open %q failed: %v", file, err)
return true
return
}
defer f.Close()

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

// Try to serve index file
// need to serve index file? (no at the moment)
if fi.IsDir() {
w.WriteHeader(http.StatusNotFound)
return true
return
}

serveContent(w, req, fi, fi.ModTime(), f)
return true
}

type GzipBytesProvider interface {
Expand Down
5 changes: 0 additions & 5 deletions modules/setting/server.go
Expand Up @@ -349,9 +349,4 @@ func loadServerFrom(rootCfg ConfigProvider) {
default:
LandingPageURL = LandingPage(landingPage)
}

HasRobotsTxt, err = util.IsFile(path.Join(CustomPath, "robots.txt"))
if err != nil {
log.Error("Unable to check if %s is a file. Error: %v", path.Join(CustomPath, "robots.txt"), err)
}
}
4 changes: 4 additions & 0 deletions public/.well-known/security.txt
@@ -0,0 +1,4 @@
Contact: ...
Acknowledgments: ...
Preferred-Languages: en
Policy: ...
lunny marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion routers/install/routes.go
Expand Up @@ -20,7 +20,7 @@ import (
func Routes() *web.Route {
base := web.NewRoute()
base.Use(common.ProtocolMiddlewares()...)
base.RouteMethods("/assets/*", "GET, HEAD", public.AssetsHandlerFunc("/assets/"))
base.RouteMethods("/assets/*", "GET, HEAD", public.FileHandlerFunc())

r := web.NewRoute()
r.Use(common.Sessioner(), Contexter())
Expand Down
7 changes: 5 additions & 2 deletions routers/web/misc/misc.go
Expand Up @@ -34,9 +34,12 @@ func DummyOK(w http.ResponseWriter, req *http.Request) {
}

func RobotsTxt(w http.ResponseWriter, req *http.Request) {
filePath := util.FilePathJoinAbs(setting.CustomPath, "robots.txt")
robotsTxt := util.FilePathJoinAbs(setting.CustomPath, "public/robots.txt")
if ok, _ := util.IsExist(robotsTxt); !ok {
robotsTxt = util.FilePathJoinAbs(setting.CustomPath, "robots.txt") // the legacy "robots.txt"
}
httpcache.SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
http.ServeFile(w, req, filePath)
http.ServeFile(w, req, robotsTxt)
}

func StaticRedirect(target string) func(w http.ResponseWriter, req *http.Request) {
Expand Down
13 changes: 5 additions & 8 deletions routers/web/web.go
Expand Up @@ -108,7 +108,7 @@ func Routes() *web.Route {
routes := web.NewRoute()

routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
routes.RouteMethods("/assets/*", "GET, HEAD", CorsHandler(), public.AssetsHandlerFunc("/assets/"))
routes.RouteMethods("/assets/*", "GET, HEAD", CorsHandler(), public.FileHandlerFunc())
routes.RouteMethods("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
routes.RouteMethods("/repo-avatars/*", "GET, HEAD", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
routes.RouteMethods("/apple-touch-icon.png", "GET, HEAD", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
Expand All @@ -132,15 +132,12 @@ func Routes() *web.Route {
routes.RouteMethods("/captcha/*", "GET,HEAD", append(mid, captcha.Captchaer(context.GetImageCaptcha()))...)
}

if setting.HasRobotsTxt {
routes.Get("/robots.txt", append(mid, misc.RobotsTxt)...)
}

if setting.Metrics.Enabled {
prometheus.MustRegister(metrics.NewCollector())
routes.Get("/metrics", append(mid, Metrics)...)
}

routes.Get("/robots.txt", append(mid, misc.RobotsTxt)...)
routes.Get("/ssh_info", misc.SSHInfo)
routes.Get("/api/healthz", healthcheck.Check)

Expand Down Expand Up @@ -336,8 +333,7 @@ func registerRoutes(m *web.Route) {

// FIXME: not all routes need go through same middleware.
// Especially some AJAX requests, we can reduce middleware number to improve performance.
// Routers.
// for health check

m.Get("/", Home)
m.Get("/sitemap.xml", sitemapEnabled, ignExploreSignIn, HomeSitemap)
m.Group("/.well-known", func() {
Expand All @@ -349,7 +345,8 @@ func registerRoutes(m *web.Route) {
m.Get("/change-password", func(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
})
})
m.Any("/*", CorsHandler(), public.FileHandlerFunc())
}, CorsHandler())

m.Group("/explore", func() {
m.Get("", func(ctx *context.Context) {
Expand Down
1 change: 1 addition & 0 deletions tests/integration/links_test.go
Expand Up @@ -38,6 +38,7 @@ func TestLinksNoLogin(t *testing.T) {
"/user2/repo1/projects/1",
"/assets/img/404.png",
"/assets/img/500.png",
"/.well-known/security.txt",
}

for _, link := range links {
Expand Down