支持Range/If-Range读取静态资源片段

This commit is contained in:
刘祥超
2021-01-10 22:35:34 +08:00
parent da4eef95f1
commit 4883fed093
4 changed files with 323 additions and 14 deletions

View File

@@ -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,20 +266,99 @@ 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以便于加快速度
defer func() {
_ = reader.Close()
pool.Put(buf)
}()
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)
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
}

View File

@@ -0,0 +1,127 @@
package nodes
import (
"crypto/rand"
"fmt"
"io"
"strconv"
"strings"
)
// 分解Range
func httpRequestParseContentRange(rangeValue string) (result [][]int, ok bool) {
// 参考RFChttps://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[:])
}

View File

@@ -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)
}
}

View File

@@ -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