From 4883fed0933e1ab7cac97b5a8c429c62f6561061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=A5=A5=E8=B6=85?= Date: Sun, 10 Jan 2021 22:35:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81Range/If-Range=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E9=9D=99=E6=80=81=E8=B5=84=E6=BA=90=E7=89=87=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/nodes/http_request_root.go | 150 ++++++++++++++++++++-- internal/nodes/http_request_utils.go | 127 ++++++++++++++++++ internal/nodes/http_request_utils_test.go | 55 ++++++++ internal/nodes/http_writer.go | 5 + 4 files changed, 323 insertions(+), 14 deletions(-) create mode 100644 internal/nodes/http_request_utils.go create mode 100644 internal/nodes/http_request_utils_test.go diff --git a/internal/nodes/http_request_root.go b/internal/nodes/http_request_root.go index 503adc3..f1a8110 100644 --- a/internal/nodes/http_request_root.go +++ b/internal/nodes/http_request_root.go @@ -150,6 +150,7 @@ func (this *HTTPRequest) doRoot() (isBreak bool) { respHeader := this.writer.Header() // mime type + contentType := "" if this.web.ResponseHeaderPolicy == nil || !this.web.ResponseHeaderPolicy.IsOn || !this.web.ResponseHeaderPolicy.ContainsHeader("CONTENT-TYPE") { ext := filepath.Ext(filePath) if len(ext) > 0 { @@ -167,11 +168,14 @@ func (this *HTTPRequest) doRoot() (isBreak bool) { if this.web.Charset.IsUpper { charset = strings.ToUpper(charset) } + contentType = mimeTypeKey + "; charset=" + charset respHeader.Set("Content-Type", mimeTypeKey+"; charset="+charset) } else { + contentType = mimeType respHeader.Set("Content-Type", mimeType) } } else { + contentType = mimeType respHeader.Set("Content-Type", mimeType) } } @@ -180,7 +184,6 @@ func (this *HTTPRequest) doRoot() (isBreak bool) { // length fileSize := stat.Size() - respHeader.Set("Content-Length", strconv.FormatInt(fileSize, 10)) // 支持 Last-Modified modifiedTime := stat.ModTime().Format("Mon, 02 Jan 2006 15:04:05 GMT") @@ -189,14 +192,11 @@ func (this *HTTPRequest) doRoot() (isBreak bool) { } // 支持 ETag - eTag := "\"et" + fmt.Sprintf("%0x", xxhash.Sum64String(filename+strconv.FormatInt(stat.ModTime().UnixNano(), 10)+strconv.FormatInt(stat.Size(), 10))) + "\"" + eTag := "\"e" + fmt.Sprintf("%0x", xxhash.Sum64String(filename+strconv.FormatInt(stat.ModTime().UnixNano(), 10)+strconv.FormatInt(stat.Size(), 10))) + "\"" if len(respHeader.Get("ETag")) == 0 { respHeader.Set("ETag", eTag) } - // proxy callback - // TODO - // 支持 If-None-Match if this.requestHeader("If-None-Match") == eTag { // 自定义Header @@ -213,8 +213,51 @@ func (this *HTTPRequest) doRoot() (isBreak bool) { return true } - // 自定义Header - this.processResponseHeaders(http.StatusOK) + // 支持Range + respHeader.Set("Accept-Ranges", "bytes") + ifRangeHeaders, ok := this.RawReq.Header["If-Range"] + supportRange := true + if ok { + supportRange = false + for _, v := range ifRangeHeaders { + if v == eTag || v == modifiedTime { + supportRange = true + } + } + } + + // 支持Range + rangeSet := [][]int{} + if supportRange { + contentRange := this.RawReq.Header.Get("Range") + if len(contentRange) > 0 { + set, ok := httpRequestParseContentRange(contentRange) + if !ok { + this.processResponseHeaders(http.StatusRequestedRangeNotSatisfiable) + this.writer.WriteHeader(http.StatusRequestedRangeNotSatisfiable) + return true + } + if len(set) > 0 { + rangeSet = set + for _, arr := range rangeSet { + if arr[0] == -1 { + arr[0] = int(fileSize) + arr[1] + arr[1] = int(fileSize) - 1 + + if arr[0] < 0 { + this.processResponseHeaders(http.StatusRequestedRangeNotSatisfiable) + this.writer.WriteHeader(http.StatusRequestedRangeNotSatisfiable) + return true + } + } + } + } + } else { + respHeader.Set("Content-Length", strconv.FormatInt(fileSize, 10)) + } + } else { + respHeader.Set("Content-Length", strconv.FormatInt(fileSize, 10)) + } reader, err := os.OpenFile(filePath, os.O_RDONLY, 0444) if err != nil { @@ -223,19 +266,98 @@ func (this *HTTPRequest) doRoot() (isBreak bool) { return true } + // 自定义Header + this.processResponseHeaders(http.StatusOK) + + // 在Range请求中不能缓存 + if len(rangeSet) > 0 { + this.cacheRef = nil // 不支持缓存 + } + this.writer.Prepare(fileSize) pool := this.bytePool(fileSize) buf := pool.Get() - _, err = io.CopyBuffer(this.writer, reader, buf) - pool.Put(buf) + defer func() { + _ = reader.Close() + pool.Put(buf) + }() - // 不使用defer,以便于加快速度 - _ = reader.Close() + if len(rangeSet) == 1 { + respHeader.Set("Content-Range", "bytes "+strconv.Itoa(rangeSet[0][0])+"-"+strconv.Itoa(rangeSet[0][1])+"/"+strconv.FormatInt(fileSize, 10)) + this.writer.WriteHeader(http.StatusPartialContent) - if err != nil { - logs.Error(err) - return true + ok, err := httpRequestReadRange(reader, buf, rangeSet[0][0], rangeSet[0][1], func(buf []byte, n int) error { + _, err := this.writer.Write(buf[:n]) + return err + }) + if err != nil { + logs.Error(err) + return true + } + if !ok { + this.processResponseHeaders(http.StatusRequestedRangeNotSatisfiable) + this.writer.WriteHeader(http.StatusRequestedRangeNotSatisfiable) + return true + } + } else if len(rangeSet) > 1 { + boundary := httpRequestGenBoundary() + respHeader.Set("Content-Type", "multipart/byteranges; boundary="+boundary) + + this.writer.WriteHeader(http.StatusPartialContent) + + for index, set := range rangeSet { + if index == 0 { + _, err = this.writer.WriteString("--" + boundary + "\r\n") + } else { + _, err = this.writer.WriteString("\r\n--" + boundary + "\r\n") + } + if err != nil { + logs.Error(err) + return true + } + + _, err = this.writer.WriteString("Content-Range: " + "bytes " + strconv.Itoa(set[0]) + "-" + strconv.Itoa(set[1]) + "/" + strconv.FormatInt(fileSize, 10) + "\r\n") + if err != nil { + logs.Error(err) + return true + } + + if len(contentType) > 0 { + _, err = this.writer.WriteString("Content-Type: " + contentType + "\r\n\r\n") + if err != nil { + logs.Error(err) + return true + } + } + + ok, err := httpRequestReadRange(reader, buf, set[0], set[1], func(buf []byte, n int) error { + _, err := this.writer.Write(buf[:n]) + return err + }) + if err != nil { + logs.Error(err) + return true + } + if !ok { + this.processResponseHeaders(http.StatusRequestedRangeNotSatisfiable) + this.writer.WriteHeader(http.StatusRequestedRangeNotSatisfiable) + return true + } + } + + _, err = this.writer.WriteString("\r\n--" + boundary + "--\r\n") + if err != nil { + logs.Error(err) + return true + } + } else { + _, err = io.CopyBuffer(this.writer, reader, buf) + + if err != nil { + logs.Error(err) + return true + } } return true diff --git a/internal/nodes/http_request_utils.go b/internal/nodes/http_request_utils.go new file mode 100644 index 0000000..575b438 --- /dev/null +++ b/internal/nodes/http_request_utils.go @@ -0,0 +1,127 @@ +package nodes + +import ( + "crypto/rand" + "fmt" + "io" + "strconv" + "strings" +) + +// 分解Range +func httpRequestParseContentRange(rangeValue string) (result [][]int, ok bool) { + // 参考RFC:https://tools.ietf.org/html/rfc7233 + index := strings.Index(rangeValue, "=") + if index == -1 { + return + } + unit := rangeValue[:index] + if unit != "bytes" { + return + } + + rangeSetString := rangeValue[index+1:] + if len(rangeSetString) == 0 { + ok = true + return + } + + pieces := strings.Split(rangeSetString, ", ") + for _, piece := range pieces { + index := strings.Index(piece, "-") + if index == -1 { + return + } + first := piece[:index] + firstInt := -1 + + var err error + last := piece[index+1:] + var lastInt = -1 + + if len(first) > 0 { + firstInt, err = strconv.Atoi(first) + if err != nil { + return + } + + if len(last) > 0 { + lastInt, err = strconv.Atoi(last) + if err != nil { + return + } + if lastInt < firstInt { + return + } + } + } else { + if len(last) == 0 { + return + } + + lastInt, err = strconv.Atoi(last) + if err != nil { + return + } + lastInt = -lastInt + } + + result = append(result, []int{firstInt, lastInt}) + } + + ok = true + return +} + +// 读取内容Range +func httpRequestReadRange(reader io.Reader, buf []byte, start int, end int, callback func(buf []byte, n int) error) (ok bool, err error) { + if start < 0 || end < 0 { + return + } + seeker, ok := reader.(io.Seeker) + if !ok { + return + } + _, err = seeker.Seek(int64(start), io.SeekStart) + if err != nil { + return false, nil + } + + offset := start + for { + n, err := reader.Read(buf) + if n > 0 { + offset += n + if end < offset { + err = callback(buf, n-(offset-end-1)) + if err != nil { + return false, err + } + return true, nil + } else { + err = callback(buf, n) + if err != nil { + return false, err + } + } + } + + if err != nil { + if err == io.EOF { + return true, nil + } + return false, err + } + } +} + +// 生成boundary +// 仿照Golang自带的函数(multipart包) +func httpRequestGenBoundary() string { + var buf [30]byte + _, err := io.ReadFull(rand.Reader, buf[:]) + if err != nil { + panic(err) + } + return fmt.Sprintf("%x", buf[:]) +} diff --git a/internal/nodes/http_request_utils_test.go b/internal/nodes/http_request_utils_test.go new file mode 100644 index 0000000..1c0d816 --- /dev/null +++ b/internal/nodes/http_request_utils_test.go @@ -0,0 +1,55 @@ +package nodes + +import ( + "github.com/iwind/TeaGo/assert" + "testing" +) + +func TestHTTPRequest_httpRequestParseContentRange(t *testing.T) { + a := assert.NewAssertion(t) + { + _, ok := httpRequestParseContentRange("") + a.IsFalse(ok) + } + { + _, ok := httpRequestParseContentRange("byte=") + a.IsFalse(ok) + } + { + _, ok := httpRequestParseContentRange("byte=") + a.IsFalse(ok) + } + { + set, ok := httpRequestParseContentRange("bytes=") + a.IsTrue(ok) + a.IsTrue(len(set) == 0) + } + { + _, ok := httpRequestParseContentRange("bytes=60-50") + a.IsFalse(ok) + } + { + set, ok := httpRequestParseContentRange("bytes=0-50") + a.IsTrue(ok) + a.IsTrue(len(set) > 0) + t.Log(set) + } + { + set, ok := httpRequestParseContentRange("bytes=0-") + a.IsTrue(ok) + a.IsTrue(len(set) > 0) + t.Log(set) + } + { + set, ok := httpRequestParseContentRange("bytes=-50") + a.IsTrue(ok) + a.IsTrue(len(set) > 0) + t.Log(set) + } + { + set, ok := httpRequestParseContentRange("bytes=0-50, 60-100") + a.IsTrue(ok) + a.IsTrue(len(set) > 0) + t.Log(set) + } +} diff --git a/internal/nodes/http_writer.go b/internal/nodes/http_writer.go index fba81c0..ac73c14 100644 --- a/internal/nodes/http_writer.go +++ b/internal/nodes/http_writer.go @@ -307,6 +307,11 @@ func (this *HTTPWriter) prepareCache(size int64) { return } + // 不支持Range + if len(this.Header().Get("Content-Range")) > 0 { + return + } + cachePolicy := sharedNodeConfig.HTTPCachePolicy if cachePolicy == nil || !cachePolicy.IsOn { return