Skip to content
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

Feature: Hosts File Editor #3012

Open
wants to merge 3 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
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
}
69 changes: 69 additions & 0 deletions Source/NETworkManager.Models/HostsFileEditor/HostsFileEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace NETworkManager.Models.HostsFileEditor;

/// <summary>
/// Class that represents a single entry in the hosts file.
/// </summary>
public class HostsFileEntry
{
/// <summary>
/// Indicates whether the entry is enabled or not.
/// </summary>
public bool IsEnabled { get; init; }

/// <summary>
/// IP address of the host.
/// </summary>
public string IPAddress { get; init; }

/// <summary>
/// Host name(s) of the host. Multiple host names are separated by a
/// space (equal to the hosts file format).
/// </summary>
public string Hostname { get; init; }

/// <summary>
/// Comment of the host.
/// </summary>
public string Comment { get; init; }

/// <summary>
/// Line of the entry in the hosts file.
/// </summary>
public string Line { get; init; }

/// <summary>
/// Creates a new instance of <see cref="HostsFileEntry" />.
/// </summary>
public HostsFileEntry()
{

}

/// <summary>
/// Creates a new instance of <see cref="HostsFileEntry" /> with parameters.
/// </summary>
/// <param name="isEnabled">Indicates whether the entry is enabled or not.</param>
/// <param name="ipAddress">IP address of the host.</param>
/// <param name="hostname">Host name(s) of the host.</param>
/// <param name="comment">Comment of the host.</param>
public HostsFileEntry(bool isEnabled, string ipAddress, string hostname, string comment)
{
IsEnabled = isEnabled;
IPAddress = ipAddress;
Hostname = hostname;
Comment = comment;
}

/// <summary>
/// Creates a new instance of <see cref="HostsFileEntry" /> with parameters.
/// </summary>
/// <param name="isEnabled">Indicates whether the entry is enabled or not.</param>
/// <param name="ipAddress">IP address of the host.</param>
/// <param name="hostname">Host name(s) of the host.</param>
/// <param name="comment">Comment of the host.</param>
/// <param name="line">Line of the entry in the hosts file.</param>
public HostsFileEntry(bool isEnabled, string ipAddress, string hostname, string comment, string line) : this(isEnabled, ipAddress, hostname, comment)
{
Line = line;
}
}
2 changes: 1 addition & 1 deletion Source/NETworkManager.Models/Lookup/OUILookup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static class OUILookup
/// </summary>
static OUILookup()
{
OUIInfoList = new List<OUIInfo>();
OUIInfoList = [];

var document = new XmlDocument();
document.Load(OuiFilePath);
Expand Down
2 changes: 1 addition & 1 deletion Source/NETworkManager.Models/Lookup/PortLookup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static class PortLookup
/// </summary>
static PortLookup()
{
PortList = new List<PortLookupInfo>();
PortList = [];

var document = new XmlDocument();
document.Load(PortsFilePath);
Expand Down
Loading