UNPKG

7.4 kBJavaScriptView Raw
1const url = require('url');
2const miniget = require('miniget');
3const querystring = require('querystring');
4const Cache = require('./cache');
5
6
7// A shared cache to keep track of html5player.js tokens.
8exports.cache = new Cache();
9
10
11/**
12 * Extract signature deciphering tokens from html5player file.
13 *
14 * @param {string} html5playerfile
15 * @param {Object} options
16 * @returns {Promise<Array.<string>>}
17 */
18exports.getTokens = (html5playerfile, options) => exports.cache.getOrSet(html5playerfile, async() => {
19 let body = await miniget(html5playerfile, options.requestOptions).text();
20 const tokens = exports.extractActions(body);
21 if (!tokens || !tokens.length) {
22 throw Error('Could not extract signature deciphering actions');
23 }
24 exports.cache.set(html5playerfile, tokens);
25 return tokens;
26});
27
28
29/**
30 * Decipher a signature based on action tokens.
31 *
32 * @param {Array.<string>} tokens
33 * @param {string} sig
34 * @returns {string}
35 */
36exports.decipher = (tokens, sig) => {
37 sig = sig.split('');
38 for (let i = 0, len = tokens.length; i < len; i++) {
39 let token = tokens[i], pos;
40 switch (token[0]) {
41 case 'r':
42 sig = sig.reverse();
43 break;
44 case 'w':
45 pos = ~~token.slice(1);
46 sig = swapHeadAndPosition(sig, pos);
47 break;
48 case 's':
49 pos = ~~token.slice(1);
50 sig = sig.slice(pos);
51 break;
52 case 'p':
53 pos = ~~token.slice(1);
54 sig.splice(0, pos);
55 break;
56 }
57 }
58 return sig.join('');
59};
60
61
62/**
63 * Swaps the first element of an array with one of given position.
64 *
65 * @param {Array.<Object>} arr
66 * @param {number} position
67 * @returns {Array.<Object>}
68 */
69const swapHeadAndPosition = (arr, position) => {
70 const first = arr[0];
71 arr[0] = arr[position % arr.length];
72 arr[position] = first;
73 return arr;
74};
75
76
77const jsVarStr = '[a-zA-Z_\\$][a-zA-Z_0-9]*';
78const jsSingleQuoteStr = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`;
79const jsDoubleQuoteStr = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`;
80const jsQuoteStr = `(?:${jsSingleQuoteStr}|${jsDoubleQuoteStr})`;
81const jsKeyStr = `(?:${jsVarStr}|${jsQuoteStr})`;
82const jsPropStr = `(?:\\.${jsVarStr}|\\[${jsQuoteStr}\\])`;
83const jsEmptyStr = `(?:''|"")`;
84const reverseStr = ':function\\(a\\)\\{' +
85 '(?:return )?a\\.reverse\\(\\)' +
86'\\}';
87const sliceStr = ':function\\(a,b\\)\\{' +
88 'return a\\.slice\\(b\\)' +
89'\\}';
90const spliceStr = ':function\\(a,b\\)\\{' +
91 'a\\.splice\\(0,b\\)' +
92'\\}';
93const swapStr = ':function\\(a,b\\)\\{' +
94 'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' +
95'\\}';
96const actionsObjRegexp = new RegExp(
97 `var (${jsVarStr})=\\{((?:(?:${
98 jsKeyStr}${reverseStr}|${
99 jsKeyStr}${sliceStr}|${
100 jsKeyStr}${spliceStr}|${
101 jsKeyStr}${swapStr
102 }),?\\r?\\n?)+)\\};`);
103const actionsFuncRegexp = new RegExp(`${`function(?: ${jsVarStr})?\\(a\\)\\{` +
104 `a=a\\.split\\(${jsEmptyStr}\\);\\s*` +
105 `((?:(?:a=)?${jsVarStr}`}${
106 jsPropStr
107}\\(a,\\d+\\);)+)` +
108 `return a\\.join\\(${jsEmptyStr}\\)` +
109 `\\}`);
110const reverseRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${reverseStr}`, 'm');
111const sliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${sliceStr}`, 'm');
112const spliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${spliceStr}`, 'm');
113const swapRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${swapStr}`, 'm');
114
115
116/**
117 * Extracts the actions that should be taken to decipher a signature.
118 *
119 * This searches for a function that performs string manipulations on
120 * the signature. We already know what the 3 possible changes to a signature
121 * are in order to decipher it. There is
122 *
123 * * Reversing the string.
124 * * Removing a number of characters from the beginning.
125 * * Swapping the first character with another position.
126 *
127 * Note, `Array#slice()` used to be used instead of `Array#splice()`,
128 * it's kept in case we encounter any older html5player files.
129 *
130 * After retrieving the function that does this, we can see what actions
131 * it takes on a signature.
132 *
133 * @param {string} body
134 * @returns {Array.<string>}
135 */
136exports.extractActions = body => {
137 const objResult = actionsObjRegexp.exec(body);
138 const funcResult = actionsFuncRegexp.exec(body);
139 if (!objResult || !funcResult) { return null; }
140
141 const obj = objResult[1].replace(/\$/g, '\\$');
142 const objBody = objResult[2].replace(/\$/g, '\\$');
143 const funcBody = funcResult[1].replace(/\$/g, '\\$');
144
145 let result = reverseRegexp.exec(objBody);
146 const reverseKey = result && result[1]
147 .replace(/\$/g, '\\$')
148 .replace(/\$|^'|^"|'$|"$/g, '');
149 result = sliceRegexp.exec(objBody);
150 const sliceKey = result && result[1]
151 .replace(/\$/g, '\\$')
152 .replace(/\$|^'|^"|'$|"$/g, '');
153 result = spliceRegexp.exec(objBody);
154 const spliceKey = result && result[1]
155 .replace(/\$/g, '\\$')
156 .replace(/\$|^'|^"|'$|"$/g, '');
157 result = swapRegexp.exec(objBody);
158 const swapKey = result && result[1]
159 .replace(/\$/g, '\\$')
160 .replace(/\$|^'|^"|'$|"$/g, '');
161
162 const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`;
163 const myreg = `(?:a=)?${obj
164 }(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` +
165 `\\(a,(\\d+)\\)`;
166 const tokenizeRegexp = new RegExp(myreg, 'g');
167 const tokens = [];
168 while ((result = tokenizeRegexp.exec(funcBody)) !== null) {
169 let key = result[1] || result[2] || result[3];
170 switch (key) {
171 case swapKey:
172 tokens.push(`w${result[4]}`);
173 break;
174 case reverseKey:
175 tokens.push('r');
176 break;
177 case sliceKey:
178 tokens.push(`s${result[4]}`);
179 break;
180 case spliceKey:
181 tokens.push(`p${result[4]}`);
182 break;
183 }
184 }
185 return tokens;
186};
187
188
189/**
190 * @param {Object} format
191 * @param {string} sig
192 */
193exports.setDownloadURL = (format, sig) => {
194 let decodedUrl;
195 if (format.url) {
196 decodedUrl = format.url;
197 } else {
198 return;
199 }
200
201 try {
202 decodedUrl = decodeURIComponent(decodedUrl);
203 } catch (err) {
204 return;
205 }
206
207 // Make some adjustments to the final url.
208 const parsedUrl = url.parse(decodedUrl, true);
209
210 // Deleting the `search` part is necessary otherwise changes to
211 // `query` won't reflect when running `url.format()`
212 delete parsedUrl.search;
213
214 let query = parsedUrl.query;
215
216 // This is needed for a speedier download.
217 // See https://github.com/fent/node-ytdl-core/issues/127
218 query.ratebypass = 'yes';
219 if (sig) {
220 // When YouTube provides a `sp` parameter the signature `sig` must go
221 // into the parameter it specifies.
222 // See https://github.com/fent/node-ytdl-core/issues/417
223 query[format.sp || 'signature'] = sig;
224 }
225
226 format.url = url.format(parsedUrl);
227};
228
229
230/**
231 * Applies `sig.decipher()` to all format URL's.
232 *
233 * @param {Array.<Object>} formats
234 * @param {string} html5player
235 * @param {Object} options
236 */
237exports.decipherFormats = async(formats, html5player, options) => {
238 let decipheredFormats = {};
239 let tokens = await exports.getTokens(html5player, options);
240 formats.forEach(format => {
241 let cipher = format.signatureCipher || format.cipher;
242 if (cipher) {
243 Object.assign(format, querystring.parse(cipher));
244 delete format.signatureCipher;
245 delete format.cipher;
246 }
247 const sig = tokens && format.s ? exports.decipher(tokens, format.s) : null;
248 exports.setDownloadURL(format, sig);
249 decipheredFormats[format.url] = format;
250 });
251 return decipheredFormats;
252};