Skip to content

Commit eaf653f

Browse files
authored
Rework raw file http header logic (#20484)
- 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: #20460 Replaces: #20455 Fixes: #20404
1 parent 7fe77f0 commit eaf653f

File tree

2 files changed

+66
-38
lines changed

2 files changed

+66
-38
lines changed

Diff for: modules/typesniffer/typesniffer.go

+10
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ func (ct SniffedType) IsRepresentableAsText() bool {
7070
return ct.IsText() || ct.IsSvgImage()
7171
}
7272

73+
// IsBrowsableType returns whether a non-text type can be displayed in a browser
74+
func (ct SniffedType) IsBrowsableBinaryType() bool {
75+
return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio()
76+
}
77+
78+
// GetMimeType returns the mime type
79+
func (ct SniffedType) GetMimeType() string {
80+
return strings.SplitN(ct.contentType, ";", 2)[0]
81+
}
82+
7383
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty.
7484
func DetectContentType(data []byte) SniffedType {
7585
if len(data) == 0 {

Diff for: routers/common/repo.go

+56-38
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ package common
77
import (
88
"fmt"
99
"io"
10+
"net/url"
1011
"path"
1112
"path/filepath"
1213
"strings"
1314
"time"
1415

15-
"code.gitea.io/gitea/modules/charset"
16+
charsetModule "code.gitea.io/gitea/modules/charset"
1617
"code.gitea.io/gitea/modules/context"
1718
"code.gitea.io/gitea/modules/git"
1819
"code.gitea.io/gitea/modules/httpcache"
@@ -42,7 +43,7 @@ func ServeBlob(ctx *context.Context, blob *git.Blob, lastModified time.Time) err
4243
}
4344

4445
// ServeData download file from io.Reader
45-
func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
46+
func ServeData(ctx *context.Context, filePath string, size int64, reader io.Reader) error {
4647
buf := make([]byte, 1024)
4748
n, err := util.ReadAtMost(reader, buf)
4849
if err != nil {
@@ -52,56 +53,73 @@ func ServeData(ctx *context.Context, name string, size int64, reader io.Reader)
5253
buf = buf[:n]
5354
}
5455

55-
ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
56+
httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute)
5657

5758
if size >= 0 {
5859
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
5960
} else {
60-
log.Error("ServeData called to serve data: %s with size < 0: %d", name, size)
61+
log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
6162
}
62-
name = path.Base(name)
6363

64-
// Google Chrome dislike commas in filenames, so let's change it to a space
65-
name = strings.ReplaceAll(name, ",", " ")
64+
fileName := path.Base(filePath)
65+
sniffedType := typesniffer.DetectContentType(buf)
66+
isPlain := sniffedType.IsText() || ctx.FormBool("render")
67+
mimeType := ""
68+
charset := ""
6669

67-
st := typesniffer.DetectContentType(buf)
68-
69-
mappedMimeType := ""
7070
if setting.MimeTypeMap.Enabled {
71-
fileExtension := strings.ToLower(filepath.Ext(name))
72-
mappedMimeType = setting.MimeTypeMap.Map[fileExtension]
71+
fileExtension := strings.ToLower(filepath.Ext(fileName))
72+
mimeType = setting.MimeTypeMap.Map[fileExtension]
7373
}
74-
if st.IsText() || ctx.FormBool("render") {
75-
cs, err := charset.DetectEncoding(buf)
76-
if err != nil {
77-
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
78-
cs = "utf-8"
74+
75+
if mimeType == "" {
76+
if sniffedType.IsBrowsableBinaryType() {
77+
mimeType = sniffedType.GetMimeType()
78+
} else if isPlain {
79+
mimeType = "text/plain"
80+
} else {
81+
mimeType = typesniffer.ApplicationOctetStream
7982
}
80-
if mappedMimeType == "" {
81-
mappedMimeType = "text/plain"
83+
}
84+
85+
if isPlain {
86+
charset, err = charsetModule.DetectEncoding(buf)
87+
if err != nil {
88+
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
89+
charset = "utf-8"
8290
}
83-
ctx.Resp.Header().Set("Content-Type", mappedMimeType+"; charset="+strings.ToLower(cs))
91+
}
92+
93+
if charset != "" {
94+
ctx.Resp.Header().Set("Content-Type", mimeType+"; charset="+strings.ToLower(charset))
8495
} else {
85-
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
86-
if mappedMimeType != "" {
87-
ctx.Resp.Header().Set("Content-Type", mappedMimeType)
88-
}
89-
if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
90-
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
91-
if st.IsSvgImage() || st.IsPDF() {
92-
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
93-
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
94-
if st.IsSvgImage() {
95-
ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
96-
} else {
97-
ctx.Resp.Header().Set("Content-Type", typesniffer.ApplicationOctetStream)
98-
}
99-
}
100-
} else {
101-
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
102-
}
96+
ctx.Resp.Header().Set("Content-Type", mimeType)
97+
}
98+
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
99+
100+
isSVG := sniffedType.IsSvgImage()
101+
102+
// serve types that can present a security risk with CSP
103+
if isSVG {
104+
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
105+
} else if sniffedType.IsPDF() {
106+
// no sandbox attribute for pdf as it breaks rendering in at least safari. this
107+
// should generally be safe as scripts inside PDF can not escape the PDF document
108+
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
109+
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
103110
}
104111

112+
disposition := "inline"
113+
if isSVG && !setting.UI.SVG.Enabled {
114+
disposition = "attachment"
115+
}
116+
117+
// encode filename per https://datatracker.ietf.org/doc/html/rfc5987
118+
encodedFileName := `filename*=UTF-8''` + url.PathEscape(fileName)
119+
120+
ctx.Resp.Header().Set("Content-Disposition", disposition+"; "+encodedFileName)
121+
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
122+
105123
_, err = ctx.Resp.Write(buf)
106124
if err != nil {
107125
return err

0 commit comments

Comments
 (0)