Skip to content

Commit ba16df8

Browse files
authored
Fix setting HTTP headers after write (#21833) (#21874)
Backport #21833
1 parent 87630a6 commit ba16df8

File tree

4 files changed

+70
-43
lines changed

4 files changed

+70
-43
lines changed

Diff for: modules/context/context.go

+49-13
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"code.gitea.io/gitea/modules/setting"
3535
"code.gitea.io/gitea/modules/templates"
3636
"code.gitea.io/gitea/modules/translation"
37+
"code.gitea.io/gitea/modules/typesniffer"
3738
"code.gitea.io/gitea/modules/util"
3839
"code.gitea.io/gitea/modules/web/middleware"
3940
"code.gitea.io/gitea/services/auth"
@@ -322,9 +323,9 @@ func (ctx *Context) plainTextInternal(skip, status int, bs []byte) {
322323
if statusPrefix == 4 || statusPrefix == 5 {
323324
log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs))
324325
}
325-
ctx.Resp.WriteHeader(status)
326326
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
327327
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
328+
ctx.Resp.WriteHeader(status)
328329
if _, err := ctx.Resp.Write(bs); err != nil {
329330
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
330331
}
@@ -345,16 +346,45 @@ func (ctx *Context) RespHeader() http.Header {
345346
return ctx.Resp.Header()
346347
}
347348

349+
type ServeHeaderOptions struct {
350+
ContentType string // defaults to "application/octet-stream"
351+
ContentTypeCharset string
352+
Disposition string // defaults to "attachment"
353+
Filename string
354+
CacheDuration time.Duration // defaults to 5 minutes
355+
}
356+
348357
// SetServeHeaders sets necessary content serve headers
349-
func (ctx *Context) SetServeHeaders(filename string) {
350-
ctx.Resp.Header().Set("Content-Description", "File Transfer")
351-
ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
352-
ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+filename)
353-
ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
354-
ctx.Resp.Header().Set("Expires", "0")
355-
ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
356-
ctx.Resp.Header().Set("Pragma", "public")
357-
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
358+
func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
359+
header := ctx.Resp.Header()
360+
361+
contentType := typesniffer.ApplicationOctetStream
362+
if opts.ContentType != "" {
363+
if opts.ContentTypeCharset != "" {
364+
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
365+
} else {
366+
contentType = opts.ContentType
367+
}
368+
}
369+
header.Set("Content-Type", contentType)
370+
header.Set("X-Content-Type-Options", "nosniff")
371+
372+
if opts.Filename != "" {
373+
disposition := opts.Disposition
374+
if disposition == "" {
375+
disposition = "attachment"
376+
}
377+
378+
backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
379+
header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
380+
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
381+
}
382+
383+
duration := opts.CacheDuration
384+
if duration == 0 {
385+
duration = 5 * time.Minute
386+
}
387+
httpcache.AddCacheControlToHeader(header, duration)
358388
}
359389

360390
// ServeContent serves content to http request
@@ -366,7 +396,9 @@ func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interfa
366396
modTime = v
367397
}
368398
}
369-
ctx.SetServeHeaders(name)
399+
ctx.SetServeHeaders(&ServeHeaderOptions{
400+
Filename: name,
401+
})
370402
http.ServeContent(ctx.Resp, ctx.Req, name, modTime, r)
371403
}
372404

@@ -378,13 +410,17 @@ func (ctx *Context) ServeFile(file string, names ...string) {
378410
} else {
379411
name = path.Base(file)
380412
}
381-
ctx.SetServeHeaders(name)
413+
ctx.SetServeHeaders(&ServeHeaderOptions{
414+
Filename: name,
415+
})
382416
http.ServeFile(ctx.Resp, ctx.Req, file)
383417
}
384418

385419
// ServeStream serves file via io stream
386420
func (ctx *Context) ServeStream(rd io.Reader, name string) {
387-
ctx.SetServeHeaders(name)
421+
ctx.SetServeHeaders(&ServeHeaderOptions{
422+
Filename: name,
423+
})
388424
_, err := io.Copy(ctx.Resp, rd)
389425
if err != nil {
390426
ctx.ServerError("Download file failed", err)

Diff for: routers/api/packages/rubygems/rubygems.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo
7575
})
7676
}
7777

78-
ctx.SetServeHeaders(filename + ".gz")
78+
ctx.SetServeHeaders(&context.ServeHeaderOptions{
79+
Filename: filename + ".gz",
80+
})
7981

8082
zw := gzip.NewWriter(ctx.Resp)
8183
defer zw.Close()
@@ -113,7 +115,9 @@ func ServePackageSpecification(ctx *context.Context) {
113115
return
114116
}
115117

116-
ctx.SetServeHeaders(filename)
118+
ctx.SetServeHeaders(&context.ServeHeaderOptions{
119+
Filename: filename,
120+
})
117121

118122
zw := zlib.NewWriter(ctx.Resp)
119123
defer zw.Close()

Diff for: routers/common/repo.go

+15-26
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package common
77
import (
88
"fmt"
99
"io"
10-
"net/url"
1110
"path"
1211
"path/filepath"
1312
"strings"
@@ -53,50 +52,44 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read
5352
buf = buf[:n]
5453
}
5554

56-
httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute)
57-
5855
if size >= 0 {
5956
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
6057
} else {
6158
log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
6259
}
6360

64-
fileName := path.Base(filePath)
61+
opts := &context.ServeHeaderOptions{
62+
Filename: path.Base(filePath),
63+
}
64+
6565
sniffedType := typesniffer.DetectContentType(buf)
6666
isPlain := sniffedType.IsText() || ctx.FormBool("render")
67-
mimeType := ""
68-
charset := ""
6967

7068
if setting.MimeTypeMap.Enabled {
71-
fileExtension := strings.ToLower(filepath.Ext(fileName))
72-
mimeType = setting.MimeTypeMap.Map[fileExtension]
69+
fileExtension := strings.ToLower(filepath.Ext(filePath))
70+
opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
7371
}
7472

75-
if mimeType == "" {
73+
if opts.ContentType == "" {
7674
if sniffedType.IsBrowsableBinaryType() {
77-
mimeType = sniffedType.GetMimeType()
75+
opts.ContentType = sniffedType.GetMimeType()
7876
} else if isPlain {
79-
mimeType = "text/plain"
77+
opts.ContentType = "text/plain"
8078
} else {
81-
mimeType = typesniffer.ApplicationOctetStream
79+
opts.ContentType = typesniffer.ApplicationOctetStream
8280
}
8381
}
8482

8583
if isPlain {
84+
var charset string
8685
charset, err = charsetModule.DetectEncoding(buf)
8786
if err != nil {
8887
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
8988
charset = "utf-8"
9089
}
90+
opts.ContentTypeCharset = strings.ToLower(charset)
9191
}
9292

93-
if charset != "" {
94-
ctx.Resp.Header().Set("Content-Type", mimeType+"; charset="+strings.ToLower(charset))
95-
} else {
96-
ctx.Resp.Header().Set("Content-Type", mimeType)
97-
}
98-
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
99-
10093
isSVG := sniffedType.IsSvgImage()
10194

10295
// serve types that can present a security risk with CSP
@@ -109,16 +102,12 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read
109102
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
110103
}
111104

112-
disposition := "inline"
105+
opts.Disposition = "inline"
113106
if isSVG && !setting.UI.SVG.Enabled {
114-
disposition = "attachment"
107+
opts.Disposition = "attachment"
115108
}
116109

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")
110+
ctx.SetServeHeaders(opts)
122111

123112
_, err = ctx.Resp.Write(buf)
124113
if err != nil {

Diff for: routers/web/feed/profile.go

-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package feed
66

77
import (
8-
"net/http"
98
"time"
109

1110
"code.gitea.io/gitea/models"
@@ -57,7 +56,6 @@ func showUserFeed(ctx *context.Context, formatType string) {
5756

5857
// writeFeed write a feeds.Feed as atom or rss to ctx.Resp
5958
func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) {
60-
ctx.Resp.WriteHeader(http.StatusOK)
6159
if formatType == "atom" {
6260
ctx.Resp.Header().Set("Content-Type", "application/atom+xml;charset=utf-8")
6361
if err := feed.WriteAtom(ctx.Resp); err != nil {

0 commit comments

Comments
 (0)