From 6be5a35c4e52b7e9e5498a5e63acdf5ad2da149d Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 30 Jun 2025 00:41:02 +0200 Subject: [PATCH 1/2] Feature: Enable / disable entries --- .../HostsFileEditor/HostsFileEditor.cs | 227 ++++++++++++++++-- .../ViewModels/HostsFileEditorViewModel.cs | 4 +- 2 files changed, 205 insertions(+), 26 deletions(-) diff --git a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs index 7f487aa89c..a692908df7 100644 --- a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs +++ b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs @@ -1,34 +1,58 @@ -using System; +using log4net; +using NETworkManager.Utilities; +using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using log4net; -using NETworkManager.Utilities; namespace NETworkManager.Models.HostsFileEditor; public static class HostsFileEditor { -#region Events + #region Events + public static event EventHandler HostsFileChanged; - + private static void OnHostsFileChanged() { Log.Debug("OnHostsFileChanged - Hosts file changed."); HostsFileChanged?.Invoke(null, EventArgs.Empty); } + #endregion - + #region Variables + private static readonly ILog Log = LogManager.GetLogger(typeof(HostsFileEditor)); private static readonly FileSystemWatcher HostsFileWatcher; - + + /// + /// Path to the hosts folder. + /// + private static string HostsFolderPath => Path.Combine(Environment.SystemDirectory, "drivers", "etc"); + /// /// Path to the hosts file. /// - private static string HostsFilePath => Path.Combine(Environment.SystemDirectory, "drivers", "etc", "hosts"); + private static string HostsFilePath => Path.Combine(HostsFolderPath, "hosts"); + + /// + /// Identifier for the hosts file backup. + /// + private static string HostsFileBackupIdentifier => "hosts_backup_NETworkManager"; + + /// + /// Number of backups to keep. + /// + private static int HostsFileBackupsToKeep => 5; + + /// + /// Last time a backup was created. + /// + private static DateTime _lastBackupTime = DateTime.MinValue; /// /// Example values in the hosts file that should be ignored. @@ -38,14 +62,14 @@ private static void OnHostsFileChanged() ("102.54.94.97", "rhino.acme.com"), ("38.25.63.10", "x.acme.com") ]; - + /// /// Regex to match a hosts file entry with optional comments, supporting IPv4, IPv6, and hostnames /// private static readonly Regex HostsFileEntryRegex = new(RegexHelper.HostsEntryRegex); #endregion - + #region Constructor static HostsFileEditor() @@ -54,20 +78,22 @@ static HostsFileEditor() try { Log.Debug("HostsFileEditor - Creating file system watcher for hosts file..."); - + // Create the file system watcher HostsFileWatcher = new FileSystemWatcher(); - HostsFileWatcher.Path = Path.GetDirectoryName(HostsFilePath) ?? throw new InvalidOperationException("Hosts file path is invalid."); - HostsFileWatcher.Filter = Path.GetFileName(HostsFilePath) ?? throw new InvalidOperationException("Hosts file name is invalid."); + HostsFileWatcher.Path = Path.GetDirectoryName(HostsFilePath) ?? + throw new InvalidOperationException("Hosts file path is invalid."); + HostsFileWatcher.Filter = Path.GetFileName(HostsFilePath) ?? + throw new InvalidOperationException("Hosts file name is invalid."); HostsFileWatcher.NotifyFilter = NotifyFilters.LastWrite; - + // Maybe fired twice. This is a known bug/feature. // See: https://stackoverflow.com/questions/1764809/filesystemwatcher-changed-event-is-raised-twice HostsFileWatcher.Changed += (_, _) => OnHostsFileChanged(); - + // Enable the file system watcher HostsFileWatcher.EnableRaisingEvents = true; - + Log.Debug("HostsFileEditor - File system watcher for hosts file created."); } catch (Exception ex) @@ -75,18 +101,24 @@ static HostsFileEditor() Log.Error("Failed to create file system watcher for hosts file.", ex); } } + #endregion #region Methods + + /// + /// Gets the entries from the hosts file asynchronously. + /// + /// A task that represents the asynchronous operation, containing all entries from the hosts file. public static Task> GetHostsFileEntriesAsync() { return Task.Run(GetHostsFileEntries); } - + /// - /// + /// Gets the entries from the hosts file. /// - /// + /// All entries from the hosts file. private static IEnumerable GetHostsFileEntries() { var hostsFileLines = File.ReadAllLines(HostsFilePath); @@ -101,18 +133,18 @@ private static IEnumerable GetHostsFileEntries() if (result.Success) { Log.Debug("GetHostsFileEntries - Line matched: " + line); - + var entry = new HostsFileEntry { IsEnabled = !result.Groups[1].Value.Equals("#"), IPAddress = result.Groups[2].Value, Hostname = result.Groups[3].Value.Replace(@"\s", "").Trim(), - Comment = result.Groups[4].Value.TrimStart('#',' '), + Comment = result.Groups[4].Value.TrimStart('#', ' '), Line = line }; - + // Skip example entries - if(!entry.IsEnabled) + if (!entry.IsEnabled) { if (ExampleValuesToIgnore.Contains((entry.IPAddress, entry.Hostname))) { @@ -120,7 +152,7 @@ private static IEnumerable GetHostsFileEntries() continue; } } - + entries.Add(entry); } else @@ -131,5 +163,152 @@ private static IEnumerable GetHostsFileEntries() return entries; } + + public static Task EnableEntryAsync(HostsFileEntry entry) + { + return Task.Run(() => EnableEntry(entry)); + } + + private static bool EnableEntry(HostsFileEntry entry) + { + // Create a backup of the hosts file before making changes + if (CreateBackup() == false) + { + Log.Error("EnableEntry - Failed to create backup before enabling entry."); + return false; + } + + // Replace the entry in the hosts file + var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList(); + + for (var i = 0; i < hostsFileLines.Count; i++) + { + if (hostsFileLines[i] == entry.Line) + hostsFileLines[i] = entry.Line.TrimStart('#', ' '); + } + + try + { + Log.Debug($"EnableEntry - Writing changes to hosts file: {HostsFilePath}"); + File.WriteAllLines(HostsFilePath, hostsFileLines); + } + catch (Exception ex) + { + Log.Error($"EnableEntry - Failed to write changes to hosts file: {HostsFilePath}", ex); + + return false; + } + + return true; + } + + public static Task DisableEntryAsync(HostsFileEntry entry) + { + return Task.Run(() => DisableEntry(entry)); + } + + private static bool DisableEntry(HostsFileEntry entry) + { + // Create a backup of the hosts file before making changes + if (CreateBackup() == false) + { + Log.Error("DisableEntry - Failed to create backup before disabling entry."); + return false; + } + + // Replace the entry in the hosts file + var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList(); + + for (var i = 0; i < hostsFileLines.Count; i++) + { + if (hostsFileLines[i] == entry.Line) + hostsFileLines[i] = "# " + entry.Line; + } + + try + { + Log.Debug($"DisableEntry - Writing changes to hosts file: {HostsFilePath}"); + File.WriteAllLines(HostsFilePath, hostsFileLines); + } + catch (Exception ex) + { + Log.Error($"DisableEntry - Failed to write changes to hosts file: {HostsFilePath}", ex); + + return false; + } + + return true; + } + + /// + /// Create a daily backup of the hosts file (before making a change). + /// + private static bool CreateBackup() + { + Log.Debug($"CreateBackup - Creating backup of hosts file: {HostsFilePath}"); + + var dateTimeNow = DateTime.Now; + + // Check if a daily backup has already been created today (in the current running instance) + if (_lastBackupTime.Date == dateTimeNow.Date) + { + Log.Debug("CreateBackup - Daily backup already created today. Skipping..."); + return true; + } + + // Get existing backup files + var backupFiles = Directory.GetFiles(HostsFolderPath, $"{HostsFileBackupIdentifier}_*") + .OrderByDescending(f => f).ToList(); + + Log.Debug($"CreateBackup - Found {backupFiles.Count} backup files in {HostsFolderPath}"); + + // Cleanup old backups if they exceed the limit + if (backupFiles.Count > HostsFileBackupsToKeep) + { + for (var i = HostsFileBackupsToKeep; i < backupFiles.Count; i++) + { + try + { + Log.Debug($"CreateBackup - Deleting old backup file: {backupFiles[i]}"); + File.Delete(backupFiles[i]); + } + catch (Exception ex) + { + Log.Error($"CreateBackup - Failed to delete old backup file: {backupFiles[i]}", ex); + } + } + } + + // Check if a daily backup already exists on disk (from previous instances) + var dailyBackupFound = backupFiles.Count > 0 && + backupFiles[0].Contains($"{HostsFileBackupIdentifier}_{dateTimeNow:yyyyMMdd}"); + + if (dailyBackupFound) + { + Log.Debug("CreateBackup - Daily backup already exists on disk. Skipping..."); + + _lastBackupTime = dateTimeNow; + + return true; + } + + // Create a new backup file with the current date + try + { + Log.Debug($"CreateBackup - Creating new backup file: {HostsFileBackupIdentifier}_{dateTimeNow:yyyyMMdd}"); + File.Copy(HostsFilePath, + Path.Combine(HostsFolderPath, $"{HostsFileBackupIdentifier}_{dateTimeNow:yyyyMMdd}")); + + _lastBackupTime = dateTimeNow; + } + catch (Exception ex) + { + Log.Error($"CreateBackup - Failed to create backup file: {HostsFilePath}", ex); + return false; + } + + return true; + } + #endregion } \ No newline at end of file diff --git a/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs b/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs index 4be7eae029..9baa98157f 100644 --- a/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs +++ b/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs @@ -253,14 +253,14 @@ await _dialogCoordinator.ShowMessageAsync(this, Strings.Error, private async Task EnableEntryAction() { - MessageBox.Show("Enable entry action is not implemented yet.", "Enable Entry", MessageBoxButton.OK, MessageBoxImage.Information); + await HostsFileEditor.EnableEntryAsync(SelectedResult); } public ICommand DisableEntryCommand => new RelayCommand(_ => DisableEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute); private async Task DisableEntryAction() { - MessageBox.Show("Disable entry action is not implemented yet.", "Disable Entry", MessageBoxButton.OK, MessageBoxImage.Information); + await HostsFileEditor.DisableEntryAsync(SelectedResult); } public ICommand AddEntryCommand => new RelayCommand(_ => AddEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute); From 631683b83c7b7a29f5dda77b4ba6800f9f6cc400 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 30 Jun 2025 00:59:47 +0200 Subject: [PATCH 2/2] Docs: #3092 --- Website/docs/application/hosts-file-editor.md | 6 ++++-- Website/docs/changelog/next-release.md | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Website/docs/application/hosts-file-editor.md b/Website/docs/application/hosts-file-editor.md index 6d952fe17b..39a3bd7c37 100644 --- a/Website/docs/application/hosts-file-editor.md +++ b/Website/docs/application/hosts-file-editor.md @@ -4,7 +4,9 @@ sidebar_position: 16 # Hosts File Editor -In the **Hosts File Editor**, you can view and modify the `hosts` file of the local computer. +The **Hosts File Editor** allows you to view, add, edit, enable, disable, or remove entries in the local computer's `hosts` file. + +Editing the `hosts` file requires administrator privileges. The application automatically creates a daily backup of the `hosts` file, retaining up to 5 backups in the same directory (Syntax: `hosts_backup_NETworkManager_YYYYMMDD`). :::info @@ -34,6 +36,6 @@ In addition, further actions can be performed using the buttons below: With `F5` you can refresh the hosts file. -Right-click on the result to enabl e or disable an entry, delete or edit an entry, or to copy or export the information. +Right-click on the result to enable, disable, edit or delete an entry, or to copy or export the information. ::: diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 6082c27913..102ec081a5 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -23,11 +23,11 @@ Release date: **xx.xx.2025** **Hosts File Editor** -- New feature to display (and edit) the `hosts` file. (See [documentation](https://borntoberoot.net/NETworkManager/docs/application/hosts-file-editor) for more details) +- New feature to display (and edit) the `hosts` file. (See [documentation](https://borntoberoot.net/NETworkManager/docs/application/hosts-file-editor) for more details) [#3012](https://github.com/BornToBeRoot/NETworkManager/pull/3012) [#3092](https://github.com/BornToBeRoot/NETworkManager/pull/3092) :::info - This feature is currently in read-only mode. Editing will be enabled / implemented in a future release. Please report any issues you encounter on the [GitHub issue tracker](https://github.com/BornToBeRoot/NETworkManager/issues) + This feature is currently in preview and may contain bugs or incomplete functionality. Please use with caution and report any issues you encounter on the [GitHub issue tracker](https://github.com/BornToBeRoot/NETworkManager/issues) :::