From af6856ced49c90e591772fb54192cfdf840b96df Mon Sep 17 00:00:00 2001 From: LuanRT Date: Wed, 3 Aug 2022 03:34:59 -0300 Subject: [PATCH] chore: tidy things up Move a few things here and there. Organization makes life easier. --- src/core/Feed.ts | 26 ++++-- src/core/OAuth.ts | 13 +-- src/core/Player.ts | 175 +++++++++++++++++++------------------ src/core/Session.ts | 111 +++++++++++------------ src/core/TabbedFeed.ts | 1 + src/deciphers/NToken.ts | 169 +++++++++++++++++++++-------------- src/deciphers/Signature.ts | 74 +++++++++------- src/proto/youtube.ts | 1 - 8 files changed, 313 insertions(+), 257 deletions(-) diff --git a/src/core/Feed.ts b/src/core/Feed.ts index b4791b85..04b6cd44 100644 --- a/src/core/Feed.ts +++ b/src/core/Feed.ts @@ -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 : diff --git a/src/core/OAuth.ts b/src/core/OAuth.ts index 365a74cc..052df8c9 100644 --- a/src/core/OAuth.ts +++ b/src/core/OAuth.ts @@ -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; \ No newline at end of file diff --git a/src/core/Player.ts b/src/core/Player.ts index a38ff58c..f742a770 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -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; } diff --git a/src/core/Session.ts b/src/core/Session.ts index 22b55c54..ebd3328f 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -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 { - 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 { + 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() { diff --git a/src/core/TabbedFeed.ts b/src/core/TabbedFeed.ts index 2d553c21..8c53d6ff 100644 --- a/src/core/TabbedFeed.ts +++ b/src/core/TabbedFeed.ts @@ -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'); diff --git a/src/deciphers/NToken.ts b/src/deciphers/NToken.ts index 8befc3b7..28167ec8 100644 --- a/src/deciphers/NToken.ts +++ b/src/deciphers/NToken.ts @@ -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 = { @@ -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, Record] = [ - { - [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, Record] = [ { + [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(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(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(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])}'); } -} +} \ No newline at end of file diff --git a/src/deciphers/Signature.ts b/src/deciphers/Signature.ts index 1a57ab0a..7999aa53 100644 --- a/src/deciphers/Signature.ts +++ b/src/deciphers/Signature.ts @@ -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(actionSequenceLength); - for (let i = 0; i < actionSequenceLength; i++) { - actionSequence[i] = [ view.getUint8(offset), view.getUint16(offset + 1, true) ]; + + const action_sequence = new Array(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() { diff --git a/src/proto/youtube.ts b/src/proto/youtube.ts index 7120c1ed..62030e5c 100644 --- a/src/proto/youtube.ts +++ b/src/proto/youtube.ts @@ -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