Skip to content

Commit f4ec03a

Browse files
Fix setting HTTP headers after write (#21833) (#21877)
Backport of #21833 Co-authored-by: techknowlogick <[email protected]>
1 parent b236983 commit f4ec03a

File tree

5 files changed

+68
-54
lines changed

5 files changed

+68
-54
lines changed

modules/context/context.go

+43-23
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,36 +346,55 @@ 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
361391
func (ctx *Context) ServeContent(name string, r io.ReadSeeker, modTime time.Time) {
362-
ctx.SetServeHeaders(name)
392+
ctx.SetServeHeaders(&ServeHeaderOptions{
393+
Filename: name,
394+
})
363395
http.ServeContent(ctx.Resp, ctx.Req, name, modTime, r)
364396
}
365397

366-
// ServeFile serves given file to response.
367-
func (ctx *Context) ServeFile(file string, names ...string) {
368-
var name string
369-
if len(names) > 0 {
370-
name = names[0]
371-
} else {
372-
name = path.Base(file)
373-
}
374-
ctx.SetServeHeaders(name)
375-
http.ServeFile(ctx.Resp, ctx.Req, file)
376-
}
377-
378398
// UploadStream returns the request body or the first form file
379399
// Only form files need to get closed.
380400
func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) {

routers/api/packages/rubygems/rubygems.go

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

80-
ctx.SetServeHeaders(filename + ".gz")
80+
ctx.SetServeHeaders(&context.ServeHeaderOptions{
81+
Filename: filename + ".gz",
82+
})
8183

8284
zw := gzip.NewWriter(ctx.Resp)
8385
defer zw.Close()
@@ -115,7 +117,9 @@ func ServePackageSpecification(ctx *context.Context) {
115117
return
116118
}
117119

118-
ctx.SetServeHeaders(filename)
120+
ctx.SetServeHeaders(&context.ServeHeaderOptions{
121+
Filename: filename,
122+
})
119123

120124
zw := zlib.NewWriter(ctx.Resp)
121125
defer zw.Close()

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 {

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
activities_model "code.gitea.io/gitea/models/activities"
@@ -59,7 +58,6 @@ func showUserFeed(ctx *context.Context, formatType string) {
5958

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

routers/web/web.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,10 @@ func RegisterRoutes(m *web.Route) {
597597

598598
m.Group("", func() {
599599
m.Get("/favicon.ico", func(ctx *context.Context) {
600-
ctx.ServeFile(path.Join(setting.StaticRootPath, "public/img/favicon.png"))
600+
ctx.SetServeHeaders(&context.ServeHeaderOptions{
601+
Filename: "favicon.png",
602+
})
603+
http.ServeFile(ctx.Resp, ctx.Req, path.Join(setting.StaticRootPath, "public/img/favicon.png"))
601604
})
602605
m.Group("/{username}", func() {
603606
m.Get(".png", func(ctx *context.Context) { ctx.Error(http.StatusNotFound) })

0 commit comments

Comments
 (0)