Fix setting HTTP headers after write (#21833)
The headers can't be modified after it was send to the client.
This commit is contained in:
parent
c144942b23
commit
43ab9324c5
5 changed files with 72 additions and 58 deletions
|
@ -34,6 +34,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
|
"code.gitea.io/gitea/modules/typesniffer"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
"code.gitea.io/gitea/services/auth"
|
"code.gitea.io/gitea/services/auth"
|
||||||
|
@ -322,9 +323,9 @@ func (ctx *Context) plainTextInternal(skip, status int, bs []byte) {
|
||||||
if statusPrefix == 4 || statusPrefix == 5 {
|
if statusPrefix == 4 || statusPrefix == 5 {
|
||||||
log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs))
|
log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs))
|
||||||
}
|
}
|
||||||
ctx.Resp.WriteHeader(status)
|
|
||||||
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
||||||
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
ctx.Resp.WriteHeader(status)
|
||||||
if _, err := ctx.Resp.Write(bs); err != nil {
|
if _, err := ctx.Resp.Write(bs); err != nil {
|
||||||
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
|
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
|
||||||
}
|
}
|
||||||
|
@ -345,36 +346,55 @@ func (ctx *Context) RespHeader() http.Header {
|
||||||
return ctx.Resp.Header()
|
return ctx.Resp.Header()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServeHeaderOptions struct {
|
||||||
|
ContentType string // defaults to "application/octet-stream"
|
||||||
|
ContentTypeCharset string
|
||||||
|
Disposition string // defaults to "attachment"
|
||||||
|
Filename string
|
||||||
|
CacheDuration time.Duration // defaults to 5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
// SetServeHeaders sets necessary content serve headers
|
// SetServeHeaders sets necessary content serve headers
|
||||||
func (ctx *Context) SetServeHeaders(filename string) {
|
func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
|
||||||
ctx.Resp.Header().Set("Content-Description", "File Transfer")
|
header := ctx.Resp.Header()
|
||||||
ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
|
|
||||||
ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+filename)
|
contentType := typesniffer.ApplicationOctetStream
|
||||||
ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
|
if opts.ContentType != "" {
|
||||||
ctx.Resp.Header().Set("Expires", "0")
|
if opts.ContentTypeCharset != "" {
|
||||||
ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
|
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
|
||||||
ctx.Resp.Header().Set("Pragma", "public")
|
} else {
|
||||||
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
|
contentType = opts.ContentType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header.Set("Content-Type", contentType)
|
||||||
|
header.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
||||||
|
if opts.Filename != "" {
|
||||||
|
disposition := opts.Disposition
|
||||||
|
if disposition == "" {
|
||||||
|
disposition = "attachment"
|
||||||
|
}
|
||||||
|
|
||||||
|
backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
|
||||||
|
header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
|
||||||
|
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := opts.CacheDuration
|
||||||
|
if duration == 0 {
|
||||||
|
duration = 5 * time.Minute
|
||||||
|
}
|
||||||
|
httpcache.AddCacheControlToHeader(header, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeContent serves content to http request
|
// ServeContent serves content to http request
|
||||||
func (ctx *Context) ServeContent(name string, r io.ReadSeeker, modTime time.Time) {
|
func (ctx *Context) ServeContent(name string, r io.ReadSeeker, modTime time.Time) {
|
||||||
ctx.SetServeHeaders(name)
|
ctx.SetServeHeaders(&ServeHeaderOptions{
|
||||||
|
Filename: name,
|
||||||
|
})
|
||||||
http.ServeContent(ctx.Resp, ctx.Req, name, modTime, r)
|
http.ServeContent(ctx.Resp, ctx.Req, name, modTime, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeFile serves given file to response.
|
|
||||||
func (ctx *Context) ServeFile(file string, names ...string) {
|
|
||||||
var name string
|
|
||||||
if len(names) > 0 {
|
|
||||||
name = names[0]
|
|
||||||
} else {
|
|
||||||
name = path.Base(file)
|
|
||||||
}
|
|
||||||
ctx.SetServeHeaders(name)
|
|
||||||
http.ServeFile(ctx.Resp, ctx.Req, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadStream returns the request body or the first form file
|
// UploadStream returns the request body or the first form file
|
||||||
// Only form files need to get closed.
|
// Only form files need to get closed.
|
||||||
func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) {
|
func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) {
|
||||||
|
|
|
@ -77,7 +77,9 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SetServeHeaders(filename + ".gz")
|
ctx.SetServeHeaders(&context.ServeHeaderOptions{
|
||||||
|
Filename: filename + ".gz",
|
||||||
|
})
|
||||||
|
|
||||||
zw := gzip.NewWriter(ctx.Resp)
|
zw := gzip.NewWriter(ctx.Resp)
|
||||||
defer zw.Close()
|
defer zw.Close()
|
||||||
|
@ -115,7 +117,9 @@ func ServePackageSpecification(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SetServeHeaders(filename)
|
ctx.SetServeHeaders(&context.ServeHeaderOptions{
|
||||||
|
Filename: filename,
|
||||||
|
})
|
||||||
|
|
||||||
zw := zlib.NewWriter(ctx.Resp)
|
zw := zlib.NewWriter(ctx.Resp)
|
||||||
defer zw.Close()
|
defer zw.Close()
|
||||||
|
|
|
@ -7,7 +7,6 @@ package common
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -53,50 +52,44 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read
|
||||||
buf = buf[:n]
|
buf = buf[:n]
|
||||||
}
|
}
|
||||||
|
|
||||||
httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute)
|
|
||||||
|
|
||||||
if size >= 0 {
|
if size >= 0 {
|
||||||
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
|
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
|
||||||
} else {
|
} else {
|
||||||
log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
|
log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := path.Base(filePath)
|
opts := &context.ServeHeaderOptions{
|
||||||
sniffedType := typesniffer.DetectContentType(buf)
|
Filename: path.Base(filePath),
|
||||||
isPlain := sniffedType.IsText() || ctx.FormBool("render")
|
|
||||||
mimeType := ""
|
|
||||||
charset := ""
|
|
||||||
|
|
||||||
if setting.MimeTypeMap.Enabled {
|
|
||||||
fileExtension := strings.ToLower(filepath.Ext(fileName))
|
|
||||||
mimeType = setting.MimeTypeMap.Map[fileExtension]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if mimeType == "" {
|
sniffedType := typesniffer.DetectContentType(buf)
|
||||||
|
isPlain := sniffedType.IsText() || ctx.FormBool("render")
|
||||||
|
|
||||||
|
if setting.MimeTypeMap.Enabled {
|
||||||
|
fileExtension := strings.ToLower(filepath.Ext(filePath))
|
||||||
|
opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ContentType == "" {
|
||||||
if sniffedType.IsBrowsableBinaryType() {
|
if sniffedType.IsBrowsableBinaryType() {
|
||||||
mimeType = sniffedType.GetMimeType()
|
opts.ContentType = sniffedType.GetMimeType()
|
||||||
} else if isPlain {
|
} else if isPlain {
|
||||||
mimeType = "text/plain"
|
opts.ContentType = "text/plain"
|
||||||
} else {
|
} else {
|
||||||
mimeType = typesniffer.ApplicationOctetStream
|
opts.ContentType = typesniffer.ApplicationOctetStream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isPlain {
|
if isPlain {
|
||||||
|
var charset string
|
||||||
charset, err = charsetModule.DetectEncoding(buf)
|
charset, err = charsetModule.DetectEncoding(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
|
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
|
||||||
charset = "utf-8"
|
charset = "utf-8"
|
||||||
}
|
}
|
||||||
|
opts.ContentTypeCharset = strings.ToLower(charset)
|
||||||
}
|
}
|
||||||
|
|
||||||
if charset != "" {
|
|
||||||
ctx.Resp.Header().Set("Content-Type", mimeType+"; charset="+strings.ToLower(charset))
|
|
||||||
} else {
|
|
||||||
ctx.Resp.Header().Set("Content-Type", mimeType)
|
|
||||||
}
|
|
||||||
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
|
||||||
|
|
||||||
isSVG := sniffedType.IsSvgImage()
|
isSVG := sniffedType.IsSvgImage()
|
||||||
|
|
||||||
// serve types that can present a security risk with CSP
|
// 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
|
||||||
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
||||||
}
|
}
|
||||||
|
|
||||||
disposition := "inline"
|
opts.Disposition = "inline"
|
||||||
if isSVG && !setting.UI.SVG.Enabled {
|
if isSVG && !setting.UI.SVG.Enabled {
|
||||||
disposition = "attachment"
|
opts.Disposition = "attachment"
|
||||||
}
|
}
|
||||||
|
|
||||||
// encode filename per https://datatracker.ietf.org/doc/html/rfc5987
|
ctx.SetServeHeaders(opts)
|
||||||
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)
|
_, err = ctx.Resp.Write(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
package feed
|
package feed
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
|
@ -59,7 +58,6 @@ func showUserFeed(ctx *context.Context, formatType string) {
|
||||||
|
|
||||||
// writeFeed write a feeds.Feed as atom or rss to ctx.Resp
|
// writeFeed write a feeds.Feed as atom or rss to ctx.Resp
|
||||||
func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) {
|
func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) {
|
||||||
ctx.Resp.WriteHeader(http.StatusOK)
|
|
||||||
if formatType == "atom" {
|
if formatType == "atom" {
|
||||||
ctx.Resp.Header().Set("Content-Type", "application/atom+xml;charset=utf-8")
|
ctx.Resp.Header().Set("Content-Type", "application/atom+xml;charset=utf-8")
|
||||||
if err := feed.WriteAtom(ctx.Resp); err != nil {
|
if err := feed.WriteAtom(ctx.Resp); err != nil {
|
||||||
|
|
|
@ -604,7 +604,10 @@ func RegisterRoutes(m *web.Route) {
|
||||||
|
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Get("/favicon.ico", func(ctx *context.Context) {
|
m.Get("/favicon.ico", func(ctx *context.Context) {
|
||||||
ctx.ServeFile(path.Join(setting.StaticRootPath, "public/img/favicon.png"))
|
ctx.SetServeHeaders(&context.ServeHeaderOptions{
|
||||||
|
Filename: "favicon.png",
|
||||||
|
})
|
||||||
|
http.ServeFile(ctx.Resp, ctx.Req, path.Join(setting.StaticRootPath, "public/img/favicon.png"))
|
||||||
})
|
})
|
||||||
m.Group("/{username}", func() {
|
m.Group("/{username}", func() {
|
||||||
m.Get(".png", func(ctx *context.Context) { ctx.Error(http.StatusNotFound) })
|
m.Get(".png", func(ctx *context.Context) { ctx.Error(http.StatusNotFound) })
|
||||||
|
|
Loading…
Reference in a new issue