Merge branch 'development' of https://github.com/dkshxd/FreeTube into development

This commit is contained in:
P.S 2024-04-24 09:45:14 +10:00
commit c4e32bce9c
4 changed files with 154 additions and 22 deletions

View File

@ -0,0 +1,50 @@
/**
* Injects the paths that the renderer process is allowed to read into the main.js file,
* by replacing __FREETUBE_ALLOWED_PATHS__ with an array of strings with the paths.
*
* This allows the main process to validate the paths which the renderer process accesses,
* to ensure that it cannot access other files on the disk, without the users permission (e.g. file picker).
*/
import { closeSync, ftruncateSync, openSync, readFileSync, readdirSync, writeSync } from 'fs'
import { dirname, join, relative, resolve } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url));
const distDirectory = resolve(__dirname, '..', 'dist')
const webDirectory = join(distDirectory, 'web')
const paths = readdirSync(distDirectory, {
recursive: true,
withFileTypes: true
})
.filter(dirent => {
// only include files not directories
return dirent.isFile() &&
// disallow the renderer process/browser windows to read the main.js file
dirent.name !== 'main.js' &&
dirent.name !== 'main.js.LICENSE.txt' &&
// filter out any web build files, in case the dist directory contains a web build
!dirent.path.startsWith(webDirectory);
})
.map(dirent => {
const joined = join(dirent.path, dirent.name)
return '/' + relative(distDirectory, joined).replaceAll('\\', '/')
})
let fileHandle
try {
fileHandle = openSync(join(distDirectory, 'main.js'), 'r+')
let contents = readFileSync(fileHandle, 'utf-8')
contents = contents.replace('__FREETUBE_ALLOWED_PATHS__', JSON.stringify(paths))
ftruncateSync(fileHandle)
writeSync(fileHandle, contents, 0, 'utf-8')
} finally {
if (typeof fileHandle !== 'undefined') {
closeSync(fileHandle)
}
}

View File

@ -42,7 +42,7 @@
"lint-style": "stylelint \"**/*.{css,scss}\"",
"lint-style-fix": "stylelint --fix \"**/*.{css,scss}\"",
"lint-yml": "eslint --ext .yml,.yaml ./",
"pack": "run-p pack:main pack:renderer",
"pack": "run-p pack:main pack:renderer && 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",

View File

@ -11,9 +11,13 @@ import * as baseHandlers from '../datastores/handlers/base'
import { extractExpiryTimestamp, ImageCache } from './ImageCache'
import { existsSync } from 'fs'
import asyncFs from 'fs/promises'
import { promisify } from 'util'
import { brotliDecompress } from 'zlib'
import packageDetails from '../../package.json'
const brotliDecompressAsync = promisify(brotliDecompress)
if (process.argv.includes('--version')) {
app.exit()
} else {
@ -21,6 +25,24 @@ if (process.argv.includes('--version')) {
}
function runApp() {
/** @type {Set<string>} */
let ALLOWED_RENDERER_FILES
if (process.env.NODE_ENV === 'production') {
// __FREETUBE_ALLOWED_PATHS__ is replaced by the injectAllowedPaths.mjs script
// eslint-disable-next-line no-undef
ALLOWED_RENDERER_FILES = new Set(__FREETUBE_ALLOWED_PATHS__)
protocol.registerSchemesAsPrivileged([{
scheme: 'app',
privileges: {
standard: true,
secure: true,
supportFetchAPI: true
}
}])
}
require('electron-context-menu')({
showSearchWithGoogle: false,
showSaveImageAs: true,
@ -222,6 +244,48 @@ function runApp() {
}
app.on('ready', async (_, __) => {
if (process.env.NODE_ENV === 'production') {
protocol.handle('app', async (request) => {
if (request.method !== 'GET') {
return new Response(null, {
status: 405,
headers: {
Allow: 'GET'
}
})
}
const { host, pathname } = new URL(request.url)
if (host !== 'bundle' || !ALLOWED_RENDERER_FILES.has(pathname)) {
return new Response(null, {
status: 400
})
}
const contents = await asyncFs.readFile(path.join(__dirname, pathname))
if (pathname.endsWith('.json.br')) {
const decompressed = await brotliDecompressAsync(contents)
return new Response(decompressed.buffer, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Content-Encoding': 'br'
}
})
} else {
return new Response(contents.buffer, {
status: 200,
headers: {
'Content-Type': contentTypeFromFileExtension(pathname.split('.').at(-1))
}
})
}
})
}
let docArray
try {
docArray = await baseHandlers.settings._findAppReadyRelatedSettings()
@ -455,6 +519,34 @@ function runApp() {
}
})
/**
* @param {string} extension
*/
function contentTypeFromFileExtension(extension) {
switch (extension) {
case 'html':
return 'text/html'
case 'css':
return 'text/css'
case 'js':
return 'text/javascript'
case 'ttf':
return 'font/ttf'
case 'woff':
return 'font/woff'
case 'svg':
return 'image/svg+xml'
case 'png':
return 'image/png'
case 'json':
return 'application/json'
case 'txt':
return 'text/plain'
default:
return 'application/octet-stream'
}
}
async function installDevTools() {
try {
/* eslint-disable */
@ -605,8 +697,7 @@ function runApp() {
if (windowStartupUrl != null) {
newWindow.loadURL(windowStartupUrl)
} else {
/* eslint-disable-next-line n/no-path-concat */
newWindow.loadFile(`${__dirname}/index.html`)
newWindow.loadURL('app://bundle/index.html')
}
}

View File

@ -34,29 +34,20 @@ export async function loadLocale(locale) {
return
}
let path
// locales are only compressed in our production Electron builds
if (process.env.IS_ELECTRON && process.env.NODE_ENV !== 'development') {
const { promisify } = require('util')
const { brotliDecompress } = require('zlib')
const brotliDecompressAsync = promisify(brotliDecompress)
try {
// decompress brotli compressed json file and then load it
const url = createWebURL(`/static/locales/${locale}.json.br`)
const compressed = await (await fetch(url)).arrayBuffer()
const decompressed = await brotliDecompressAsync(compressed)
const data = JSON.parse(decompressed.toString())
i18n.setLocaleMessage(locale, data)
} catch (err) {
console.error(locale, err)
}
path = `/static/locales/${locale}.json.br`
} else {
const url = createWebURL(`/static/locales/${locale}.json`)
const response = await fetch(url)
const data = await response.json()
i18n.setLocaleMessage(locale, data)
path = `/static/locales/${locale}.json`
}
const url = createWebURL(path)
const response = await fetch(url)
const data = await response.json()
i18n.setLocaleMessage(locale, data)
}
export default i18n