mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-07-29 18:25:47 +02:00
Fix: Centralize default download path and enhance path validation logic
This commit is contained in:
46
main.js
46
main.js
@@ -18,7 +18,8 @@ const basePath = config.localMode
|
|||||||
|
|
||||||
const configFolderPath = path.join(basePath, "config" ,"config.json");
|
const configFolderPath = path.join(basePath, "config" ,"config.json");
|
||||||
|
|
||||||
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
// Default download path (centralized, lazy loaded)
|
||||||
|
let defaultDownloadPath;
|
||||||
|
|
||||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
||||||
app.disableHardwareAcceleration();
|
app.disableHardwareAcceleration();
|
||||||
@@ -30,7 +31,7 @@ const gotLock = app.requestSingleInstanceLock();
|
|||||||
|
|
||||||
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
|
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
|
||||||
function checkNativeDependencies() {
|
function checkNativeDependencies() {
|
||||||
// Import des chemins centralisés après l'initialisation de l'app
|
// Import centralized paths after app initialization
|
||||||
const { binaryPaths } = require("./server/helpers/path");
|
const { binaryPaths } = require("./server/helpers/path");
|
||||||
|
|
||||||
const deps = [
|
const deps = [
|
||||||
@@ -113,23 +114,29 @@ async function createMainWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateDownloadPath(userPath) {
|
function validateDownloadPath(userPath) {
|
||||||
const userHome = os.homedir(); // C:\Users\<User>
|
const { isSafePath } = require("./server/helpers/validation");
|
||||||
|
|
||||||
if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader");
|
// Lazy load default path
|
||||||
|
if (!defaultDownloadPath) {
|
||||||
|
const { defaultDownloadFolder } = require("./server/helpers/path");
|
||||||
|
defaultDownloadPath = defaultDownloadFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userPath) return defaultDownloadPath;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Résolution canonique et suivi des symlinks
|
// Canonical resolution and symlink following
|
||||||
const resolved = fs.realpathSync(path.resolve(userPath));
|
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||||
const normalizedHome = path.resolve(userHome) + path.sep;
|
|
||||||
|
|
||||||
if (!resolved.startsWith(normalizedHome)) {
|
// Use the same validation as backend (allows all drives except system folders)
|
||||||
throw new Error("Chemin non autorisé : uniquement les sous-dossiers du dossier utilisateur sont permis !");
|
if (!isSafePath(resolved)) {
|
||||||
|
throw new Error("Path not allowed: system folders are blocked!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved;
|
return resolved;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Invalid download path: ${userPath} - ${err.message}`);
|
logger.error(`Invalid download path: ${userPath} - ${err.message}`);
|
||||||
throw new Error(`Chemin invalide ou inaccessible : ${err.message}`);
|
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,14 +145,19 @@ function validateDownloadPath(userPath) {
|
|||||||
ipcMain.handle("select-download-folder", async () => {
|
ipcMain.handle("select-download-folder", async () => {
|
||||||
try {
|
try {
|
||||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (result.canceled) {
|
||||||
const validatedPath = validateDownloadPath(result.filePaths[0]);
|
logger.info("Folder selection cancelled by user");
|
||||||
logger.info(`Folder Checked and Valid : ${validatedPath}`);
|
return null;
|
||||||
|
}
|
||||||
|
if (result.filePaths.length > 0) {
|
||||||
|
const selectedPath = result.filePaths[0];
|
||||||
|
const validatedPath = validateDownloadPath(selectedPath);
|
||||||
|
logger.info(`Folder selected and validated: ${validatedPath}`);
|
||||||
return validatedPath;
|
return validatedPath;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`An Error Occured when validating folder : ${err.message}`);
|
logger.warn(`Unsafe or invalid folder rejected: ${err.message}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -155,7 +167,13 @@ ipcMain.handle("validate-download-path", (event, userPath) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle("get-default-download-path", () => defaultDownloadPath);
|
ipcMain.handle("get-default-download-path", () => {
|
||||||
|
if (!defaultDownloadPath) {
|
||||||
|
const { defaultDownloadFolder } = require("./server/helpers/path");
|
||||||
|
defaultDownloadPath = defaultDownloadFolder;
|
||||||
|
}
|
||||||
|
return defaultDownloadPath;
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on("set-progress", (event, percent) => {
|
ipcMain.on("set-progress", (event, percent) => {
|
||||||
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
|
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
|
||||||
|
|||||||
@@ -45,18 +45,15 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
.getElementById("changePath")
|
.getElementById("changePath")
|
||||||
.addEventListener("click", async () => {
|
.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
const selectedPath =
|
// selectDownloadFolder already returns a validated path
|
||||||
|
const validatedPath =
|
||||||
await window.electronAPI.selectDownloadFolder();
|
await window.electronAPI.selectDownloadFolder();
|
||||||
|
|
||||||
if (!selectedPath) return; // annulé
|
if (!validatedPath) return; // cancelled
|
||||||
|
|
||||||
// Validation back obligatoire
|
|
||||||
const validatedPath =
|
|
||||||
await window.electronAPI.getValidatedDownloadPath(selectedPath);
|
|
||||||
|
|
||||||
await applyPathFromBack(validatedPath);
|
await applyPathFromBack(validatedPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Dossier non autorisé.");
|
alert("Folder not allowed.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ async function downloadController(req, res) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ Invalid URL !");
|
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ Invalid URL !");
|
||||||
if (options.outputFolder && !isSafePath(options.outputFolder)) return res.status(400).send("❌ Save Path Not Allowed.");
|
if (options.outputFolder && !isSafePath(options.outputFolder)) {
|
||||||
|
logger.warn(`Unsafe download path rejected: ${options.outputFolder}`);
|
||||||
|
return res.status(400).send("❌ Save Path Not Allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
// Get output folder when the download is finished,
|
// Get output folder when the download is finished
|
||||||
const filePath = await fetchDownload(options, listeners, speedListeners);
|
const outputFolder = await fetchDownload(options, listeners, speedListeners);
|
||||||
notifyDownloadFinished(filePath);
|
notifyDownloadFinished(outputFolder);
|
||||||
res.send("✅ Download Done !");
|
res.send("✅ Download Done !");
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
const { app } = require("electron");
|
const { app } = require("electron");
|
||||||
const config = require("../../config");
|
const config = require("../../config");
|
||||||
|
|
||||||
const { logger } = require("../logger.js");
|
const { logger } = require("../logger.js");
|
||||||
|
|
||||||
// Centralisation de tous les chemins de ressources
|
// Centralized resource paths
|
||||||
const resourcesPath = config.localMode
|
const resourcesPath = config.localMode
|
||||||
? path.join(__dirname, "../../ressources")
|
? path.join(__dirname, "../../ressources")
|
||||||
: process.resourcesPath;
|
: process.resourcesPath;
|
||||||
|
|
||||||
// Chemins des binaires
|
// Default download folder (centralized)
|
||||||
|
const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||||
|
|
||||||
|
// Binary paths
|
||||||
let userYtDlp;
|
let userYtDlp;
|
||||||
let ffmpegPath;
|
let ffmpegPath;
|
||||||
let denoPath;
|
let denoPath;
|
||||||
@@ -32,14 +36,14 @@ if (config.localMode) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chemins des icônes de notification
|
// Notification icon paths
|
||||||
const iconPaths = {
|
const iconPaths = {
|
||||||
confirm: path.join(resourcesPath, "confirm-icon.png"),
|
confirm: path.join(resourcesPath, "confirm-icon.png"),
|
||||||
error: path.join(resourcesPath, "error.png"),
|
error: path.join(resourcesPath, "error.png"),
|
||||||
app: path.join(resourcesPath, "app-icon.ico")
|
app: path.join(resourcesPath, "app-icon.ico")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Chemins des binaires pour vérification
|
// Binary paths for verification
|
||||||
const binaryPaths = {
|
const binaryPaths = {
|
||||||
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"),
|
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"),
|
||||||
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"),
|
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"),
|
||||||
@@ -51,4 +55,4 @@ if (!userYtDlp){ logger.error("Missing YT-DLP")}
|
|||||||
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
|
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
|
||||||
if (!denoPath){ logger.error("Missing DENO")}
|
if (!denoPath){ logger.error("Missing DENO")}
|
||||||
|
|
||||||
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath };
|
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder };
|
||||||
|
|||||||
@@ -12,10 +12,35 @@ function isValidUrl(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isSafePath(folder) {
|
function isSafePath(folder) {
|
||||||
if (!folder || folder.length < 3) return false;
|
if (!folder || typeof folder !== "string") return false;
|
||||||
const unsafe = ["System32", "/etc", "\\Windows"];
|
|
||||||
const resolved = path.resolve(folder);
|
try {
|
||||||
return !unsafe.some(u => resolved.includes(u));
|
// Normalize path and resolve symlinks
|
||||||
|
const resolved = path.resolve(folder).toLowerCase().replace(/\//g, "\\");
|
||||||
|
|
||||||
|
// Block Windows system directories (on any drive)
|
||||||
|
const unsafePaths = [
|
||||||
|
"\\windows\\",
|
||||||
|
"\\system32\\",
|
||||||
|
"\\program files\\",
|
||||||
|
"\\program files (x86)\\",
|
||||||
|
"\\programdata\\",
|
||||||
|
"\\$recycle.bin\\",
|
||||||
|
"\\system volume information\\"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if path contains any unsafe directory
|
||||||
|
if (unsafePaths.some(unsafe => resolved.includes(unsafe))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow all drives (C:, D:, E:, etc.) but block system folders
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
// In case of path resolution error
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { isValidUrl, isSafePath };
|
module.exports = { isValidUrl, isSafePath };
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const { execFile } = require("child_process");
|
const { execFile } = require("child_process");
|
||||||
const { userYtDlp } = require("../helpers/path");
|
const { userYtDlp, defaultDownloadFolder } = require("../helpers/path");
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
||||||
@@ -9,8 +8,16 @@ const notify = require("../helpers/notify")
|
|||||||
function fetchDownload(options, listeners, speedListeners) {
|
function fetchDownload(options, listeners, speedListeners) {
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
const outputFolder = options.outputFolder || defaultDownloadFolder;
|
||||||
|
|
||||||
|
// Create download folder if it doesn't exist
|
||||||
|
try {
|
||||||
fs.mkdirSync(outputFolder, { recursive: true });
|
fs.mkdirSync(outputFolder, { recursive: true });
|
||||||
|
logger.info(`Output folder ready: ${outputFolder}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Failed to create output folder: ${err.message}`);
|
||||||
|
return reject(new Error(`Unable to create download folder: ${err.message}`));
|
||||||
|
}
|
||||||
|
|
||||||
const args = buildYtDlpArgs({ ...options, outputFolder });
|
const args = buildYtDlpArgs({ ...options, outputFolder });
|
||||||
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
||||||
@@ -31,7 +38,7 @@ function fetchDownload(options, listeners, speedListeners) {
|
|||||||
if (!line.trim()) return;
|
if (!line.trim()) return;
|
||||||
logger.info(`[yt-dlp] ${line}`);
|
logger.info(`[yt-dlp] ${line}`);
|
||||||
|
|
||||||
/* Barre de Chargement*/
|
/* Progress Bar */
|
||||||
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
|
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
|
||||||
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
||||||
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
|
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
|
||||||
|
|||||||
Reference in New Issue
Block a user