191 lines
4.5 KiB
Go
191 lines
4.5 KiB
Go
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
|
//go:build plus
|
|
|
|
package nodes
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/TeaOSLab/EdgeNode/internal/compressions"
|
|
"github.com/TeaOSLab/EdgeNode/internal/utils/readers"
|
|
"github.com/iwind/TeaGo/types"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
defaultM3u8EncryptParam = "ge_m3u8_token"
|
|
m3u8EncryptExtension = ".ge-m3u8-key"
|
|
)
|
|
|
|
// process hls
|
|
func (this *HTTPRequest) processHLSBefore() (blocked bool) {
|
|
var requestPath = this.RawReq.URL.Path
|
|
|
|
// .ge-m3u8-key
|
|
if !strings.HasSuffix(requestPath, m3u8EncryptExtension) {
|
|
return
|
|
}
|
|
|
|
blocked = true
|
|
|
|
tokenBytes, base64Err := base64.StdEncoding.DecodeString(this.RawReq.URL.Query().Get(defaultM3u8EncryptParam))
|
|
if base64Err != nil {
|
|
this.writeCode(http.StatusBadRequest, "invalid m3u8 token: bad format", "invalid m3u8 token: bad format")
|
|
return
|
|
}
|
|
|
|
if len(tokenBytes) != 32 {
|
|
this.writeCode(http.StatusBadRequest, "invalid m3u8 token: bad length", "invalid m3u8 token: bad length")
|
|
return
|
|
}
|
|
|
|
_, _ = this.writer.Write(tokenBytes[:16])
|
|
|
|
return
|
|
}
|
|
|
|
// process m3u8 file
|
|
func (this *HTTPRequest) processM3u8Response(resp *http.Response) error {
|
|
var requestPath = this.RawReq.URL.Path
|
|
|
|
// .m3u8
|
|
if strings.HasSuffix(requestPath, ".m3u8") &&
|
|
this.web.HLS.Encrypting.MatchURL(this.URL()) {
|
|
return this.processM3u8File(resp)
|
|
}
|
|
|
|
// .ts
|
|
if strings.HasSuffix(requestPath, ".ts") {
|
|
var token = this.RawReq.URL.Query().Get(defaultM3u8EncryptParam)
|
|
if len(token) > 0 {
|
|
return this.processTSFile(resp, token)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (this *HTTPRequest) processTSFile(resp *http.Response, token string) error {
|
|
rawToken, err := base64.StdEncoding.DecodeString(token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(rawToken) != 32 {
|
|
return errors.New("invalid token length")
|
|
}
|
|
|
|
var key = rawToken[:16]
|
|
var iv = rawToken[16:]
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return fmt.Errorf("create cipher failed: %w", err)
|
|
}
|
|
var stream = cipher.NewCBCEncrypter(block, iv)
|
|
var reader = readers.NewFilterReaderCloser(resp.Body)
|
|
var blockSize = stream.BlockSize()
|
|
reader.Add(func(p []byte, readErr error) error {
|
|
var l = len(p)
|
|
if l == 0 {
|
|
return nil
|
|
}
|
|
|
|
var mod = l % blockSize
|
|
if mod != 0 {
|
|
p = append(p, bytes.Repeat([]byte{'0'}, blockSize-mod)...)
|
|
}
|
|
stream.CryptBlocks(p, p)
|
|
return readErr
|
|
})
|
|
resp.Body = reader
|
|
|
|
return nil
|
|
}
|
|
|
|
func (this *HTTPRequest) processM3u8File(resp *http.Response) error {
|
|
const maxSize = 1 << 20
|
|
|
|
// 检查内容长度
|
|
if resp.Body == nil || resp.ContentLength == 0 || resp.ContentLength > maxSize {
|
|
return nil
|
|
}
|
|
|
|
// 解压缩
|
|
compressions.WrapHTTPResponse(resp)
|
|
|
|
// 读取内容
|
|
data, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
|
|
if err != nil {
|
|
_ = resp.Body.Close()
|
|
return err
|
|
}
|
|
|
|
// 超出尺寸直接返回
|
|
if len(data) == maxSize {
|
|
resp.Body = io.NopCloser(io.MultiReader(bytes.NewBuffer(data), resp.Body))
|
|
return nil
|
|
}
|
|
|
|
var lines = bytes.Split(data, []byte{'\n'})
|
|
var addedKey = false
|
|
|
|
var ivBytes = make([]byte, 16)
|
|
var keyBytes = make([]byte, 16)
|
|
|
|
_, ivErr := rand.Read(ivBytes)
|
|
_, keyErr := rand.Read(keyBytes)
|
|
if ivErr != nil || keyErr != nil {
|
|
resp.Body = io.NopCloser(bytes.NewBuffer(data))
|
|
return nil
|
|
}
|
|
|
|
var ivString = fmt.Sprintf("%x", ivBytes)
|
|
var token = url.QueryEscape(base64.StdEncoding.EncodeToString(append(keyBytes, ivBytes...)))
|
|
for index, line := range lines {
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
|
|
if bytes.HasPrefix(line, []byte("#EXT-X-KEY")) {
|
|
goto returnData
|
|
}
|
|
|
|
if !addedKey && bytes.HasPrefix(line, []byte("#EXTINF")) {
|
|
this.URL()
|
|
var keyPath = strings.TrimSuffix(this.RawReq.URL.Path, ".m3u8") + m3u8EncryptExtension + "?" + defaultM3u8EncryptParam + "=" + token
|
|
lines[index] = append([]byte("#EXT-X-KEY:METHOD=AES-128,URI=\""+this.requestScheme()+"://"+this.ReqHost+keyPath+"\",IV=0x"+ivString+
|
|
"\n"), line...)
|
|
addedKey = true
|
|
continue
|
|
}
|
|
|
|
if line[0] != '#' && bytes.Contains(line, []byte(".ts")) {
|
|
if bytes.ContainsRune(line, '?') {
|
|
lines[index] = append(line, []byte("&"+defaultM3u8EncryptParam+"="+token)...)
|
|
} else {
|
|
lines[index] = append(line, []byte("?"+defaultM3u8EncryptParam+"="+token)...)
|
|
}
|
|
}
|
|
}
|
|
|
|
if addedKey {
|
|
this.tags = append(this.tags, "m3u8")
|
|
}
|
|
|
|
returnData:
|
|
data = bytes.Join(lines, []byte{'\n'})
|
|
resp.Body = io.NopCloser(bytes.NewBuffer(data))
|
|
resp.ContentLength = int64(len(data))
|
|
resp.Header.Set("Content-Length", types.String(resp.ContentLength))
|
|
|
|
return nil
|
|
}
|