|
1 | 1 | const querystring = require('querystring');
|
2 | 2 | const Cache = require('./cache');
|
3 | 3 | const utils = require('./utils');
|
| 4 | +const vm = require('vm'); |
4 | 5 |
|
5 |
| - |
6 |
| -// A shared cache to keep track of html5player.js tokens. |
| 6 | +// A shared cache to keep track of html5player js functions. |
7 | 7 | exports.cache = new Cache();
|
8 | 8 |
|
9 |
| - |
10 | 9 | /**
|
11 |
| - * Extract signature deciphering tokens from html5player file. |
| 10 | + * Extract signature deciphering and n parameter transform functions from html5player file. |
12 | 11 | *
|
13 | 12 | * @param {string} html5playerfile
|
14 | 13 | * @param {Object} options
|
15 | 14 | * @returns {Promise<Array.<string>>}
|
16 | 15 | */
|
17 |
| -exports.getTokens = (html5playerfile, options) => exports.cache.getOrSet(html5playerfile, async() => { |
| 16 | +exports.getFunctions = (html5playerfile, options) => exports.cache.getOrSet(html5playerfile, async() => { |
18 | 17 | 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'); |
22 | 21 | }
|
23 |
| - exports.cache.set(html5playerfile, tokens); |
24 |
| - return tokens; |
| 22 | + exports.cache.set(html5playerfile, functions); |
| 23 | + return functions; |
25 | 24 | });
|
26 | 25 |
|
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 |
| - |
115 | 26 | /**
|
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 |
131 | 29 | *
|
132 | 30 | * @param {string} body
|
133 | 31 | * @returns {Array.<string>}
|
134 | 32 | */
|
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 | + } |
182 | 55 | }
|
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; |
185 | 72 | };
|
186 | 73 |
|
187 |
| - |
188 | 74 | /**
|
| 75 | + * Apply decipher and n-transform to individual format |
| 76 | + * |
189 | 77 | * @param {Object} format
|
190 |
| - * @param {string} sig |
| 78 | + * @param {vm.Script} decipherScript |
| 79 | + * @param {vm.Script} nTransformScript |
191 | 80 | */
|
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; |
221 | 102 | };
|
222 | 103 |
|
223 |
| - |
224 | 104 | /**
|
225 |
| - * Applies `sig.decipher()` to all format URL's. |
| 105 | + * Applies decipher and n parameter transforms to all format URL's. |
226 | 106 | *
|
227 | 107 | * @param {Array.<Object>} formats
|
228 | 108 | * @param {string} html5player
|
229 | 109 | * @param {Object} options
|
230 | 110 | */
|
231 | 111 | exports.decipherFormats = async(formats, html5player, options) => {
|
232 | 112 | 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; |
234 | 116 | 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); |
243 | 118 | decipheredFormats[format.url] = format;
|
244 | 119 | });
|
245 | 120 | return decipheredFormats;
|
|
0 commit comments