From c9f0ddd573de297c0b384e422e6c1737454926e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20=28perso=29?= <4016501+unixfox@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:28:42 +0200 Subject: [PATCH] feat(Player): Add support for Proof of Identity tokens (#708) * Fix different usages of potoken. * Fix linting. * Add mention about invidious youtube-trusted-session-generator. --------- Co-authored-by: Luan --- README.md | 1 + src/Innertube.ts | 6 ++++-- src/core/Player.ts | 13 +++++++++++-- src/core/Session.ts | 18 +++++++++++++----- src/core/endpoints/PlayerEndpoint.ts | 5 ++++- src/types/Endpoints.ts | 7 +++++++ 6 files changed, 40 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5f6d9cdea..a22ebed75 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ const youtube = await Innertube.create(/* options */); | `cache` | `ICache` | Used to cache algorithms, session data, and OAuth2 tokens. | `undefined` | | `cookie` | `string` | YouTube cookies. | `undefined` | | `fetch` | `FetchFunction` | Fetch function to use. | `fetch` | +| `po_token` | `string` | Proof of Origin Token. This is an attestation token generated by BotGuard/DroidGuard. It is used to confirm that the request is coming from a genuine device. To get a valid one, please refer to [Invidious' Trusted Session Generator](https://github.com/iv-org/youtube-trusted-session-generator). | `undefined` | diff --git a/src/Innertube.ts b/src/Innertube.ts index 40ee2ca68..0727fc0c1 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -97,7 +97,8 @@ export default class Innertube { video_id: next_payload.videoId, playlist_id: next_payload?.playlistId, client: client, - sts: this.#session.player?.sts + sts: this.#session.player?.sts, + po_token: this.#session.po_token }); const player_response = this.actions.execute(PlayerEndpoint.PATH, player_payload); @@ -116,7 +117,8 @@ export default class Innertube { PlayerEndpoint.PATH, PlayerEndpoint.build({ video_id: video_id, client: client, - sts: this.#session.player?.sts + sts: this.#session.player?.sts, + po_token: this.#session.po_token }) ); diff --git a/src/core/Player.ts b/src/core/Player.ts index dc6f4cf64..30b610fa4 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -12,6 +12,7 @@ export default class Player { sts: number; nsig_sc?: string; sig_sc?: string; + po_token?: string; constructor(player_id: string, signature_timestamp: number, sig_sc?: string, nsig_sc?: string) { this.player_id = player_id; @@ -20,7 +21,7 @@ export default class Player { this.sig_sc = sig_sc; } - static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch): Promise { + static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch, po_token?: string): Promise { const url = new URL('/iframe_api', Constants.URLS.YT_BASE); const res = await fetch(url); @@ -41,6 +42,7 @@ export default class Player { const cached_player = await Player.fromCache(cache, player_id); if (cached_player) { Log.info(TAG, 'Found up-to-date player data in cache.'); + cached_player.po_token = po_token; return cached_player; } } @@ -67,7 +69,10 @@ export default class Player { Log.info(TAG, `Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`); - return await Player.fromSource(player_id, sig_timestamp, cache, sig_sc, nsig_sc); + const player = await Player.fromSource(player_id, sig_timestamp, cache, sig_sc, nsig_sc); + player.po_token = po_token; + + return player; } decipher(url?: string, signature_cipher?: string, cipher?: string, this_response_nsig_cache?: Map): string { @@ -123,6 +128,10 @@ export default class Player { url_components.searchParams.set('n', nsig); } + // @NOTE: SABR requests should include the PoToken (not base64d, but as bytes!) in the payload. + if (url_components.searchParams.get('sabr') !== '1' && this.po_token) + url_components.searchParams.set('pot', this.po_token); + const client = url_components.searchParams.get('c'); switch (client) { diff --git a/src/core/Session.ts b/src/core/Session.ts index c12c4316e..6a14e4cd7 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -176,6 +176,10 @@ export type SessionOptions = { * Fetch function to use. */ fetch?: FetchFunction; + /** + * Proof of Origin Token. This is an attestation token generated by BotGuard/DroidGuard. It is used to confirm that the request is coming from a genuine device. + */ + po_token?: string; } export type SessionData = { @@ -217,8 +221,9 @@ export default class Session extends EventEmitter { key: string; api_version: string; account_index: number; + po_token?: string; - constructor(context: Context, api_key: string, api_version: string, account_index: number, player?: Player, cookie?: string, fetch?: FetchFunction, cache?: ICache) { + constructor(context: Context, api_key: string, api_version: string, account_index: number, player?: Player, cookie?: string, fetch?: FetchFunction, cache?: ICache, po_token?: string) { super(); this.http = new HTTPClient(this, cookie, fetch); this.actions = new Actions(this); @@ -230,6 +235,7 @@ export default class Session extends EventEmitter { this.api_version = api_version; this.context = context; this.player = player; + this.po_token = po_token; } on(type: 'auth', listener: OAuth2AuthEventHandler): void; @@ -263,12 +269,13 @@ export default class Session extends EventEmitter { options.fetch, options.on_behalf_of_user, options.cache, - options.enable_session_cache + options.enable_session_cache, + options.po_token ); return new Session( context, api_key, api_version, account_index, - options.retrieve_player === false ? undefined : await Player.create(options.cache, options.fetch), + options.retrieve_player === false ? undefined : await Player.create(options.cache, options.fetch, options.po_token), options.cookie, options.fetch, options.cache ); } @@ -327,9 +334,10 @@ export default class Session extends EventEmitter { fetch: FetchFunction = Platform.shim.fetch, on_behalf_of_user?: string, cache?: ICache, - enable_session_cache = true + enable_session_cache = true, + po_token?: string ) { - const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user }; + const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user, po_token }; let session_data: SessionData | undefined; diff --git a/src/core/endpoints/PlayerEndpoint.ts b/src/core/endpoints/PlayerEndpoint.ts index 98de106db..a25b3123e 100644 --- a/src/core/endpoints/PlayerEndpoint.ts +++ b/src/core/endpoints/PlayerEndpoint.ts @@ -37,7 +37,10 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest { ...{ client: opts.client, playlistId: opts.playlist_id, - params: opts.params + params: opts.params, + serviceIntegrityDimensions: { + poToken: opts.po_token || '' + } } }; } \ No newline at end of file diff --git a/src/types/Endpoints.ts b/src/types/Endpoints.ts index 718137979..50de68e7b 100644 --- a/src/types/Endpoints.ts +++ b/src/types/Endpoints.ts @@ -29,6 +29,9 @@ export interface IPlayerRequest { playlistId?: string; params?: string; client?: InnerTubeClient; + serviceIntegrityDimensions?: { + poToken: string + } } export type PlayerEndpointOptions = { @@ -52,6 +55,10 @@ export type PlayerEndpointOptions = { * Additional protobuf parameters. */ params?: string; + /** + * Token for serviceIntegrityDimensions + */ + po_token?: string; } export type NextEndpointOptions = {