Skip to content

Feature: Hosts File Editor #3012

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Source/GlobalAssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

[assembly: AssemblyVersion("2025.1.18.0")]
[assembly: AssemblyFileVersion("2025.1.18.0")]
[assembly: AssemblyVersion("2025.3.16.0")]
[assembly: AssemblyFileVersion("2025.3.16.0")]
36 changes: 36 additions & 0 deletions Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Source/NETworkManager.Localization/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -3879,4 +3879,16 @@ Right-click for more options.</value>
<data name="ProfileFile" xml:space="preserve">
<value>Profile file</value>
</data>
<data name="ApplicationName_HostsFileEditor" xml:space="preserve">
<value>Hosts File Editor</value>
</data>
<data name="HostsFileEditor" xml:space="preserve">
<value>Hosts File Editor</value>
</data>
<data name="HostsFileEditorAdminMessage" xml:space="preserve">
<value>To edit the hosts file, the application must be started with elevated rights!</value>
</data>
<data name="Comment" xml:space="preserve">
<value>Comment</value>
</data>
</root>
10 changes: 5 additions & 5 deletions Source/NETworkManager.Models/AWS/AWSProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ public static class AWSProfile
{
public static List<AWSProfileInfo> GetDefaultList()
{
return new List<AWSProfileInfo>
{
new(false, "default", "eu-central-1"),
new(false, "default", "us-east-1")
};
return
[
new AWSProfileInfo(false, "default", "eu-central-1"),
new AWSProfileInfo(false, "default", "us-east-1")
];
}
}
4 changes: 2 additions & 2 deletions Source/NETworkManager.Models/AWS/AWSProfileInfo.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace NETworkManager.Models.AWS;

/// <summary>
/// Class is used to store informations about an AWS profile.
/// Class is used to store information about an AWS profile.
/// </summary>
public class AWSProfileInfo
{
Expand All @@ -15,7 +15,7 @@ public AWSProfileInfo()
/// <summary>
/// Create an instance of <see cref="AWSProfileInfo" /> with parameters.
/// </summary>
/// <param name="IsEnabled"><see cref="IsEnabled" />.</param>
/// <param name="isEnabled"><see cref="IsEnabled" />.</param>
/// <param name="profile"><see cref="Profile" />.</param>
/// <param name="region"><see cref="Region" />.</param>
public AWSProfileInfo(bool isEnabled, string profile, string region)
Expand Down
7 changes: 5 additions & 2 deletions Source/NETworkManager.Models/ApplicationManager.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using MahApps.Metro.IconPacks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using MahApps.Metro.IconPacks;

namespace NETworkManager.Models;

Expand Down Expand Up @@ -92,6 +92,9 @@ public static Canvas GetIcon(ApplicationName name)
case ApplicationName.SNTPLookup:
canvas.Children.Add(new PackIconMaterial { Kind = PackIconMaterialKind.ClockCheckOutline });
break;
case ApplicationName.HostsFileEditor:
canvas.Children.Add(new PackIconMaterial { Kind = PackIconMaterialKind.FileEditOutline });
break;
case ApplicationName.DiscoveryProtocol:
canvas.Children.Add(new PackIconMaterial { Kind = PackIconMaterialKind.SwapHorizontal });
break;
Expand Down
5 changes: 5 additions & 0 deletions Source/NETworkManager.Models/ApplicationName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ public enum ApplicationName
/// </summary>
SNTPLookup,

/// <summary>
/// Hosts file editor application.
/// </summary>
HostsFileEditor,

/// <summary>
/// Discovery protocol application.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using NETworkManager.Models.HostsFileEditor;
using Newtonsoft.Json;

namespace NETworkManager.Models.Export;

public static partial class ExportManager
{
/// <summary>
/// Method to export objects from type <see cref="HostsFileEntry" /> to a file.
/// </summary>
/// <param name="filePath">Path to the export file.</param>
/// <param name="fileType">Allowed <see cref="ExportFileType" /> are CSV, XML or JSON.</param>
/// <param name="collection">Objects as <see cref="IReadOnlyList{HostsFileEntry}" /> to export.</param>
public static void Export(string filePath, ExportFileType fileType, IReadOnlyList<HostsFileEntry> collection)
{
switch (fileType)
{
case ExportFileType.Csv:
CreateCsv(collection, filePath);
break;
case ExportFileType.Xml:
CreateXml(collection, filePath);
break;
case ExportFileType.Json:
CreateJson(collection, filePath);
break;
case ExportFileType.Txt:
default:
throw new ArgumentOutOfRangeException(nameof(fileType), fileType, null);
}
}

/// <summary>
/// Creates a CSV file from the given <see cref="HostsFileEntry" /> collection.
/// </summary>
/// <param name="collection">Objects as <see cref="IReadOnlyList{HostsFileEntry}" /> to export.</param>
/// <param name="filePath">Path to the export file.</param>
private static void CreateCsv(IEnumerable<HostsFileEntry> collection, string filePath)
{
var stringBuilder = new StringBuilder();

stringBuilder.AppendLine(
$"{nameof(HostsFileEntry.IsEnabled)},{nameof(HostsFileEntry.IPAddress)},{nameof(HostsFileEntry.Hostname)},{nameof(HostsFileEntry.Comment)}");

foreach (var info in collection)
stringBuilder.AppendLine($"{info.IsEnabled},{info.IPAddress},{info.Hostname},{info.Comment}");

File.WriteAllText(filePath, stringBuilder.ToString());
}

/// <summary>
/// Creates a XML file from the given <see cref="HostsFileEntry" /> collection.
/// </summary>
/// <param name="collection">Objects as <see cref="IReadOnlyList{HostsFileEntry}" /> to export.</param>
/// <param name="filePath">Path to the export file.</param>
private static void CreateXml(IEnumerable<HostsFileEntry> collection, string filePath)
{
var document = new XDocument(DefaultXDeclaration,
new XElement(ApplicationName.HostsFileEditor.ToString(),
new XElement(nameof(HostsFileEntry) + "s",
from info in collection
select
new XElement(nameof(HostsFileEntry),
new XElement(nameof(HostsFileEntry.IsEnabled), info.IsEnabled),
new XElement(nameof(HostsFileEntry.IPAddress), info.IPAddress),
new XElement(nameof(HostsFileEntry.Hostname), info.Hostname),
new XElement(nameof(HostsFileEntry.Comment), info.Comment)))));

document.Save(filePath);
}

/// <summary>
/// Creates a JSON file from the given <see cref="HostsFileEntry" /> collection.
/// </summary>
/// <param name="collection">Objects as <see cref="IReadOnlyList{HostsFileEntry}" /> to export.</param>
/// <param name="filePath">Path to the export file.</param>
private static void CreateJson(IReadOnlyList<HostsFileEntry> collection, string filePath)
{
var jsonData = new object[collection.Count];

for (var i = 0; i < collection.Count; i++)
jsonData[i] = new
{
collection[i].IsEnabled,
collection[i].IPAddress,
collection[i].Hostname,
collection[i].Comment
};

File.WriteAllText(filePath, JsonConvert.SerializeObject(jsonData, Formatting.Indented));
}
}
135 changes: 135 additions & 0 deletions Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using log4net;
using NETworkManager.Utilities;

namespace NETworkManager.Models.HostsFileEditor;

public static class HostsFileEditor
{
#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;

/// <summary>
/// Path to the hosts file.
/// </summary>
private static string HostsFilePath => Path.Combine(Environment.SystemDirectory, "drivers", "etc", "hosts");

/// <summary>
/// Example values in the hosts file that should be ignored.
/// </summary>
private static readonly HashSet<(string IPAddress, string Hostname)> ExampleValuesToIgnore =
[
("102.54.94.97", "rhino.acme.com"),
("38.25.63.10", "x.acme.com")
];

/// <summary>
/// Regex to match a hosts file entry with optional comments, supporting IPv4, IPv6, and hostnames
/// </summary>
private static readonly Regex HostsFileEntryRegex = new(RegexHelper.HostsEntryRegex);

#endregion

#region Constructor

static HostsFileEditor()
{
// Create a file system watcher to monitor changes to the hosts file
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.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)
{
Log.Error("Failed to create file system watcher for hosts file.", ex);
}
}
#endregion

#region Methods
public static Task<IEnumerable<HostsFileEntry>> GetHostsFileEntriesAsync()
{
return Task.Run(GetHostsFileEntries);
}

/// <summary>
///
/// </summary>
/// <returns></returns>
private static IEnumerable<HostsFileEntry> GetHostsFileEntries()
{
var hostsFileLines = File.ReadAllLines(HostsFilePath);

// Parse the hosts file content
var entries = new List<HostsFileEntry>();

foreach (var line in hostsFileLines)
{
var result = HostsFileEntryRegex.Match(line.Trim());

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('#',' '),
Line = line
};

// Skip example entries
if(!entry.IsEnabled)
{
if (ExampleValuesToIgnore.Contains((entry.IPAddress, entry.Hostname)))
{
Log.Debug("GetHostsFileEntries - Matched example entry. Skipping...");
continue;
}
}

entries.Add(entry);
}
else
{
Log.Debug("GetHostsFileEntries - Line not matched: " + line);
}
}

return entries;
}
#endregion
}
Loading