parent
7b1fa07cdf
commit
1519511baf
60
download.go
60
download.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -31,6 +32,7 @@ import (
|
|||
type TsInfo struct {
|
||||
Name string
|
||||
Url string
|
||||
Seq uint64 // 如果是aes加密并且没有iv, 这个seq需要充当iv
|
||||
}
|
||||
|
||||
type GetProgress_Resp struct {
|
||||
|
|
@ -180,6 +182,7 @@ func (this *downloadEnv) RunDownload(req RunDownload_Req) (resp RunDownload_Resp
|
|||
return resp
|
||||
}
|
||||
}
|
||||
beginSeq := parseBeginSeq(m3u8Body)
|
||||
// 获取m3u8地址的内容体
|
||||
encInfo, err := this.getEncryptInfo(req.M3u8Url, string(m3u8Body))
|
||||
if err != nil {
|
||||
|
|
@ -188,7 +191,7 @@ func (this *downloadEnv) RunDownload(req RunDownload_Req) (resp RunDownload_Resp
|
|||
return resp
|
||||
}
|
||||
this.SetProgressBarTitle("[3/6]获取ts列表")
|
||||
tsList, errMsg := getTsList(req.M3u8Url, string(m3u8Body))
|
||||
tsList, errMsg := getTsList(beginSeq, req.M3u8Url, string(m3u8Body))
|
||||
if errMsg != "" {
|
||||
resp.ErrMsg = "获取ts列表错误: " + errMsg
|
||||
return resp
|
||||
|
|
@ -278,6 +281,16 @@ func (this *downloadEnv) RunDownload(req RunDownload_Req) (resp RunDownload_Resp
|
|||
return resp
|
||||
}
|
||||
|
||||
func parseBeginSeq(body []byte) uint64 {
|
||||
data := M3u8Parse(string(body))
|
||||
seq := data.GetPart(`#EXT-X-MEDIA-SEQUENCE`).TextFull
|
||||
u, err := strconv.ParseUint(seq, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
var gOldEnv *downloadEnv
|
||||
var gOldEnvLocker sync.Mutex
|
||||
|
||||
|
|
@ -350,6 +363,10 @@ func (this *downloadEnv) getEncryptInfo(m3u8Url string, html string) (info *Encr
|
|||
if uri == "" {
|
||||
return nil, nil
|
||||
}
|
||||
method := keyPart.KeyValue["METHOD"]
|
||||
if method == EncryptMethod_NONE {
|
||||
return nil, nil
|
||||
}
|
||||
keyUrl, errMsg := resolveRefUrl(m3u8Url, uri)
|
||||
if errMsg != "" {
|
||||
return nil, errors.New(errMsg)
|
||||
|
|
@ -359,12 +376,19 @@ func (this *downloadEnv) getEncryptInfo(m3u8Url string, html string) (info *Encr
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iv, err := hex.DecodeString(strings.TrimPrefix(keyPart.KeyValue["IV"], "0x"))
|
||||
if method == EncryptMethod_AES128 && len(res) != 16 { // Aes 128
|
||||
return nil, errors.New("getEncryptInfo invalid key " + strconv.Quote(string(res)))
|
||||
}
|
||||
var iv []byte
|
||||
ivs := keyPart.KeyValue["IV"]
|
||||
if ivs != "" {
|
||||
iv, err = hex.DecodeString(strings.TrimPrefix(ivs, "0x"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &EncryptInfo{
|
||||
Method: keyPart.KeyValue["METHOD"],
|
||||
Method: method,
|
||||
Key: res,
|
||||
Iv: iv,
|
||||
}, nil
|
||||
|
|
@ -379,7 +403,7 @@ func splitLineWithTrimSpace(s string) []string {
|
|||
return tmp
|
||||
}
|
||||
|
||||
func getTsList(m38uUrl string, body string) (tsList []TsInfo, errMsg string) {
|
||||
func getTsList(beginSeq uint64, m38uUrl string, body string) (tsList []TsInfo, errMsg string) {
|
||||
index := 0
|
||||
|
||||
for _, line := range splitLineWithTrimSpace(body) {
|
||||
|
|
@ -394,6 +418,7 @@ func getTsList(m38uUrl string, body string) (tsList []TsInfo, errMsg string) {
|
|||
tsList = append(tsList, TsInfo{
|
||||
Name: fmt.Sprintf("%05d.ts", index), // ts视频片段命名规则
|
||||
Url: after,
|
||||
Seq: beginSeq + uint64(index-1),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -417,7 +442,7 @@ func (this *downloadEnv) downloadTsFile(ts TsInfo, download_dir string, encInfo
|
|||
// 解密出视频 ts 源文件
|
||||
if encInfo != nil {
|
||||
//解密 ts 文件,算法:aes 128 cbc pack5
|
||||
origData, err = AesDecrypt(origData, encInfo)
|
||||
origData, err = AesDecrypt(ts.Seq, origData, encInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -540,30 +565,29 @@ func isDirExists(path string) bool {
|
|||
|
||||
// ============================== 加解密相关 ==============================
|
||||
|
||||
func PKCS7UnPadding(origData []byte) []byte {
|
||||
length := len(origData)
|
||||
unpadding := int(origData[length-1])
|
||||
return origData[:(length - unpadding)]
|
||||
}
|
||||
|
||||
func AesDecrypt(crypted []byte, encInfo *EncryptInfo) ([]byte, error) {
|
||||
func AesDecrypt(seq uint64, crypted []byte, encInfo *EncryptInfo) ([]byte, error) {
|
||||
block, err := aes.NewCipher(encInfo.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
iv := encInfo.Iv
|
||||
if len(iv) == 0 {
|
||||
if len(encInfo.Key) > blockSize {
|
||||
return nil, errors.New(fmt.Sprint("AesDecrypt invalid size ", blockSize, " ", len(encInfo.Key)))
|
||||
if encInfo.Method == EncryptMethod_AES128 {
|
||||
iv = make([]byte, 16)
|
||||
binary.BigEndian.PutUint64(iv[8:], seq)
|
||||
} else {
|
||||
return nil, errors.New("AesDecrypt method not support " + strconv.Quote(encInfo.Method))
|
||||
}
|
||||
iv = encInfo.Key[:blockSize]
|
||||
}
|
||||
blockMode := cipher.NewCBCDecrypter(block, iv)
|
||||
origData := make([]byte, len(crypted))
|
||||
blockMode.CryptBlocks(origData, crypted)
|
||||
origData = PKCS7UnPadding(origData)
|
||||
return origData, nil
|
||||
length := len(origData)
|
||||
unpadding := int(origData[length-1])
|
||||
if length-unpadding < 0 {
|
||||
return nil, fmt.Errorf(`invalid length of unpadding %v - %v`, length, unpadding)
|
||||
}
|
||||
return origData[:(length - unpadding)], nil
|
||||
}
|
||||
|
||||
func getFileSha256(targetFile string) (v string) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package m3u8d
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
|
@ -50,7 +51,7 @@ func TestGetTsList(t *testing.T) {
|
|||
}
|
||||
|
||||
func tGetTsList(m3u8Url string, m3u8Content string, expectTs0Url string) {
|
||||
list, errMsg := getTsList(m3u8Url, m3u8Content)
|
||||
list, errMsg := getTsList(0, m3u8Url, m3u8Content)
|
||||
if errMsg != "" {
|
||||
panic(errMsg)
|
||||
}
|
||||
|
|
@ -88,6 +89,7 @@ func TestFull(t *testing.T) {
|
|||
M3u8Url: m3u8Url,
|
||||
SaveDir: saveDir,
|
||||
FileName: "all",
|
||||
ThreadCount: 8,
|
||||
})
|
||||
if resp2.ErrMsg != "" {
|
||||
panic(resp2.ErrMsg)
|
||||
|
|
@ -118,3 +120,17 @@ func TestGetFileName(t *testing.T) {
|
|||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseOldEnv(t *testing.T) {
|
||||
encInfo := EncryptInfo{
|
||||
Method: EncryptMethod_AES128,
|
||||
Key: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6},
|
||||
Iv: nil,
|
||||
}
|
||||
before := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 4}
|
||||
after, err := AesDecrypt(1, before, &encInfo)
|
||||
checkErr(err)
|
||||
if bytes.Equal(after, []byte{69, 46, 52, 180, 68, 205, 99, 220, 193, 44, 116, 174, 96, 196, 199, 87, 214, 77, 67, 5, 37, 8, 139, 146, 229, 120, 164, 76, 107, 0, 204, 0}) == false {
|
||||
panic("expect bytes failed")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
m3u8.go
8
m3u8.go
|
|
@ -11,9 +11,15 @@ type EncryptInfo struct {
|
|||
Iv []byte
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.2.4
|
||||
const (
|
||||
EncryptMethod_NONE = `NONE`
|
||||
EncryptMethod_AES128 = `AES-128`
|
||||
EncryptMethod_SIMPLE_AES = `SAMPLE-AES` // TODO
|
||||
)
|
||||
|
||||
type M3u8Content struct {
|
||||
PartList []M3u8Part `json:",omitempty"`
|
||||
TsList []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type M3u8Part struct {
|
||||
|
|
|
|||
22
m3u8_test.go
22
m3u8_test.go
|
|
@ -16,7 +16,7 @@ func TestM3u8Parse(t *testing.T) {
|
|||
#EXT-X-KEY:METHOD=AES-128,URI="/20230502/xthms/2000kb/hls/key.key"
|
||||
`)
|
||||
part := info.GetPart("#EXT-X-KEY")
|
||||
if part.KeyValue["METHOD"] != "AES-128" {
|
||||
if part.KeyValue["METHOD"] != EncryptMethod_AES128 {
|
||||
panic("method")
|
||||
}
|
||||
if part.KeyValue["URI"] != "/20230502/xthms/2000kb/hls/key.key" {
|
||||
|
|
@ -48,3 +48,23 @@ func checkErr(err error) {
|
|||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestM3u8Parse2(t *testing.T) {
|
||||
seq1 := parseBeginSeq([]byte(`#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXT-X-MEDIA-SEQUENCE:0`))
|
||||
if seq1 != 0 {
|
||||
panic(seq1)
|
||||
}
|
||||
seq2 := parseBeginSeq([]byte(`#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXT-X-MEDIA-SEQUENCE:2`))
|
||||
if seq2 != 2 {
|
||||
panic(seq2)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue