dischord/extractor/youtube/youtube_decrypt.go

246 lines
5.7 KiB
Go

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 end == -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 end == -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
}