mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-27 00:29:16 +00:00
fix(Player): Add support for new variants (#1152)
* fix(JsAnalyzer): Add prototype alias tracking, improve scoping, and add es6 class support * fix(JsExtractor): Allow `ClassExpression` and extract prototype assignments * feat(helpers): Misc changes & improve argument handling * fix(matchers): Add new nsig matcher * feat(utils): Add `NSIG_PROCESSOR_FN` * fix(Player): Update player URL and integrate new nsig processing * chore(Constants): Update TV client version to 7.20260311.12.00 (ignore, unrelated) - Unrelated. Updated it for testing some things. * chore(JsAnalyzer): Remove commented debug log (oops...) * chore(JsExtractor): Correct typo in comment * chore: rename some stuff * chore: lint * chore: Simplify code in parseFunctionArguments
This commit is contained in:
@@ -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<string, unknown>;
|
||||
// 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<string, unknown>;
|
||||
|
||||
if (typeof result !== 'object' || result === null) {
|
||||
throw new PlayerError('Got invalid result from player script evaluation.');
|
||||
|
||||
@@ -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' ];
|
||||
@@ -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 || ''}");`;
|
||||
}
|
||||
@@ -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<string>;
|
||||
dependents: Set<string>;
|
||||
prototypeAliases: Map<string, Set<VariableMetadata>>;
|
||||
predeclared: boolean;
|
||||
}
|
||||
|
||||
@@ -64,9 +75,10 @@ export class JsAnalyzer {
|
||||
private readonly hasExtractions: boolean;
|
||||
private readonly extractionStates: ExtractionState[];
|
||||
private readonly dependentsTracker: Map<string, Set<string>> = new Map();
|
||||
private pendingPrototypeAliasBinding: [string, VariableMetadata] | null = null;
|
||||
|
||||
public declaredVariables: Map<string, VariableMetadata> = new Map();
|
||||
public iifeParamName: string | null = null;
|
||||
public readonly declaredVariables: Map<string, VariableMetadata> = 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<VariableMetadata>();
|
||||
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<string>(),
|
||||
predeclared: false,
|
||||
prototypeAliases: new Map<string, Set<VariableMetadata>>(),
|
||||
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<string>(),
|
||||
predeclared: false,
|
||||
prototypeAliases: new Map<string, Set<VariableMetadata>>(),
|
||||
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<string>(),
|
||||
prototypeAliases: new Map<string, Set<VariableMetadata>>(),
|
||||
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<string>(), 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user