chore: tidy things up

Move a few things here and there. Organization makes life easier.
This commit is contained in:
LuanRT
2022-08-03 03:34:59 -03:00
parent 3cdaab8b7a
commit af6856ced4
8 changed files with 313 additions and 257 deletions

View File

@@ -1,27 +1,34 @@
import AppendContinuationItemsAction from '../parser/classes/actions/AppendContinuationItemsAction';
import Parser, { ParsedResponse, ReloadContinuationItemsCommand } from '../parser/index';
import { Memo, ObservedArray } from '../parser/helpers';
import { InnertubeError } from '../utils/Utils';
import Actions from './Actions';
import Post from '../parser/classes/Post';
import BackstagePost from '../parser/classes/BackstagePost';
import Channel from '../parser/classes/Channel';
import CompactVideo from '../parser/classes/CompactVideo';
import ContinuationItem from '../parser/classes/ContinuationItem';
import GridChannel from '../parser/classes/GridChannel';
import GridPlaylist from '../parser/classes/GridPlaylist';
import GridVideo from '../parser/classes/GridVideo';
import Playlist from '../parser/classes/Playlist';
import PlaylistPanelVideo from '../parser/classes/PlaylistPanelVideo';
import PlaylistVideo from '../parser/classes/PlaylistVideo';
import Post from '../parser/classes/Post';
import Tab from '../parser/classes/Tab';
import ReelShelf from '../parser/classes/ReelShelf';
import RichShelf from '../parser/classes/RichShelf';
import Shelf from '../parser/classes/Shelf';
import Tab from '../parser/classes/Tab';
import TwoColumnBrowseResults from '../parser/classes/TwoColumnBrowseResults';
import TwoColumnSearchResults from '../parser/classes/TwoColumnSearchResults';
import Video from '../parser/classes/Video';
import WatchCardCompactVideo from '../parser/classes/WatchCardCompactVideo';
import { Memo, ObservedArray } from '../parser/helpers';
import Parser, { ParsedResponse, ReloadContinuationItemsCommand } from '../parser/index';
import { InnertubeError } from '../utils/Utils';
import Actions from './Actions';
import AppendContinuationItemsAction from '../parser/classes/actions/AppendContinuationItemsAction';
import ContinuationItem from '../parser/classes/ContinuationItem';
import Video from '../parser/classes/Video';
// TODO: add a way subdivide into sections and return subfeeds?
class Feed {
@@ -37,6 +44,7 @@ class Feed {
this.#page = Parser.parseResponse(data);
}
// Xxx: this can be extremely confusing — maybe refactor?
const memo =
this.#page.on_response_received_commands ?
this.#page.on_response_received_commands_memo :

View File

@@ -1,6 +1,6 @@
import Session from './Session';
import Constants from '../utils/Constants';
import { OAuthError, uuidv4 } from '../utils/Utils';
import Session from './Session';
export interface Credentials {
/**
@@ -61,10 +61,6 @@ class OAuth {
await this.#session.cache?.set('youtubei_oauth_credentials', data.buffer);
}
async removeCache() {
await this.#session.cache?.remove('youtubei_oauth_credentials');
}
async #loadCachedCredentials() {
const data = await this.#session.cache?.get('youtubei_oauth_credentials');
if (!data) return false;
@@ -86,6 +82,10 @@ class OAuth {
return true;
}
async removeCache() {
await this.#session.cache?.remove('youtubei_oauth_credentials');
}
/**
* Asks the server for a user code and verification URL.
*/
@@ -266,4 +266,5 @@ class OAuth {
Reflect.has(this.#credentials, 'expires') || false;
}
}
export default OAuth;
export default OAuth;

View File

@@ -1,9 +1,11 @@
import { getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils';
import Constants from '../utils/Constants';
import Signature from '../deciphers/Signature';
import NToken from '../deciphers/NToken';
import UniversalCache from '../utils/Cache';
import { FetchFunction } from '../utils/HTTPClient';
import { getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils';
import Constants from '../utils/Constants';
import UniversalCache from '../utils/Cache';
import NToken from '../deciphers/NToken';
import Signature from '../deciphers/Signature';
export default class Player {
#ntoken;
@@ -18,87 +20,6 @@ export default class Player {
this.#player_id = player_id;
}
static async fromCache(cache: UniversalCache, player_id: string) {
const buffer = await cache.get(player_id);
if (!buffer)
return null;
const view = new DataView(buffer);
const version = view.getUint32(0, true);
if (version !== Player.LIBRARY_VERSION)
return null;
const sig_timestamp = view.getUint32(4, true);
const sig_decipher_len = view.getUint32(8, true);
const sig_decipher_buf = buffer.slice(12, 12 + sig_decipher_len);
const ntoken_transform_buf = buffer.slice(12 + sig_decipher_len);
return new Player(Signature.fromArrayBuffer(sig_decipher_buf), NToken.fromArrayBuffer(ntoken_transform_buf), sig_timestamp, player_id);
}
static async fromSource(cache: UniversalCache | undefined, sig_timestamp: number, sig_decipher_sc: string, ntoken_sc: string, player_id: string) {
const player = new Player(Signature.fromSourceCode(sig_decipher_sc), NToken.fromSourceCode(ntoken_sc), sig_timestamp, player_id);
await player.cache(cache);
return player;
}
async cache(cache?: UniversalCache) {
if (!cache) return;
const ntokenBuf = this.#ntoken.toArrayBuffer();
const sigDecipherBuf = this.#signature.toArrayBuffer();
const buffer = new ArrayBuffer(12 + sigDecipherBuf.byteLength + ntokenBuf.byteLength);
const view = new DataView(buffer);
view.setUint32(0, Player.LIBRARY_VERSION, true);
view.setUint32(4, this.#signature_timestamp, true);
view.setUint32(8, sigDecipherBuf.byteLength, true);
new Uint8Array(buffer).set(new Uint8Array(sigDecipherBuf), 12);
new Uint8Array(buffer).set(new Uint8Array(ntokenBuf), 12 + sigDecipherBuf.byteLength);
await cache.set(this.#player_id, new Uint8Array(buffer));
}
decipher(url?: string, signature_cipher?: string, cipher?: string) {
url = url || signature_cipher || cipher;
if (!url)
throw new PlayerError('No valid URL to decipher');
const args = new URLSearchParams(url);
const url_components = new URL(args.get('url') || url);
url_components.searchParams.set('ratebypass', 'yes');
if (signature_cipher || cipher) {
const signature = this.#signature.decipher(url);
const sp = args.get('sp');
sp ?
url_components.searchParams.set(sp, signature) :
url_components.searchParams.set('signature', signature);
}
const n = url_components.searchParams.get('n');
if (n) {
const ntoken = this.#ntoken.transform(n);
url_components.searchParams.set('n', ntoken);
}
return url_components.toString();
}
get url() {
return new URL(`/s/player/${this.#player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE).toString();
}
get sts() {
return this.#signature_timestamp;
}
static async create(cache: UniversalCache | undefined, fetch: FetchFunction = globalThis.fetch) {
const url = new URL('/iframe_api', Constants.URLS.YT_BASE);
const res = await fetch(url);
@@ -141,6 +62,80 @@ export default class Player {
return await Player.fromSource(cache, sig_timestamp, sig_decipher_sc, ntoken_sc, player_id);
}
decipher(url?: string, signature_cipher?: string, cipher?: string) {
url = url || signature_cipher || cipher;
if (!url)
throw new PlayerError('No valid URL to decipher');
const args = new URLSearchParams(url);
const url_components = new URL(args.get('url') || url);
url_components.searchParams.set('ratebypass', 'yes');
if (signature_cipher || cipher) {
const signature = this.#signature.decipher(url);
const sp = args.get('sp');
sp ?
url_components.searchParams.set(sp, signature) :
url_components.searchParams.set('signature', signature);
}
const n = url_components.searchParams.get('n');
if (n) {
const ntoken = this.#ntoken.transform(n);
url_components.searchParams.set('n', ntoken);
}
return url_components.toString();
}
static async fromCache(cache: UniversalCache, player_id: string) {
const buffer = await cache.get(player_id);
if (!buffer)
return null;
const view = new DataView(buffer);
const version = view.getUint32(0, true);
if (version !== Player.LIBRARY_VERSION)
return null;
const sig_timestamp = view.getUint32(4, true);
const sig_decipher_len = view.getUint32(8, true);
const sig_decipher_buf = buffer.slice(12, 12 + sig_decipher_len);
const ntoken_transform_buf = buffer.slice(12 + sig_decipher_len);
return new Player(Signature.fromArrayBuffer(sig_decipher_buf), NToken.fromArrayBuffer(ntoken_transform_buf), sig_timestamp, player_id);
}
static async fromSource(cache: UniversalCache | undefined, sig_timestamp: number, sig_decipher_sc: string, ntoken_sc: string, player_id: string) {
const player = new Player(Signature.fromSourceCode(sig_decipher_sc), NToken.fromSourceCode(ntoken_sc), sig_timestamp, player_id);
await player.cache(cache);
return player;
}
async cache(cache?: UniversalCache) {
if (!cache) return;
const ntoken_buf = this.#ntoken.toArrayBuffer();
const sig_decipher_buf = this.#signature.toArrayBuffer();
const buffer = new ArrayBuffer(12 + sig_decipher_buf.byteLength + ntoken_buf.byteLength);
const view = new DataView(buffer);
view.setUint32(0, Player.LIBRARY_VERSION, true);
view.setUint32(4, this.#signature_timestamp, true);
view.setUint32(8, sig_decipher_buf.byteLength, true);
new Uint8Array(buffer).set(new Uint8Array(sig_decipher_buf), 12);
new Uint8Array(buffer).set(new Uint8Array(ntoken_buf), 12 + sig_decipher_buf.byteLength);
await cache.set(this.#player_id, new Uint8Array(buffer));
}
/**
* Extracts the signature timestamp from the player source code.
*/
@@ -173,6 +168,14 @@ export default class Player {
return sc;
}
get url() {
return new URL(`/s/player/${this.#player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE).toString();
}
get sts() {
return this.#signature_timestamp;
}
static get LIBRARY_VERSION() {
return 1;
}

View File

@@ -9,6 +9,12 @@ import HTTPClient, { FetchFunction } from '../utils/HTTPClient';
import { DeviceCategory, generateRandomString, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils';
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth';
export enum ClientType {
WEB = 'WEB',
MUSIC = 'WEB_REMIX',
ANDROID = 'ANDROID',
}
export interface Context {
client: {
hl: string;
@@ -39,12 +45,6 @@ export interface Context {
};
}
export enum ClientType {
WEB = 'WEB',
MUSIC = 'WEB_REMIX',
ANDROID = 'ANDROID',
}
export interface SessionOptions {
lang?: string;
device_category?: DeviceCategory;
@@ -60,6 +60,7 @@ export default class Session extends EventEmitterLike {
#key;
#context;
#player;
oauth;
http;
logged_in;
@@ -96,49 +97,6 @@ export default class Session extends EventEmitterLike {
super.once(type, listener);
}
async signIn(credentials?: Credentials): Promise<void> {
return new Promise(async (resolve, reject) => {
const error_handler: OAuthAuthErrorEventHandler = (err) => {
reject(err);
};
this.once('auth', (data) => {
this.off('auth-error', error_handler);
if (data.status === 'SUCCESS') {
this.logged_in = true;
resolve();
}
reject(data);
});
this.once('auth-error', error_handler);
try {
await this.oauth.init(credentials);
if (this.oauth.validateCredentials()) {
await this.oauth.refreshIfRequired();
this.logged_in = true;
resolve();
}
} catch (err) {
reject(err);
}
});
}
async signOut() {
if (!this.logged_in)
throw new InnertubeError('You are not signed in');
const response = await this.oauth.revokeCredentials();
this.logged_in = false;
return response;
}
static async create(options: SessionOptions = {}) {
const { context, api_key, api_version } = await Session.getSessionData(options.lang, options.device_category, options.client_type, options.timezone, options.fetch);
return new Session(context, api_key, api_version, await Player.create(options.cache, options.fetch), options.cookie, options.fetch, options.cache);
@@ -146,8 +104,8 @@ export default class Session extends EventEmitterLike {
static async getSessionData(
lang = 'en-US',
deviceCategory: DeviceCategory = 'desktop',
clientName: ClientType = ClientType.WEB,
device_category: DeviceCategory = 'desktop',
client_name: ClientType = ClientType.WEB,
tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
fetch: FetchFunction = globalThis.fetch
) {
@@ -187,11 +145,11 @@ export default class Session extends EventEmitterLike {
remoteHost: device_info[3],
visitorData: visitor_data,
userAgent: device_info[14],
clientName,
clientName: client_name,
clientVersion: device_info[16],
osName: device_info[17],
osVersion: device_info[18],
platform: deviceCategory.toUpperCase(),
platform: device_category.toUpperCase(),
clientFormFactor: 'UNKNOWN_FORM_FACTOR',
userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT',
timeZone: device_info[79],
@@ -210,11 +168,48 @@ export default class Session extends EventEmitterLike {
}
};
return {
context,
api_key,
api_version
};
return { context, api_key, api_version };
}
async signIn(credentials?: Credentials): Promise<void> {
return new Promise(async (resolve, reject) => {
const error_handler: OAuthAuthErrorEventHandler = (err) => reject(err);
this.once('auth', (data) => {
this.off('auth-error', error_handler);
if (data.status === 'SUCCESS') {
this.logged_in = true;
resolve();
}
reject(data);
});
this.once('auth-error', error_handler);
try {
await this.oauth.init(credentials);
if (this.oauth.validateCredentials()) {
await this.oauth.refreshIfRequired();
this.logged_in = true;
resolve();
}
} catch (err) {
reject(err);
}
});
}
async signOut() {
if (!this.logged_in)
throw new InnertubeError('You are not signed in');
const response = await this.oauth.revokeCredentials();
this.logged_in = false;
return response;
}
get key() {

View File

@@ -27,6 +27,7 @@ class TabbedFeed extends Feed {
return this;
const response = await tab.endpoint.call(this.#actions);
if (!response)
throw new InnertubeError('Failed to call endpoint');

View File

@@ -2,25 +2,25 @@ import { NTOKEN_REGEX, BASE64_DIALECT } from '../utils/Constants';
import { InnertubeError } from '../utils/Utils';
export enum NTokenTransformOperation {
NO_OP = 0,
PUSH,
REVERSE_1,
REVERSE_2,
SPLICE,
SWAP0_1,
SWAP0_2,
ROTATE_1,
ROTATE_2,
BASE64_DIA,
TRANSLATE_1,
TRANSLATE_2,
NO_OP = 0,
PUSH,
REVERSE_1,
REVERSE_2,
SPLICE,
SWAP0_1,
SWAP0_2,
ROTATE_1,
ROTATE_2,
BASE64_DIA,
TRANSLATE_1,
TRANSLATE_2,
}
export enum NTokenTransformOpType {
FUNC,
N_ARR,
LITERAL,
REF
FUNC,
N_ARR,
LITERAL,
REF
}
const OP_LOOKUP: Record<string, NTokenTransformOperation> = {
@@ -96,39 +96,39 @@ export class NTokenTransforms {
}
static push(arr: any[], item: any) {
if (Array.isArray(arr?.[0])) arr.push([ NTokenTransformOpType.LITERAL, item ]);
else arr.push(item);
if (Array.isArray(arr?.[0])) {
arr.push([ NTokenTransformOpType.LITERAL, item ]);
} else {
arr.push(item);
}
}
}
const TRANSFORM_FUNCTIONS: [Record<number, any>, Record<number, any>] = [
{
[NTokenTransformOperation.PUSH]: NTokenTransforms.push,
[NTokenTransformOperation.SPLICE]: NTokenTransforms.splice,
[NTokenTransformOperation.SWAP0_1]: NTokenTransforms.swap0,
[NTokenTransformOperation.SWAP0_2]: NTokenTransforms.swap0,
[NTokenTransformOperation.ROTATE_1]: NTokenTransforms.rotate,
[NTokenTransformOperation.ROTATE_2]: NTokenTransforms.rotate,
[NTokenTransformOperation.REVERSE_1]: NTokenTransforms.reverse,
[NTokenTransformOperation.REVERSE_2]: NTokenTransforms.reverse,
[NTokenTransformOperation.BASE64_DIA]: () => NTokenTransforms.getBase64Dia(false),
[NTokenTransformOperation.TRANSLATE_1]: (...args: any[]) => NTokenTransforms.translate1.apply(null, [ ...args, false ] as any),
[NTokenTransformOperation.TRANSLATE_2]: NTokenTransforms.translate2
},
{
[NTokenTransformOperation.PUSH]: NTokenTransforms.push,
[NTokenTransformOperation.SPLICE]: NTokenTransforms.splice,
[NTokenTransformOperation.SWAP0_1]: NTokenTransforms.swap0,
[NTokenTransformOperation.SWAP0_2]: NTokenTransforms.swap0,
[NTokenTransformOperation.ROTATE_1]: NTokenTransforms.rotate,
[NTokenTransformOperation.ROTATE_2]: NTokenTransforms.rotate,
[NTokenTransformOperation.REVERSE_1]: NTokenTransforms.reverse,
[NTokenTransformOperation.REVERSE_2]: NTokenTransforms.reverse,
[NTokenTransformOperation.BASE64_DIA]: () => NTokenTransforms.getBase64Dia(true),
[NTokenTransformOperation.TRANSLATE_1]: (...args: any[]) => NTokenTransforms.translate1.apply(null, [ ...args, true ] as any),
[NTokenTransformOperation.TRANSLATE_2]: NTokenTransforms.translate2
}
];
const TRANSFORM_FUNCTIONS: [Record<number, any>, Record<number, any>] = [ {
[NTokenTransformOperation.PUSH]: NTokenTransforms.push,
[NTokenTransformOperation.SPLICE]: NTokenTransforms.splice,
[NTokenTransformOperation.SWAP0_1]: NTokenTransforms.swap0,
[NTokenTransformOperation.SWAP0_2]: NTokenTransforms.swap0,
[NTokenTransformOperation.ROTATE_1]: NTokenTransforms.rotate,
[NTokenTransformOperation.ROTATE_2]: NTokenTransforms.rotate,
[NTokenTransformOperation.REVERSE_1]: NTokenTransforms.reverse,
[NTokenTransformOperation.REVERSE_2]: NTokenTransforms.reverse,
[NTokenTransformOperation.BASE64_DIA]: () => NTokenTransforms.getBase64Dia(false),
[NTokenTransformOperation.TRANSLATE_1]: (...args: any[]) => NTokenTransforms.translate1.apply(null, [ ...args, false ] as any),
[NTokenTransformOperation.TRANSLATE_2]: NTokenTransforms.translate2
}, {
[NTokenTransformOperation.PUSH]: NTokenTransforms.push,
[NTokenTransformOperation.SPLICE]: NTokenTransforms.splice,
[NTokenTransformOperation.SWAP0_1]: NTokenTransforms.swap0,
[NTokenTransformOperation.SWAP0_2]: NTokenTransforms.swap0,
[NTokenTransformOperation.ROTATE_1]: NTokenTransforms.rotate,
[NTokenTransformOperation.ROTATE_2]: NTokenTransforms.rotate,
[NTokenTransformOperation.REVERSE_1]: NTokenTransforms.reverse,
[NTokenTransformOperation.REVERSE_2]: NTokenTransforms.reverse,
[NTokenTransformOperation.BASE64_DIA]: () => NTokenTransforms.getBase64Dia(true),
[NTokenTransformOperation.TRANSLATE_1]: (...args: any[]) => NTokenTransforms.translate1.apply(null, [ ...args, true ] as any),
[NTokenTransformOperation.TRANSLATE_2]: NTokenTransforms.translate2
} ];
export type NTokenCall = [number, number[]];
export type NTokenInstruction = [NTokenTransformOpType, (NTokenTransformOperation | number)?, number?];
@@ -136,16 +136,22 @@ export type NTokenTransformer = [NTokenInstruction[], NTokenCall[]];
export default class NToken {
private transformer: NTokenTransformer;
constructor(transformer: NTokenTransformer) {
this.transformer = transformer;
}
static fromSourceCode(raw: string) {
const transformationData = NToken.getTransformationData(raw);
const transformations = transformationData.map((el) => {
const transformation_data = NToken.getTransformationData(raw);
const transformations = transformation_data.map((el) => {
if (el != null && typeof el != 'number') {
const is_reverse_base64 = el.includes('case 65:');
const func = NToken.getFunc(el)?.[0];
const opcode = func ? OP_LOOKUP[func] : undefined;
if (opcode) {
el = [ NTokenTransformOpType.FUNC, opcode, 0 + is_reverse_base64 ];
} else if (el == 'b') {
@@ -156,6 +162,7 @@ export default class NToken {
} else if (el != null) {
el = [ NTokenTransformOpType.LITERAL, el ];
}
return el;
});
@@ -165,22 +172,23 @@ export default class NToken {
// Parses and emulates calls to the functions of the transformations array
const function_body = raw.replace(/\n/g, '').match(/try\{(.*?)\}catch/s)?.[1];
if (!function_body) {
throw new InnertubeError('Invalid NToken transformation function.', { transformation: raw });
}
const function_calls = [
...function_body.matchAll(NTOKEN_REGEX.CALLS)
].map((params) =>
[
parseInt(params[1]),
params[2].split(',').map((param: string) => {
const param_value = param.match(/c\[(.*?)\]/)?.[1];
if (!param_value) {
throw new InnertubeError('Unexpected NToken transformation function parameter.', { transformation: raw, param });
}
return parseInt(param_value);
})
] as NTokenCall
const function_calls = [ ...function_body.matchAll(NTOKEN_REGEX.CALLS) ].map((params) => [
parseInt(params[1]),
params[2].split(',').map((param: string) => {
const param_value = param.match(/c\[(.*?)\]/)?.[1];
if (!param_value) {
throw new InnertubeError('Unexpected NToken transformation function parameter.', { transformation: raw, param });
}
return parseInt(param_value);
})
] as NTokenCall
);
return new NToken([ transformations, function_calls ]);
@@ -220,6 +228,7 @@ export default class NToken {
console.error(new Error(`Could not transform n-token, download may be throttled.\nOriginal Token:${n}\nError:\n${(e as Error).stack}`));
return n;
}
return nToken.join('');
}
@@ -241,6 +250,7 @@ export default class NToken {
// We've got a 3 * 32 bit header to store the library version and the size of the two arrays
let size = 4 * 3;
for (const instruction of this.transformer[0]) {
switch (instruction[0]) {
case NTokenTransformOpType.FUNC:
@@ -258,6 +268,7 @@ export default class NToken {
break;
}
}
for (const call of this.transformer[1]) {
size += 2 + call[1].length;
}
@@ -266,21 +277,28 @@ export default class NToken {
const view = new DataView(buffer);
let offset = 0;
view.setUint32(offset, NToken.LIBRARY_VERSION, true);
offset += 4;
view.setUint32(offset, this.transformer[0].length, true);
offset += 4;
view.setUint32(offset, this.transformer[1].length, true);
offset += 4;
for (const instruction of this.transformer[0]) {
switch (instruction[0]) {
case NTokenTransformOpType.FUNC:
{
if (instruction[1] === undefined || instruction[2] === undefined)
throw new InnertubeError('Invalid NTokenInstruction.', { transformation: this.transformer, instruction });
const opcode = (instruction[0] << 6) | instruction[2];
view.setUint8(offset, opcode);
offset += 1;
view.setUint8(offset, instruction[1]);
offset += 1;
}
@@ -297,17 +315,23 @@ export default class NToken {
{
if (instruction[1] === undefined)
throw new InnertubeError('Invalid NTokenInstruction.', { transformation: this.transformer, instruction });
const type = typeof instruction[1] === 'string' ? 1 : 0;
const opcode = (instruction[0] << 6) | type;
view.setUint8(offset, opcode);
offset += 1;
if (type === 0) {
view.setInt32(offset, instruction[1], true);
offset += 4;
} else {
const encoded = new TextEncoder().encode(instruction[1] as any);
view.setUint32(offset, encoded.byteLength, true);
offset += 4;
for (let i = 0; i < encoded.byteLength; i++) {
view.setUint8(offset, encoded[i]);
offset += 1;
@@ -317,11 +341,14 @@ export default class NToken {
break;
}
}
for (const call of this.transformer[1]) {
view.setUint8(offset, call[0]);
offset += 1;
view.setUint8(offset, call[1].length);
offset += 1;
for (const param of call[1]) {
view.setUint8(offset, param);
offset += 1;
@@ -334,19 +361,25 @@ export default class NToken {
static fromArrayBuffer(buffer: ArrayBuffer) {
const view = new DataView(buffer);
let offset = 0;
const version = view.getUint32(offset, true);
offset += 4;
if (version !== NToken.LIBRARY_VERSION)
throw new TypeError('Invalid library version');
const transformations_length = view.getUint32(offset, true);
offset += 4;
const function_calls_length = view.getUint32(offset, true);
offset += 4;
const transformations = new Array<NTokenInstruction>(transformations_length);
for (let i = 0; i < transformations_length; i++) {
const opcode = view.getUint8(offset++);
const op = opcode >> 6;
switch (op) {
case NTokenTransformOpType.FUNC:
{
@@ -355,17 +388,16 @@ export default class NToken {
transformations[i] = [ op, operation, is_reverse_base64 ];
}
break;
case NTokenTransformOpType.N_ARR:
case NTokenTransformOpType.REF:
{
transformations[i] = [ op ];
}
break;
case NTokenTransformOpType.LITERAL:
{
const type = opcode & 0b00000001;
if (type === 0) {
const literal = view.getInt32(offset, true);
offset += 4;
@@ -373,27 +405,34 @@ export default class NToken {
} else {
const length = view.getUint32(offset, true);
offset += 4;
const literal = new Uint8Array(length);
for (let i = 0; i < length; i++) {
literal[i] = view.getUint8(offset++);
}
transformations[i] = [ op, new TextDecoder().decode(literal) as any ];
}
}
break;
default:
throw new Error('Invalid opcode');
}
}
const function_calls = new Array<NTokenCall>(function_calls_length);
for (let i = 0; i < function_calls_length; i++) {
const index = view.getUint8(offset++);
const num_params = view.getUint8(offset++);
const params = new Array<number>(num_params);
for (let j = 0; j < num_params; j++) {
params[j] = view.getUint8(offset++);
}
function_calls[i] = [ index, params ];
}
@@ -423,4 +462,4 @@ export default class NToken {
.replace(/\[b/g, '["b"').replace(/}]/g, '"]').replace(/},/g, '}",')
.replace(/""/g, '').replace(/length]\)}"/g, 'length])}');
}
}
}

View File

@@ -1,25 +1,25 @@
import { SIG_REGEX } from '../utils/Constants';
export enum SignatureOperation {
REVERSE,
SPLICE,
SWAP
REVERSE,
SPLICE,
SWAP
}
export type SignatureInstruction = [SignatureOperation, number];
export type SignatureInstruction = [ SignatureOperation, number ];
export default class Signature {
private actionSequence;
private action_sequence;
constructor(actionSequence: SignatureInstruction[]) {
this.actionSequence = actionSequence;
constructor(action_sequence: SignatureInstruction[]) {
this.action_sequence = action_sequence;
}
static fromSourceCode(sigDecipherSc: string) {
static fromSourceCode(raw_sc: string) {
let func: RegExpExecArray | null;
const functions = [];
while ((func = SIG_REGEX.FUNCTIONS.exec(sigDecipherSc)) !== null) {
while ((func = SIG_REGEX.FUNCTIONS.exec(raw_sc)) !== null) {
if (func[0].includes('reverse')) {
functions[0] = func[1];
} else if (func[0].includes('splice')) {
@@ -31,51 +31,49 @@ export default class Signature {
let actions: RegExpExecArray | null;
const actionSequence: SignatureInstruction[] = [];
const action_sequence: SignatureInstruction[] = [];
while ((actions = SIG_REGEX.ACTIONS.exec(sigDecipherSc)) !== null) {
while ((actions = SIG_REGEX.ACTIONS.exec(raw_sc)) !== null) {
const action = actions.groups;
if (!action) continue;
switch (action.name) {
case functions[0]:
actionSequence.push([ SignatureOperation.REVERSE, 0 ]);
action_sequence.push([ SignatureOperation.REVERSE, 0 ]);
break;
case functions[1]:
actionSequence.push([ SignatureOperation.SPLICE, parseInt(action.param) ]);
action_sequence.push([ SignatureOperation.SPLICE, parseInt(action.param) ]);
break;
case functions[2]:
actionSequence.push([ SignatureOperation.SWAP, parseInt(action.param) ]);
action_sequence.push([ SignatureOperation.SWAP, parseInt(action.param) ]);
break;
default:
}
}
return new Signature(actionSequence);
return new Signature(action_sequence);
}
decipher(url: string) {
const args = new URLSearchParams(url);
const signature = args.get('s')?.split('');
if (!signature)
throw new TypeError('Invalid input signature');
for (const action of this.actionSequence) {
for (const action of this.action_sequence) {
switch (action[0]) {
case SignatureOperation.REVERSE:
signature.reverse();
break;
case SignatureOperation.SPLICE:
signature.splice(0, action[1]);
break;
case SignatureOperation.SWAP:
const index = action[1];
const origArrI = signature[0];
const orig_arr = signature[0];
signature[0] = signature[index % signature.length];
signature[index % signature.length] = origArrI;
signature[index % signature.length] = orig_arr;
break;
default:
break;
}
@@ -85,43 +83,55 @@ export default class Signature {
}
toJSON() {
return [ ...this.actionSequence ];
return [ ...this.action_sequence ];
}
toArrayBuffer() {
// Array buffer encoding assumes that the index of the action is a short (16 bit unsigned)
const buffer = new ArrayBuffer(4 + 4 + this.actionSequence.length * (1 + 2));
const buffer = new ArrayBuffer(4 + 4 + this.action_sequence.length * (1 + 2));
const view = new DataView(buffer);
let offset = 0;
view.setUint32(offset, Signature.LIBRARY_VERSION, true);
offset += 4;
view.setUint32(offset, this.actionSequence.length, true);
view.setUint32(offset, this.action_sequence.length, true);
offset += 4;
for (let i = 0; i < this.actionSequence.length; i++) {
view.setUint8(offset, this.actionSequence[i][0]);
for (let i = 0; i < this.action_sequence.length; i++) {
view.setUint8(offset, this.action_sequence[i][0]);
offset += 1;
view.setUint16(offset, this.actionSequence[i][1], true);
view.setUint16(offset, this.action_sequence[i][1], true);
offset += 2;
}
return buffer;
}
static fromArrayBuffer(buffer: ArrayBuffer) {
const view = new DataView(buffer);
let offset = 0;
const version = view.getUint32(offset, true);
offset += 4;
if (version !== Signature.LIBRARY_VERSION)
throw new TypeError('Invalid library version');
const actionSequenceLength = view.getUint32(offset, true);
const action_sequenceLength = view.getUint32(offset, true);
offset += 4;
const actionSequence = new Array<SignatureInstruction>(actionSequenceLength);
for (let i = 0; i < actionSequenceLength; i++) {
actionSequence[i] = [ view.getUint8(offset), view.getUint16(offset + 1, true) ];
const action_sequence = new Array<SignatureInstruction>(action_sequenceLength);
for (let i = 0; i < action_sequenceLength; i++) {
action_sequence[i] = [ view.getUint8(offset), view.getUint16(offset + 1, true) ];
offset += 3;
}
return new Signature(actionSequence);
return new Signature(action_sequence);
}
static get LIBRARY_VERSION() {

View File

@@ -1,4 +1,3 @@
/* eslint-disable */
// @generated by protobuf-ts 2.7.0
// @generated from protobuf file "youtube.proto" (package "youtube", syntax proto2)
// tslint:disable