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()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 不支持Range
|
||||
if len(this.Header().Get("Content-Range")) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cachePolicy := sharedNodeConfig.HTTPCachePolicy
|
||||
if cachePolicy == nil || !cachePolicy.IsOn {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user