diff --git a/main.js b/main.js index b218ea1..5f815a8 100644 --- a/main.js +++ b/main.js @@ -18,7 +18,8 @@ const basePath = config.localMode 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.disableHardwareAcceleration(); @@ -30,7 +31,7 @@ const gotLock = app.requestSingleInstanceLock(); // Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno) 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 deps = [ @@ -113,23 +114,29 @@ async function createMainWindow() { } function validateDownloadPath(userPath) { - const userHome = os.homedir(); // C:\Users\ + const { isSafePath } = require("./server/helpers/validation"); + + // Lazy load default path + if (!defaultDownloadPath) { + const { defaultDownloadFolder } = require("./server/helpers/path"); + defaultDownloadPath = defaultDownloadFolder; + } - if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader"); + if (!userPath) return defaultDownloadPath; try { - // Résolution canonique et suivi des symlinks + // Canonical resolution and symlink following const resolved = fs.realpathSync(path.resolve(userPath)); - const normalizedHome = path.resolve(userHome) + path.sep; - - if (!resolved.startsWith(normalizedHome)) { - throw new Error("Chemin non autorisé : uniquement les sous-dossiers du dossier utilisateur sont permis !"); + + // Use the same validation as backend (allows all drives except system folders) + if (!isSafePath(resolved)) { + throw new Error("Path not allowed: system folders are blocked!"); } return resolved; } catch (err) { 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 () => { try { const result = await dialog.showOpenDialog({ properties: ["openDirectory"] }); - if (!result.canceled && result.filePaths.length > 0) { - const validatedPath = validateDownloadPath(result.filePaths[0]); - logger.info(`Folder Checked and Valid : ${validatedPath}`); + if (result.canceled) { + logger.info("Folder selection cancelled by user"); + 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 null; } catch (err) { - logger.error(`An Error Occured when validating folder : ${err.message}`); + logger.warn(`Unsafe or invalid folder rejected: ${err.message}`); 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) => { if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1 diff --git a/public/script/custompath.js b/public/script/custompath.js index ccf01f0..992f896 100644 --- a/public/script/custompath.js +++ b/public/script/custompath.js @@ -45,18 +45,15 @@ window.addEventListener("DOMContentLoaded", async () => { .getElementById("changePath") .addEventListener("click", async () => { try { - const selectedPath = + // selectDownloadFolder already returns a validated path + const validatedPath = await window.electronAPI.selectDownloadFolder(); - if (!selectedPath) return; // annulé - - // Validation back obligatoire - const validatedPath = - await window.electronAPI.getValidatedDownloadPath(selectedPath); + if (!validatedPath) return; // cancelled await applyPathFromBack(validatedPath); } catch (err) { - alert("Dossier non autorisé."); + alert("Folder not allowed."); } }); }); diff --git a/server/controller/download.controller.js b/server/controller/download.controller.js index bf4bd6b..f875859 100644 --- a/server/controller/download.controller.js +++ b/server/controller/download.controller.js @@ -16,11 +16,14 @@ async function downloadController(req, res) { }; 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, - const filePath = await fetchDownload(options, listeners, speedListeners); - notifyDownloadFinished(filePath); + // Get output folder when the download is finished + const outputFolder = await fetchDownload(options, listeners, speedListeners); + notifyDownloadFinished(outputFolder); res.send("✅ Download Done !"); } catch (err) { diff --git a/server/helpers/path.js b/server/helpers/path.js index a22bb0f..2a7d823 100644 --- a/server/helpers/path.js +++ b/server/helpers/path.js @@ -1,16 +1,20 @@ const path = require("path"); const fs = require("fs"); +const os = require("os"); const { app } = require("electron"); const config = require("../../config"); const { logger } = require("../logger.js"); -// Centralisation de tous les chemins de ressources +// Centralized resource paths const resourcesPath = config.localMode ? path.join(__dirname, "../../ressources") : process.resourcesPath; -// Chemins des binaires +// Default download folder (centralized) +const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader"); + +// Binary paths let userYtDlp; let ffmpegPath; let denoPath; @@ -32,14 +36,14 @@ if (config.localMode) { } -// Chemins des icônes de notification +// Notification icon paths const iconPaths = { confirm: path.join(resourcesPath, "confirm-icon.png"), error: path.join(resourcesPath, "error.png"), app: path.join(resourcesPath, "app-icon.ico") }; -// Chemins des binaires pour vérification +// Binary paths for verification const binaryPaths = { ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.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 (!denoPath){ logger.error("Missing DENO")} -module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath }; +module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder }; diff --git a/server/helpers/validation.js b/server/helpers/validation.js index b6ede30..37f1a05 100644 --- a/server/helpers/validation.js +++ b/server/helpers/validation.js @@ -12,10 +12,35 @@ function isValidUrl(url) { } function isSafePath(folder) { - if (!folder || folder.length < 3) return false; - const unsafe = ["System32", "/etc", "\\Windows"]; - const resolved = path.resolve(folder); - return !unsafe.some(u => resolved.includes(u)); + if (!folder || typeof folder !== "string") return false; + + try { + // 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 }; diff --git a/server/services/download.services.js b/server/services/download.services.js index 789fbd7..8957a71 100644 --- a/server/services/download.services.js +++ b/server/services/download.services.js @@ -1,6 +1,5 @@ const { execFile } = require("child_process"); -const { userYtDlp } = require("../helpers/path"); -const path = require("path"); +const { userYtDlp, defaultDownloadFolder } = require("../helpers/path"); const fs = require("fs"); const { logger } = require("../logger"); const { buildYtDlpArgs } = require("../helpers/buildArgs"); @@ -9,8 +8,16 @@ const notify = require("../helpers/notify") function fetchDownload(options, listeners, speedListeners) { return new Promise((resolve, reject) => { - const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader"); - fs.mkdirSync(outputFolder, { recursive: true }); + const outputFolder = options.outputFolder || defaultDownloadFolder; + + // Create download folder if it doesn't exist + try { + 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 }); logger.info(`[yt-dlp args] ${args.join(" ")}`); @@ -31,7 +38,7 @@ function fetchDownload(options, listeners, speedListeners) { if (!line.trim()) return; logger.info(`[yt-dlp] ${line}`); - /* Barre de Chargement*/ + /* Progress Bar */ if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset")); const match = line.match(/\[download\]\s+(\d+\.\d+)%/); if (match) listeners.forEach(fn => fn(parseFloat(match[1])));