-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make repository response support HTTP range request #24592
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
af7b92d
fix
wxiaoguang 7466f24
add test
wxiaoguang 2ee853c
fix error handling
wxiaoguang cd980df
Merge branch 'main' into support-range-request
wxiaoguang 15cdc92
Update modules/httplib/serve.go
wxiaoguang 1928b83
Merge branch 'main' into support-range-request
GiteaBot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// Copyright 2023 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package httplib | ||
|
||
import ( | ||
"bytes" | ||
"net/http" | ||
) | ||
|
||
type MockResponseWriter struct { | ||
header http.Header | ||
|
||
StatusCode int | ||
BodyBuffer bytes.Buffer | ||
} | ||
|
||
func (m *MockResponseWriter) Header() http.Header { | ||
return m.header | ||
} | ||
|
||
func (m *MockResponseWriter) Write(bytes []byte) (int, error) { | ||
if m.StatusCode == 0 { | ||
m.StatusCode = http.StatusOK | ||
} | ||
return m.BodyBuffer.Write(bytes) | ||
} | ||
|
||
func (m *MockResponseWriter) WriteHeader(statusCode int) { | ||
m.StatusCode = statusCode | ||
} | ||
|
||
func NewMockResponseWriter() *MockResponseWriter { | ||
return &MockResponseWriter{header: http.Header{}} | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
// Copyright 2023 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package httplib | ||
|
||
import ( | ||
"bytes" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"path" | ||
"path/filepath" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
charsetModule "code.gitea.io/gitea/modules/charset" | ||
"code.gitea.io/gitea/modules/httpcache" | ||
"code.gitea.io/gitea/modules/log" | ||
"code.gitea.io/gitea/modules/setting" | ||
"code.gitea.io/gitea/modules/typesniffer" | ||
"code.gitea.io/gitea/modules/util" | ||
) | ||
|
||
type ServeHeaderOptions struct { | ||
ContentType string // defaults to "application/octet-stream" | ||
ContentTypeCharset string | ||
ContentLength *int64 | ||
Disposition string // defaults to "attachment" | ||
Filename string | ||
CacheDuration time.Duration // defaults to 5 minutes | ||
LastModified time.Time | ||
} | ||
|
||
// ServeSetHeaders sets necessary content serve headers | ||
func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { | ||
header := w.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.ContentLength != nil { | ||
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10)) | ||
} | ||
|
||
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.SetCacheControlInHeader(header, duration) | ||
|
||
if !opts.LastModified.IsZero() { | ||
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat)) | ||
} | ||
} | ||
|
||
// ServeData download file from io.Reader | ||
func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath string, mineBuf []byte) { | ||
// do not set "Content-Length", because the length could only be set by callers, and it needs to support range requests | ||
opts := &ServeHeaderOptions{ | ||
Filename: path.Base(filePath), | ||
} | ||
|
||
sniffedType := typesniffer.DetectContentType(mineBuf) | ||
|
||
// the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later | ||
isPlain := sniffedType.IsText() || r.FormValue("render") != "" | ||
|
||
if setting.MimeTypeMap.Enabled { | ||
fileExtension := strings.ToLower(filepath.Ext(filePath)) | ||
opts.ContentType = setting.MimeTypeMap.Map[fileExtension] | ||
} | ||
|
||
if opts.ContentType == "" { | ||
if sniffedType.IsBrowsableBinaryType() { | ||
opts.ContentType = sniffedType.GetMimeType() | ||
} else if isPlain { | ||
opts.ContentType = "text/plain" | ||
} else { | ||
opts.ContentType = typesniffer.ApplicationOctetStream | ||
} | ||
} | ||
|
||
if isPlain { | ||
charset, err := charsetModule.DetectEncoding(mineBuf) | ||
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) | ||
} | ||
|
||
isSVG := sniffedType.IsSvgImage() | ||
|
||
// serve types that can present a security risk with CSP | ||
if isSVG { | ||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") | ||
} else if sniffedType.IsPDF() { | ||
// no sandbox attribute for pdf as it breaks rendering in at least safari. this | ||
// should generally be safe as scripts inside PDF can not escape the PDF document | ||
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion | ||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") | ||
} | ||
|
||
opts.Disposition = "inline" | ||
if isSVG && !setting.UI.SVG.Enabled { | ||
opts.Disposition = "attachment" | ||
} | ||
|
||
ServeSetHeaders(w, opts) | ||
} | ||
|
||
const mimeDetectionBufferLen = 1024 | ||
|
||
func ServeContentByReader(r *http.Request, w http.ResponseWriter, filePath string, size int64, reader io.Reader) { | ||
buf := make([]byte, mimeDetectionBufferLen) | ||
n, err := util.ReadAtMost(reader, buf) | ||
if err != nil { | ||
http.Error(w, "serve content: unable to pre-read", http.StatusRequestedRangeNotSatisfiable) | ||
return | ||
} | ||
if n >= 0 { | ||
buf = buf[:n] | ||
} | ||
setServeHeadersByFile(r, w, filePath, buf) | ||
|
||
// reset the reader to the beginning | ||
reader = io.MultiReader(bytes.NewReader(buf), reader) | ||
|
||
rangeHeader := r.Header.Get("Range") | ||
|
||
// if no size or no supported range, serve as 200 (complete response) | ||
if size <= 0 || !strings.HasPrefix(rangeHeader, "bytes=") { | ||
if size >= 0 { | ||
w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) | ||
} | ||
_, _ = io.Copy(w, reader) // just like http.ServeContent, not necessary to handle the error | ||
return | ||
} | ||
|
||
// do our best to support the minimal "Range" request (no support for multiple range: "Range: bytes=0-50, 100-150") | ||
// | ||
// GET /... | ||
// Range: bytes=0-1023 | ||
// | ||
// HTTP/1.1 206 Partial Content | ||
// Content-Range: bytes 0-1023/146515 | ||
// Content-Length: 1024 | ||
|
||
_, rangeParts, _ := strings.Cut(rangeHeader, "=") | ||
rangeBytesStart, rangeBytesEnd, found := strings.Cut(rangeParts, "-") | ||
start, err := strconv.ParseInt(rangeBytesStart, 10, 64) | ||
if start < 0 || start >= size { | ||
err = errors.New("invalid start range") | ||
} | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable) | ||
return | ||
} | ||
end, err := strconv.ParseInt(rangeBytesEnd, 10, 64) | ||
if rangeBytesEnd == "" && found { | ||
err = nil | ||
end = size - 1 | ||
} | ||
if end >= size { | ||
end = size - 1 | ||
} | ||
if end < start { | ||
err = errors.New("invalid end range") | ||
} | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusBadRequest) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC StatusRequestedRangeNotSatisfiable is only for "start is out of range" |
||
return | ||
} | ||
|
||
partialLength := end - start + 1 | ||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size)) | ||
w.Header().Set("Content-Length", strconv.FormatInt(partialLength, 10)) | ||
if _, err = io.CopyN(io.Discard, reader, start); err != nil { | ||
http.Error(w, "serve content: unable to skip", http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
w.WriteHeader(http.StatusPartialContent) | ||
_, _ = io.CopyN(w, reader, partialLength) // just like http.ServeContent, not necessary to handle the error | ||
} | ||
|
||
func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, filePath string, modTime time.Time, reader io.ReadSeeker) { | ||
buf := make([]byte, mimeDetectionBufferLen) | ||
n, err := util.ReadAtMost(reader, buf) | ||
if err != nil { | ||
http.Error(w, "serve content: unable to read", http.StatusInternalServerError) | ||
return | ||
} | ||
if _, err = reader.Seek(0, io.SeekStart); err != nil { | ||
http.Error(w, "serve content: unable to seek", http.StatusInternalServerError) | ||
return | ||
} | ||
if n >= 0 { | ||
buf = buf[:n] | ||
} | ||
setServeHeadersByFile(r, w, filePath, buf) | ||
http.ServeContent(w, r, path.Base(filePath), modTime, reader) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference to
http.ServeContent
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
http.ServeContent
only works withio.ReadSeeker
, can't be used bycommon.ServeBlob
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this
ServeContentByReader
is the old function (only gets added range support), the original function iscommon.ServeData
The
ServerContentBySeekReader
below is a new function usinghttp.ServeContent