246 lines
5.7 KiB
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 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
|
|
}
|