[Hosts]Handle read-only hosts file (#29562)

* handle read-only hosts file
This commit is contained in:
Davide Giacometti
2023-11-03 17:10:26 +01:00
committed by GitHub
parent d5b9c31847
commit 16e26a200e
8 changed files with 106 additions and 21 deletions

View File

@@ -5,6 +5,7 @@
using System.IO.Abstractions.TestingHelpers; using System.IO.Abstractions.TestingHelpers;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Hosts.Exceptions;
using Hosts.Helpers; using Hosts.Helpers;
using Hosts.Models; using Hosts.Models;
using Hosts.Settings; using Hosts.Settings;
@@ -18,11 +19,13 @@ namespace Hosts.Tests
[TestClass] [TestClass]
public class HostsServiceTest public class HostsServiceTest
{ {
private static Mock<IUserSettings> _userSettings;
private static Mock<IElevationHelper> _elevationHelper; private static Mock<IElevationHelper> _elevationHelper;
[ClassInitialize] [ClassInitialize]
public static void ClassInitialize(TestContext context) public static void ClassInitialize(TestContext context)
{ {
_userSettings = new Mock<IUserSettings>();
_elevationHelper = new Mock<IElevationHelper>(); _elevationHelper = new Mock<IElevationHelper>();
_elevationHelper.Setup(m => m.IsElevated).Returns(true); _elevationHelper.Setup(m => m.IsElevated).Returns(true);
} }
@@ -31,8 +34,7 @@ namespace Hosts.Tests
public void Hosts_Exists() public void Hosts_Exists()
{ {
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>(); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
var result = service.Exists(); var result = service.Exists();
@@ -43,8 +45,7 @@ namespace Hosts.Tests
public void Hosts_Not_Exists() public void Hosts_Not_Exists()
{ {
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>(); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
var result = service.Exists(); var result = service.Exists();
Assert.IsFalse(result); Assert.IsFalse(result);
@@ -65,8 +66,7 @@ namespace Hosts.Tests
"; ";
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>(); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync(); var data = await service.ReadAsync();
@@ -91,8 +91,7 @@ namespace Hosts.Tests
"; ";
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>(); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync(); var data = await service.ReadAsync();
@@ -118,8 +117,7 @@ namespace Hosts.Tests
"; ";
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>(); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync(); var data = await service.ReadAsync();
@@ -138,9 +136,7 @@ namespace Hosts.Tests
public async Task Empty_Hosts() public async Task Empty_Hosts()
{ {
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>(); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
await service.WriteAsync(string.Empty, Enumerable.Empty<Entry>()); await service.WriteAsync(string.Empty, Enumerable.Empty<Entry>());
@@ -203,7 +199,6 @@ namespace Hosts.Tests
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>(); var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Bottom); userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Bottom);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
@@ -228,8 +223,7 @@ namespace Hosts.Tests
"; ";
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>(); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync(); var data = await service.ReadAsync();
@@ -243,12 +237,37 @@ namespace Hosts.Tests
public async Task Save_NotRunningElevatedException() public async Task Save_NotRunningElevatedException()
{ {
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>();
var elevationHelper = new Mock<IElevationHelper>(); var elevationHelper = new Mock<IElevationHelper>();
elevationHelper.Setup(m => m.IsElevated).Returns(false); elevationHelper.Setup(m => m.IsElevated).Returns(false);
var service = new HostsService(fileSystem, userSettings.Object, elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, elevationHelper.Object);
await Assert.ThrowsExceptionAsync<NotRunningElevatedException>(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty<Entry>())); await Assert.ThrowsExceptionAsync<NotRunningElevatedException>(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty<Entry>()));
} }
[TestMethod]
public async Task Save_ReadOnlyHostsException()
{
var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var hostsFile = new MockFileData(string.Empty);
hostsFile.Attributes = System.IO.FileAttributes.ReadOnly;
fileSystem.AddFile(service.HostsFilePath, hostsFile);
await Assert.ThrowsExceptionAsync<ReadOnlyHostsException>(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty<Entry>()));
}
[TestMethod]
public void Remove_ReadOnly()
{
var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var hostsFile = new MockFileData(string.Empty);
hostsFile.Attributes = System.IO.FileAttributes.ReadOnly;
fileSystem.AddFile(service.HostsFilePath, hostsFile);
service.RemoveReadOnly();
var readOnly = fileSystem.FileInfo.FromFileName(service.HostsFilePath).Attributes.HasFlag(System.IO.FileAttributes.ReadOnly);
Assert.IsFalse(readOnly);
}
} }
} }

View File

@@ -4,7 +4,7 @@
using System; using System;
namespace Hosts.Helpers namespace Hosts.Exceptions
{ {
public class NotRunningElevatedException : Exception public class NotRunningElevatedException : Exception
{ {

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Hosts.Exceptions
{
public class ReadOnlyHostsException : Exception
{
}
}

View File

@@ -13,6 +13,7 @@ using System.Net.NetworkInformation;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Hosts.Exceptions;
using Hosts.Models; using Hosts.Models;
using Hosts.Settings; using Hosts.Settings;
using ManagedCommon; using ManagedCommon;
@@ -129,6 +130,11 @@ namespace Hosts.Helpers
throw new NotRunningElevatedException(); throw new NotRunningElevatedException();
} }
if (_fileSystem.FileInfo.FromFileName(HostsFilePath).IsReadOnly)
{
throw new ReadOnlyHostsException();
}
var lines = new List<string>(); var lines = new List<string>();
if (entries.Any()) if (entries.Any())
@@ -288,6 +294,15 @@ namespace Hosts.Helpers
} }
} }
public void RemoveReadOnly()
{
var fileInfo = _fileSystem.FileInfo.FromFileName(HostsFilePath);
if (fileInfo.IsReadOnly)
{
fileInfo.IsReadOnly = false;
}
}
public void Dispose() public void Dispose()
{ {
Dispose(disposing: true); Dispose(disposing: true);

View File

@@ -24,5 +24,7 @@ namespace Hosts.Helpers
void CleanupBackup(); void CleanupBackup();
void OpenHostsFile(); void OpenHostsFile();
void RemoveReadOnly();
} }
} }

View File

@@ -377,7 +377,15 @@
IsOpen="{x:Bind ViewModel.Error, Mode=TwoWay}" IsOpen="{x:Bind ViewModel.Error, Mode=TwoWay}"
Message="{x:Bind ViewModel.ErrorMessage, Mode=TwoWay}" Message="{x:Bind ViewModel.ErrorMessage, Mode=TwoWay}"
Severity="Error" Severity="Error"
Visibility="{x:Bind ViewModel.Error, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}" /> Visibility="{x:Bind ViewModel.Error, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}">
<InfoBar.ActionButton>
<Button
x:Uid="MakeWritable"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.OverwriteHostsCommand}"
Visibility="{x:Bind ViewModel.IsReadOnly, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</InfoBar.ActionButton>
</InfoBar>
<InfoBar <InfoBar
x:Uid="FileChanged" x:Uid="FileChanged"
Margin="0,8,0,0" Margin="0,8,0,0"
@@ -385,7 +393,10 @@
Severity="Informational" Severity="Informational"
Visibility="{x:Bind ViewModel.FileChanged, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}"> Visibility="{x:Bind ViewModel.FileChanged, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}">
<InfoBar.ActionButton> <InfoBar.ActionButton>
<Button x:Uid="Reload" Command="{x:Bind ViewModel.ReadHostsCommand}" /> <Button
x:Uid="Reload"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.ReadHostsCommand}" />
</InfoBar.ActionButton> </InfoBar.ActionButton>
</InfoBar> </InfoBar>
</StackPanel> </StackPanel>

View File

@@ -228,6 +228,10 @@
<value>The hosts file cannot be saved because the program isn't running as administrator.</value> <value>The hosts file cannot be saved because the program isn't running as administrator.</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment> <comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data> </data>
<data name="FileSaveError_ReadOnly" xml:space="preserve">
<value>The hosts file cannot be saved because it is read-only.</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="FilterBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> <data name="FilterBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Filters</value> <value>Filters</value>
</data> </data>
@@ -246,6 +250,9 @@
<value>Hosts</value> <value>Hosts</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment> <comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data> </data>
<data name="MakeWritable.Content" xml:space="preserve">
<value>Make writable</value>
</data>
<data name="MoveDown.Text" xml:space="preserve"> <data name="MoveDown.Text" xml:space="preserve">
<value>Move down</value> <value>Move down</value>
</data> </data>

View File

@@ -15,6 +15,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Collections; using CommunityToolkit.WinUI.Collections;
using Hosts.Exceptions;
using Hosts.Helpers; using Hosts.Helpers;
using Hosts.Models; using Hosts.Models;
using Hosts.Settings; using Hosts.Settings;
@@ -48,6 +49,9 @@ namespace Hosts.ViewModels
[ObservableProperty] [ObservableProperty]
private string _errorMessage; private string _errorMessage;
[ObservableProperty]
private bool _isReadOnly;
[ObservableProperty] [ObservableProperty]
private bool _fileChanged; private bool _fileChanged;
@@ -262,6 +266,13 @@ namespace Hosts.ViewModels
_hostsService.OpenHostsFile(); _hostsService.OpenHostsFile();
} }
[RelayCommand]
public void OverwriteHosts()
{
_hostsService.RemoveReadOnly();
_ = Task.Run(SaveAsync);
}
public void Dispose() public void Dispose()
{ {
Dispose(disposing: true); Dispose(disposing: true);
@@ -374,6 +385,7 @@ namespace Hosts.ViewModels
{ {
bool error = true; bool error = true;
string errorMessage = string.Empty; string errorMessage = string.Empty;
bool isReadOnly = false;
try try
{ {
@@ -385,6 +397,12 @@ namespace Hosts.ViewModels
var resourceLoader = ResourceLoaderInstance.ResourceLoader; var resourceLoader = ResourceLoaderInstance.ResourceLoader;
errorMessage = resourceLoader.GetString("FileSaveError_NotElevated"); errorMessage = resourceLoader.GetString("FileSaveError_NotElevated");
} }
catch (ReadOnlyHostsException)
{
isReadOnly = true;
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
errorMessage = resourceLoader.GetString("FileSaveError_ReadOnly");
}
catch (IOException ex) when ((ex.HResult & 0x0000FFFF) == 32) catch (IOException ex) when ((ex.HResult & 0x0000FFFF) == 32)
{ {
// There are some edge cases where a big hosts file is being locked by svchost.exe https://github.com/microsoft/PowerToys/issues/28066 // There are some edge cases where a big hosts file is being locked by svchost.exe https://github.com/microsoft/PowerToys/issues/28066
@@ -402,6 +420,7 @@ namespace Hosts.ViewModels
{ {
Error = error; Error = error;
ErrorMessage = errorMessage; ErrorMessage = errorMessage;
IsReadOnly = isReadOnly;
}); });
} }