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);
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)
:::