diff --git a/package-lock.json b/package-lock.json index e61524e6..e360907c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.0.0", - "jintr": "^3.2.1", + "jintr": "^3.3.0", "tslib": "^2.5.0", "undici": "^5.19.1" }, @@ -6155,9 +6155,9 @@ } }, "node_modules/jintr": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jintr/-/jintr-3.2.1.tgz", - "integrity": "sha512-yjKUBuwTTg4nc4izMysxuIk0BKh45hnbc1KnXE6LxagIGZn5od+I2elpuRY9IIm3EiKiUZxhxV89a0iX+xoEZg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jintr/-/jintr-3.3.0.tgz", + "integrity": "sha512-ZsaajJ4Hr5XR0tSPhOZOTjFhxA0qscKNSOs41NRjx7ZOGwpfdp8NKIBEUtvUPbA37JXyv1sJlgeOOZHjr3h76Q==", "funding": [ "https://github.com/sponsors/LuanRT" ], diff --git a/package.json b/package.json index 0dce1209..72eb9a62 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.0.0", - "jintr": "^3.2.1", + "jintr": "^3.3.0", "tslib": "^2.5.0", "undici": "^5.19.1" }, diff --git a/src/core/Player.ts b/src/core/Player.ts index 21ec1ad1..80f77c4e 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -1,6 +1,15 @@ -import { Log, LZW, Constants } from '../utils/index.js'; -import { Platform, getRandomUserAgent, getStringBetweenStrings, findFunction, PlayerError } from '../utils/Utils.js'; -import type { ICache, FetchFunction } from '../types/index.js'; +import { Jinter } from 'jintr'; +import type { FetchFunction, ICache } from '../types/index.js'; +import { Constants, Log, LZW } from '../utils/index.js'; +import { + type ASTLookupResult, + findFunction, + findVariable, + getRandomUserAgent, + getStringBetweenStrings, + Platform, + PlayerError +} from '../utils/Utils.js'; const TAG = 'Player'; @@ -63,9 +72,12 @@ export default class Player { const player_js = await player_res.text(); + const ast = Jinter.parseScript(player_js, { ecmaVersion: 'latest', ranges: true }); + const sig_timestamp = this.extractSigTimestamp(player_js); - const sig_sc = this.extractSigSourceCode(player_js); - const nsig_sc = this.extractNSigSourceCode(player_js); + const global_variable = this.extractGlobalVariable(player_js, ast); + const sig_sc = this.extractSigSourceCode(player_js, global_variable); + const nsig_sc = this.extractNSigSourceCode(player_js, ast, global_variable); Log.info(TAG, `Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`); @@ -223,8 +235,27 @@ export default class Player { return parseInt(getStringBetweenStrings(data, 'signatureTimestamp:', ',') || '0'); } - static extractSigSourceCode(data: string): string | undefined { - const match = data.match(/function\(([A-Za-z_0-9]+)\)\{([A-Za-z_0-9]+=[A-Za-z_0-9]+\.split\(""\)(.+?)\.join\(""\))\}/); + static extractGlobalVariable(data: string, ast: ReturnType): ASTLookupResult | undefined { + let variable = findVariable(data, { includes: '-_w8_', ast }); + + // For redundancy/the above fails: + if (!variable) + variable = findVariable(data, { includes: 'Untrusted URL{', ast }); + + if (!variable) + variable = findVariable(data, { includes: '1969', ast }); + + if (!variable) + variable = findVariable(data, { includes: '1970', ast }); + + if (!variable) + variable = findVariable(data, { includes: 'playerfallback', ast }); + + return variable; + } + + static extractSigSourceCode(data: string, global_variable?: ASTLookupResult): string | undefined { + const match = data.match(/function\(([A-Za-z_0-9]+)\)\{([A-Za-z_0-9]+=[A-Za-z_0-9]+\.split\((?:[^)]+)\)(.+?)\.join\((?:[^)]+)\))\}/); if (!match) { Log.warn(TAG, 'Failed to extract signature decipher algorithm.'); @@ -232,30 +263,45 @@ export default class Player { } const var_name = match[1]; - const obj_name = match[3].split(/\.|\[/)[0]?.replace(';', '').trim(); const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};'); if (!functions || !var_name) Log.warn(TAG, 'Failed to extract signature decipher algorithm.'); - return `function descramble_sig(${var_name}) { let ${obj_name}={${functions}}; ${match[2]} } descramble_sig(sig);`; + return `${global_variable?.result || ''} function descramble_sig(${var_name}) { let ${obj_name}={${functions}}; ${match[2]} } descramble_sig(sig);`; } - static extractNSigSourceCode(data: string): string | undefined { - // This used to be the prefix of the error tag (leaving it here for reference). - let nsig_function = findFunction(data, { includes: 'enhanced_except' }); - + static extractNSigSourceCode(data: string, ast?: ReturnType, global_variable?: ASTLookupResult): string | undefined { + let nsig_function; + + if (global_variable) { + nsig_function = findFunction(data, { includes: `new Date(${global_variable.name}`, ast }); + + // For redundancy/the above fails: + if (!nsig_function) + nsig_function = findFunction(data, { includes: '.push(String.fromCharCode(', ast }); + + if (!nsig_function) + nsig_function = findFunction(data, { includes: '.reverse().forEach(function', ast }); + + if (nsig_function) + return `${global_variable.result} var ${nsig_function.result} ${nsig_function.name}(nsig);`; + } + // This is the suffix of the error tag. - if (!nsig_function) - nsig_function = findFunction(data, { includes: '-_w8_' }); + nsig_function = findFunction(data, { includes: '-_w8_', ast }); // Usually, only this function uses these dates in the entire script. if (!nsig_function) - nsig_function = findFunction(data, { includes: '1969' }); + nsig_function = findFunction(data, { includes: '1969', ast }); + + // This used to be the prefix of the error tag (leaving it here for reference). + if (!nsig_function) + nsig_function = findFunction(data, { includes: 'enhanced_except', ast }); if (nsig_function) - return `${nsig_function.result} ${nsig_function.name}(nsig);`; + return `let ${nsig_function.result} ${nsig_function.name}(nsig);`; } get url(): string { @@ -263,6 +309,6 @@ export default class Player { } static get LIBRARY_VERSION(): number { - return 13; + return 14; } } \ No newline at end of file diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 7a923c57..e0f8c77a 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -1,13 +1,14 @@ -import { Memo } from '../parser/helpers.js'; -import { Text } from '../parser/misc.js'; -import * as Log from './Log.js'; -import userAgents from './user-agents.js'; +import type { Node } from 'estree'; import { Jinter } from 'jintr'; import type { EmojiRun, TextRun } from '../parser/misc.js'; import type { FetchFunction } from '../types/index.js'; import type PlatformShim from '../types/PlatformShim.js'; -import type { Node } from 'estree'; + +import { Memo } from '../parser/helpers.js'; +import { Text } from '../parser/misc.js'; +import * as Log from './Log.js'; +import userAgents from './user-agents.js'; const TAG_ = 'Utils'; @@ -17,6 +18,7 @@ export class Platform { static load(platform: PlatformShim): void { shim = platform; } + static get shim(): PlatformShim { if (!shim) { throw new Error('Platform is not loaded'); @@ -24,6 +26,7 @@ export class Platform { return shim; } } + export class InnertubeError extends Error { date: Date; version: string; @@ -41,12 +44,23 @@ export class InnertubeError extends Error { } } -export class ParsingError extends InnertubeError { } -export class MissingParamError extends InnertubeError { } -export class OAuth2Error extends InnertubeError { } -export class PlayerError extends Error { } -export class SessionError extends Error { } -export class ChannelError extends Error { } +export class ParsingError extends InnertubeError { +} + +export class MissingParamError extends InnertubeError { +} + +export class OAuth2Error extends InnertubeError { +} + +export class PlayerError extends Error { +} + +export class SessionError extends Error { +} + +export class ChannelError extends Error { +} /** * Compares given objects. May not work correctly for @@ -251,7 +265,7 @@ export function getCookie(cookies: string, name: string, matchWholeName = false) return match ? match[2] : undefined; } -export type FindFunctionArgs = { +export type ASTLookupArgs = { /** * The name of the function. */ @@ -266,9 +280,14 @@ export type FindFunctionArgs = { * A regular expression that the function's code must match. */ regexp?: RegExp; + + /** + * The abstract syntax tree of the source code. + */ + ast?: ReturnType; }; -export type FindFunctionResult = { +export type ASTLookupResult = { start: number; end: number; name: string; @@ -277,7 +296,7 @@ export type FindFunctionResult = { }; /** - * Finds a function in a source string based on the provided search criteria. + * Searches for a function in the given code based on specified criteria. * * @example * ```ts @@ -286,12 +305,14 @@ export type FindFunctionResult = { * console.log(result); * // Output: { start: 69, end: 110, name: 'bar', node: { ... }, result: 'bar = function() { console.log("bar"); };' } * ``` + * + * @returns An object containing the function's details if found, `undefined` otherwise. */ -export function findFunction(source: string, args: FindFunctionArgs): FindFunctionResult | undefined { - const { name, includes, regexp } = args; +export function findFunction(source: string, args: ASTLookupArgs): ASTLookupResult | undefined { + const { name, includes, regexp, ast } = args; - const node = Jinter.parseScript(source); - const stack = [ node ] as (Node & { start: number; end: number})[]; + const node = ast ? ast : Jinter.parseScript(source); + const stack = [ node ] as (Node & { start: number; end: number })[]; for (let i = 0; i < stack.length; i++) { const current = stack[i]; @@ -307,7 +328,7 @@ export function findFunction(source: string, args: FindFunctionArgs): FindFuncti if ( (name && current.expression.left.name === name) || - (includes && code.indexOf(includes) > -1) || + (includes && code.includes(includes)) || (regexp && regexp.test(code)) ) { return { @@ -329,4 +350,76 @@ export function findFunction(source: string, args: FindFunctionArgs): FindFuncti } } } +} + +/** + * Searches for a variable declaration in the given code based on specified criteria. + * + * @example + * ```ts + * // Find a variable by name + * const code = 'const x = 5; let y = "hello";'; + * const a = findVariable(code, { name: 'y' }); + * console.log(a?.result); + * + * // Find a variable containing specific text + * const b = findVariable(code, { includes: 'hello' }); + * console.log(b?.result); + * + * // Find a variable matching a pattern + * const c = findVariable(code, { regexp: /y\s*=\s*"hello"/ }); + * console.log(c?.result); + * ``` + * + * @returns An object containing the variable's details if found, `undefined` otherwise. + */ +export function findVariable(code: string, options: ASTLookupArgs): ASTLookupResult | undefined { + const ast = options.ast ? options.ast : Jinter.parseScript(code, { ecmaVersion: 'latest', ranges: true }); + + let found: ASTLookupResult | undefined; + + function walk(node: Node): void { + if (found) return; + + if (node.type === 'VariableDeclaration') { + const [ start, end ] = node.range!; + const node_source = code.slice(start, end); + + for (const declarator of node.declarations) { + if (declarator.id.type === 'Identifier') { + const var_name = declarator.id.name; + if (options.name && var_name === options.name) { + found = { start, end, name: var_name, node, result: node_source }; + return; + } + } + } + if ( + (options.includes && node_source.includes(options.includes)) || + (options.regexp && options.regexp.test(node_source))) { + found = { start, end, name: (node.declarations?.[0]?.id as any)?.name, node, result: node_source }; + return; + } + } + + for (const key in node) { + if (Object.prototype.hasOwnProperty.call(node, key)) { + const child = node[key as keyof typeof node] as any; + if (Array.isArray(child)) { + for (const c of child) { + if (c && typeof c.type === 'string') { + walk(c); + if (found) return; + } + } + } else if (child && typeof child.type === 'string') { + walk(child); + if (found) return; + } + } + } + } + + walk(ast); + return found; } \ No newline at end of file