summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--modules/typesniffer/typesniffer.go10
-rw-r--r--routers/common/repo.go94
2 files changed, 66 insertions, 38 deletions
diff --git a/modules/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go
index b6a6646d50..e50928e8c2 100644
--- a/modules/typesniffer/typesniffer.go
+++ b/modules/typesniffer/typesniffer.go
@@ -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 {
diff --git a/routers/common/repo.go b/routers/common/repo.go
index b3cd749115..a9e80fad48 100644
--- a/routers/common/repo.go
+++ b/routers/common/repo.go
@@ -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