sig.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. var fs = require('fs');
  2. var path = require('path');
  3. var url = require('url');
  4. var request = require('./request');
  5. var cache = require('./cache');
  6. /**
  7. * Extract signature deciphering tokens from html5player file.
  8. *
  9. * @param {String} html5playerfile
  10. * @param {Boolean} debug
  11. * @param {Function(!Error, Array.<String>)}
  12. */
  13. exports.getTokens = function(html5playerfile, debug, callback) {
  14. var key, cachedTokens;
  15. var rs = /(?:html5)?player-([a-zA-Z0-9\-_]+)(?:\.js|\/)/
  16. .exec(html5playerfile);
  17. if (rs) {
  18. key = rs[1];
  19. cachedTokens = cache.get(key);
  20. } else {
  21. console.warn('could not extract html5player key:', html5playerfile);
  22. }
  23. if (cachedTokens) {
  24. callback(null, cachedTokens);
  25. } else {
  26. html5playerfile = 'http:' + html5playerfile;
  27. request(html5playerfile, function(err, body) {
  28. if (err) return callback(err);
  29. var tokens = exports.extractActions(body);
  30. if (!tokens || !tokens.length) {
  31. if (debug) {
  32. var filename = key + '.js';
  33. var filepath = path.resolve(
  34. __dirname, '../test/files/html5player/' + filename);
  35. fs.writeFile(filepath, body);
  36. var html5player = require('../test/html5player.json');
  37. if (!html5player[key]) {
  38. html5player[key] = [];
  39. fs.writeFile(
  40. path.resolve(__dirname, '../test/html5player.json'),
  41. JSON.stringify(html5player, null, 2));
  42. }
  43. }
  44. callback(
  45. new Error('Could not extract signature deciphering actions'));
  46. return;
  47. }
  48. cache.set(key, tokens);
  49. callback(null, tokens);
  50. });
  51. }
  52. };
  53. /**
  54. * Gets signature for formats.
  55. *
  56. * @param {Array.<Object>} formats
  57. * @param {String} html5playerfile
  58. * @param {Boolean} debug
  59. * @param {Function(!Error)} callback
  60. */
  61. exports.get = function(formats, html5playerfile, debug, callback) {
  62. if (formats[0] && formats[0].s) {
  63. exports.getTokens(html5playerfile, debug, function(err, tokens) {
  64. if (err) return callback(err);
  65. exports.decipherFormats(formats, tokens, debug);
  66. callback(null);
  67. });
  68. } else {
  69. formats.forEach(function(format) {
  70. var sig = format.sig || '';
  71. format.url = exports.getDownloadURL(format, sig, debug);
  72. });
  73. callback(null);
  74. }
  75. };
  76. /**
  77. * @param {Object} format
  78. * @param {Array.<String>} tokens
  79. * @param {Boolean} debug
  80. * @return {!String}
  81. */
  82. exports.getDownloadURL = function(format, sig, debug) {
  83. var decodedUrl;
  84. if (format.url) {
  85. decodedUrl = format.url;
  86. } else if (format.stream) {
  87. if (format.conn) {
  88. decodedUrl = format.conn;
  89. if (decodedUrl[decodedUrl.length - 1] !== '/') {
  90. decodedUrl += '/';
  91. }
  92. decodedUrl += format.stream;
  93. } else {
  94. decodedUrl = format.stream;
  95. }
  96. } else {
  97. if (debug) {
  98. console.warn('download url not found for itag ' + format.itag);
  99. }
  100. return null;
  101. }
  102. try {
  103. decodedUrl = decodeURIComponent(decodedUrl);
  104. } catch (err) {
  105. if (debug) {
  106. console.warn('could not decode url: ' + err.message);
  107. }
  108. return null;
  109. }
  110. // Make some adjustments to the final url.
  111. var parsedUrl = url.parse(decodedUrl, true);
  112. // Deleting the `search` part is necessary otherwise changes to
  113. // `query` won't reflect when running `url.format()`
  114. delete parsedUrl.search;
  115. var query = parsedUrl.query;
  116. query.ratebypass = 'yes';
  117. if (sig) {
  118. query.signature = sig;
  119. }
  120. return url.format(parsedUrl);
  121. };
  122. /**
  123. * Applies `sig.decipher()` to all format URL's.
  124. *
  125. * @param {Array.<Object>} formats
  126. * @param {Array.<String} tokens
  127. * @param {Boolean} debug
  128. */
  129. exports.decipherFormats = function(formats, tokens, debug) {
  130. formats.forEach(function(format) {
  131. var sig = format.s ? exports.decipher(tokens, format.s) : null;
  132. format.url = exports.getDownloadURL(format, sig, debug);
  133. });
  134. };
  135. /**
  136. * Decipher a signature based on action tokens.
  137. *
  138. * @param {Array.<String>} tokens
  139. * @param {String} sig
  140. * @return {String}
  141. */
  142. exports.decipher = function(tokens, sig) {
  143. sig = sig.split('');
  144. var pos;
  145. for (var i = 0, len = tokens.length; i < len; i++) {
  146. var token = tokens[i];
  147. switch (token[0]) {
  148. case 'r':
  149. sig = sig.reverse();
  150. break;
  151. case 'w':
  152. pos = ~~token.slice(1);
  153. sig = swapHeadAndPosition(sig, pos);
  154. break;
  155. case 's':
  156. pos = ~~token.slice(1);
  157. sig = sig.slice(pos);
  158. break;
  159. case 'p':
  160. pos = ~~token.slice(1);
  161. sig.splice(0, pos);
  162. break;
  163. }
  164. }
  165. return sig.join('');
  166. };
  167. /**
  168. * Swaps the first element of an array with one of given position.
  169. *
  170. * @param {Array.<Object>} arr
  171. * @param {Number} position
  172. * @return {Array.<Object>}
  173. */
  174. function swapHeadAndPosition(arr, position) {
  175. var first = arr[0];
  176. arr[0] = arr[position % arr.length];
  177. arr[position] = first;
  178. return arr;
  179. }
  180. var jsvarStr = '[a-zA-Z_\\$][a-zA-Z_0-9]*';
  181. var reverseStr = ':function\\(a\\)\\{' +
  182. '(?:return )?a\\.reverse\\(\\)' +
  183. '\\}';
  184. var sliceStr = ':function\\(a,b\\)\\{' +
  185. 'return a\\.slice\\(b\\)' +
  186. '\\}';
  187. var spliceStr = ':function\\(a,b\\)\\{' +
  188. 'a\\.splice\\(0,b\\)' +
  189. '\\}';
  190. var swapStr = ':function\\(a,b\\)\\{' +
  191. 'var c=a\\[0\\];a\\[0\\]=a\\[b%a\\.length\\];a\\[b\\]=c(?:;return a)?' +
  192. '\\}';
  193. var actionsObjRegexp = new RegExp(
  194. 'var (' + jsvarStr + ')=\\{((?:(?:' +
  195. jsvarStr + reverseStr + '|' +
  196. jsvarStr + sliceStr + '|' +
  197. jsvarStr + spliceStr + '|' +
  198. jsvarStr + swapStr +
  199. '),?\\n?)+)\\};'
  200. );
  201. var actionsFuncRegexp = new RegExp('function(?: ' + jsvarStr + ')?\\(a\\)\\{' +
  202. 'a=a\\.split\\(""\\);\\s*' +
  203. '((?:(?:a=)?' + jsvarStr + '\\.' + jsvarStr + '\\(a,\\d+\\);)+)' +
  204. 'return a\\.join\\(""\\)' +
  205. '\\}'
  206. );
  207. var reverseRegexp = new RegExp('(?:^|,)(' + jsvarStr + ')' + reverseStr, 'm');
  208. var sliceRegexp = new RegExp('(?:^|,)(' + jsvarStr + ')' + sliceStr, 'm');
  209. var spliceRegexp = new RegExp('(?:^|,)(' + jsvarStr + ')' + spliceStr, 'm');
  210. var swapRegexp = new RegExp('(?:^|,)(' + jsvarStr + ')' + swapStr, 'm');
  211. /**
  212. * Extracts the actions that should be taken to decypher a signature.
  213. *
  214. *
  215. * @param {String} body
  216. * @return {Array.<String>}
  217. */
  218. exports.extractActions = function(body) {
  219. var objResult = actionsObjRegexp.exec(body);
  220. if (!objResult) { return null; }
  221. var funcResult = actionsFuncRegexp.exec(body);
  222. if (!funcResult) { return null; }
  223. var obj = objResult[1].replace(/\$/g, '\\$');
  224. var objBody = objResult[2].replace(/\$/g, '\\$');
  225. var funcbody = funcResult[1].replace(/\$/g, '\\$');
  226. var result = reverseRegexp.exec(objBody);
  227. var reverseKey = result && result[1].replace(/\$/g, '\\$');
  228. result = sliceRegexp.exec(objBody);
  229. var sliceKey = result && result[1].replace(/\$/g, '\\$');
  230. result = spliceRegexp.exec(objBody);
  231. var spliceKey = result && result[1].replace(/\$/g, '\\$');
  232. result = swapRegexp.exec(objBody);
  233. var swapKey = result && result[1].replace(/\$/g, '\\$');
  234. var myreg = '(?:a=)?' + obj + '\\.(' +
  235. [reverseKey, sliceKey, spliceKey, swapKey].join('|') + ')\\(a,(\\d+)\\)';
  236. var tokenizeRegexp = new RegExp(myreg, 'g');
  237. var tokens = [];
  238. while ((result = tokenizeRegexp.exec(funcbody)) !== null) {
  239. switch (result[1]) {
  240. case swapKey:
  241. tokens.push('w' + result[2]);
  242. break;
  243. case reverseKey:
  244. tokens.push('r');
  245. break;
  246. case sliceKey:
  247. tokens.push('s' + result[2]);
  248. break;
  249. case spliceKey:
  250. tokens.push('p' + result[2]);
  251. break;
  252. }
  253. }
  254. return tokens;
  255. };