diff --git a/.gitignore b/.gitignore index 7e27ca2..e6b8a5a 100644 --- a/.gitignore +++ b/.gitignore @@ -86,6 +86,10 @@ typings/ site-history.json bookmarks.json bookmarks.backup.json +search-history.json + +# Portable user data folder +user-data/ # AppImage / SteamOS squashfs-root/ diff --git a/main.js b/main.js index 6600577..0247857 100644 --- a/main.js +++ b/main.js @@ -158,12 +158,13 @@ const gpuConfig = new GPUConfig(); const pluginManager = new PluginManager(); // Initialize portable data paths BEFORE app.ready (must be done early) -// This only affects Linux when NEBULA_PORTABLE=1 is set +// This enables portable mode on all platforms (Windows, macOS, Linux) +// Data is stored in 'user-data' folder within the application directory portableData.initialize(); /** * Get the path for a user data file (bookmarks, history, etc.) - * Uses portable path on Linux when in portable mode, otherwise uses __dirname + * Uses portable path when in portable mode, otherwise uses __dirname * @param {string} filename - The filename (e.g., 'bookmarks.json') * @returns {string} The full path to the file */ @@ -177,7 +178,7 @@ function getDataFilePath(filename) { /** * Get the directory path for user data files - * Uses portable path on Linux when in portable mode, otherwise uses __dirname + * Uses portable path when in portable mode, otherwise uses __dirname * @returns {string} The directory path */ function getDataDirPath() { @@ -968,7 +969,11 @@ ipcMain.handle('save-site-history', async (event, history) => { ipcMain.handle('clear-site-history', async () => { const filePath = getDataFilePath('site-history.json'); try { - await fs.promises.writeFile(filePath, JSON.stringify([], null, 2)); + if (portableData.isPortableMode()) { + await portableData.writeSecureFileAsync(filePath, JSON.stringify([], null, 2)); + } else { + await fs.promises.writeFile(filePath, JSON.stringify([], null, 2)); + } return true; } catch (err) { return false; @@ -1110,8 +1115,20 @@ ipcMain.handle('clear-browser-data', async () => { // Also reset on-disk history JSON files managed by the app const siteHistoryPath = getDataFilePath('site-history.json'); const searchHistoryPath = getDataFilePath('search-history.json'); - try { await fs.promises.writeFile(siteHistoryPath, JSON.stringify([], null, 2)); } catch {} - try { await fs.promises.writeFile(searchHistoryPath, JSON.stringify([], null, 2)); } catch {} + try { + if (portableData.isPortableMode()) { + await portableData.writeSecureFileAsync(siteHistoryPath, JSON.stringify([], null, 2)); + } else { + await fs.promises.writeFile(siteHistoryPath, JSON.stringify([], null, 2)); + } + } catch {} + try { + if (portableData.isPortableMode()) { + await portableData.writeSecureFileAsync(searchHistoryPath, JSON.stringify([], null, 2)); + } else { + await fs.promises.writeFile(searchHistoryPath, JSON.stringify([], null, 2)); + } + } catch {} return true; // Indicate success } catch (error) { @@ -1124,7 +1141,11 @@ ipcMain.handle('clear-browser-data', async () => { ipcMain.handle('clear-search-history', async () => { const filePath = getDataFilePath('search-history.json'); try { - await fs.promises.writeFile(filePath, JSON.stringify([], null, 2)); + if (portableData.isPortableMode()) { + await portableData.writeSecureFileAsync(filePath, JSON.stringify([], null, 2)); + } else { + await fs.promises.writeFile(filePath, JSON.stringify([], null, 2)); + } return true; } catch (err) { return false; @@ -1183,7 +1204,7 @@ ipcMain.handle('open-tab-in-new-window', (event, url) => { }); ipcMain.handle('save-site-history-entry', async (event, url) => { - const filePath = path.join(__dirname, 'site-history.json'); + const filePath = getDataFilePath('site-history.json'); try { let data = []; try { @@ -1195,7 +1216,11 @@ ipcMain.handle('save-site-history-entry', async (event, url) => { // Add to beginning and clamp size data.unshift(url); if (data.length > 100) data = data.slice(0, 100); - await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2)); + if (portableData.isPortableMode()) { + await portableData.writeSecureFileAsync(filePath, JSON.stringify(data, null, 2)); + } else { + await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2)); + } return true; } catch (err) { console.error('[MAIN] Error saving site history entry:', err); diff --git a/package.json b/package.json index 962babb..f415f41 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,12 @@ "repo": "NebulaBrowser" } ], + "files": [ + "**/*", + "!user-data/**", + "!*.backup.json" + ], + "extraResources": [], "mac": { "category": "public.app-category.productivity", "icon": "assets/images/Logos/Nebula-Favicon.icns" diff --git a/portable-data.js b/portable-data.js index b0cf6c3..2bfa7ac 100644 --- a/portable-data.js +++ b/portable-data.js @@ -1,13 +1,15 @@ /** * Portable Data Manager for Nebula Browser * - * Handles portable user data storage on Linux when running from extracted AppImage - * or portable installations. Does not affect Windows or macOS behavior. + * Handles portable user data storage for all platforms (Windows, macOS, Linux). + * Data is stored in a 'user-data' folder within the application directory, + * keeping all user data local to the compiled project. * * Security considerations: - * - Data is stored with restricted permissions (0700 for directories, 0600 for files) + * - Data is stored with restricted permissions (0700 for directories, 0600 for files on Unix) * - Path validation prevents directory traversal attacks - * - Only enabled when explicitly set via NEBULA_PORTABLE environment variable + * - Portable mode is enabled by default on all platforms + * - Can be disabled via NEBULA_PORTABLE=0 environment variable */ const { app } = require('electron'); @@ -23,37 +25,67 @@ class PortableDataManager { } /** - * Check if we're running in portable mode on Linux - * Portable mode is determined by: - * 1. NEBULA_PORTABLE environment variable is set and truthy - * 2. NEBULA_PORTABLE_PATH environment variable provides the data path - * 3. Platform must be Linux (does not affect Windows or macOS) + * Get the application's root directory + * Works for both development and packaged builds + */ + _getAppRootDir() { + // In packaged app, process.resourcesPath points to resources folder + // We want the parent directory (the app folder itself) + if (app.isPackaged) { + // For packaged apps: + // - Windows: path\to\app\resources -> path\to\app + // - macOS: path/to/App.app/Contents/Resources -> path/to/App.app/Contents + // - Linux: path/to/app/resources -> path/to/app + const resourcesPath = process.resourcesPath; + + if (process.platform === 'darwin') { + // On macOS, go up two levels from Resources to get to App.app parent folder + // But we want to store data inside the .app bundle's Contents folder for portability + return path.dirname(resourcesPath); // Contents folder + } else { + // Windows and Linux: go up one level from resources + return path.dirname(resourcesPath); + } + } else { + // Development mode: use __dirname (the directory containing main.js) + return __dirname; + } + } + + /** + * Check if we're running in portable mode + * Portable mode is enabled by default on all platforms. + * + * Can be disabled by setting NEBULA_PORTABLE=0 or NEBULA_PORTABLE=false + * Can specify custom path via NEBULA_PORTABLE_PATH environment variable */ isPortableMode() { if (this._isPortable !== null) { return this._isPortable; } - // Only portable mode on Linux - if (process.platform !== 'linux') { - this._isPortable = false; - return false; + // Check if NEBULA_PORTABLE is explicitly set to disable + const portableEnv = process.env.NEBULA_PORTABLE; + if (portableEnv !== undefined) { + const isDisabled = portableEnv === '0' || + portableEnv.toLowerCase() === 'false' || + portableEnv.toLowerCase() === 'no'; + + if (isDisabled) { + this._isPortable = false; + console.log('[Portable] Portable mode disabled via NEBULA_PORTABLE environment variable'); + return false; + } } - // Check if NEBULA_PORTABLE is set and truthy - const portableEnv = process.env.NEBULA_PORTABLE; - const isTruthy = portableEnv && - (portableEnv === '1' || - portableEnv.toLowerCase() === 'true' || - portableEnv.toLowerCase() === 'yes'); - - this._isPortable = isTruthy; + // Portable mode is enabled by default on all platforms + this._isPortable = true; return this._isPortable; } /** * Get the portable data directory path - * Uses NEBULA_PORTABLE_PATH if set, otherwise returns null + * Uses NEBULA_PORTABLE_PATH if set, otherwise creates 'user-data' folder in app directory */ getPortableDataPath() { if (this._portableDataPath !== null) { @@ -65,21 +97,29 @@ class PortableDataManager { return ''; } - const portablePath = process.env.NEBULA_PORTABLE_PATH; - if (!portablePath) { - console.warn('[Portable] NEBULA_PORTABLE is set but NEBULA_PORTABLE_PATH is missing'); - this._portableDataPath = ''; - return ''; + // First, check if custom path is provided via environment variable + const customPath = process.env.NEBULA_PORTABLE_PATH; + if (customPath) { + const resolvedPath = path.resolve(customPath); + if (this._isPathSafe(resolvedPath)) { + this._portableDataPath = resolvedPath; + console.log(`[Portable] Using custom portable path: ${resolvedPath}`); + return this._portableDataPath; + } else { + console.warn('[Portable] Custom path is unsafe, using default location'); + } } - // Validate and resolve the path - const resolvedPath = path.resolve(portablePath); + // Default: create 'user-data' folder in the application directory + const appRoot = this._getAppRootDir(); + const dataPath = path.join(appRoot, 'user-data'); - // Security: ensure path doesn't contain suspicious patterns - if (this._isPathSafe(resolvedPath)) { - this._portableDataPath = resolvedPath; + // Validate the path + if (this._isPathSafe(dataPath)) { + this._portableDataPath = dataPath; + console.log(`[Portable] Using portable data path: ${dataPath}`); } else { - console.error('[Portable] Unsafe path detected, falling back to default'); + console.error('[Portable] Default path is unsafe, falling back to system default'); this._portableDataPath = ''; } @@ -113,7 +153,8 @@ class PortableDataManager { this._ensureSecureDirectory(dataPath); // Create subdirectories for organized storage - const subdirs = ['Cache', 'Cookies', 'Local Storage', 'Session Storage', 'IndexedDB']; + // Note: Don't create 'Cache', 'Cookies', 'Network' - Electron manages these internally + const subdirs = ['Local Storage', 'Session Storage', 'IndexedDB']; for (const subdir of subdirs) { this._ensureSecureDirectory(path.join(dataPath, subdir)); } @@ -163,28 +204,40 @@ class PortableDataManager { /** * Ensure a directory exists with secure permissions + * On Unix systems (macOS, Linux), applies restricted permissions (0700) + * On Windows, creates directory with default permissions (ACLs handle security) */ _ensureSecureDirectory(dirPath) { if (!fs.existsSync(dirPath)) { - // Create with restricted permissions (owner only: rwx------) - fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 }); + if (process.platform === 'win32') { + // Windows: create directory with default permissions + // Windows ACLs handle security through inheritance + fs.mkdirSync(dirPath, { recursive: true }); + } else { + // Unix (macOS, Linux): create with restricted permissions (owner only: rwx------) + fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 }); + } console.log(`[Portable] Created secure directory: ${dirPath}`); } else { - // Verify and fix permissions on existing directory - try { - const stats = fs.statSync(dirPath); - if (stats.isDirectory()) { - // Set secure permissions - fs.chmodSync(dirPath, 0o700); + // Verify and fix permissions on existing directory (Unix only) + if (process.platform !== 'win32') { + try { + const stats = fs.statSync(dirPath); + if (stats.isDirectory()) { + // Set secure permissions + fs.chmodSync(dirPath, 0o700); + } + } catch (err) { + console.warn(`[Portable] Could not verify permissions for ${dirPath}:`, err.message); } - } catch (err) { - console.warn(`[Portable] Could not verify permissions for ${dirPath}:`, err.message); } } } /** * Write a file with secure permissions + * On Unix systems, applies restricted permissions (0600) + * On Windows, writes with default permissions */ writeSecureFile(filePath, data) { // Ensure parent directory exists with secure permissions @@ -192,41 +245,81 @@ class PortableDataManager { this._ensureSecureDirectory(dir); // Write file - fs.writeFileSync(filePath, data, { mode: 0o600 }); + if (process.platform === 'win32') { + fs.writeFileSync(filePath, data); + } else { + fs.writeFileSync(filePath, data, { mode: 0o600 }); + } } /** * Async version of secure file write + * On Unix systems, applies restricted permissions (0600) + * On Windows, writes with default permissions */ async writeSecureFileAsync(filePath, data) { // Ensure parent directory exists with secure permissions const dir = path.dirname(filePath); this._ensureSecureDirectory(dir); - // Write file with restricted permissions (owner only: rw-------) - await fs.promises.writeFile(filePath, data, { mode: 0o600 }); + // Write file with restricted permissions (owner only: rw------- on Unix) + if (process.platform === 'win32') { + await fs.promises.writeFile(filePath, data); + } else { + await fs.promises.writeFile(filePath, data, { mode: 0o600 }); + } } /** * Validate path safety (prevent directory traversal) + * Works across Windows, macOS, and Linux */ _isPathSafe(testPath) { // Resolve to absolute path const resolved = path.resolve(testPath); - // Check for suspicious patterns - const dangerous = ['..', '~root', '/etc', '/var/run', '/proc', '/sys', '/dev']; - for (const pattern of dangerous) { - if (resolved.includes(pattern)) { - return false; - } + // Check for directory traversal patterns + if (resolved.includes('..')) { + return false; } - // Ensure it's not trying to write to system directories - const systemPaths = ['/bin', '/sbin', '/usr/bin', '/usr/sbin', '/boot', '/lib', '/lib64']; - for (const sysPath of systemPaths) { - if (resolved.startsWith(sysPath)) { - return false; + // Platform-specific system path checks + if (process.platform === 'win32') { + // Windows: block system directories + const dangerousWin = [ + 'C:\\Windows', + 'C:\\Program Files', + 'C:\\Program Files (x86)', + 'C:\\ProgramData' + ]; + const resolvedLower = resolved.toLowerCase(); + for (const pattern of dangerousWin) { + if (resolvedLower.startsWith(pattern.toLowerCase())) { + return false; + } + } + } else if (process.platform === 'darwin') { + // macOS: block system directories + const dangerousMac = ['/System', '/Library', '/usr', '/bin', '/sbin', '/etc', '/var']; + for (const pattern of dangerousMac) { + if (resolved.startsWith(pattern) && !resolved.includes('.app')) { + return false; + } + } + } else { + // Linux: block system directories + const dangerousLinux = ['~root', '/etc', '/var/run', '/proc', '/sys', '/dev']; + for (const pattern of dangerousLinux) { + if (resolved.includes(pattern)) { + return false; + } + } + + const systemPaths = ['/bin', '/sbin', '/usr/bin', '/usr/sbin', '/boot', '/lib', '/lib64']; + for (const sysPath of systemPaths) { + if (resolved.startsWith(sysPath)) { + return false; + } } } @@ -257,8 +350,10 @@ class PortableDataManager { return { isPortable: this.isPortableMode(), portablePath: this.getPortableDataPath(), + appRootDir: this._getAppRootDir(), initialized: this._initialized, platform: process.platform, + isPackaged: app.isPackaged, envPortable: process.env.NEBULA_PORTABLE, envPath: process.env.NEBULA_PORTABLE_PATH }; diff --git a/renderer/home.html b/renderer/home.html index 0104eaf..5e7e56f 100644 --- a/renderer/home.html +++ b/renderer/home.html @@ -32,7 +32,7 @@ Google
- Bing + Bing
DuckDuckGo