diff --git a/src/core/Player.ts b/src/core/Player.ts index 4ca3eb59..02d1de47 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -4,12 +4,13 @@ import { Constants, BinarySerializer, Log } from '../utils/index.js'; import { getRandomUserAgent, getStringBetweenStrings, + getNsigProcessorFn, Platform, PlayerError } from '../utils/Utils.js'; import { JsExtractor, JsAnalyzer } from '../utils/index.js'; -import { nMatcher, sigMatcher, timestampMatcher } from '../utils/javascript/matchers.js'; +import { nsigMatcher, timestampMatcher } from '../utils/javascript/matchers.js'; import type { ExtractionConfig } from '../utils/javascript/JsAnalyzer.js'; import type { BuildScriptResult } from '../utils/javascript/JsExtractor.js'; @@ -71,7 +72,7 @@ export default class Player { } } - const player_url = new URL(`/s/player/${player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE); + const player_url = new URL(`/s/player/${player_id}/player_es6.vflset/en_US/base.js`, Constants.URLS.YT_BASE); Log.info(TAG, `Could not find any cached player. Will download a new player from ${player_url}.`); @@ -87,13 +88,11 @@ export default class Player { const player_js = await player_res.text(); - const sigFunctionName = 'sigFunction'; - const nFunctionName = 'nFunction'; + const nsigFunctionName = 'nsigFunction'; const timestampVarName = 'signatureTimestampVar'; const extractions: ExtractionConfig[] = [ - { friendlyName: sigFunctionName, match: sigMatcher }, - { friendlyName: nFunctionName, match: nMatcher }, + { friendlyName: nsigFunctionName, match: nsigMatcher }, { friendlyName: timestampVarName, match: timestampMatcher, collectDependencies: false } ]; @@ -110,12 +109,8 @@ export default class Player { Log.warn(TAG, 'Failed to extract signature timestamp.'); } - if (!result.exported.includes(sigFunctionName)) { - Log.warn(TAG, 'Failed to extract signature decipher function.'); - } - - if (!result.exported.includes(nFunctionName)) { - Log.warn(TAG, 'Failed to extract n decipher function.'); + if (!result.exported.includes(nsigFunctionName)) { + Log.warn(TAG, 'Failed to extract n/sig decipher function.'); } const signatureTimestamp = result.exportedRawValues?.[timestampVarName]; @@ -145,10 +140,11 @@ export default class Player { const sp = args.get('sp'); if (this.data && ((signature_cipher || cipher) || n)) { - const eval_args: { sig?: string | null; n?: string | null } = {}; + const eval_args: { sig?: string | null; n?: string | null; sp?: string | null } = {}; if (signature_cipher || cipher) { eval_args.sig = s; + eval_args.sp = sp; } if (n) { @@ -161,7 +157,12 @@ export default class Player { } if (Object.keys(eval_args).length > 0) { - const result = await Platform.shim.eval(this.data, eval_args) as Record; + // Shallow copy to avoid mutating the original data. + const data = { ...this.data }; + + data.output = `${data.output}\n${getNsigProcessorFn(eval_args.n, eval_args.sp, eval_args.sig)}`; + + const result = await Platform.shim.eval(data, eval_args) as Record; if (typeof result !== 'object' || result === null) { throw new PlayerError('Got invalid result from player script evaluation.'); diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index abe40659..2246e08a 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -76,7 +76,7 @@ export const CLIENTS = { }, TV: { NAME: 'TVHTML5', - VERSION: '7.20250219.14.00', + VERSION: '7.20260311.12.00', USER_AGENT: 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version' }, TV_SIMPLY: { @@ -133,4 +133,4 @@ export const INNERTUBE_HEADERS_BASE = { 'content-type': 'application/json' } as const; -export const SUPPORTED_CLIENTS = [ 'IOS', 'WEB', 'MWEB', 'YTKIDS', 'YTMUSIC', 'ANDROID', 'ANDROID_VR', 'YTSTUDIO_ANDROID', 'YTMUSIC_ANDROID', 'TV', 'TV_SIMPLY', 'TV_EMBEDDED', 'WEB_EMBEDDED', 'WEB_CREATOR' ]; +export const SUPPORTED_CLIENTS = [ 'IOS', 'WEB', 'MWEB', 'YTKIDS', 'YTMUSIC', 'ANDROID', 'ANDROID_VR', 'YTSTUDIO_ANDROID', 'YTMUSIC_ANDROID', 'TV', 'TV_SIMPLY', 'TV_EMBEDDED', 'WEB_EMBEDDED', 'WEB_CREATOR' ]; \ No newline at end of file diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 74d18d72..d484057a 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -261,4 +261,34 @@ export function getCookie(cookies: string, name: string, matchWholeName = false) const regex = matchWholeName ? `(^|\\s?)\\b${name}\\b=([^;]+)` : `(^|s?)${name}=([^;]+)`; const match = cookies.match(new RegExp(regex)); return match ? match[2] : undefined; +} + +export function getNsigProcessorFn(n?: string | null, sp?: string | null, s?: string | null) { + return `function process(n = "", sp = "", s = "") { + const mockStreamingURL = "https://ytjs.googlevideo.com/videoplayback?expire=1234567890&"+"n="+encodeURIComponent(n); + const urlCtorFunction = exportedVars.nsigFunction || (() => { throw new Error('No n/sig decipher function extracted') }); + const urlCtor = urlCtorFunction(mockStreamingURL, sp, s); + + const proto = Object.getPrototypeOf(urlCtor); + const properties = Object.getOwnPropertyNames(proto); + const methodBlacklist = ['constructor', 'clone', 'set', 'get']; + + for (const prop of properties) { + if (methodBlacklist.includes(prop)) + continue; + + if (typeof urlCtor[prop] === 'function') + urlCtor[prop](); + } + + const sigResult = urlCtor.get(sp); + const nResult = urlCtor.get('n'); + + return { + sig: sigResult ? decodeURIComponent(sigResult) : undefined, + n: nResult ? decodeURIComponent(nResult) : undefined + }; +} + +return process("${n || ''}", "${sp || ''}", "${s || ''}");`; } \ No newline at end of file diff --git a/src/utils/javascript/JsAnalyzer.ts b/src/utils/javascript/JsAnalyzer.ts index 1da84878..446af0bb 100644 --- a/src/utils/javascript/JsAnalyzer.ts +++ b/src/utils/javascript/JsAnalyzer.ts @@ -1,6 +1,5 @@ -import type { ESTree } from 'meriyah'; -import { parseScript } from 'meriyah'; -import { WALK_STOP, jsBuiltIns, memberBaseName, memberToString, walkAst } from './helpers.js'; +import { parseScript, type ESTree } from 'meriyah'; +import { jsBuiltIns, memberBaseName, memberToString, walkAst } from './helpers.js'; export interface ExtractionConfig { /** @@ -14,9 +13,20 @@ export interface ExtractionConfig { collectDependencies?: boolean; /** * When `true`, traversal stops once the extraction is matched and all its dependencies (when `collectDependencies=true`) resolve. - * Only useful for small functions/vars without too many dependencies. + * Only useful for small functions/vars without too many dependencies. Deeper dependency trees will usually have the unresolvable + * member expression here and there, for example: + * ```js + * var Vmi = g.dX.window, Wr = Vmi?.yt?.config_ || Vmi?.ytcfg?.data_ || {}; + * ``` + * + * Since `Vmi.ytcfg` is a dependency, it will never resolve because it comes from `g.dX.window`, which is an external object we don't have access to. + * In cases like this, `stopWhenReady` option does nothing useful. */ stopWhenReady?: boolean; + /** + * If `true`, dependency collection is limited to the match context node itself. + */ + onlyProcessMatchContext?: boolean; /** * Name for easier identification of extractions. */ @@ -35,6 +45,7 @@ export interface VariableMetadata { node?: any; dependencies: Set; dependents: Set; + prototypeAliases: Map>; predeclared: boolean; } @@ -64,9 +75,10 @@ export class JsAnalyzer { private readonly hasExtractions: boolean; private readonly extractionStates: ExtractionState[]; private readonly dependentsTracker: Map> = new Map(); + private pendingPrototypeAliasBinding: [string, VariableMetadata] | null = null; - public declaredVariables: Map = new Map(); public iifeParamName: string | null = null; + public readonly declaredVariables: Map = new Map(); /** * Creates a new instance over the provided source. @@ -135,6 +147,34 @@ export class JsAnalyzer { const left = assignment.left; const right = assignment.right; + // Detect things like `a.b = g.c.prototype` so later `a.b.foo = ...` can be attributed back to `g.c`. + if ( + right.type === 'MemberExpression' && + !right.computed && + right.property.type === 'Identifier' && + right.property.name === 'prototype' + ) { + const prototypeSourceExpr = memberToString(right, this.source); + const aliasTargetExpr = left.type === 'Identifier' ? left.name : memberToString(left, this.source); + + if (prototypeSourceExpr) { + const prototypeOwnerMeta = this.declaredVariables.get( + prototypeSourceExpr.replace('.prototype', '') + ); + + if (aliasTargetExpr && prototypeOwnerMeta) { + const aliasedPrototypeMembers = new Set(); + const aliasExpr = `${aliasTargetExpr}.`; // Had to add a dot here so we can detect it later when matching member expressions.. + + // Activate an alias binding context, so subsequent member assignments to the alias (`a.b.foo = ...`) can be tracked. + // NOTE: This assumes that the alias members come right after this declaration and are grouped together in the code, hehe :) + this.pendingPrototypeAliasBinding = [ aliasExpr, prototypeOwnerMeta ]; + + prototypeOwnerMeta.prototypeAliases.set(aliasExpr, aliasedPrototypeMembers); + } + } + } + if (left.type === 'Identifier') { // This identifier existing means it was a pre-declared and // we just got to it. @@ -150,6 +190,33 @@ export class JsAnalyzer { if (this.onMatch(existingVariable.node, existingVariable)) return; } else if (assignment.left.type === 'MemberExpression') { const memberName = memberToString(assignment.left, this.source); + const activeAliasExpr = this.pendingPrototypeAliasBinding?.[0]; + + // While an alias binding is active, collect member assignments made through the alias (`g.q.foo = ...`). + if (activeAliasExpr && (memberName?.includes(activeAliasExpr) || memberName === activeAliasExpr.slice(0, -1))) { + const aliasOwnerMeta = this.declaredVariables.get(this.pendingPrototypeAliasBinding?.[1].name || ''); + if (aliasOwnerMeta) { + const existingAliasedMembers = aliasOwnerMeta.prototypeAliases.get(activeAliasExpr); + + const aliasedMemberMeta: VariableMetadata = { + name: memberName, + node: currentNode, + dependents: this.dependentsTracker.get(memberName) || new Set(), + predeclared: false, + prototypeAliases: new Map>(), + dependencies: this.findDependencies(right, memberName) + }; + + if (existingAliasedMembers) { + existingAliasedMembers.add(aliasedMemberMeta); + } else { + aliasOwnerMeta.prototypeAliases.set(activeAliasExpr, new Set([ aliasedMemberMeta ])); + } + } + } else { + this.pendingPrototypeAliasBinding = null; + } + if (!memberName || this.declaredVariables.has(memberName)) continue; const metadata: VariableMetadata = { @@ -157,6 +224,7 @@ export class JsAnalyzer { node: currentNode, dependents: this.dependentsTracker.get(memberName) || new Set(), predeclared: false, + prototypeAliases: new Map>(), dependencies: this.findDependencies(right, memberName) }; @@ -176,6 +244,8 @@ export class JsAnalyzer { break; } case 'VariableDeclaration': { + this.pendingPrototypeAliasBinding = null; + for (const declaration of currentNode.declarations) { if (declaration.id.type !== 'Identifier') continue; @@ -183,15 +253,15 @@ export class JsAnalyzer { name: declaration.id.name, node: declaration, dependents: this.dependentsTracker.get(declaration.id.name) || new Set(), + prototypeAliases: new Map>(), dependencies: new Set(), predeclared: false }; const init = declaration.init; - // "var x, y, z;" if (!init && currentNode.kind === 'var') { - metadata.predeclared = true; + metadata.predeclared = true; // "var x, y, z;" } else if (init && this.needsDependencyAnalysis(init)) { metadata.dependencies = this.findDependencies(init, metadata.name); } @@ -227,6 +297,7 @@ export class JsAnalyzer { case 'ConditionalExpression': case 'ObjectExpression': case 'SequenceExpression': + case 'ClassExpression': case 'Identifier': return true; default: @@ -250,9 +321,23 @@ export class JsAnalyzer { for (const state of this.extractionStates) { if (!state.node) { if (node.type === 'VariableDeclarator' && !node.init) continue; + result = state.config.match(node); + if (!result) continue; state.node = node; + + matched = true; + + if (metadata) { + state.metadata = metadata; + state.dependents = metadata.dependents; + state.dependencies = metadata.dependencies; + if (typeof result !== 'boolean') + state.matchContext = result; + } + + this.refreshExtractionState(state); } else if (state.node !== node) { // Use this as a chance to refresh readiness in case dependencies were resolved since last time // we checked. @@ -261,21 +346,7 @@ export class JsAnalyzer { if (this.shouldStopTraversal()) { return true; } - - continue; } - - matched = true; - - if (metadata) { - state.metadata = metadata; - state.dependents = metadata.dependents; - state.dependencies = metadata.dependencies; - if (typeof result !== 'boolean') - state.matchContext = result; - } - - this.refreshExtractionState(state); } if (!matched) return false; @@ -425,6 +496,9 @@ export class JsAnalyzer { walkAst(rootNode, { enter: (n, parent) => { switch (n.type) { + // Note for anybody debugging this in the future: + // *DO NOT* add MethodDefinition here. + // MethodDefinition.value is a FunctionExpression, so it is already handled... case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': { @@ -458,9 +532,12 @@ export class JsAnalyzer { break; } case 'VariableDeclaration': { - const scope = currentScope(); + // var hoists to function scope... + const targetScope = n.kind === 'var' + ? scopeStack.findLast((s) => s.type === 'function') ?? currentScope() + : currentScope(); for (const d of n.declarations) { - collectBindingIdentifiers(d.id, scope.names); + collectBindingIdentifiers(d.id, targetScope.names); } break; } @@ -479,8 +556,10 @@ export class JsAnalyzer { // Ignore if it's a property name (e.g., "obj.prop" or "{prop: 1}"", we don't care about the "prop" name itself). if (parent?.type === 'Property' && parent.key === n && !parent.computed) return; + // Ignore class method names. They are declarations, not external dependencies. + if (parent?.type === 'MethodDefinition' && parent.key === n && !parent.computed) return; if (parent?.type === 'MemberExpression' && parent.property === n && !parent.computed) { - if (parent.object.type === 'ThisExpression') return; // Skip 'this.property' stuff. + if (parent.object.type === 'ThisExpression') return; // Skip 'this.property', etc. const full = memberToString(parent, this.source); if (!full) return; @@ -491,6 +570,7 @@ export class JsAnalyzer { dependencies.add(full); } else if (parent.object.type === 'Identifier') { const baseName = parent.object.name; + const declaredBaseVariable = this.declaredVariables.get(baseName); if ( (declaredBaseVariable || baseName === this.iifeParamName) && @@ -511,6 +591,10 @@ export class JsAnalyzer { return; } + if (parent?.type === 'MetaProperty') { + return; // Skip stuff like "new.target" or "import.meta" + } + if (isInScope(n.name) || jsBuiltIns.has(n.name)) return; // It's a free variable, so it's a dependency. @@ -530,6 +614,12 @@ export class JsAnalyzer { } break; } + case 'ForStatement': + case 'ForInStatement': + case 'ForOfStatement': { + scopeStack.push({ names: new Set(), type: 'block' }); + break; + } } }, leave: (n: any) => { @@ -539,6 +629,9 @@ export class JsAnalyzer { case 'ArrowFunctionExpression': case 'BlockStatement': case 'CatchClause': + case 'ForStatement': + case 'ForInStatement': + case 'ForOfStatement': if (scopeStack.length > 1) scopeStack.pop(); break; } @@ -562,4 +655,4 @@ export class JsAnalyzer { public getSource(): string { return this.source; } -} +} \ No newline at end of file diff --git a/src/utils/javascript/JsExtractor.ts b/src/utils/javascript/JsExtractor.ts index 666f15f9..2fd91e6a 100644 --- a/src/utils/javascript/JsExtractor.ts +++ b/src/utils/javascript/JsExtractor.ts @@ -89,6 +89,8 @@ export class JsExtractor { if (!node) return true; switch (node.type) { + case 'ClassExpression': + return true; case 'Literal': { const literal = node as ESTree.Literal & { regex?: unknown }; return ( @@ -168,6 +170,12 @@ export class JsExtractor { } return this.isSafeInitializer(node.object, mode); } + + // Allow cases such as a.b = c.prototype; + if (!node.computed && node.property.type === 'Identifier' && node.property.name === 'prototype') { + return true; + } + return false; } case 'LogicalExpression': @@ -269,16 +277,17 @@ export class JsExtractor { initSource = initializerFallback; } else { const left = assignmentTarget?.left; + const isPrototypeAlias = init?.type === 'MemberExpression' && !init.computed && init.property.type === 'Identifier' && init.property.name === 'prototype'; - // Often useless.. - // e.g. `xyz.someProp = ...` - if (left?.type === 'MemberExpression' && init) { + // Skip things we don't need. + if (!isPrototypeAlias && left?.type === 'MemberExpression' && init) { if ( canDisallow && left.object.type === 'Identifier' && init.type !== 'FunctionExpression' && init.type !== 'ArrowFunctionExpression' && - init.type !== 'LogicalExpression' + init.type !== 'LogicalExpression' && + init.type !== 'ClassExpression' ) { return `${indent}// Skipped ${memberToString(left, source)} assignment.`; } @@ -287,7 +296,7 @@ export class JsExtractor { // e.g. `someVar = someOtherVarFuncOrCall` initSource = extractNodeSource(init, source) ?.trim() - .replace(/;\s*$/, '') || 'kk'; + .replace(/;\s*$/, '') || 'undefined // [JsExtractor] Failed to extract initializer source.'; } } @@ -333,10 +342,18 @@ export class JsExtractor { predeclaredVarSet.add(name); } - const visit = (metadata?: VariableMetadata, depth: number = 0) => { + const visit = (metadata?: VariableMetadata, depth: number = 0, whitelistedDep?: string) => { if (!metadata || depth > maxDepth) return; for (const dependency of metadata.dependencies) { + if (whitelistedDep && whitelistedDep !== dependency) { + // If we haven't yet encountered the whitelisted dependency, skip this one. + // And if we have, delete the whitelist var so that all subsequent dependencies are included. + if (!seen.has(whitelistedDep)) + continue; + whitelistedDep = undefined; + } + if (seen.has(dependency)) continue; @@ -352,13 +369,19 @@ export class JsExtractor { registerPredeclaredVar(dependency); } - // Usually not used by anything we care about. Less code = better. - // e.g. `x.y = ...` - if (!dependency.includes('.')) { - visit(dependencyMetadata, depth + 1); - } + visit(dependencyMetadata, depth + 1, whitelistedDep); snippets.push(this.renderNode(dependencyMetadata.node, shouldPredeclare, config)); + + if (dependencyMetadata.prototypeAliases.size > 0) { + for (const [ , aliasMembers ] of dependencyMetadata.prototypeAliases) { + for (const member of aliasMembers) { + // This is deeper than the first visit, so no need to pass the whitelist, we want all deps of the member to be included. + visit(member, depth); + snippets.push(this.renderNode(member.node, shouldPredeclare, config)); + } + } + } } }; @@ -372,13 +395,26 @@ export class JsExtractor { snippets.push(`${indent}//#region --- start [${fname || 'Unknown'}] ---`); const shouldPredeclare = (forceVarPredeclaration || extraction.metadata.predeclared) && !shouldSkip; + const onlyProcessMatchContext = extraction.config.onlyProcessMatchContext; if (shouldPredeclare) { registerPredeclaredVar(extraction.metadata.name); } if (extraction.config.collectDependencies && !shouldSkip) { - visit(extraction.metadata); + let whitelistedDep; + + const matchContextNode = extraction.matchContext; + + if (matchContextNode?.type === 'NewExpression' && onlyProcessMatchContext) { + if (matchContextNode.callee.type === 'Identifier') { + whitelistedDep = matchContextNode.callee.name; + } else if (matchContextNode.callee.type === 'MemberExpression') { + whitelistedDep = memberToString(matchContextNode.callee, this.analyzer.getSource()) || undefined; + } + } + + visit(extraction.metadata, undefined, whitelistedDep); } if (extraction.matchContext && fname) { @@ -402,7 +438,10 @@ export class JsExtractor { } if (!shouldSkip) { - snippets.push(this.renderNode(extraction.metadata.node, shouldPredeclare, config)); + if (!onlyProcessMatchContext) { + snippets.push(this.renderNode(extraction.metadata.node, shouldPredeclare, config)); + } + snippets.push(`${indent}//#endregion --- end [${fname || 'Unknown'}] ---\n`); } } @@ -410,12 +449,16 @@ export class JsExtractor { const output = []; - // Not required by any means, but add it anyway, "just in case". - output.push('const window = Object.assign({}, globalThis);'); - output.push('const document = {};'); - output.push('const self = window;\n'); + output.push('const __jsExtractorGlobal = typeof globalThis !== \'undefined\' ? globalThis :'); + output.push(`${indent}typeof self !== 'undefined' ? self :`); + output.push(`${indent}typeof window !== 'undefined' ? window :`); + output.push(`${indent}typeof global !== 'undefined' ? global : {};\n`); output.push(`const exportedVars = (function(${this.analyzer.iifeParamName}) {`); + output.push(`${indent}const window = typeof __jsExtractorGlobal.window !== 'undefined' ? __jsExtractorGlobal.window : Object.create(null);`); + output.push(`${indent}const document = typeof __jsExtractorGlobal.document !== 'undefined' ? __jsExtractorGlobal.document : {};`); + output.push(`${indent}const self = typeof __jsExtractorGlobal.self !== 'undefined' ? __jsExtractorGlobal.self : window;\n`); + if (predeclaredVarSet.size > 0) { output.push(`${indent}var ${Array.from(predeclaredVarSet).join(', ')};\n`); } @@ -433,7 +476,7 @@ export class JsExtractor { if (decl?.node?.type === 'VariableDeclarator' && decl.node.init?.type === 'FunctionExpression') { currentFunctionNode = decl.node; } - } else if (node.type === 'CallExpression') { + } else if (node.type === 'CallExpression' || node.type === 'NewExpression' || node.type === 'VariableDeclarator') { currentFunctionNode = node; } @@ -450,8 +493,9 @@ export class JsExtractor { const rawJson = JSON.stringify(exportedRawValues, null, indent.length); const rawJsonLines = rawJson.split('\n'); - // Indent all lines except the first one... - const formattedRawJson = `${rawJsonLines[0]}\n${rawJsonLines.slice(1).map((line) => indent + line).join('\n')}`; + // Indent all lines except the first one.. + const formattedRawJson = + `${rawJsonLines[0]}\n${rawJsonLines.slice(1).map((line) => indent + line).join('\n')}`; output.push(`${indent}const rawValues = ${formattedRawJson};\n`); diff --git a/src/utils/javascript/helpers.ts b/src/utils/javascript/helpers.ts index aba5460a..2f50645b 100644 --- a/src/utils/javascript/helpers.ts +++ b/src/utils/javascript/helpers.ts @@ -52,7 +52,7 @@ export interface AstVisitObject { export type AstVisitor = AstVisitFn | AstVisitObject; /** - * Performs a non-recursive traversal of an ESTree AST. + * Performs traversal of an ESTree AST. * @param root - Root AST node to start the traversal from. * @param visitor - Callbacks invoked when nodes are entered or left. * @remarks @@ -132,7 +132,7 @@ export function getNodeSourceRange(node: ESTree.Node | null | undefined): [numbe if (Array.isArray(node.range)) return node.range; if (typeof node.start === 'number' && typeof node.end === 'number') return [ node.start, node.end ]; return null; -} +}; /** * Extracts the source code corresponding to a given AST node. @@ -143,7 +143,7 @@ export function getNodeSourceRange(node: ESTree.Node | null | undefined): [numbe export function extractNodeSource(node: ESTree.Node | null | undefined, source: string): string | null { const range = getNodeSourceRange(node); return range ? source.slice(range[0], range[1]) : null; -} +}; /** * Converts a member expression into its dot/bracket string form. @@ -183,7 +183,7 @@ export function memberToString(memberExpression: ESTree.Node, source: string): s } return base ? base + segments.join('') : null; -} +}; /** * Retrieves the base identifier for a member expression chain. @@ -203,7 +203,7 @@ export function memberBaseName(memberExpression: ESTree.MemberExpression, source if (target?.type === 'ThisExpression') return 'this'; return null; -} +}; /** * Analyzes an AST node to determine if it's a function call or a function @@ -233,6 +233,14 @@ export function createWrapperFunction(analyzer: JsAnalyzer, name: string, node: node.id.type === 'Identifier' ) { return generateWrapper(name, node.id.name, parseFunctionArguments(analyzer, node.init.params)); + } else if ( + node.type === 'NewExpression' && + node.callee.type === 'MemberExpression' && + node.callee.object.type === 'Identifier' + ) { + const targetFunction = memberToString(node.callee, analyzer.getSource()); + if (!targetFunction) return undefined; + return generateWrapper(name, targetFunction, parseFunctionArguments(analyzer, node.arguments), true); } } @@ -242,10 +250,10 @@ export function createWrapperFunction(analyzer: JsAnalyzer, name: string, node: * @param targetFunction - The name of the target function to call. * @param args - The arguments to pass to the target function. */ -function generateWrapper(functionName: string, targetFunction: string, args: string): string { +function generateWrapper(functionName: string, targetFunction: string, args: string[], useNew: boolean = false): string { return [ - `${indent}function ${functionName}(input) {`, - `${indent}${indent}return ${targetFunction}(${args});`, + `${indent}function ${functionName}(${args.join(', ')}) {`, + `${indent}${indent}return ${useNew ? 'new ' : ''}${targetFunction}(${args.join(', ')});`, `${indent}}` ].join('\n'); } @@ -264,9 +272,18 @@ function parseFunctionArguments(analyzer: JsAnalyzer, args: ESTree.Node[]) { params.push(arg.name); } else if (arg.type === 'Literal' && (typeof arg.value === 'string' || typeof arg.value === 'number')) { params.push(JSON.stringify(arg.value)); + } else if (arg.type === 'UnaryExpression') { + const argSource = extractNodeSource(arg, analyzer.getSource()); + if (argSource) { + params.push(argSource.trim()); + } + } else if (arg.type === 'AssignmentPattern' && arg.left.type === 'Identifier') { + params.push(arg.left.name); + } else if (arg.type === 'Identifier') { + params.push(arg.name); } else if (!params.includes('input')) params.push('input'); } - return params.join(', '); + return params; } \ No newline at end of file diff --git a/src/utils/javascript/matchers.ts b/src/utils/javascript/matchers.ts index 05fd8e3f..4e14fa50 100644 --- a/src/utils/javascript/matchers.ts +++ b/src/utils/javascript/matchers.ts @@ -1,68 +1,54 @@ import type { ESTree } from 'meriyah'; import { WALK_STOP, walkAst } from './helpers.js'; -export function sigMatcher(node: ESTree.Node) { - if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') { - const idNode = node.id; - const initNode = node.init; +export function nsigMatcher(node: ESTree.Node) { + if (node.type !== 'VariableDeclarator') + return false; - if (idNode.type === 'Identifier' && initNode?.type === 'FunctionExpression' && initNode.params.length === 3) { - const functionInitNode = initNode.body; - if (!functionInitNode || functionInitNode.type !== 'BlockStatement') return false; + const init = node.init; - for (const st of functionInitNode.body) { - if (st?.type === 'ExpressionStatement') { - const expression = st.expression; - if ( - expression.type === 'LogicalExpression' && - expression.operator === '&&' && - expression.left.type === 'Identifier' && - expression.right.type === 'SequenceExpression' - ) { - const firstExp = expression.right.expressions[0]; - if ( - firstExp.type === 'AssignmentExpression' && - firstExp.operator === '=' && - firstExp.left.type === 'Identifier' && - firstExp.right.type === 'CallExpression' && - firstExp.right.callee.type === 'Identifier' - ) { - const rightArguments = firstExp.right.arguments; - // sigFn(64, decodeURIComponent(sig)) - if (rightArguments.length >= 1) { - const callExpression = rightArguments.find((exp) => exp.type === 'CallExpression'); - if ( - callExpression?.type === 'CallExpression' && - callExpression?.callee.type === 'Identifier' && - callExpression.callee.name === 'decodeURIComponent' && - callExpression.arguments[0].type === 'Identifier' - ) { - return firstExp.right; - } - } - } - } - } + if (!init || init.type !== 'FunctionExpression') + return false; + + if (init.params.length < 3) + return false; + + const [ url, sigName, sigValue ] = init.params; + + if (url.type !== 'Identifier' || sigName.type !== 'AssignmentPattern' || sigValue.type !== 'AssignmentPattern') + return false; + + const body = init.body; + const blockStatementBody = body?.body || []; + + let hasUrlCtor = false; + let hasSetAlr = false; + + for (const statement of blockStatementBody) { + if (statement.type !== 'ExpressionStatement') + continue; + + const expr = statement.expression; + + if (expr.type === 'AssignmentExpression' && expr.operator === '=' && expr.left.type === 'Identifier' && expr.left.name === url.name) { + const right = expr.right; + if (right.type === 'NewExpression' && right.callee.type === 'MemberExpression') { + hasUrlCtor = true; + } + } + + if (expr.type === 'CallExpression' && expr.callee.type === 'MemberExpression') { + const args = expr.arguments; + if (args.length === 2 && args[0].type === 'Literal' && args[0].value === 'alr' && args[1].type === 'Literal' && args[1].value === 'yes') { + hasSetAlr = true; } } } - return false; -} - -export function nMatcher(node: ESTree.Node) { - if (node.type !== 'VariableDeclarator') + if (!hasUrlCtor || !hasSetAlr) return false; - if ( - node.id.type === 'Identifier' && - node.init?.type === 'ArrayExpression' && - node.init.elements[0]?.type === 'Identifier' - ) { - return node.init.elements[0]; - } - - return false; + return node; } export function timestampMatcher(node: ESTree.Node) {