Skip to content

Commit af7b92d

Browse files
committed
fix
1 parent f5b70a0 commit af7b92d

File tree

9 files changed

+276
-183
lines changed

9 files changed

+276
-183
lines changed

modules/context/context_serve.go

+5-56
Original file line numberDiff line numberDiff line change
@@ -4,71 +4,20 @@
44
package context
55

66
import (
7-
"fmt"
87
"io"
98
"net/http"
10-
"net/url"
11-
"strconv"
12-
"strings"
13-
"time"
149

15-
"code.gitea.io/gitea/modules/httpcache"
16-
"code.gitea.io/gitea/modules/typesniffer"
10+
"code.gitea.io/gitea/modules/httplib"
1711
)
1812

19-
type ServeHeaderOptions struct {
20-
ContentType string // defaults to "application/octet-stream"
21-
ContentTypeCharset string
22-
ContentLength *int64
23-
Disposition string // defaults to "attachment"
24-
Filename string
25-
CacheDuration time.Duration // defaults to 5 minutes
26-
LastModified time.Time
27-
}
28-
29-
// SetServeHeaders sets necessary content serve headers
30-
func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
31-
header := ctx.Resp.Header()
32-
33-
contentType := typesniffer.ApplicationOctetStream
34-
if opts.ContentType != "" {
35-
if opts.ContentTypeCharset != "" {
36-
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
37-
} else {
38-
contentType = opts.ContentType
39-
}
40-
}
41-
header.Set("Content-Type", contentType)
42-
header.Set("X-Content-Type-Options", "nosniff")
43-
44-
if opts.ContentLength != nil {
45-
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
46-
}
47-
48-
if opts.Filename != "" {
49-
disposition := opts.Disposition
50-
if disposition == "" {
51-
disposition = "attachment"
52-
}
53-
54-
backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
55-
header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
56-
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
57-
}
58-
59-
duration := opts.CacheDuration
60-
if duration == 0 {
61-
duration = 5 * time.Minute
62-
}
63-
httpcache.SetCacheControlInHeader(header, duration)
13+
type ServeHeaderOptions httplib.ServeHeaderOptions
6414

65-
if !opts.LastModified.IsZero() {
66-
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
67-
}
15+
func (ctx *Context) SetServeHeaders(opt *ServeHeaderOptions) {
16+
httplib.ServeSetHeaders(ctx.Resp, (*httplib.ServeHeaderOptions)(opt))
6817
}
6918

7019
// ServeContent serves content to http request
7120
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
72-
ctx.SetServeHeaders(opts)
21+
httplib.ServeSetHeaders(ctx.Resp, (*httplib.ServeHeaderOptions)(opts))
7322
http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r)
7423
}
File renamed without changes.

modules/httplib/serve.go

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package httplib
5+
6+
import (
7+
"bytes"
8+
"errors"
9+
"fmt"
10+
"io"
11+
"net/http"
12+
"net/url"
13+
"path"
14+
"path/filepath"
15+
"strconv"
16+
"strings"
17+
"time"
18+
19+
charsetModule "code.gitea.io/gitea/modules/charset"
20+
"code.gitea.io/gitea/modules/httpcache"
21+
"code.gitea.io/gitea/modules/log"
22+
"code.gitea.io/gitea/modules/setting"
23+
"code.gitea.io/gitea/modules/typesniffer"
24+
"code.gitea.io/gitea/modules/util"
25+
)
26+
27+
type ServeHeaderOptions struct {
28+
ContentType string // defaults to "application/octet-stream"
29+
ContentTypeCharset string
30+
ContentLength *int64
31+
Disposition string // defaults to "attachment"
32+
Filename string
33+
CacheDuration time.Duration // defaults to 5 minutes
34+
LastModified time.Time
35+
}
36+
37+
// ServeSetHeaders sets necessary content serve headers
38+
func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
39+
header := w.Header()
40+
41+
contentType := typesniffer.ApplicationOctetStream
42+
if opts.ContentType != "" {
43+
if opts.ContentTypeCharset != "" {
44+
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
45+
} else {
46+
contentType = opts.ContentType
47+
}
48+
}
49+
header.Set("Content-Type", contentType)
50+
header.Set("X-Content-Type-Options", "nosniff")
51+
52+
if opts.ContentLength != nil {
53+
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
54+
}
55+
56+
if opts.Filename != "" {
57+
disposition := opts.Disposition
58+
if disposition == "" {
59+
disposition = "attachment"
60+
}
61+
62+
backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
63+
header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
64+
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
65+
}
66+
67+
duration := opts.CacheDuration
68+
if duration == 0 {
69+
duration = 5 * time.Minute
70+
}
71+
httpcache.SetCacheControlInHeader(header, duration)
72+
73+
if !opts.LastModified.IsZero() {
74+
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
75+
}
76+
}
77+
78+
// ServeData download file from io.Reader
79+
func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath string, mineBuf []byte) error {
80+
// do not set "Content-Length", because the length could only be set by callers, and it needs to support range requests
81+
opts := &ServeHeaderOptions{
82+
Filename: path.Base(filePath),
83+
}
84+
85+
sniffedType := typesniffer.DetectContentType(mineBuf)
86+
isPlain := sniffedType.IsText() || r.FormValue("render") != ""
87+
88+
if setting.MimeTypeMap.Enabled {
89+
fileExtension := strings.ToLower(filepath.Ext(filePath))
90+
opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
91+
}
92+
93+
if opts.ContentType == "" {
94+
if sniffedType.IsBrowsableBinaryType() {
95+
opts.ContentType = sniffedType.GetMimeType()
96+
} else if isPlain {
97+
opts.ContentType = "text/plain"
98+
} else {
99+
opts.ContentType = typesniffer.ApplicationOctetStream
100+
}
101+
}
102+
103+
if isPlain {
104+
charset, err := charsetModule.DetectEncoding(mineBuf)
105+
if err != nil {
106+
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
107+
charset = "utf-8"
108+
}
109+
opts.ContentTypeCharset = strings.ToLower(charset)
110+
}
111+
112+
isSVG := sniffedType.IsSvgImage()
113+
114+
// serve types that can present a security risk with CSP
115+
if isSVG {
116+
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
117+
} else if sniffedType.IsPDF() {
118+
// no sandbox attribute for pdf as it breaks rendering in at least safari. this
119+
// should generally be safe as scripts inside PDF can not escape the PDF document
120+
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
121+
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
122+
}
123+
124+
opts.Disposition = "inline"
125+
if isSVG && !setting.UI.SVG.Enabled {
126+
opts.Disposition = "attachment"
127+
}
128+
129+
ServeSetHeaders(w, opts)
130+
return nil
131+
}
132+
133+
const mimeDetectionBufferLen = 1024
134+
135+
func ServeContentByReader(r *http.Request, w http.ResponseWriter, filePath string, size int64, reader io.Reader) error {
136+
buf := make([]byte, mimeDetectionBufferLen)
137+
n, err := util.ReadAtMost(reader, buf)
138+
if err != nil {
139+
return err
140+
}
141+
if n >= 0 {
142+
buf = buf[:n]
143+
}
144+
if err = setServeHeadersByFile(r, w, filePath, buf); err != nil {
145+
return err
146+
}
147+
148+
// reset the reader to the beginning
149+
reader = io.MultiReader(bytes.NewReader(buf), reader)
150+
151+
rangeHeader := r.Header.Get("Range")
152+
153+
// if no size or no supported range, serve as 200 (complete response)
154+
if size <= 0 || !strings.HasPrefix(rangeHeader, "bytes=") {
155+
if size >= 0 {
156+
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
157+
}
158+
_, err = io.Copy(w, reader)
159+
return err
160+
}
161+
162+
// do our best to support the minimal "Range" request (no support for multiple range: "Range: bytes=0-50, 100-150")
163+
//
164+
// GET /...
165+
// Range: bytes=0-1023
166+
//
167+
// HTTP/1.1 206 Partial Content
168+
// Content-Range: bytes 0-1023/146515
169+
// Content-Length: 1024
170+
171+
_, rangeParts, _ := strings.Cut(rangeHeader, "=")
172+
rangeBytesStart, rangeBytesEnd, found := strings.Cut(rangeParts, "-")
173+
start, err := strconv.ParseInt(rangeBytesStart, 10, 64)
174+
if err != nil || start < 0 || start >= size {
175+
http.Error(w, err.Error(), http.StatusBadRequest)
176+
return errors.New("invalid start range")
177+
}
178+
end, err := strconv.ParseInt(rangeBytesEnd, 10, 64)
179+
if rangeBytesEnd == "" && found {
180+
err = nil
181+
end = size - 1
182+
}
183+
if err != nil || end < start || end >= size {
184+
http.Error(w, err.Error(), http.StatusBadRequest)
185+
return errors.New("invalid end range")
186+
}
187+
188+
partialLength := end - start + 1
189+
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size))
190+
w.Header().Set("Content-Length", strconv.FormatInt(partialLength, 10))
191+
if _, err = io.CopyN(io.Discard, reader, start); err != nil {
192+
return fmt.Errorf("unable to skip first %d bytes: %w", start, err)
193+
}
194+
195+
w.WriteHeader(http.StatusPartialContent)
196+
_, err = io.CopyN(w, reader, partialLength)
197+
return err
198+
}
199+
200+
func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, filePath string, size int64, modTime time.Time, reader io.ReadSeeker) error {
201+
buf := make([]byte, mimeDetectionBufferLen)
202+
n, err := util.ReadAtMost(reader, buf)
203+
if err != nil {
204+
return err
205+
}
206+
if _, err = reader.Seek(0, io.SeekStart); err != nil {
207+
return err
208+
}
209+
if n >= 0 {
210+
buf = buf[:n]
211+
}
212+
if err = setServeHeadersByFile(r, w, filePath, buf); err != nil {
213+
return err
214+
}
215+
http.ServeContent(w, r, path.Base(filePath), modTime, reader)
216+
return nil
217+
}

modules/lfs/content_store.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import (
1818

1919
var (
2020
// ErrHashMismatch occurs if the content has does not match OID
21-
ErrHashMismatch = errors.New("Content hash does not match OID")
21+
ErrHashMismatch = errors.New("content hash does not match OID")
2222
// ErrSizeMismatch occurs if the content size does not match
23-
ErrSizeMismatch = errors.New("Content size does not match")
23+
ErrSizeMismatch = errors.New("content size does not match")
2424
)
2525

2626
// ContentStore provides a simple file system based storage.
@@ -105,7 +105,7 @@ func (s *ContentStore) Verify(pointer Pointer) (bool, error) {
105105
}
106106

107107
// ReadMetaObject will read a git_model.LFSMetaObject and return a reader
108-
func ReadMetaObject(pointer Pointer) (io.ReadCloser, error) {
108+
func ReadMetaObject(pointer Pointer) (io.ReadSeekCloser, error) {
109109
contentStore := NewContentStore()
110110
return contentStore.Get(pointer)
111111
}

routers/api/v1/repo/file.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
150150
return
151151
}
152152

153+
// FIXME: code from #19689, what if the file is large ... OOM ...
153154
buf, err := io.ReadAll(dataRc)
154155
if err != nil {
155156
_ = dataRc.Close()
@@ -164,31 +165,31 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
164165
// Check if the blob represents a pointer
165166
pointer, _ := lfs.ReadPointer(bytes.NewReader(buf))
166167

167-
// if its not a pointer just serve the data directly
168+
// if it's not a pointer, just serve the data directly
168169
if !pointer.IsValid() {
169170
// First handle caching for the blob
170171
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
171172
return
172173
}
173174

174175
// OK not cached - serve!
175-
if err := common.ServeData(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)); err != nil {
176+
if err := common.ServeContentByReader(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)); err != nil {
176177
ctx.ServerError("ServeBlob", err)
177178
}
178179
return
179180
}
180181

181-
// Now check if there is a meta object for this pointer
182+
// Now check if there is a MetaObject for this pointer
182183
meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
183184

184-
// If there isn't one just serve the data directly
185+
// If there isn't one, just serve the data directly
185186
if err == git_model.ErrLFSObjectNotExist {
186187
// Handle caching for the blob SHA (not the LFS object OID)
187188
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
188189
return
189190
}
190191

191-
if err := common.ServeData(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)); err != nil {
192+
if err := common.ServeContentByReader(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)); err != nil {
192193
ctx.ServerError("ServeBlob", err)
193194
}
194195
return
@@ -218,7 +219,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
218219
}
219220
defer lfsDataRc.Close()
220221

221-
if err := common.ServeData(ctx.Context, ctx.Repo.TreePath, meta.Size, lfsDataRc); err != nil {
222+
if err := common.ServeContentByReadSeeker(ctx.Context, ctx.Repo.TreePath, meta.Size, lastModified, lfsDataRc); err != nil {
222223
ctx.ServerError("ServeData", err)
223224
}
224225
}

0 commit comments

Comments
 (0)