invidious/assets/js/sabr_potoken.js
2026-01-07 12:50:41 +01:00

271 lines
9.1 KiB
JavaScript

/**
* BotGuard Service - PoToken generation for SABR streaming
* Ported from Kira project (https://github.com/LuanRT/kira)
*
* This module handles:
* - BotGuard challenge fetching and processing
* - Integrity token generation
* - WebPO minter creation for content-bound tokens
* - Cold start token generation for quick fallback
*/
'use strict';
var BotguardService = (function() {
var WAA_REQUEST_KEY = 'O43z0dpjhgX20SCx4KAo';
// Use the API key from bgutils-js/Kira which has access to Web Anti-Abuse API
var GOOG_API_KEY = 'AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw';
var botguardClient = null;
var initializationPromise = null;
var integrityTokenBasedMinter = null;
var bgChallenge = null;
/**
* Build URL for BotGuard API calls (using YouTube endpoint, not googleapis.com)
* @param {string} action - 'Create' or 'GenerateIT'
* @param {boolean} useTrustedEnv
* @returns {string}
*/
function buildURL(action, useTrustedEnv) {
// Use YouTube's endpoint instead of googleapis.com to avoid CORS issues
var baseUrl = 'https://www.youtube.com/api/jnn/v1/';
return baseUrl + action;
}
/**
* Fetch with proxy support for CORS compliance
* All external URLs must go through the Invidious proxy to avoid CORS issues
* @param {string} url - URL to fetch
* @param {Object} options - Fetch options
* @returns {Promise<Response>}
*/
async function fetchWithProxy(url, options) {
var parsedUrl = new URL(url);
var host = parsedUrl.host;
// Build proxy URL with __host and __path parameters
// We use __path instead of putting the path in the URL to avoid issues with special chars like $
var proxyUrl = new URL('/proxy', window.location.origin);
proxyUrl.searchParams.set('__host', host);
proxyUrl.searchParams.set('__path', parsedUrl.pathname);
// Copy original query parameters
parsedUrl.searchParams.forEach(function(value, key) {
proxyUrl.searchParams.set(key, value);
});
// Pass custom headers through __headers parameter
if (options && options.headers) {
var headersArray = [];
for (var key in options.headers) {
if (options.headers.hasOwnProperty(key)) {
headersArray.push([key, options.headers[key]]);
}
}
proxyUrl.searchParams.set('__headers', JSON.stringify(headersArray));
}
// Make the proxied request (headers are passed through __headers param)
return fetch(proxyUrl.toString(), {
method: options?.method || 'GET',
body: options?.body
});
}
/**
* Initialize the BotGuard client
* @returns {Promise<Object|undefined>}
*/
async function init() {
if (initializationPromise) {
return await initializationPromise;
}
return setup();
}
/**
* Internal setup function
* @returns {Promise<Object|undefined>}
*/
async function setup() {
if (initializationPromise) {
return await initializationPromise;
}
initializationPromise = _initBotguard();
try {
botguardClient = await initializationPromise;
return botguardClient;
} finally {
initializationPromise = null;
}
}
/**
* Internal BotGuard initialization
* @returns {Promise<Object|undefined>}
*/
async function _initBotguard() {
// Check if BG (bgutils-js) is available
if (typeof BG === 'undefined') {
console.error('[BotguardService]', 'bgutils-js not loaded');
return undefined;
}
try {
// First call (Create) uses direct fetch - no proxy needed
var challengeResponse = await fetch(buildURL('Create', true), {
method: 'POST',
headers: {
'content-type': 'application/json+protobuf',
'x-goog-api-key': GOOG_API_KEY,
'x-user-agent': 'grpc-web-javascript/0.1'
},
body: JSON.stringify([WAA_REQUEST_KEY])
});
var challengeResponseData = await challengeResponse.json();
bgChallenge = BG.Challenge.parseChallengeData(challengeResponseData);
if (!bgChallenge) {
console.error('[BotguardService]', 'Failed to parse challenge data');
return undefined;
}
var interpreterJavascript = bgChallenge.interpreterJavascript?.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (!interpreterJavascript) {
console.error('[BotguardService]', 'Could not get interpreter javascript. Interpreter Hash:', bgChallenge.interpreterHash);
return undefined;
}
// Inject the interpreter script if not already present
if (!document.getElementById(bgChallenge.interpreterHash)) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.id = bgChallenge.interpreterHash;
script.textContent = interpreterJavascript;
document.head.appendChild(script);
}
// Create the BotGuard client
botguardClient = await BG.BotGuardClient.create({
globalObj: globalThis,
globalName: bgChallenge.globalName,
program: bgChallenge.program
});
// Generate integrity token and create WebPO minter
if (bgChallenge) {
var webPoSignalOutput = [];
var botguardResponse = await botguardClient.snapshot({ webPoSignalOutput: webPoSignalOutput });
var integrityTokenResponse = await fetchWithProxy(buildURL('GenerateIT', true), {
method: 'POST',
headers: {
'content-type': 'application/json+protobuf',
'x-goog-api-key': GOOG_API_KEY,
'x-user-agent': 'grpc-web-javascript/0.1'
},
body: JSON.stringify([WAA_REQUEST_KEY, botguardResponse])
});
var integrityTokenResponseData = await integrityTokenResponse.json();
var integrityToken = integrityTokenResponseData[0];
if (!integrityToken) {
console.error('[BotguardService]', 'Could not get integrity token. Interpreter Hash:', bgChallenge.interpreterHash);
return botguardClient;
}
integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken: integrityToken }, webPoSignalOutput);
}
return botguardClient;
} catch (error) {
console.error('[BotguardService]', 'Error initializing BotGuard:', error);
return undefined;
}
}
/**
* Mint a cold start token (quick fallback)
* @param {string} contentBinding - Content binding (usually video ID)
* @returns {string}
*/
function mintColdStartToken(contentBinding) {
if (typeof BG === 'undefined') {
console.error('[BotguardService]', 'bgutils-js not loaded');
return '';
}
return BG.PoToken.generateColdStartToken(contentBinding);
}
/**
* Check if BotGuard is fully initialized
* @returns {boolean}
*/
function isInitialized() {
return !!botguardClient && !!integrityTokenBasedMinter;
}
/**
* Mint a WebPO token for content binding
* @param {string} contentBinding - Content binding (usually video ID)
* @returns {Promise<string>}
*/
async function mintWebPoToken(contentBinding) {
if (!integrityTokenBasedMinter) {
throw new Error('WebPO minter not initialized');
}
return await integrityTokenBasedMinter.mintAsWebsafeString(contentBinding);
}
/**
* Dispose of the BotGuard client
*/
function dispose() {
if (botguardClient && bgChallenge) {
try {
botguardClient.shutdown();
} catch (e) {
// Ignore shutdown errors
}
botguardClient = null;
integrityTokenBasedMinter = null;
var script = document.getElementById(bgChallenge.interpreterHash);
if (script) {
script.remove();
}
bgChallenge = null;
}
}
/**
* Reinitialize BotGuard
* @returns {Promise<Object|undefined>}
*/
async function reinit() {
if (initializationPromise) {
return initializationPromise;
}
dispose();
return setup();
}
return {
init: init,
mintColdStartToken: mintColdStartToken,
mintWebPoToken: mintWebPoToken,
isInitialized: isInitialized,
dispose: dispose,
reinit: reinit
};
})();
// Export for use in other modules
window.BotguardService = BotguardService;