Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "freetube",
"productName": "FreeTube",
"description": "A private YouTube client",
"version": "0.23.11",
"version": "0.23.12",
"license": "AGPL-3.0-or-later",
"main": "./dist/main.js",
"private": true,
Expand Down
10 changes: 3 additions & 7 deletions src/botGuardScript.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import { BG, buildURL, GOOG_API_KEY } from 'bgutils-js'
/**
* Based on: https://github.com/LuanRT/BgUtils/blob/main/examples/node/innertube-challenge-fetcher-example.ts
* @param {string} videoId
* @param {string} visitorData
* @param {import('youtubei.js').Session['context']} context
*/
export default async function (videoId, visitorData, context) {
export default async function (videoId, context) {
const requestKey = 'O43z0dpjhgX20SCx4KAo'

const challengeResponse = await fetch(
Expand All @@ -19,7 +18,7 @@ export default async function (videoId, visitorData, context) {
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
'X-Goog-Visitor-Id': visitorData,
'X-Goog-Visitor-Id': context.client.visitorData,
'X-Youtube-Client-Version': context.client.clientVersion,
'X-Youtube-Client-Name': '1'
},
Expand Down Expand Up @@ -83,8 +82,5 @@ export default async function (videoId, visitorData, context) {

const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken: response[0] }, webPoSignalOutput)

const contentPoToken = await integrityTokenBasedMinter.mintAsWebsafeString(videoId)
const sessionPoToken = await integrityTokenBasedMinter.mintAsWebsafeString(visitorData)

return { contentPoToken, sessionPoToken }
return await integrityTokenBasedMinter.mintAsWebsafeString(videoId)
}
2 changes: 1 addition & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const IpcChannels = {

SET_INVIDIOUS_AUTHORIZATION: 'set-invidious-authorization',

GENERATE_PO_TOKENS: 'generate-po-tokens',
GENERATE_PO_TOKEN: 'generate-po-token',

WRITE_SCREENSHOT: 'write-screenshot',
}
Expand Down
5 changes: 2 additions & 3 deletions src/index.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<body>
<div id="app"></div>
<% if (process.env.SUPPORTS_LOCAL_API) { %>
<% if (process.env.IS_ELECTRON) { %>
<iframe
id="sigFrame"
src="<%= sigFrameSrc %>"
Expand All @@ -24,8 +24,7 @@
style="display: none; pointer-events: none"
tabindex="-1"
></iframe>
<% } %>
<% if (!process.env.IS_ELECTRON) { %>
<% } else { %>
<script>
// This is the service worker with the Advanced caching

Expand Down
4 changes: 2 additions & 2 deletions src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -891,8 +891,8 @@ function runApp() {
})
})

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

ipcMain.on(IpcChannels.ENABLE_PROXY, (_, url) => {
Expand Down
14 changes: 6 additions & 8 deletions src/main/poTokenGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ import { readFile } from 'fs/promises'
import { join } from 'path'

/**
* Generates a poToken (proof of origin token) using `bgutils-js`.
* Generates a content-bound 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} videoId
* @param {string} visitorData
* @param {string} context
* @param {string|undefined} proxyUrl
* @returns {Promise<{ contentPoToken: string, sessionPoToken: string }>}
* @returns {Promise<string>}
*/
export async function generatePoToken(videoId, visitorData, context, proxyUrl) {
export async function generatePoToken(videoId, context, proxyUrl) {
const sessionUuid = crypto.randomUUID()

const theSession = session.fromPartition(`potoken-${sessionUuid}`, { cache: false })
Expand Down Expand Up @@ -96,7 +95,7 @@ export async function generatePoToken(videoId, visitorData, context, proxyUrl) {
}
})

const script = await getScript(videoId, visitorData, context)
const script = await getScript(videoId, context)

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

Expand All @@ -110,10 +109,9 @@ let cachedScript

/**
* @param {string} videoId
* @param {string} visitorData
* @param {string} context
*/
async function getScript(videoId, visitorData, context) {
async function getScript(videoId, context) {
if (!cachedScript) {
const pathToScript = process.env.NODE_ENV === 'development'
? join(__dirname, '../../dist/botGuardScript.js')
Expand All @@ -129,5 +127,5 @@ async function getScript(videoId, visitorData, context) {
cachedScript = content.replace(match[0], `;${functionName}(FT_PARAMS)`)
}

return cachedScript.replace('FT_PARAMS', `"${videoId}","${visitorData}",${context}`)
return cachedScript.replace('FT_PARAMS', `"${videoId}",${context}`)
}
49 changes: 27 additions & 22 deletions src/renderer/helpers/api/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const TRACKING_PARAM_NAMES = [

if (process.env.SUPPORTS_LOCAL_API) {
Platform.shim.eval = (data, env) => {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
const properties = []

if (env.n) {
Expand All @@ -43,23 +43,31 @@ if (process.env.SUPPORTS_LOCAL_API) {
? crypto.randomUUID()
: `${Date.now()}-${Math.floor(Math.random() * 10000)}`

const iframe = document.getElementById('sigFrame')
if (process.env.IS_ELECTRON) {
const iframe = document.getElementById('sigFrame')

/** @param {MessageEvent} event */
const listener = (event) => {
if (event.source === iframe.contentWindow && typeof event.data === 'string') {
const data = JSON.parse(event.data)
/** @param {MessageEvent} event */
const listener = (event) => {
if (event.source === iframe.contentWindow && typeof event.data === 'string') {
const data = JSON.parse(event.data)

if (data.id === messageId) {
window.removeEventListener('message', listener)
if (data.id === messageId) {
window.removeEventListener('message', listener)

resolve(data.result)
if (data.error) {
reject(data.error)
} else {
resolve(data.result)
}
}
}
}
}

window.addEventListener('message', listener)
iframe.contentWindow.postMessage(JSON.stringify({ id: messageId, code }), '*')
window.addEventListener('message', listener)
iframe.contentWindow.postMessage(JSON.stringify({ id: messageId, code }), '*')
} else {
reject(new Error('Please setup the eval function for the n/sig deciphering'))
}
})
}
}
Expand Down Expand Up @@ -285,23 +293,20 @@ export async function getLocalSearchContinuation(continuationData) {
export async function getLocalVideoInfo(id) {
const webInnertube = await createInnertube({ withPlayer: true, generateSessionLocally: false })

// based on the videoId (added to the body of the /player request and to caption URLs)
// based on the videoId
let contentPoToken
// based on the visitor data (added to the streaming URLs)
let sessionPoToken

if (process.env.IS_ELECTRON) {
const { ipcRenderer } = require('electron')

try {
({ contentPoToken, sessionPoToken } = await ipcRenderer.invoke(
IpcChannels.GENERATE_PO_TOKENS,
contentPoToken = await ipcRenderer.invoke(
IpcChannels.GENERATE_PO_TOKEN,
id,
webInnertube.session.context.client.visitorData,
JSON.stringify(webInnertube.session.context)
))
)

webInnertube.session.player.po_token = sessionPoToken
webInnertube.session.player.po_token = contentPoToken
} catch (error) {
console.error('Local API, poToken generation failed', error)
throw error
Expand Down Expand Up @@ -400,9 +405,9 @@ export async function getLocalVideoInfo(id) {
let url = info.streaming_data.dash_manifest_url

if (url.includes('?')) {
url += `&pot=${encodeURIComponent(sessionPoToken)}&mpd_version=7`
url += `&pot=${encodeURIComponent(contentPoToken)}&mpd_version=7`
} else {
url += `${url.endsWith('/') ? '' : '/'}pot/${encodeURIComponent(sessionPoToken)}/mpd_version/7`
url += `${url.endsWith('/') ? '' : '/'}pot/${encodeURIComponent(contentPoToken)}/mpd_version/7`
}

info.streaming_data.dash_manifest_url = url
Expand Down
17 changes: 12 additions & 5 deletions src/renderer/sigFrameScript.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ window.addEventListener('message', (event) => {
// eslint-disable-next-line @stylistic/semi
const data = JSON.parse(event.data);

window.parent.postMessage(JSON.stringify({
id: data.id,
// eslint-disable-next-line no-new-func
result: new Function(data.code)()
}), '*')
try {
window.parent.postMessage(JSON.stringify({
id: data.id,
// eslint-disable-next-line no-new-func
result: new Function(data.code)()
}), '*')
} catch (error) {
window.parent.postMessage(JSON.stringify({
id: data.id,
error
}), '*')
}
})