mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00: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/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) }) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 KN4CK3R
					KN4CK3R