init
This commit is contained in:
245
extractor/youtube/youtube_decrypt.go
Normal file
245
extractor/youtube/youtube_decrypt.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package youtube
|
||||
|
||||
import (
|
||||
exutil "git.nobrain.org/r4/dischord/extractor/util"
|
||||
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDecryptGettingFunctionName = errors.New("error getting signature decryption function name")
|
||||
ErrDecryptGettingFunction = errors.New("error getting signature decryption function")
|
||||
ErrDecryptGettingOpTable = errors.New("error getting signature decryption operation table")
|
||||
ErrGettingBaseJs = errors.New("unable to get base.js")
|
||||
)
|
||||
|
||||
type decryptorOp struct {
|
||||
fn func(a *string, b int)
|
||||
arg int
|
||||
}
|
||||
|
||||
type decryptor struct {
|
||||
// base.js version ID, used for caching
|
||||
versionId string
|
||||
// The actual decryption algorithm can be split up into a list of known
|
||||
// operations
|
||||
ops []decryptorOp
|
||||
}
|
||||
|
||||
func (d *decryptor) decrypt(input string) (string, error) {
|
||||
if err := updateDecryptor(d); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
s := input
|
||||
for _, op := range d.ops {
|
||||
op.fn(&s, op.arg)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type configData struct {
|
||||
PlayerJsUrl string `json:"PLAYER_JS_URL"`
|
||||
}
|
||||
|
||||
func updateDecryptor(d *decryptor) error {
|
||||
prefix := "(function() {window.ytplayer={};\nytcfg.set("
|
||||
endStr := ");"
|
||||
// Get base.js URL
|
||||
var url string
|
||||
var funcErr error
|
||||
err := exutil.GetHTMLScriptFunc("https://www.youtube.com", false, func(code string) bool {
|
||||
if strings.HasPrefix(code, prefix) {
|
||||
// Cut out the JSON part
|
||||
code = code[len(prefix):]
|
||||
end := strings.Index(code, endStr)
|
||||
if end == -1 {
|
||||
funcErr = ErrGettingBaseJs
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse config data
|
||||
var data configData
|
||||
if err := json.Unmarshal([]byte(code[:end]), &data); err != nil {
|
||||
funcErr = ErrGettingBaseJs
|
||||
return false
|
||||
}
|
||||
|
||||
url = "https://www.youtube.com" + data.PlayerJsUrl
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if funcErr != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get base.js version ID
|
||||
sp := strings.SplitN(strings.TrimPrefix(url, "/s/player/"), "/", 2)
|
||||
if len(sp) != 2 {
|
||||
return ErrGettingBaseJs
|
||||
}
|
||||
verId := sp[0]
|
||||
|
||||
if d.versionId == verId {
|
||||
// Decryptor already up-to-date
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get base.js contents
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return ErrGettingBaseJs
|
||||
}
|
||||
|
||||
// Copy contents to buffer
|
||||
buf := new(strings.Builder)
|
||||
_, err = io.Copy(buf, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get decryption operations
|
||||
ops, err := getDecryptOps(buf.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.versionId = verId
|
||||
d.ops = ops
|
||||
return nil
|
||||
}
|
||||
|
||||
var decryptFunctionNameRegexp = regexp.MustCompile(`[a-zA-Z]*&&\([a-zA-Z]*=([a-zA-Z]*)\(decodeURIComponent\([a-zA-Z]*\)\),[a-zA-Z]*\.set\([a-zA-Z]*,encodeURIComponent\([a-zA-Z]*\)\)\)`)
|
||||
|
||||
func getDecryptFunction(baseJs string) (string, error) {
|
||||
idx := decryptFunctionNameRegexp.FindSubmatchIndex([]byte(baseJs))
|
||||
if len(idx) != 4 {
|
||||
return "", ErrDecryptGettingFunctionName
|
||||
}
|
||||
fnName := baseJs[idx[2]:idx[3]]
|
||||
|
||||
startMatch := fnName + `=function(a){a=a.split("");`
|
||||
endMatch := `;return a.join("")};`
|
||||
start := strings.Index(baseJs, startMatch)
|
||||
if start == -1 {
|
||||
return "", ErrDecryptGettingFunction
|
||||
}
|
||||
fn := baseJs[start+len(startMatch):]
|
||||
end := strings.Index(fn, endMatch)
|
||||
if start == -1 {
|
||||
return "", ErrDecryptGettingFunction
|
||||
}
|
||||
return fn[:end], nil
|
||||
}
|
||||
|
||||
func getDecryptOps(baseJs string) ([]decryptorOp, error) {
|
||||
// Extract main decryptor function JS
|
||||
decrFn, err := getDecryptFunction(baseJs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get decyptor operation JS
|
||||
var ops string
|
||||
{
|
||||
sp := strings.SplitN(decrFn, ".", 2)
|
||||
if len(sp) != 2 {
|
||||
return nil, ErrDecryptGettingOpTable
|
||||
}
|
||||
opsObjName := sp[0]
|
||||
|
||||
startMatch := `var ` + opsObjName + `={`
|
||||
endMatch := `};`
|
||||
start := strings.Index(baseJs, startMatch)
|
||||
if start == -1 {
|
||||
return nil, ErrDecryptGettingOpTable
|
||||
}
|
||||
ops = baseJs[start+len(startMatch):]
|
||||
end := strings.Index(ops, endMatch)
|
||||
if start == -1 {
|
||||
return nil, ErrDecryptGettingOpTable
|
||||
}
|
||||
ops = ops[:end]
|
||||
}
|
||||
|
||||
// Make a decryptor operation table that associates the operation
|
||||
// names with a specific action on an input string
|
||||
opTable := make(map[string]func(a *string, b int))
|
||||
{
|
||||
lns := strings.Split(ops, "\n")
|
||||
if len(lns) != 3 {
|
||||
return nil, ErrDecryptGettingOpTable
|
||||
}
|
||||
for _, ln := range lns {
|
||||
sp := strings.Split(ln, ":")
|
||||
if len(sp) != 2 {
|
||||
return nil, ErrDecryptGettingOpTable
|
||||
}
|
||||
name := sp[0]
|
||||
fn := sp[1]
|
||||
switch {
|
||||
case strings.HasPrefix(fn, `function(a){a.reverse()}`):
|
||||
opTable[name] = func(a *string, b int) {
|
||||
// Reverse a
|
||||
var res string
|
||||
for _, c := range *a {
|
||||
res = string(c) + res
|
||||
}
|
||||
*a = res
|
||||
}
|
||||
case strings.HasPrefix(fn, `function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}`):
|
||||
opTable[name] = func(a *string, b int) {
|
||||
// Swap a[0] and a[b % len(a)]
|
||||
c := []byte(*a)
|
||||
c[0], c[b%len(*a)] = c[b%len(*a)], c[0]
|
||||
*a = string(c)
|
||||
}
|
||||
case strings.HasPrefix(fn, `function(a,b){a.splice(0,b)}`):
|
||||
opTable[name] = func(a *string, b int) {
|
||||
// Slice off all elements of a up to a[b]
|
||||
*a = (*a)[b:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse all operations in the main decryptor function and return them in
|
||||
// order
|
||||
var res []decryptorOp
|
||||
for _, fn := range strings.Split(decrFn, ";") {
|
||||
sp := strings.SplitN(fn, ".", 2)
|
||||
if len(sp) != 2 {
|
||||
return nil, ErrDecryptGettingOpTable
|
||||
}
|
||||
sp = strings.SplitN(sp[1], "(", 2)
|
||||
if len(sp) != 2 {
|
||||
return nil, ErrDecryptGettingOpTable
|
||||
}
|
||||
name := sp[0]
|
||||
argS := strings.TrimSuffix(strings.TrimPrefix(sp[1], "a,"), ")")
|
||||
arg, err := strconv.Atoi(argS)
|
||||
if err != nil {
|
||||
return nil, ErrDecryptGettingOpTable
|
||||
}
|
||||
callableOp, exists := opTable[name]
|
||||
if !exists {
|
||||
return nil, ErrDecryptGettingOpTable
|
||||
}
|
||||
res = append(res, decryptorOp{callableOp, arg})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
Reference in New Issue
Block a user