code.gitea.io/gitea@v1.19.3/modules/public/public.go (about) 1 // Copyright 2016 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package public 5 6 import ( 7 "net/http" 8 "os" 9 "path" 10 "path/filepath" 11 "strings" 12 13 "code.gitea.io/gitea/modules/container" 14 "code.gitea.io/gitea/modules/httpcache" 15 "code.gitea.io/gitea/modules/log" 16 "code.gitea.io/gitea/modules/setting" 17 ) 18 19 // Options represents the available options to configure the handler. 20 type Options struct { 21 Directory string 22 Prefix string 23 CorsHandler func(http.Handler) http.Handler 24 } 25 26 // AssetsURLPathPrefix is the path prefix for static asset files 27 const AssetsURLPathPrefix = "/assets/" 28 29 // AssetsHandlerFunc implements the static handler for serving custom or original assets. 30 func AssetsHandlerFunc(opts *Options) http.HandlerFunc { 31 custPath := filepath.Join(setting.CustomPath, "public") 32 if !filepath.IsAbs(custPath) { 33 custPath = filepath.Join(setting.AppWorkPath, custPath) 34 } 35 if !filepath.IsAbs(opts.Directory) { 36 opts.Directory = filepath.Join(setting.AppWorkPath, opts.Directory) 37 } 38 if !strings.HasSuffix(opts.Prefix, "/") { 39 opts.Prefix += "/" 40 } 41 42 return func(resp http.ResponseWriter, req *http.Request) { 43 if req.Method != "GET" && req.Method != "HEAD" { 44 resp.WriteHeader(http.StatusNotFound) 45 return 46 } 47 48 file := req.URL.Path 49 file = file[len(opts.Prefix):] 50 if len(file) == 0 { 51 resp.WriteHeader(http.StatusNotFound) 52 return 53 } 54 if strings.Contains(file, "\\") { 55 resp.WriteHeader(http.StatusBadRequest) 56 return 57 } 58 file = "/" + file 59 60 var written bool 61 if opts.CorsHandler != nil { 62 written = true 63 opts.CorsHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { 64 written = false 65 })).ServeHTTP(resp, req) 66 } 67 if written { 68 return 69 } 70 71 // custom files 72 if opts.handle(resp, req, http.Dir(custPath), file) { 73 return 74 } 75 76 // internal files 77 if opts.handle(resp, req, fileSystem(opts.Directory), file) { 78 return 79 } 80 81 resp.WriteHeader(http.StatusNotFound) 82 } 83 } 84 85 // parseAcceptEncoding parse Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5 as compress methods 86 func parseAcceptEncoding(val string) container.Set[string] { 87 parts := strings.Split(val, ";") 88 types := make(container.Set[string]) 89 for _, v := range strings.Split(parts[0], ",") { 90 types.Add(strings.TrimSpace(v)) 91 } 92 return types 93 } 94 95 // setWellKnownContentType will set the Content-Type if the file is a well-known type. 96 // See the comments of detectWellKnownMimeType 97 func setWellKnownContentType(w http.ResponseWriter, file string) { 98 mimeType := detectWellKnownMimeType(filepath.Ext(file)) 99 if mimeType != "" { 100 w.Header().Set("Content-Type", mimeType) 101 } 102 } 103 104 func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool { 105 // use clean to keep the file is a valid path with no . or .. 106 f, err := fs.Open(path.Clean(file)) 107 if err != nil { 108 if os.IsNotExist(err) { 109 return false 110 } 111 w.WriteHeader(http.StatusInternalServerError) 112 log.Error("[Static] Open %q failed: %v", file, err) 113 return true 114 } 115 defer f.Close() 116 117 fi, err := f.Stat() 118 if err != nil { 119 w.WriteHeader(http.StatusInternalServerError) 120 log.Error("[Static] %q exists, but fails to open: %v", file, err) 121 return true 122 } 123 124 // Try to serve index file 125 if fi.IsDir() { 126 w.WriteHeader(http.StatusNotFound) 127 return true 128 } 129 130 if httpcache.HandleFileETagCache(req, w, fi) { 131 return true 132 } 133 134 setWellKnownContentType(w, file) 135 136 serveContent(w, req, fi, fi.ModTime(), f) 137 return true 138 }