Skip to content

Commit

Permalink
Switch back to the WEB client for the local API with potokens
Browse files Browse the repository at this point in the history
  • Loading branch information
absidue committed Dec 22, 2024
1 parent f419aa5 commit 3fe27fe
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 112 deletions.
15 changes: 14 additions & 1 deletion _scripts/dev-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ const web = process.argv.indexOf('--web') !== -1

let mainConfig
let rendererConfig
let botGuardScriptConfig
let webConfig
let SHAKA_LOCALES_TO_BE_BUNDLED

if (!web) {
mainConfig = require('./webpack.main.config')
rendererConfig = require('./webpack.renderer.config')
botGuardScriptConfig = require('./webpack.botGuardScript.config')

SHAKA_LOCALES_TO_BE_BUNDLED = rendererConfig.SHAKA_LOCALES_TO_BE_BUNDLED
delete rendererConfig.SHAKA_LOCALES_TO_BE_BUNDLED
Expand Down Expand Up @@ -98,6 +100,14 @@ function setupNotifyLocaleUpdate(compiler, devServer) {
})
}

function startBotGuardScript() {
webpack(botGuardScriptConfig, (err) => {
if (err) console.error(err)

console.log(`\nCompiled ${botGuardScriptConfig.name} script!`)
})
}

function startMain() {
const compiler = webpack(mainConfig)
const { name } = compiler
Expand Down Expand Up @@ -196,7 +206,10 @@ function startWeb () {
})
}
if (!web) {
startRenderer(startMain)
startRenderer(() => {
startBotGuardScript()
startMain()
})
} else {
startWeb()
}
2 changes: 2 additions & 0 deletions _scripts/injectAllowedPaths.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const paths = readdirSync(distDirectory, {
// disallow the renderer process/browser windows to read the main.js file
dirent.name !== 'main.js' &&
dirent.name !== 'main.js.LICENSE.txt' &&
// disallow the renderer process/browser windows to read the botGuardScript.js file
dirent.name !== 'botGuardScript.js' &&
// filter out any web build files, in case the dist directory contains a web build
!dirent.parentPath.startsWith(webDirectory)
})
Expand Down
23 changes: 23 additions & 0 deletions _scripts/webpack.botGuardScript.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const path = require('path')

/** @type {import('webpack').Configuration} */
module.exports = {
name: 'botGuardScript',
// Always use production mode, as we use the output as a function body and the debug output doesn't work for that
mode: 'production',
devtool: false,
target: 'web',
entry: {
botGuardScript: path.join(__dirname, '../src/botGuardScript.js'),
},
output: {
filename: '[name].js',
path: path.join(__dirname, '../dist'),
library: {
type: 'modern-module'
}
},
experiments: {
outputModule: true
}
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@
"lint-style": "stylelint \"**/*.{css,scss}\"",
"lint-style-fix": "stylelint --fix \"**/*.{css,scss}\"",
"lint-yml": "eslint --config eslint.config.mjs \"**/*.yml\" \"**/*.yaml\"",
"pack": "run-p pack:main pack:renderer && node _scripts/injectAllowedPaths.mjs",
"pack": "run-p pack:main pack:renderer pack:botGuardScript && node _scripts/injectAllowedPaths.mjs",
"pack:main": "webpack --mode=production --node-env=production --config _scripts/webpack.main.config.js",
"pack:renderer": "webpack --mode=production --node-env=production --config _scripts/webpack.renderer.config.js",
"pack:web": "webpack --mode=production --node-env=production --config _scripts/webpack.web.config.js",
"pack:botGuardScript": "webpack --config _scripts/webpack.botGuardScript.config.js",
"postinstall": "run-s --silent rebuild:electron patch-shaka",
"prettier": "prettier --write \"{src,_scripts}/**/*.{js,vue}\"",
"rebuild:electron": "electron-builder install-app-deps",
Expand All @@ -61,6 +62,7 @@
"@fortawesome/vue-fontawesome": "^2.0.10",
"@seald-io/nedb": "^4.0.4",
"autolinker": "^4.0.0",
"bgutils-js": "^3.1.0",
"electron-context-menu": "^4.0.4",
"lodash.debounce": "^4.0.8",
"marked": "^15.0.4",
Expand Down
37 changes: 37 additions & 0 deletions src/botGuardScript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { BG } from 'bgutils-js'

// This script has it's own webpack config, as it gets passed as a string to Electron's evaluateJavaScript function
// in src/main/poTokenGenerator.js
export default async function(visitorData) {
const requestKey = 'O43z0dpjhgX20SCx4KAo'

const bgConfig = {
fetch: (input, init) => fetch(input, init),
requestKey,
globalObj: window,
identifier: visitorData
}

const challenge = await BG.Challenge.create(bgConfig)

if (!challenge) {
throw new Error('Could not get challenge')
}

const interpreterJavascript = challenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue

if (interpreterJavascript) {
// eslint-disable-next-line no-new-func
new Function(interpreterJavascript)()
} else {
console.warn('Unable to load VM.')
}

const poTokenResult = await BG.PoToken.generate({
program: challenge.program,
globalName: challenge.globalName,
bgConfig
})

return poTokenResult.poToken
}
4 changes: 3 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ const IpcChannels = {
PLAYER_CACHE_GET: 'player-cache-get',
PLAYER_CACHE_SET: 'player-cache-set',

SET_INVIDIOUS_AUTHORIZATION: 'set-invidious-authorization'
SET_INVIDIOUS_AUTHORIZATION: 'set-invidious-authorization',

GENERATE_PO_TOKEN: 'generate-po-token',
}

const DBActions = {
Expand Down
26 changes: 8 additions & 18 deletions src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { brotliDecompress } from 'zlib'
import contextMenu from 'electron-context-menu'

import packageDetails from '../../package.json'
import { generatePoToken } from './poTokenGenerator'

const brotliDecompressAsync = promisify(brotliDecompress)

Expand Down Expand Up @@ -427,24 +428,9 @@ function runApp() {
requestHeaders.Referer = 'https://www.youtube.com/'
requestHeaders.Origin = 'https://www.youtube.com'

// Make iOS requests work and look more realistic
if (requestHeaders['x-youtube-client-name'] === '5') {
delete requestHeaders.Referer
delete requestHeaders.Origin
delete requestHeaders['Sec-Fetch-Site']
delete requestHeaders['Sec-Fetch-Mode']
delete requestHeaders['Sec-Fetch-Dest']
delete requestHeaders['sec-ch-ua']
delete requestHeaders['sec-ch-ua-mobile']
delete requestHeaders['sec-ch-ua-platform']

requestHeaders['User-Agent'] = requestHeaders['x-user-agent']
delete requestHeaders['x-user-agent']
} else {
requestHeaders['Sec-Fetch-Site'] = 'same-origin'
requestHeaders['Sec-Fetch-Mode'] = 'same-origin'
requestHeaders['X-Youtube-Bootstrap-Logged-In'] = 'false'
}
requestHeaders['Sec-Fetch-Site'] = 'same-origin'
requestHeaders['Sec-Fetch-Mode'] = 'same-origin'
requestHeaders['X-Youtube-Bootstrap-Logged-In'] = 'false'
} else if (urlObj.origin.endsWith('.googlevideo.com') && urlObj.pathname === '/videoplayback') {
requestHeaders.Referer = 'https://www.youtube.com/'
requestHeaders.Origin = 'https://www.youtube.com'
Expand Down Expand Up @@ -884,6 +870,10 @@ function runApp() {
})
})

ipcMain.handle(IpcChannels.GENERATE_PO_TOKEN, (_, visitorData) => {
return generatePoToken(visitorData)
})

ipcMain.on(IpcChannels.ENABLE_PROXY, (_, url) => {
session.defaultSession.setProxy({
proxyRules: url
Expand Down
140 changes: 140 additions & 0 deletions src/main/poTokenGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { session, WebContentsView } from 'electron'
import { readFile } from 'fs/promises'
import { join } from 'path'

/**
* Generates a poToken (proof of origin token) using `bgutils-js`.
* The script to generate it is `src/botGuardScript.js`
*
* This is intentionally split out into it's own thing, with it's own temporary in-memory session,
* as the BotGuard stuff accesses the global `document` and `window` objects and also requires making some requests.
* So we definitely don't want it running in the same places as the rest of the FreeTube code with the user data.
* @param {string} visitorData
* @returns {Promise<string>}
*/
export async function generatePoToken(visitorData) {
const sessionUuid = crypto.randomUUID()

const theSession = session.fromPartition(`potoken-${sessionUuid}`, { cache: false })

theSession.setPermissionCheckHandler(() => false)
// eslint-disable-next-line n/no-callback-literal
theSession.setPermissionRequestHandler((webContents, permission, callback) => callback(false))

theSession.setUserAgent(
theSession.getUserAgent()
.split(' ')
.filter(part => !part.includes('Electron'))
.join(' ')
)

const webContentsView = new WebContentsView({
webPreferences: {
backgroundThrottling: false,
safeDialogs: true,
sandbox: true,
v8CacheOptions: 'none',
session: theSession,
offscreen: true
}
})

webContentsView.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))

webContentsView.webContents.setAudioMuted(true)
webContentsView.setBounds({
x: 0,
y: 0,
width: 1920,
height: 1080
})

webContentsView.webContents.debugger.attach()

await webContentsView.webContents.loadURL('data:text/html,', {
baseURLForDataURL: 'https://www.youtube.com'
})

await webContentsView.webContents.debugger.sendCommand('Emulation.setUserAgentOverride', {
userAgent: theSession.getUserAgent(),
acceptLanguage: 'en-US',
platform: 'Win32',
userAgentMetadata: {
brands: [
{
brand: 'Not/A)Brand',
version: '99'
},
{
brand: 'Chromium',
version: process.versions.chrome.split('.')[0]
}
],
fullVersionList: [
{
brand: 'Not/A)Brand',
version: '99.0.0.0'
},
{
brand: 'Chromium',
version: process.versions.chrome
}
],
platform: 'Windows',
platformVersion: '10.0.0',
architecture: 'x86',
model: '',
mobile: false,
bitness: '64',
wow64: false
}
})

await webContentsView.webContents.debugger.sendCommand('Emulation.setDeviceMetricsOverride', {
width: 1920,
height: 1080,
deviceScaleFactor: 1,
mobile: false,
screenWidth: 1920,
screenHeight: 1080,
positionX: 0,
positionY: 0,
screenOrientation: {
type: 'landscapePrimary',
angle: 0
}
})

const script = await getScript(visitorData)

const response = await webContentsView.webContents.executeJavaScript(script)

webContentsView.webContents.close({ waitForBeforeUnload: false })
await theSession.closeAllConnections()

return response
}

let cachedScript

/**
* @param {string} visitorData
*/
async function getScript(visitorData) {
if (!cachedScript) {
const pathToScript = process.env.NODE_ENV === 'development'
? join(__dirname, '../../dist/botGuardScript.js')
/* eslint-disable-next-line n/no-path-concat */
: `${__dirname}/botGuardScript.js`

const content = await readFile(pathToScript, 'utf-8')

const match = content.match(/export{(\w+) as default};/)

const functionName = match[1]

cachedScript = content.replace(match[0], `;${functionName}("FT_VISITOR_DATA")`)
}

return cachedScript.replace('FT_VISITOR_DATA', visitorData)
}
Loading

0 comments on commit 3fe27fe

Please sign in to comment.