mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	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:
		@@ -34,6 +34,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/templates"
 | 
			
		||||
	"code.gitea.io/gitea/modules/translation"
 | 
			
		||||
	"code.gitea.io/gitea/modules/typesniffer"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web/middleware"
 | 
			
		||||
	"code.gitea.io/gitea/services/auth"
 | 
			
		||||
@@ -322,9 +323,9 @@ func (ctx *Context) plainTextInternal(skip, status int, bs []byte) {
 | 
			
		||||
	if statusPrefix == 4 || statusPrefix == 5 {
 | 
			
		||||
		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("X-Content-Type-Options", "nosniff")
 | 
			
		||||
	ctx.Resp.WriteHeader(status)
 | 
			
		||||
	if _, err := ctx.Resp.Write(bs); err != nil {
 | 
			
		||||
		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()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
func (ctx *Context) SetServeHeaders(filename string) {
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Description", "File Transfer")
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+filename)
 | 
			
		||||
	ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
 | 
			
		||||
	ctx.Resp.Header().Set("Expires", "0")
 | 
			
		||||
	ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
 | 
			
		||||
	ctx.Resp.Header().Set("Pragma", "public")
 | 
			
		||||
	ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
 | 
			
		||||
func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
 | 
			
		||||
	header := ctx.Resp.Header()
 | 
			
		||||
 | 
			
		||||
	contentType := typesniffer.ApplicationOctetStream
 | 
			
		||||
	if opts.ContentType != "" {
 | 
			
		||||
		if opts.ContentTypeCharset != "" {
 | 
			
		||||
			contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
 | 
			
		||||
		} else {
 | 
			
		||||
			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
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
// Only form files need to get closed.
 | 
			
		||||
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)
 | 
			
		||||
	defer zw.Close()
 | 
			
		||||
@@ -115,7 +117,9 @@ func ServePackageSpecification(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetServeHeaders(filename)
 | 
			
		||||
	ctx.SetServeHeaders(&context.ServeHeaderOptions{
 | 
			
		||||
		Filename: filename,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	zw := zlib.NewWriter(ctx.Resp)
 | 
			
		||||
	defer zw.Close()
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ package common
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -53,50 +52,44 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read
 | 
			
		||||
		buf = buf[:n]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute)
 | 
			
		||||
 | 
			
		||||
	if size >= 0 {
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileName := path.Base(filePath)
 | 
			
		||||
	sniffedType := typesniffer.DetectContentType(buf)
 | 
			
		||||
	isPlain := sniffedType.IsText() || ctx.FormBool("render")
 | 
			
		||||
	mimeType := ""
 | 
			
		||||
	charset := ""
 | 
			
		||||
 | 
			
		||||
	if setting.MimeTypeMap.Enabled {
 | 
			
		||||
		fileExtension := strings.ToLower(filepath.Ext(fileName))
 | 
			
		||||
		mimeType = setting.MimeTypeMap.Map[fileExtension]
 | 
			
		||||
	opts := &context.ServeHeaderOptions{
 | 
			
		||||
		Filename: path.Base(filePath),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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() {
 | 
			
		||||
			mimeType = sniffedType.GetMimeType()
 | 
			
		||||
			opts.ContentType = sniffedType.GetMimeType()
 | 
			
		||||
		} else if isPlain {
 | 
			
		||||
			mimeType = "text/plain"
 | 
			
		||||
			opts.ContentType = "text/plain"
 | 
			
		||||
		} else {
 | 
			
		||||
			mimeType = typesniffer.ApplicationOctetStream
 | 
			
		||||
			opts.ContentType = typesniffer.ApplicationOctetStream
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if isPlain {
 | 
			
		||||
		var charset string
 | 
			
		||||
		charset, err = charsetModule.DetectEncoding(buf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
 | 
			
		||||
			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()
 | 
			
		||||
 | 
			
		||||
	// 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'")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disposition := "inline"
 | 
			
		||||
	opts.Disposition = "inline"
 | 
			
		||||
	if isSVG && !setting.UI.SVG.Enabled {
 | 
			
		||||
		disposition = "attachment"
 | 
			
		||||
		opts.Disposition = "attachment"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// encode filename per https://datatracker.ietf.org/doc/html/rfc5987
 | 
			
		||||
	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")
 | 
			
		||||
	ctx.SetServeHeaders(opts)
 | 
			
		||||
 | 
			
		||||
	_, err = ctx.Resp.Write(buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@
 | 
			
		||||
package feed
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) {
 | 
			
		||||
	ctx.Resp.WriteHeader(http.StatusOK)
 | 
			
		||||
	if formatType == "atom" {
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Type", "application/atom+xml;charset=utf-8")
 | 
			
		||||
		if err := feed.WriteAtom(ctx.Resp); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -604,7 +604,10 @@ func RegisterRoutes(m *web.Route) {
 | 
			
		||||
 | 
			
		||||
	m.Group("", func() {
 | 
			
		||||
		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.Get(".png", func(ctx *context.Context) { ctx.Error(http.StatusNotFound) })
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user