diff --git a/ZuneModCore/Mods/WebservicesMod.cs b/ZuneModCore/Mods/WebservicesMod.cs index c49b411..20da00d 100644 --- a/ZuneModCore/Mods/WebservicesMod.cs +++ b/ZuneModCore/Mods/WebservicesMod.cs @@ -10,10 +10,10 @@ using System.Net; using System.Net.Http; using System.Reflection.PortableExecutable; using System.Text; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using ZuneModCore.Services; +using ZuneModCore.Win32; using static ZuneModCore.Mods.FeaturesOverrideMod; namespace ZuneModCore.Mods; @@ -146,6 +146,18 @@ public partial class WebservicesMod(ModMetadata metadata, IModCoreConfig modConf await RecordAppliedMethod(ApplicationMethod.BinaryPatch); } + // Also set up the metaservices hosts + var ipAddress = IPAddress.Parse("135.181.88.32"); + IEnumerable hostnames = [ + "toc.music.metaservices.microsoft.com", + "info.music.metaservices.microsoft.com", + "images.metaservices.microsoft.com", + "redir.metaservices.microsoft.com", + "windowsmedia.com", + ]; + var newEntries = hostnames.Select(h => new HostsEntry(ipAddress, h)); + await HostsEditor.AddOrUpdateHostnamesWithIPAsync(newEntries); + return errorMessage; } @@ -303,57 +315,36 @@ public partial class WebservicesMod(ModMetadata metadata, IModCoreConfig modConf return null; } - private static async Task AddEntriesToHostsFile(string newHost) + private async Task AddEntriesToHostsFile(string newHost) { - var hostEntry = await Dns.GetHostEntryAsync(newHost); - var ipAddress = hostEntry?.AddressList?.FirstOrDefault(); - if (ipAddress is null) + List newEntries = new(_knownSubdomains.Length); + + foreach (var subdomain in _knownSubdomains.Select(e => e.subdomain)) { - return $"Failed to resolve host '{newHost}' to an IP address."; + var newHostname = GetDomainName(subdomain, newHost); + + IPAddress? ipAddress; + try + { + var dnsEntry = await Dns.GetHostEntryAsync(newHostname); + ipAddress = dnsEntry?.AddressList?.FirstOrDefault(); + if (ipAddress is null) + { + log.LogError("Failed to resolve host '{hostname}' to an IP address.", newHostname); + continue; + } + } + catch (Exception ex) + { + log.Log(LogLevel.Error, ex, "Failed to resolve host '{hostname}' to an IP address.", newHostname); + continue; + } + + var oldHostname = GetDomainName(subdomain, "zune.net"); + newEntries.Add(new(ipAddress, oldHostname)); } - var hostsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), @"drivers\etc\hosts"); - var hostLines = (await File.ReadAllLinesAsync(hostsPath)).ToList(); - - var domains = _knownSubdomains - .Select(e => GetDomainName(e.subdomain, "zune.net")) - .ToHashSet(); - - // Update existing entries - Regex rx = HostsEntryRegex(); - for (int l = 0; l < hostLines.Count; l++) - { - var line = hostLines[l].Trim(); - - // Ignore comments and empty lines - if (line.Length <= 0 || line[0] is '#') - continue; - - var match = rx.Match(line); - if (!match.Success) - continue; - - var domain = match.Groups["name"].Value; - - // Ignore entries unrelated to Zune - if (!domain.EndsWith("zune.net")) - continue; - - // Reconstruct entry with new IP address - hostLines[l] = $"{ipAddress} {domain}"; - - // Track that we handled this domain - domains.Remove(domain); - } - - // Add any entries that didn't already exist - if (domains.Count > 0) - { - foreach (var domain in domains) - hostLines.Add($"{ipAddress} {domain}"); - } - - await File.WriteAllLinesAsync(hostsPath, hostLines); + await HostsEditor.AddOrUpdateHostnamesWithIPAsync(newEntries); return null; } @@ -441,8 +432,5 @@ public partial class WebservicesMod(ModMetadata metadata, IModCoreConfig modConf return Enum.Parse(methodText); } - [GeneratedRegex(@"(?\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(?[\w.-]+?)(\s+|$)")] - private static partial Regex HostsEntryRegex(); - private enum ApplicationMethod { None, BinaryPatch, HostsEntries } } diff --git a/ZuneModCore/Win32/HostsEditor.cs b/ZuneModCore/Win32/HostsEditor.cs new file mode 100644 index 0000000..6ebde0c --- /dev/null +++ b/ZuneModCore/Win32/HostsEditor.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace ZuneModCore.Win32; + +public static partial class HostsEditor +{ + private static readonly string _hostsPath = Environment.ExpandEnvironmentVariables(@"%SystemRoot%\System32\drivers\etc\hosts"); + + [GeneratedRegex(@"(?\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(?[\w.-]+?)(\s+|$)")] + private static partial Regex HostsEntryRegex(); + + public static async Task AddOrUpdateHostnamesWithIPAsync(IEnumerable newEntries) + { + var unhandledEntries = newEntries + .ToDictionary(e => e.Hostname, e => e.IPAddress); + + var hostLines = await ReadHostsFileAsync(); + + // Update existing entries + Regex rx = HostsEntryRegex(); + for (int l = 0; l < hostLines.Count; l++) + { + var line = hostLines[l].Trim(); + + // Ignore comments and empty lines + if (line.Length <= 0 || line[0] is '#') + continue; + + var match = rx.Match(line); + if (!match.Success) + continue; + + var hostname = match.Groups["name"].Value; + var ip = match.Groups["ip"].Value; + + // Ignore entries we've already handled + if (!unhandledEntries.TryGetValue(hostname, out var newIP)) + continue; + + // Avoid changing lines when we don't have to. This + // preserves trivia such as whitespace and comments when possible + if (IPAddress.Parse(ip) == newIP) + continue; + + // Reconstruct entry with new IP address + hostLines[l] = $"{newIP} {hostname}"; + + // Track that we handled this domain + unhandledEntries.Remove(hostname); + } + + // Add any entries that didn't already exist + if (unhandledEntries.Count > 0) + { + foreach (var (hostname, ip) in unhandledEntries) + hostLines.Add($"{ip} {hostname}"); + } + + await WriteHostsFileAsync(hostLines); + } + + private static async Task> ReadHostsFileAsync() + { + var lines = await File.ReadAllLinesAsync(_hostsPath); + return lines.ToList(); + } + + private static async Task WriteHostsFileAsync(List lines) + { + await File.WriteAllLinesAsync(_hostsPath, lines); + } +} + +public class HostsEntry(IPAddress ipAddress, string hostname, string? originalString = null) + : IEquatable +{ + private IPAddress _ipAddress = ipAddress; + private string _hostname = hostname; + private string? _originalString = originalString; + + public IPAddress IPAddress + { + get => _ipAddress; + set + { + _originalString = null; + _ipAddress = value; + } + } + + public string Hostname + { + get => _hostname; + set + { + _originalString = null; + _hostname = value; + } + } + + public bool Equals(HostsEntry? other) + { + if (other is null) + return false; + + return IPAddress.Equals(other.IPAddress) + && Hostname.Equals(other.Hostname, StringComparison.InvariantCulture); + } + + public override bool Equals(object obj) => Equals(obj as HostsEntry); + + public override string ToString() => _originalString ?? $"{IPAddress} {Hostname}"; + + public override int GetHashCode() => (IPAddress, Hostname).GetHashCode(); +}