mirror of
https://github.com/TeaOSLab/EdgeNode.git
synced 2025-11-14 07:30:25 +08:00
支持Range/If-Range读取静态资源片段
This commit is contained in:
@@ -150,6 +150,7 @@ func (this *HTTPRequest) doRoot() (isBreak bool) {
|
|||||||
respHeader := this.writer.Header()
|
respHeader := this.writer.Header()
|
||||||
|
|
||||||
// mime type
|
// mime type
|
||||||
|
contentType := ""
|
||||||
if this.web.ResponseHeaderPolicy == nil || !this.web.ResponseHeaderPolicy.IsOn || !this.web.ResponseHeaderPolicy.ContainsHeader("CONTENT-TYPE") {
|
if this.web.ResponseHeaderPolicy == nil || !this.web.ResponseHeaderPolicy.IsOn || !this.web.ResponseHeaderPolicy.ContainsHeader("CONTENT-TYPE") {
|
||||||
ext := filepath.Ext(filePath)
|
ext := filepath.Ext(filePath)
|
||||||
if len(ext) > 0 {
|
if len(ext) > 0 {
|
||||||
@@ -167,11 +168,14 @@ func (this *HTTPRequest) doRoot() (isBreak bool) {
|
|||||||
if this.web.Charset.IsUpper {
|
if this.web.Charset.IsUpper {
|
||||||
charset = strings.ToUpper(charset)
|
charset = strings.ToUpper(charset)
|
||||||
}
|
}
|
||||||
|
contentType = mimeTypeKey + "; charset=" + charset
|
||||||
respHeader.Set("Content-Type", mimeTypeKey+"; charset="+charset)
|
respHeader.Set("Content-Type", mimeTypeKey+"; charset="+charset)
|
||||||
} else {
|
} else {
|
||||||
|
contentType = mimeType
|
||||||
respHeader.Set("Content-Type", mimeType)
|
respHeader.Set("Content-Type", mimeType)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
contentType = mimeType
|
||||||
respHeader.Set("Content-Type", mimeType)
|
respHeader.Set("Content-Type", mimeType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +184,6 @@ func (this *HTTPRequest) doRoot() (isBreak bool) {
|
|||||||
|
|
||||||
// length
|
// length
|
||||||
fileSize := stat.Size()
|
fileSize := stat.Size()
|
||||||
respHeader.Set("Content-Length", strconv.FormatInt(fileSize, 10))
|
|
||||||
|
|
||||||
// 支持 Last-Modified
|
// 支持 Last-Modified
|
||||||
modifiedTime := stat.ModTime().Format("Mon, 02 Jan 2006 15:04:05 GMT")
|
modifiedTime := stat.ModTime().Format("Mon, 02 Jan 2006 15:04:05 GMT")
|
||||||
@@ -189,14 +192,11 @@ func (this *HTTPRequest) doRoot() (isBreak bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 支持 ETag
|
// 支持 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 {
|
if len(respHeader.Get("ETag")) == 0 {
|
||||||
respHeader.Set("ETag", eTag)
|
respHeader.Set("ETag", eTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// proxy callback
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
// 支持 If-None-Match
|
// 支持 If-None-Match
|
||||||
if this.requestHeader("If-None-Match") == eTag {
|
if this.requestHeader("If-None-Match") == eTag {
|
||||||
// 自定义Header
|
// 自定义Header
|
||||||
@@ -213,8 +213,51 @@ func (this *HTTPRequest) doRoot() (isBreak bool) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义Header
|
// 支持Range
|
||||||
this.processResponseHeaders(http.StatusOK)
|
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)
|
reader, err := os.OpenFile(filePath, os.O_RDONLY, 0444)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -223,19 +266,98 @@ func (this *HTTPRequest) doRoot() (isBreak bool) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自定义Header
|
||||||
|
this.processResponseHeaders(http.StatusOK)
|
||||||
|
|
||||||
|
// 在Range请求中不能缓存
|
||||||
|
if len(rangeSet) > 0 {
|
||||||
|
this.cacheRef = nil // 不支持缓存
|
||||||
|
}
|
||||||
|
|
||||||
this.writer.Prepare(fileSize)
|
this.writer.Prepare(fileSize)
|
||||||
|
|
||||||
pool := this.bytePool(fileSize)
|
pool := this.bytePool(fileSize)
|
||||||
buf := pool.Get()
|
buf := pool.Get()
|
||||||
_, err = io.CopyBuffer(this.writer, reader, buf)
|
defer func() {
|
||||||
pool.Put(buf)
|
_ = reader.Close()
|
||||||
|
pool.Put(buf)
|
||||||
|
}()
|
||||||
|
|
||||||
// 不使用defer,以便于加快速度
|
if len(rangeSet) == 1 {
|
||||||
_ = reader.Close()
|
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 {
|
ok, err := httpRequestReadRange(reader, buf, rangeSet[0][0], rangeSet[0][1], func(buf []byte, n int) error {
|
||||||
logs.Error(err)
|
_, err := this.writer.Write(buf[:n])
|
||||||
return true
|
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
|
return true
|
||||||
|
|||||||
127
internal/nodes/http_request_utils.go
Normal file
127
internal/nodes/http_request_utils.go
Normal file
@@ -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[:])
|
||||||
|
}
|
||||||
55
internal/nodes/http_request_utils_test.go
Normal file
55
internal/nodes/http_request_utils_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -307,6 +307,11 @@ func (this *HTTPWriter) prepareCache(size int64) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 不支持Range
|
||||||
|
if len(this.Header().Get("Content-Range")) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cachePolicy := sharedNodeConfig.HTTPCachePolicy
|
cachePolicy := sharedNodeConfig.HTTPCachePolicy
|
||||||
if cachePolicy == nil || !cachePolicy.IsOn {
|
if cachePolicy == nil || !cachePolicy.IsOn {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user