Skip to content

Commit 2266982

Browse files
fix: Direct cipher signature & n-transform functions to circumvent throttling. (#1022)
* Add files via upload * Add files via upload * Update getInfoBandwidth.js * Update getInfoBandwidth.js * Add files via upload * extract functions. * test functions not tokens. * get functions not tokens * Delete getInfoBandwidth.js * relax no-new-func to warn * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Update .eslintrc.yml * neater code, more efficient process. * Add files via upload * Add files via upload
1 parent 40d0c54 commit 2266982

File tree

3 files changed

+10240
-308
lines changed

3 files changed

+10240
-308
lines changed

lib/sig.js

+79-204
Original file line numberDiff line numberDiff line change
@@ -1,245 +1,120 @@
11
const querystring = require('querystring');
22
const Cache = require('./cache');
33
const utils = require('./utils');
4+
const vm = require('vm');
45

5-
6-
// A shared cache to keep track of html5player.js tokens.
6+
// A shared cache to keep track of html5player js functions.
77
exports.cache = new Cache();
88

9-
109
/**
11-
* Extract signature deciphering tokens from html5player file.
10+
* Extract signature deciphering and n parameter transform functions from html5player file.
1211
*
1312
* @param {string} html5playerfile
1413
* @param {Object} options
1514
* @returns {Promise<Array.<string>>}
1615
*/
17-
exports.getTokens = (html5playerfile, options) => exports.cache.getOrSet(html5playerfile, async() => {
16+
exports.getFunctions = (html5playerfile, options) => exports.cache.getOrSet(html5playerfile, async() => {
1817
const body = await utils.exposedMiniget(html5playerfile, options).text();
19-
const tokens = exports.extractActions(body);
20-
if (!tokens || !tokens.length) {
21-
throw Error('Could not extract signature deciphering actions');
18+
const functions = exports.extractFunctions(body);
19+
if (!functions || !functions.length) {
20+
throw Error('Could not extract functions');
2221
}
23-
exports.cache.set(html5playerfile, tokens);
24-
return tokens;
22+
exports.cache.set(html5playerfile, functions);
23+
return functions;
2524
});
2625

27-
28-
/**
29-
* Decipher a signature based on action tokens.
30-
*
31-
* @param {Array.<string>} tokens
32-
* @param {string} sig
33-
* @returns {string}
34-
*/
35-
exports.decipher = (tokens, sig) => {
36-
sig = sig.split('');
37-
for (let i = 0, len = tokens.length; i < len; i++) {
38-
let token = tokens[i], pos;
39-
switch (token[0]) {
40-
case 'r':
41-
sig = sig.reverse();
42-
break;
43-
case 'w':
44-
pos = ~~token.slice(1);
45-
sig = swapHeadAndPosition(sig, pos);
46-
break;
47-
case 's':
48-
pos = ~~token.slice(1);
49-
sig = sig.slice(pos);
50-
break;
51-
case 'p':
52-
pos = ~~token.slice(1);
53-
sig.splice(0, pos);
54-
break;
55-
}
56-
}
57-
return sig.join('');
58-
};
59-
60-
61-
/**
62-
* Swaps the first element of an array with one of given position.
63-
*
64-
* @param {Array.<Object>} arr
65-
* @param {number} position
66-
* @returns {Array.<Object>}
67-
*/
68-
const swapHeadAndPosition = (arr, position) => {
69-
const first = arr[0];
70-
arr[0] = arr[position % arr.length];
71-
arr[position] = first;
72-
return arr;
73-
};
74-
75-
76-
const jsVarStr = '[a-zA-Z_\\$][a-zA-Z_0-9]*';
77-
const jsSingleQuoteStr = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`;
78-
const jsDoubleQuoteStr = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`;
79-
const jsQuoteStr = `(?:${jsSingleQuoteStr}|${jsDoubleQuoteStr})`;
80-
const jsKeyStr = `(?:${jsVarStr}|${jsQuoteStr})`;
81-
const jsPropStr = `(?:\\.${jsVarStr}|\\[${jsQuoteStr}\\])`;
82-
const jsEmptyStr = `(?:''|"")`;
83-
const reverseStr = ':function\\(a\\)\\{' +
84-
'(?:return )?a\\.reverse\\(\\)' +
85-
'\\}';
86-
const sliceStr = ':function\\(a,b\\)\\{' +
87-
'return a\\.slice\\(b\\)' +
88-
'\\}';
89-
const spliceStr = ':function\\(a,b\\)\\{' +
90-
'a\\.splice\\(0,b\\)' +
91-
'\\}';
92-
const swapStr = ':function\\(a,b\\)\\{' +
93-
'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' +
94-
'\\}';
95-
const actionsObjRegexp = new RegExp(
96-
`var (${jsVarStr})=\\{((?:(?:${
97-
jsKeyStr}${reverseStr}|${
98-
jsKeyStr}${sliceStr}|${
99-
jsKeyStr}${spliceStr}|${
100-
jsKeyStr}${swapStr
101-
}),?\\r?\\n?)+)\\};`);
102-
const actionsFuncRegexp = new RegExp(`${`function(?: ${jsVarStr})?\\(a\\)\\{` +
103-
`a=a\\.split\\(${jsEmptyStr}\\);\\s*` +
104-
`((?:(?:a=)?${jsVarStr}`}${
105-
jsPropStr
106-
}\\(a,\\d+\\);)+)` +
107-
`return a\\.join\\(${jsEmptyStr}\\)` +
108-
`\\}`);
109-
const reverseRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${reverseStr}`, 'm');
110-
const sliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${sliceStr}`, 'm');
111-
const spliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${spliceStr}`, 'm');
112-
const swapRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${swapStr}`, 'm');
113-
114-
11526
/**
116-
* Extracts the actions that should be taken to decipher a signature.
117-
*
118-
* This searches for a function that performs string manipulations on
119-
* the signature. We already know what the 3 possible changes to a signature
120-
* are in order to decipher it. There is
121-
*
122-
* * Reversing the string.
123-
* * Removing a number of characters from the beginning.
124-
* * Swapping the first character with another position.
125-
*
126-
* Note, `Array#slice()` used to be used instead of `Array#splice()`,
127-
* it's kept in case we encounter any older html5player files.
128-
*
129-
* After retrieving the function that does this, we can see what actions
130-
* it takes on a signature.
27+
* Extracts the actions that should be taken to decipher a signature
28+
* and tranform the n parameter
13129
*
13230
* @param {string} body
13331
* @returns {Array.<string>}
13432
*/
135-
exports.extractActions = body => {
136-
const objResult = actionsObjRegexp.exec(body);
137-
const funcResult = actionsFuncRegexp.exec(body);
138-
if (!objResult || !funcResult) { return null; }
139-
140-
const obj = objResult[1].replace(/\$/g, '\\$');
141-
const objBody = objResult[2].replace(/\$/g, '\\$');
142-
const funcBody = funcResult[1].replace(/\$/g, '\\$');
143-
144-
let result = reverseRegexp.exec(objBody);
145-
const reverseKey = result && result[1]
146-
.replace(/\$/g, '\\$')
147-
.replace(/\$|^'|^"|'$|"$/g, '');
148-
result = sliceRegexp.exec(objBody);
149-
const sliceKey = result && result[1]
150-
.replace(/\$/g, '\\$')
151-
.replace(/\$|^'|^"|'$|"$/g, '');
152-
result = spliceRegexp.exec(objBody);
153-
const spliceKey = result && result[1]
154-
.replace(/\$/g, '\\$')
155-
.replace(/\$|^'|^"|'$|"$/g, '');
156-
result = swapRegexp.exec(objBody);
157-
const swapKey = result && result[1]
158-
.replace(/\$/g, '\\$')
159-
.replace(/\$|^'|^"|'$|"$/g, '');
160-
161-
const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`;
162-
const myreg = `(?:a=)?${obj
163-
}(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` +
164-
`\\(a,(\\d+)\\)`;
165-
const tokenizeRegexp = new RegExp(myreg, 'g');
166-
const tokens = [];
167-
while ((result = tokenizeRegexp.exec(funcBody)) !== null) {
168-
let key = result[1] || result[2] || result[3];
169-
switch (key) {
170-
case swapKey:
171-
tokens.push(`w${result[4]}`);
172-
break;
173-
case reverseKey:
174-
tokens.push('r');
175-
break;
176-
case sliceKey:
177-
tokens.push(`s${result[4]}`);
178-
break;
179-
case spliceKey:
180-
tokens.push(`p${result[4]}`);
181-
break;
33+
exports.extractFunctions = body => {
34+
const functions = [];
35+
const extractManipulations = caller => {
36+
const functionName = utils.between(caller, `a=a.split("");`, `.`);
37+
if (!functionName) return '';
38+
const functionStart = `var ${functionName}={`;
39+
const ndx = body.indexOf(functionStart);
40+
if (ndx < 0) return '';
41+
const subBody = body.slice(ndx + functionStart.length - 1);
42+
return `var ${functionName}=${utils.cutAfterJSON(subBody)}`;
43+
};
44+
const extractDecipher = () => {
45+
const functionName = utils.between(body, `a.set("alr","yes");c&&(c=`, `(decodeURIC`);
46+
if (functionName && functionName.length) {
47+
const functionStart = `${functionName}=function(a)`;
48+
const ndx = body.indexOf(functionStart);
49+
if (ndx >= 0) {
50+
const subBody = body.slice(ndx + functionStart.length);
51+
let functionBody = `var ${functionStart}${utils.cutAfterJSON(subBody)}`;
52+
functionBody = `${extractManipulations(functionBody)};${functionBody};${functionName}(sig);`;
53+
functions.push(functionBody);
54+
}
18255
}
183-
}
184-
return tokens;
56+
};
57+
const extractNCode = () => {
58+
const functionName = utils.between(body, `&&(b=a.get("n"))&&(b=`, `(b)`);
59+
if (functionName && functionName.length) {
60+
const functionStart = `${functionName}=function(a)`;
61+
const ndx = body.indexOf(functionStart);
62+
if (ndx >= 0) {
63+
const subBody = body.slice(ndx + functionStart.length);
64+
const functionBody = `var ${functionStart}${utils.cutAfterJSON(subBody)};${functionName}(ncode);`;
65+
functions.push(functionBody);
66+
}
67+
}
68+
};
69+
extractDecipher();
70+
extractNCode();
71+
return functions;
18572
};
18673

187-
18874
/**
75+
* Apply decipher and n-transform to individual format
76+
*
18977
* @param {Object} format
190-
* @param {string} sig
78+
* @param {vm.Script} decipherScript
79+
* @param {vm.Script} nTransformScript
19180
*/
192-
exports.setDownloadURL = (format, sig) => {
193-
let decodedUrl;
194-
if (format.url) {
195-
decodedUrl = format.url;
196-
} else {
197-
return;
198-
}
199-
200-
try {
201-
decodedUrl = decodeURIComponent(decodedUrl);
202-
} catch (err) {
203-
return;
204-
}
205-
206-
// Make some adjustments to the final url.
207-
const parsedUrl = new URL(decodedUrl);
208-
209-
// This is needed for a speedier download.
210-
// See https://github.com/fent/node-ytdl-core/issues/127
211-
parsedUrl.searchParams.set('ratebypass', 'yes');
212-
213-
if (sig) {
214-
// When YouTube provides a `sp` parameter the signature `sig` must go
215-
// into the parameter it specifies.
216-
// See https://github.com/fent/node-ytdl-core/issues/417
217-
parsedUrl.searchParams.set(format.sp || 'signature', sig);
218-
}
219-
220-
format.url = parsedUrl.toString();
81+
exports.setDownloadURL = (format, decipherScript, nTransformScript) => {
82+
const decipher = url => {
83+
const args = querystring.parse(url);
84+
if (!args.s || !decipherScript) return args.url;
85+
const components = new URL(decodeURIComponent(args.url));
86+
components.searchParams.set(args.sp ? args.sp : 'signature',
87+
decipherScript.runInNewContext({ sig: decodeURIComponent(args.s) }));
88+
return components.toString();
89+
};
90+
const ncode = url => {
91+
const components = new URL(decodeURIComponent(url));
92+
const n = components.searchParams.get('n');
93+
if (!n || !nTransformScript) return url;
94+
components.searchParams.set('n', nTransformScript.runInNewContext({ ncode: n }));
95+
return components.toString();
96+
};
97+
const cipher = !format.url;
98+
const url = format.url || format.signatureCipher || format.cipher;
99+
format.url = cipher ? ncode(decipher(url)) : ncode(url);
100+
delete format.signatureCipher;
101+
delete format.cipher;
221102
};
222103

223-
224104
/**
225-
* Applies `sig.decipher()` to all format URL's.
105+
* Applies decipher and n parameter transforms to all format URL's.
226106
*
227107
* @param {Array.<Object>} formats
228108
* @param {string} html5player
229109
* @param {Object} options
230110
*/
231111
exports.decipherFormats = async(formats, html5player, options) => {
232112
let decipheredFormats = {};
233-
let tokens = await exports.getTokens(html5player, options);
113+
let functions = await exports.getFunctions(html5player, options);
114+
const decipherScript = functions.length ? new vm.Script(functions[0]) : null;
115+
const nTransformScript = functions.length > 1 ? new vm.Script(functions[1]) : null;
234116
formats.forEach(format => {
235-
let cipher = format.signatureCipher || format.cipher;
236-
if (cipher) {
237-
Object.assign(format, querystring.parse(cipher));
238-
delete format.signatureCipher;
239-
delete format.cipher;
240-
}
241-
const sig = tokens && format.s ? exports.decipher(tokens, format.s) : null;
242-
exports.setDownloadURL(format, sig);
117+
exports.setDownloadURL(format, decipherScript, nTransformScript);
243118
decipheredFormats[format.url] = format;
244119
});
245120
return decipheredFormats;

0 commit comments

Comments
 (0)