parent
7b1fa07cdf
commit
1519511baf
60
download.go
60
download.go
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -31,6 +32,7 @@ import (
|
||||||
type TsInfo struct {
|
type TsInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Url string
|
Url string
|
||||||
|
Seq uint64 // 如果是aes加密并且没有iv, 这个seq需要充当iv
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetProgress_Resp struct {
|
type GetProgress_Resp struct {
|
||||||
|
|
@ -180,6 +182,7 @@ func (this *downloadEnv) RunDownload(req RunDownload_Req) (resp RunDownload_Resp
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
beginSeq := parseBeginSeq(m3u8Body)
|
||||||
// 获取m3u8地址的内容体
|
// 获取m3u8地址的内容体
|
||||||
encInfo, err := this.getEncryptInfo(req.M3u8Url, string(m3u8Body))
|
encInfo, err := this.getEncryptInfo(req.M3u8Url, string(m3u8Body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -188,7 +191,7 @@ func (this *downloadEnv) RunDownload(req RunDownload_Req) (resp RunDownload_Resp
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
this.SetProgressBarTitle("[3/6]获取ts列表")
|
this.SetProgressBarTitle("[3/6]获取ts列表")
|
||||||
tsList, errMsg := getTsList(req.M3u8Url, string(m3u8Body))
|
tsList, errMsg := getTsList(beginSeq, req.M3u8Url, string(m3u8Body))
|
||||||
if errMsg != "" {
|
if errMsg != "" {
|
||||||
resp.ErrMsg = "获取ts列表错误: " + errMsg
|
resp.ErrMsg = "获取ts列表错误: " + errMsg
|
||||||
return resp
|
return resp
|
||||||
|
|
@ -278,6 +281,16 @@ func (this *downloadEnv) RunDownload(req RunDownload_Req) (resp RunDownload_Resp
|
||||||
return 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 gOldEnv *downloadEnv
|
||||||
var gOldEnvLocker sync.Mutex
|
var gOldEnvLocker sync.Mutex
|
||||||
|
|
||||||
|
|
@ -350,6 +363,10 @@ func (this *downloadEnv) getEncryptInfo(m3u8Url string, html string) (info *Encr
|
||||||
if uri == "" {
|
if uri == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
method := keyPart.KeyValue["METHOD"]
|
||||||
|
if method == EncryptMethod_NONE {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
keyUrl, errMsg := resolveRefUrl(m3u8Url, uri)
|
keyUrl, errMsg := resolveRefUrl(m3u8Url, uri)
|
||||||
if errMsg != "" {
|
if errMsg != "" {
|
||||||
return nil, errors.New(errMsg)
|
return nil, errors.New(errMsg)
|
||||||
|
|
@ -359,12 +376,19 @@ func (this *downloadEnv) getEncryptInfo(m3u8Url string, html string) (info *Encr
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return &EncryptInfo{
|
return &EncryptInfo{
|
||||||
Method: keyPart.KeyValue["METHOD"],
|
Method: method,
|
||||||
Key: res,
|
Key: res,
|
||||||
Iv: iv,
|
Iv: iv,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
@ -379,7 +403,7 @@ func splitLineWithTrimSpace(s string) []string {
|
||||||
return tmp
|
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
|
index := 0
|
||||||
|
|
||||||
for _, line := range splitLineWithTrimSpace(body) {
|
for _, line := range splitLineWithTrimSpace(body) {
|
||||||
|
|
@ -394,6 +418,7 @@ func getTsList(m38uUrl string, body string) (tsList []TsInfo, errMsg string) {
|
||||||
tsList = append(tsList, TsInfo{
|
tsList = append(tsList, TsInfo{
|
||||||
Name: fmt.Sprintf("%05d.ts", index), // ts视频片段命名规则
|
Name: fmt.Sprintf("%05d.ts", index), // ts视频片段命名规则
|
||||||
Url: after,
|
Url: after,
|
||||||
|
Seq: beginSeq + uint64(index-1),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -417,7 +442,7 @@ func (this *downloadEnv) downloadTsFile(ts TsInfo, download_dir string, encInfo
|
||||||
// 解密出视频 ts 源文件
|
// 解密出视频 ts 源文件
|
||||||
if encInfo != nil {
|
if encInfo != nil {
|
||||||
//解密 ts 文件,算法:aes 128 cbc pack5
|
//解密 ts 文件,算法:aes 128 cbc pack5
|
||||||
origData, err = AesDecrypt(origData, encInfo)
|
origData, err = AesDecrypt(ts.Seq, origData, encInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -540,30 +565,29 @@ func isDirExists(path string) bool {
|
||||||
|
|
||||||
// ============================== 加解密相关 ==============================
|
// ============================== 加解密相关 ==============================
|
||||||
|
|
||||||
func PKCS7UnPadding(origData []byte) []byte {
|
func AesDecrypt(seq uint64, crypted []byte, encInfo *EncryptInfo) ([]byte, error) {
|
||||||
length := len(origData)
|
|
||||||
unpadding := int(origData[length-1])
|
|
||||||
return origData[:(length - unpadding)]
|
|
||||||
}
|
|
||||||
|
|
||||||
func AesDecrypt(crypted []byte, encInfo *EncryptInfo) ([]byte, error) {
|
|
||||||
block, err := aes.NewCipher(encInfo.Key)
|
block, err := aes.NewCipher(encInfo.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
blockSize := block.BlockSize()
|
|
||||||
iv := encInfo.Iv
|
iv := encInfo.Iv
|
||||||
if len(iv) == 0 {
|
if len(iv) == 0 {
|
||||||
if len(encInfo.Key) > blockSize {
|
if encInfo.Method == EncryptMethod_AES128 {
|
||||||
return nil, errors.New(fmt.Sprint("AesDecrypt invalid size ", blockSize, " ", len(encInfo.Key)))
|
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)
|
blockMode := cipher.NewCBCDecrypter(block, iv)
|
||||||
origData := make([]byte, len(crypted))
|
origData := make([]byte, len(crypted))
|
||||||
blockMode.CryptBlocks(origData, crypted)
|
blockMode.CryptBlocks(origData, crypted)
|
||||||
origData = PKCS7UnPadding(origData)
|
length := len(origData)
|
||||||
return origData, nil
|
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) {
|
func getFileSha256(targetFile string) (v string) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package m3u8d
|
package m3u8d
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -50,7 +51,7 @@ func TestGetTsList(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func tGetTsList(m3u8Url string, m3u8Content string, expectTs0Url string) {
|
func tGetTsList(m3u8Url string, m3u8Content string, expectTs0Url string) {
|
||||||
list, errMsg := getTsList(m3u8Url, m3u8Content)
|
list, errMsg := getTsList(0, m3u8Url, m3u8Content)
|
||||||
if errMsg != "" {
|
if errMsg != "" {
|
||||||
panic(errMsg)
|
panic(errMsg)
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +89,7 @@ func TestFull(t *testing.T) {
|
||||||
M3u8Url: m3u8Url,
|
M3u8Url: m3u8Url,
|
||||||
SaveDir: saveDir,
|
SaveDir: saveDir,
|
||||||
FileName: "all",
|
FileName: "all",
|
||||||
|
ThreadCount: 8,
|
||||||
})
|
})
|
||||||
if resp2.ErrMsg != "" {
|
if resp2.ErrMsg != "" {
|
||||||
panic(resp2.ErrMsg)
|
panic(resp2.ErrMsg)
|
||||||
|
|
@ -118,3 +120,17 @@ func TestGetFileName(t *testing.T) {
|
||||||
t.Fail()
|
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
|
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 {
|
type M3u8Content struct {
|
||||||
PartList []M3u8Part `json:",omitempty"`
|
PartList []M3u8Part `json:",omitempty"`
|
||||||
TsList []string `json:",omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type M3u8Part struct {
|
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"
|
#EXT-X-KEY:METHOD=AES-128,URI="/20230502/xthms/2000kb/hls/key.key"
|
||||||
`)
|
`)
|
||||||
part := info.GetPart("#EXT-X-KEY")
|
part := info.GetPart("#EXT-X-KEY")
|
||||||
if part.KeyValue["METHOD"] != "AES-128" {
|
if part.KeyValue["METHOD"] != EncryptMethod_AES128 {
|
||||||
panic("method")
|
panic("method")
|
||||||
}
|
}
|
||||||
if part.KeyValue["URI"] != "/20230502/xthms/2000kb/hls/key.key" {
|
if part.KeyValue["URI"] != "/20230502/xthms/2000kb/hls/key.key" {
|
||||||
|
|
@ -48,3 +48,23 @@ func checkErr(err error) {
|
||||||
panic(err)
|
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