Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support authentication with the Basic scheme for Invidious instances #5569

Merged
merged 2 commits into from
Aug 23, 2024
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
4 changes: 3 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ const IpcChannels = {
SHOW_VIDEO_STATISTICS: 'show-video-statistics',

PLAYER_CACHE_GET: 'player-cache-get',
PLAYER_CACHE_SET: 'player-cache-set'
PLAYER_CACHE_SET: 'player-cache-set',

SET_INVIDIOUS_AUTHORIZATION: 'set-invidious-authorization'
}

const DBActions = {
Expand Down
132 changes: 91 additions & 41 deletions src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,15 +403,19 @@ function runApp() {
sameSite: 'no_restriction',
})

// make InnerTube requests work with the fetch function
// InnerTube rejects requests if the referer isn't YouTube or empty
const innertubeAndMediaRequestFilter = { urls: ['https://www.youtube.com/youtubei/*', 'https://*.googlevideo.com/videoplayback?*'] }

session.defaultSession.webRequest.onBeforeSendHeaders(innertubeAndMediaRequestFilter, ({ requestHeaders, url, resourceType }, callback) => {
requestHeaders.Referer = 'https://www.youtube.com/'
requestHeaders.Origin = 'https://www.youtube.com'
const onBeforeSendHeadersRequestFilter = {
urls: ['https://*/*', 'http://*/*'],
types: ['xhr', 'media', 'image']
}
session.defaultSession.webRequest.onBeforeSendHeaders(onBeforeSendHeadersRequestFilter, ({ requestHeaders, url, resourceType, webContents }, callback) => {
const urlObj = new URL(url)

if (url.startsWith('https://www.youtube.com/youtubei/')) {
// make InnerTube requests work with the fetch function
// InnerTube rejects requests if the referer isn't YouTube or empty
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
Expand All @@ -430,41 +434,50 @@ function runApp() {
requestHeaders['Sec-Fetch-Mode'] = 'same-origin'
requestHeaders['X-Youtube-Bootstrap-Logged-In'] = 'false'
}
} else {
} else if (urlObj.origin.endsWith('.googlevideo.com') && urlObj.pathname === '/videoplayback') {
requestHeaders.Referer = 'https://www.youtube.com/'
requestHeaders.Origin = 'https://www.youtube.com'

// YouTube doesn't send the Content-Type header for the media requests, so we shouldn't either
delete requestHeaders['Content-Type']
}

// YouTube throttles the adaptive formats if you request a chunk larger than 10MiB.
// For the DASH formats we are fine as video.js doesn't seem to ever request chunks that big.
// The legacy formats don't have any chunk size limits.
// For the audio formats we need to handle it ourselves, as the browser requests the entire audio file,
// which means that for most videos that are longer than 10 mins, we get throttled, as the audio track file sizes surpass that 10MiB limit.
// YouTube throttles the adaptive formats if you request a chunk larger than 10MiB.
// For the DASH formats we are fine as video.js doesn't seem to ever request chunks that big.
// The legacy formats don't have any chunk size limits.
// For the audio formats we need to handle it ourselves, as the browser requests the entire audio file,
// which means that for most videos that are longer than 10 mins, we get throttled, as the audio track file sizes surpass that 10MiB limit.

// This code checks if the file is larger than the limit, by checking the `clen` query param,
// which YouTube helpfully populates with the content length for us.
// If it does surpass that limit, it then checks if the requested range is larger than the limit
// (seeking right at the end of the video, would result in a small enough range to be under the chunk limit)
// if that surpasses the limit too, it then limits the requested range to 10MiB, by setting the range to `start-${start + 10MiB}`.
if (resourceType === 'media' && url.includes('&mime=audio') && requestHeaders.Range) {
const TEN_MIB = 10 * 1024 * 1024
// This code checks if the file is larger than the limit, by checking the `clen` query param,
// which YouTube helpfully populates with the content length for us.
// If it does surpass that limit, it then checks if the requested range is larger than the limit
// (seeking right at the end of the video, would result in a small enough range to be under the chunk limit)
// if that surpasses the limit too, it then limits the requested range to 10MiB, by setting the range to `start-${start + 10MiB}`.
if (resourceType === 'media' && urlObj.searchParams.get('mime')?.startsWith('audio/') && requestHeaders.Range) {
const TEN_MIB = 10 * 1024 * 1024

const contentLength = parseInt(new URL(url).searchParams.get('clen'))
const contentLength = parseInt(new URL(url).searchParams.get('clen'))

if (contentLength > TEN_MIB) {
const [startStr, endStr] = requestHeaders.Range.split('=')[1].split('-')
if (contentLength > TEN_MIB) {
const [startStr, endStr] = requestHeaders.Range.split('=')[1].split('-')

const start = parseInt(startStr)
const start = parseInt(startStr)

// handle open ended ranges like `0-` and `1234-`
const end = endStr.length === 0 ? contentLength : parseInt(endStr)
// handle open ended ranges like `0-` and `1234-`
const end = endStr.length === 0 ? contentLength : parseInt(endStr)

if (end - start > TEN_MIB) {
const newEnd = start + TEN_MIB
if (end - start > TEN_MIB) {
const newEnd = start + TEN_MIB

requestHeaders.Range = `bytes=${start}-${newEnd}`
requestHeaders.Range = `bytes=${start}-${newEnd}`
}
}
}
} else if (webContents) {
const invidiousAuthorization = invidiousAuthorizations.get(webContents.id)

if (invidiousAuthorization && url.startsWith(invidiousAuthorization.url)) {
requestHeaders.Authorization = invidiousAuthorization.authorization
}
}

// eslint-disable-next-line n/no-callback-literal
Expand All @@ -488,8 +501,10 @@ function runApp() {
const imageCache = new ImageCache()

protocol.handle('imagecache', (request) => {
const [requestUrl, rawWebContentsId] = request.url.split('#')

return new Promise((resolve, reject) => {
const url = decodeURIComponent(request.url.substring(13))
const url = decodeURIComponent(requestUrl.substring(13))
if (imageCache.has(url)) {
const cached = imageCache.get(url)

Expand All @@ -499,9 +514,22 @@ function runApp() {
return
}

let headers

if (rawWebContentsId) {
const invidiousAuthorization = invidiousAuthorizations.get(parseInt(rawWebContentsId))

if (invidiousAuthorization && url.startsWith(invidiousAuthorization.url)) {
headers = {
Authorization: invidiousAuthorization.authorization
}
}
}

const newRequest = net.request({
method: request.method,
url
url,
headers
})

// Electron doesn't allow certain headers to be set:
Expand Down Expand Up @@ -548,19 +576,20 @@ function runApp() {
})
})

const imageRequestFilter = { urls: ['https://*/*', 'http://*/*'] }
const imageRequestFilter = { urls: ['https://*/*', 'http://*/*'], types: ['image'] }
session.defaultSession.webRequest.onBeforeRequest(imageRequestFilter, (details, callback) => {
// the requests made by the imagecache:// handler to fetch the image,
// are allowed through, as their resourceType is 'other'
if (details.resourceType === 'image') {
// eslint-disable-next-line n/no-callback-literal
callback({
redirectURL: `imagecache://${encodeURIComponent(details.url)}`
})
} else {
// eslint-disable-next-line n/no-callback-literal
callback({})

let redirectURL = `imagecache://${encodeURIComponent(details.url)}`

if (details.webContents) {
redirectURL += `#${details.webContents.id}`
}

callback({
redirectURL
})
})

// --- end of `if experimentsDisableDiskCache` ---
Expand Down Expand Up @@ -1011,6 +1040,21 @@ function runApp() {
await asyncFs.writeFile(filePath, new Uint8Array(value))
})

/** @type {Map<number, { url: string, authorization: string }>} */
const invidiousAuthorizations = new Map()

ipcMain.on(IpcChannels.SET_INVIDIOUS_AUTHORIZATION, (event, authorization, url) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}

if (!authorization) {
invidiousAuthorizations.delete(event.sender.id)
} else if (typeof authorization === 'string' && typeof url === 'string') {
invidiousAuthorizations.set(event.sender.id, { authorization, url })
}
})

// ************************************************* //
// DB related IPC calls
// *********** //
Expand Down Expand Up @@ -1376,6 +1420,12 @@ function runApp() {
}
})

app.on('web-contents-created', (_, webContents) => {
webContents.once('destroyed', () => {
invidiousAuthorizations.delete(webContents.id)
})
})

/*
* Check if an argument was passed and send it over to the GUI (Linux / Windows).
* Remove freetube:// protocol if present
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/ft-list-channel/ft-list-channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export default defineComponent({
}
},
computed: {
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},
listType: function () {
return this.$store.getters.getListType
Expand Down Expand Up @@ -81,7 +81,7 @@ export default defineComponent({
// Can be prefixed with `https://` or `//` (protocol relative)
const thumbnailUrl = this.data.authorThumbnails[2].url

this.thumbnail = youtubeImageUrlToInvidious(thumbnailUrl, this.currentInvidiousInstance)
this.thumbnail = youtubeImageUrlToInvidious(thumbnailUrl, this.currentInvidiousInstanceUrl)

this.channelName = this.data.author
this.id = this.data.authorId
Expand Down
8 changes: 4 additions & 4 deletions src/renderer/components/ft-list-playlist/ft-list-playlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export default defineComponent({
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},

quickBookmarkPlaylistId() {
Expand Down Expand Up @@ -131,7 +131,7 @@ export default defineComponent({
parseInvidiousData: function () {
this.title = this.data.title
if (this.thumbnailCanBeShown) {
this.thumbnail = this.data.playlistThumbnail.replace('https://i.ytimg.com', this.currentInvidiousInstance).replace('hqdefault', 'mqdefault')
this.thumbnail = this.data.playlistThumbnail.replace('https://i.ytimg.com', this.currentInvidiousInstanceUrl).replace('hqdefault', 'mqdefault')
}
this.channelName = this.data.author
this.channelId = this.data.authorId
Expand Down Expand Up @@ -159,7 +159,7 @@ export default defineComponent({
if (this.thumbnailCanBeShown && this.data.videos.length > 0) {
const thumbnailURL = `https://i.ytimg.com/vi/${this.data.videos[0].videoId}/mqdefault.jpg`
if (this.backendPreference === 'invidious') {
this.thumbnail = thumbnailURL.replace('https://i.ytimg.com', this.currentInvidiousInstance)
this.thumbnail = thumbnailURL.replace('https://i.ytimg.com', this.currentInvidiousInstanceUrl)
} else {
this.thumbnail = thumbnailURL
}
Expand Down
10 changes: 5 additions & 5 deletions src/renderer/components/ft-list-video/ft-list-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ export default defineComponent({
return this.$store.getters.getBackendPreference
},

currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},

showPlaylists: function () {
Expand Down Expand Up @@ -182,7 +182,7 @@ export default defineComponent({
},

invidiousUrl: function () {
let videoUrl = `${this.currentInvidiousInstance}/watch?v=${this.id}`
let videoUrl = `${this.currentInvidiousInstanceUrl}/watch?v=${this.id}`
// `playlistId` can be undefined
if (this.playlistSharable) {
// `index` seems can be ignored
Expand All @@ -192,7 +192,7 @@ export default defineComponent({
},

invidiousChannelUrl: function () {
return `${this.currentInvidiousInstance}/channel/${this.channelId}`
return `${this.currentInvidiousInstanceUrl}/channel/${this.channelId}`
},

youtubeUrl: function () {
Expand Down Expand Up @@ -338,7 +338,7 @@ export default defineComponent({

let baseUrl
if (this.backendPreference === 'invidious') {
baseUrl = this.currentInvidiousInstance
baseUrl = this.currentInvidiousInstanceUrl
} else {
baseUrl = 'https://i.ytimg.com'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export default defineComponent({
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},
toBeAddedToPlaylistVideoList: function () {
return this.$store.getters.getToBeAddedToPlaylistVideoList
Expand Down Expand Up @@ -129,7 +129,7 @@ export default defineComponent({
if (this.playlist.videos.length > 0) {
const thumbnailURL = `https://i.ytimg.com/vi/${this.playlist.videos[0].videoId}/mqdefault.jpg`
if (this.backendPreference === 'invidious') {
this.thumbnail = thumbnailURL.replace('https://i.ytimg.com', this.currentInvidiousInstance)
this.thumbnail = thumbnailURL.replace('https://i.ytimg.com', this.currentInvidiousInstanceUrl)
} else {
this.thumbnail = thumbnailURL
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export default defineComponent({
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},
profileList: function () {
return this.$store.getters.getProfileList
Expand Down Expand Up @@ -76,7 +76,7 @@ export default defineComponent({
})
subscriptions.forEach((channel) => {
if (this.backendPreference === 'invidious') {
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance)
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstanceUrl)
}
channel.selected = false
})
Expand All @@ -92,7 +92,7 @@ export default defineComponent({
})
subscriptions.forEach((channel) => {
if (this.backendPreference === 'invidious') {
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance)
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstanceUrl)
}
channel.selected = false
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export default defineComponent({
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},
profileList: function () {
return this.$store.getters.getProfileList
Expand Down Expand Up @@ -71,7 +71,7 @@ export default defineComponent({
return index === -1
}).map((channel) => {
if (this.backendPreference === 'invidious') {
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance)
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstanceUrl)
}
channel.selected = false
return channel
Expand All @@ -92,7 +92,7 @@ export default defineComponent({
return index === -1
}).map((channel) => {
if (this.backendPreference === 'invidious') {
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance)
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstanceUrl)
}
channel.selected = false
return channel
Expand Down
Loading