]> source.dussan.org Git - gitea.git/commitdiff
Rework raw file http header logic (#20484)
authorsilverwind <me@silverwind.io>
Fri, 29 Jul 2022 15:26:55 +0000 (17:26 +0200)
committerGitHub <noreply@github.com>
Fri, 29 Jul 2022 15:26:55 +0000 (17:26 +0200)
- Always respect the user's configured mime type map
- Allow more types like image/pdf/video/audio to serve with correct content-type
- Shorten cache duration of raw files to 5 minutes, matching GitHub
- Don't set `content-disposition: attachment`, let the browser decide whether it wants to download or display a file directly
- Implement rfc5987 for filenames, remove previous hack. Confirmed it working in Safari.
- Make PDF attachment work in Safari by removing `sandbox` attribute.

This change will make a lot more file types open directly in browser now. Logic should generally be more readable than before with less `if` nesting and such.

Replaces: https://github.com/go-gitea/gitea/pull/20460
Replaces: https://github.com/go-gitea/gitea/pull/20455
Fixes: https://github.com/go-gitea/gitea/issues/20404
modules/typesniffer/typesniffer.go
routers/common/repo.go

index b6a6646d50ce94e0e702e690dae99bf60b26cc83..e50928e8c260a8236ab7ec1ac6ca2e590f6da447 100644 (file)
@@ -70,6 +70,16 @@ func (ct SniffedType) IsRepresentableAsText() bool {
        return ct.IsText() || ct.IsSvgImage()
 }
 
+// IsBrowsableType returns whether a non-text type can be displayed in a browser
+func (ct SniffedType) IsBrowsableBinaryType() bool {
+       return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio()
+}
+
+// GetMimeType returns the mime type
+func (ct SniffedType) GetMimeType() string {
+       return strings.SplitN(ct.contentType, ";", 2)[0]
+}
+
 // DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty.
 func DetectContentType(data []byte) SniffedType {
        if len(data) == 0 {
index b3cd749115fb180da606c8be36519725c3efec52..a9e80fad48c8d250934a4539a6dfbb6635c497eb 100644 (file)
@@ -7,12 +7,13 @@ package common
 import (
        "fmt"
        "io"
+       "net/url"
        "path"
        "path/filepath"
        "strings"
        "time"
 
-       "code.gitea.io/gitea/modules/charset"
+       charsetModule "code.gitea.io/gitea/modules/charset"
        "code.gitea.io/gitea/modules/context"
        "code.gitea.io/gitea/modules/git"
        "code.gitea.io/gitea/modules/httpcache"
@@ -42,7 +43,7 @@ func ServeBlob(ctx *context.Context, blob *git.Blob, lastModified time.Time) err
 }
 
 // ServeData download file from io.Reader
-func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
+func ServeData(ctx *context.Context, filePath string, size int64, reader io.Reader) error {
        buf := make([]byte, 1024)
        n, err := util.ReadAtMost(reader, buf)
        if err != nil {
@@ -52,56 +53,73 @@ func ServeData(ctx *context.Context, name string, size int64, reader io.Reader)
                buf = buf[:n]
        }
 
-       ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
+       httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute)
 
        if size >= 0 {
                ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
        } else {
-               log.Error("ServeData called to serve data: %s with size < 0: %d", name, size)
+               log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
        }
-       name = path.Base(name)
 
-       // Google Chrome dislike commas in filenames, so let's change it to a space
-       name = strings.ReplaceAll(name, ",", " ")
+       fileName := path.Base(filePath)
+       sniffedType := typesniffer.DetectContentType(buf)
+       isPlain := sniffedType.IsText() || ctx.FormBool("render")
+       mimeType := ""
+       charset := ""
 
-       st := typesniffer.DetectContentType(buf)
-
-       mappedMimeType := ""
        if setting.MimeTypeMap.Enabled {
-               fileExtension := strings.ToLower(filepath.Ext(name))
-               mappedMimeType = setting.MimeTypeMap.Map[fileExtension]
+               fileExtension := strings.ToLower(filepath.Ext(fileName))
+               mimeType = setting.MimeTypeMap.Map[fileExtension]
        }
-       if st.IsText() || ctx.FormBool("render") {
-               cs, err := charset.DetectEncoding(buf)
-               if err != nil {
-                       log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
-                       cs = "utf-8"
+
+       if mimeType == "" {
+               if sniffedType.IsBrowsableBinaryType() {
+                       mimeType = sniffedType.GetMimeType()
+               } else if isPlain {
+                       mimeType = "text/plain"
+               } else {
+                       mimeType = typesniffer.ApplicationOctetStream
                }
-               if mappedMimeType == "" {
-                       mappedMimeType = "text/plain"
+       }
+
+       if isPlain {
+               charset, err = charsetModule.DetectEncoding(buf)
+               if err != nil {
+                       log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
+                       charset = "utf-8"
                }
-               ctx.Resp.Header().Set("Content-Type", mappedMimeType+"; charset="+strings.ToLower(cs))
+       }
+
+       if charset != "" {
+               ctx.Resp.Header().Set("Content-Type", mimeType+"; charset="+strings.ToLower(charset))
        } else {
-               ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
-               if mappedMimeType != "" {
-                       ctx.Resp.Header().Set("Content-Type", mappedMimeType)
-               }
-               if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
-                       ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
-                       if st.IsSvgImage() || st.IsPDF() {
-                               ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
-                               ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
-                               if st.IsSvgImage() {
-                                       ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
-                               } else {
-                                       ctx.Resp.Header().Set("Content-Type", typesniffer.ApplicationOctetStream)
-                               }
-                       }
-               } else {
-                       ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
-               }
+               ctx.Resp.Header().Set("Content-Type", mimeType)
+       }
+       ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
+
+       isSVG := sniffedType.IsSvgImage()
+
+       // serve types that can present a security risk with CSP
+       if isSVG {
+               ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
+       } else if sniffedType.IsPDF() {
+               // no sandbox attribute for pdf as it breaks rendering in at least safari. this
+               // should generally be safe as scripts inside PDF can not escape the PDF document
+               // see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
+               ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
        }
 
+       disposition := "inline"
+       if isSVG && !setting.UI.SVG.Enabled {
+               disposition = "attachment"
+       }
+
+       // encode filename per https://datatracker.ietf.org/doc/html/rfc5987
+       encodedFileName := `filename*=UTF-8''` + url.PathEscape(fileName)
+
+       ctx.Resp.Header().Set("Content-Disposition", disposition+"; "+encodedFileName)
+       ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
+
        _, err = ctx.Resp.Write(buf)
        if err != nil {
                return err