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/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
	 KN4CK3R
					KN4CK3R