@@ -7,12 +7,13 @@ package common
77import (
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