]> source.dussan.org Git - gitea.git/commitdiff
Fix mime-type detection for HTTP server (#18370)
authorwxiaoguang <wxiaoguang@gmail.com>
Sun, 23 Jan 2022 12:19:49 +0000 (20:19 +0800)
committerGitHub <noreply@github.com>
Sun, 23 Jan 2022 12:19:49 +0000 (20:19 +0800)
Bypass the unstable behavior of Golang's mime.TypeByExtension

modules/public/dynamic.go [deleted file]
modules/public/mime_types.go [new file with mode: 0644]
modules/public/public.go
modules/public/serve_dynamic.go [new file with mode: 0644]
modules/public/serve_static.go [new file with mode: 0644]
modules/public/static.go [deleted file]

diff --git a/modules/public/dynamic.go b/modules/public/dynamic.go
deleted file mode 100644 (file)
index 955c01e..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-//go:build !bindata
-// +build !bindata
-
-package public
-
-import (
-       "io"
-       "net/http"
-       "os"
-       "time"
-)
-
-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/mime_types.go b/modules/public/mime_types.go
new file mode 100644 (file)
index 0000000..f8c92e8
--- /dev/null
@@ -0,0 +1,41 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package public
+
+import "strings"
+
+// wellKnownMimeTypesLower comes from Golang's builtin mime package: `builtinTypesLower`, see the comment of detectWellKnownMimeType
+var wellKnownMimeTypesLower = map[string]string{
+       ".avif": "image/avif",
+       ".css":  "text/css; charset=utf-8",
+       ".gif":  "image/gif",
+       ".htm":  "text/html; charset=utf-8",
+       ".html": "text/html; charset=utf-8",
+       ".jpeg": "image/jpeg",
+       ".jpg":  "image/jpeg",
+       ".js":   "text/javascript; charset=utf-8",
+       ".json": "application/json",
+       ".mjs":  "text/javascript; charset=utf-8",
+       ".pdf":  "application/pdf",
+       ".png":  "image/png",
+       ".svg":  "image/svg+xml",
+       ".wasm": "application/wasm",
+       ".webp": "image/webp",
+       ".xml":  "text/xml; charset=utf-8",
+
+       // well, there are some types missing from the builtin list
+       ".txt": "text/plain; charset=utf-8",
+}
+
+// detectWellKnownMimeType will return the mime-type for a well-known file ext name
+// The purpose of this function is to bypass the unstable behavior of Golang's mime.TypeByExtension
+// mime.TypeByExtension would use OS's mime-type config to overwrite the well-known types (see its document).
+// If the user's OS has incorrect mime-type config, it would make Gitea can not respond a correct Content-Type to browsers.
+// For example, if Gitea returns `text/plain` for a `.js` file, the browser couldn't run the JS due to security reasons.
+// detectWellKnownMimeType makes the Content-Type for well-known files stable.
+func detectWellKnownMimeType(ext string) string {
+       ext = strings.ToLower(ext)
+       return wellKnownMimeTypesLower[ext]
+}
index 91ecf42a3cac5c60e1a03786a5780be5cb265fad..7804e945e798a86b8f46f4994deca0cd8c7520c9 100644 (file)
@@ -92,6 +92,15 @@ func parseAcceptEncoding(val string) map[string]bool {
        return types
 }
 
+// setWellKnownContentType will set the Content-Type if the file is a well-known type.
+// See the comments of detectWellKnownMimeType
+func setWellKnownContentType(w http.ResponseWriter, file string) {
+       mimeType := detectWellKnownMimeType(filepath.Ext(file))
+       if mimeType != "" {
+               w.Header().Set("Content-Type", mimeType)
+       }
+}
+
 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))
@@ -122,6 +131,8 @@ func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.Fi
                return true
        }
 
+       setWellKnownContentType(w, file)
+
        serveContent(w, req, fi, fi.ModTime(), f)
        return true
 }
diff --git a/modules/public/serve_dynamic.go b/modules/public/serve_dynamic.go
new file mode 100644 (file)
index 0000000..955c01e
--- /dev/null
@@ -0,0 +1,24 @@
+// Copyright 2016 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+//go:build !bindata
+// +build !bindata
+
+package public
+
+import (
+       "io"
+       "net/http"
+       "os"
+       "time"
+)
+
+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/serve_static.go b/modules/public/serve_static.go
new file mode 100644 (file)
index 0000000..8e82175
--- /dev/null
@@ -0,0 +1,82 @@
+// Copyright 2016 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+//go:build bindata
+// +build bindata
+
+package public
+
+import (
+       "bytes"
+       "io"
+       "net/http"
+       "os"
+       "path/filepath"
+       "time"
+
+       "code.gitea.io/gitea/modules/timeutil"
+)
+
+// GlobalModTime provide a global mod time for embedded asset files
+func GlobalModTime(filename string) time.Time {
+       return timeutil.GetExecutableModTime()
+}
+
+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 io.ReadAll(f)
+}
+
+func AssetNames() []string {
+       realFS := Assets.(vfsgen۰FS)
+       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) {
+       encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding"))
+       if encodings["gzip"] {
+               if cf, ok := fi.(*vfsgen۰CompressedFileInfo); ok {
+                       rdGzip := bytes.NewReader(cf.GzipBytes())
+                       // all static files are managed by Gitea, so we can make sure every file has the correct ext name
+                       // then we can get the correct Content-Type, we do not need to do http.DetectContentType on the decompressed data
+                       mimeType := detectWellKnownMimeType(filepath.Ext(fi.Name()))
+                       if mimeType == "" {
+                               mimeType = "application/octet-stream"
+                       }
+                       w.Header().Set("Content-Type", mimeType)
+                       w.Header().Set("Content-Encoding", "gzip")
+                       http.ServeContent(w, req, fi.Name(), modtime, rdGzip)
+                       return
+               }
+       }
+
+       http.ServeContent(w, req, fi.Name(), modtime, content)
+       return
+}
diff --git a/modules/public/static.go b/modules/public/static.go
deleted file mode 100644 (file)
index d373c71..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-//go:build bindata
-// +build bindata
-
-package public
-
-import (
-       "bytes"
-       "compress/gzip"
-       "io"
-       "mime"
-       "net/http"
-       "os"
-       "path/filepath"
-       "time"
-
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/timeutil"
-)
-
-// GlobalModTime provide a global mod time for embedded asset files
-func GlobalModTime(filename string) time.Time {
-       return timeutil.GetExecutableModTime()
-}
-
-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 io.ReadAll(f)
-}
-
-func AssetNames() []string {
-       realFS := Assets.(vfsgen۰FS)
-       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) {
-       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
-}