Skip to content

Commit

Permalink
SLVS-1498 Show infobar when server certificate can't be verified
Browse files Browse the repository at this point in the history
  • Loading branch information
gabriela-trutan-sonarsource committed Jan 9, 2025
1 parent 2832d37 commit acde5c9
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/Core/DocumentationLinks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public static class DocumentationLinks
public const string OpenInIdeIssueLocation = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/troubleshooting/#no-matching-issue-found";
public const string OpenInIdeBindingSetup = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/troubleshooting/#no-matching-project-found";
public const string UnbindingProject = "https://docs.sonarsource.com/sonarqube-for-ide/visual-studio/team-features/connected-mode-setup/#unbinding-a-project";
public const string SslCertificate = "https://docs.sonarsource.com/sonarqube-for-ide/intellij/team-features/advanced-configuration/#client-ssl-certificates";

public static readonly Uri UnbindingProjectUri = new(UnbindingProject);
public static readonly Uri ConnectedModeUri = new(ConnectedMode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using System.Security.Cryptography.X509Certificates;
using NSubstitute.ExceptionExtensions;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Notifications;
using SonarLint.VisualStudio.SLCore.Core;
using SonarLint.VisualStudio.SLCore.Listener.Http;
using SonarLint.VisualStudio.SLCore.Listener.Http.Model;
Expand All @@ -37,6 +38,9 @@ public class HttpConfigurationListenerTests
private ISystemProxyDetector proxySettingsDetector;
private TestLogger testLogger;
private HttpConfigurationListener testSubject;
private INotificationService notificationService;
private IBrowserService browserService;
private IOutputWindowService outputWindowService;

[TestInitialize]
public void TestInitialize()
Expand All @@ -45,7 +49,17 @@ public void TestInitialize()
certificateChainValidator = Substitute.For<ICertificateChainValidator>();
certificateDtoConverter = Substitute.For<ICertificateDtoConverter>();
proxySettingsDetector = Substitute.For<ISystemProxyDetector>();
testSubject = new HttpConfigurationListener(testLogger, certificateChainValidator, certificateDtoConverter, proxySettingsDetector);
notificationService = Substitute.For<INotificationService>();
browserService = Substitute.For<IBrowserService>();
outputWindowService = Substitute.For<IOutputWindowService>();

testSubject = new HttpConfigurationListener(testLogger,
certificateChainValidator,
certificateDtoConverter,
proxySettingsDetector,
notificationService,
browserService,
outputWindowService);
}

[TestMethod]
Expand All @@ -54,7 +68,10 @@ public void MefCtor_CheckIsExported() =>
MefTestHelpers.CreateExport<ILogger>(),
MefTestHelpers.CreateExport<ICertificateDtoConverter>(),
MefTestHelpers.CreateExport<ICertificateChainValidator>(),
MefTestHelpers.CreateExport<ISystemProxyDetector>()
MefTestHelpers.CreateExport<ISystemProxyDetector>(),
MefTestHelpers.CreateExport<INotificationService>(),
MefTestHelpers.CreateExport<IBrowserService>(),
MefTestHelpers.CreateExport<IOutputWindowService>()
);

[TestMethod]
Expand Down Expand Up @@ -168,6 +185,28 @@ public async Task CheckServerTrustedAsync_Exception_ReturnsFalse()
testLogger.AssertPartialOutputStringExists(exceptionReason);
}

[TestMethod]
public async Task CheckServerTrustedAsync_CertificateIsNotValid_ShowsNotification()
{
var (primaryCertificateDto, primaryCertificate) = SetUpCertificate("some certificate");
certificateChainValidator.ValidateChain(primaryCertificate, Arg.Is<IEnumerable<X509Certificate2>>(x => !x.Any())).Returns(false);

await testSubject.CheckServerTrustedAsync(new CheckServerTrustedParams([primaryCertificateDto], "ignored"));

notificationService.Received(1).ShowNotification(Arg.Is<INotification>(x => IsExpectedNotification(x)));
}

[TestMethod]
public async Task CheckServerTrustedAsync_CertificateIsValid_DoesNotShowNotification()
{
var (primaryCertificateDto, primaryCertificate) = SetUpCertificate("some certificate");
certificateChainValidator.ValidateChain(primaryCertificate, Arg.Is<IEnumerable<X509Certificate2>>(x => !x.Any())).Returns(true);

await testSubject.CheckServerTrustedAsync(new CheckServerTrustedParams([primaryCertificateDto], "ignored"));

notificationService.DidNotReceive().ShowNotification(Arg.Any<INotification>());
}

private (X509CertificateDto certificateDto, X509Certificate2 certificate) SetUpCertificate(string certificateName)
{
var certificateDto = new X509CertificateDto(certificateName);
Expand All @@ -181,4 +220,15 @@ public async Task CheckServerTrustedAsync_Exception_ReturnsFalse()
private void MockProxyConfigured(string hostName, int port) => proxySettingsDetector.GetProxyUri(Arg.Any<Uri>()).Returns(new Uri($"{hostName}:{port}"));

private static string BuildUri(string scheme, string host) => $"{scheme}://{host}";

private static bool IsExpectedNotification(INotification x) =>
x.Id == HttpConfigurationListener.ServerCertificateInvalidNotificationId
&& x.Message == SLCoreStrings.ServerCertificateInfobar_CertificateInvalidMessage
&& HasExpectedActions(x);

private static bool HasExpectedActions(INotification notification) =>
notification.Actions.Count() == 2
&& notification.Actions.Any(x => x.CommandText == SLCoreStrings.ServerCertificateInfobar_LearnMore)
&& notification.Actions.Any(x => x.CommandText == SLCoreStrings.ServerCertificateInfobar_ShowLogs);

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

using System.ComponentModel.Composition;
using SonarLint.VisualStudio.Core;
using SonarLint.VisualStudio.Core.Notifications;
using SonarLint.VisualStudio.SLCore.Core;
using SonarLint.VisualStudio.SLCore.Listener.Http;
using SonarLint.VisualStudio.SLCore.Listener.Http.Model;
Expand All @@ -34,15 +35,28 @@ internal class HttpConfigurationListener : IHttpConfigurationListener
private readonly ICertificateChainValidator chainValidator;
private readonly ICertificateDtoConverter certificateDtoConverter;
private readonly ISystemProxyDetector proxySettingsDetector;
private readonly INotificationService notificationService;
private readonly IBrowserService browserService;
private readonly IOutputWindowService outputWindowService;
private static readonly List<string> socksHosts = ["socks4", "socks5"];
public const string ServerCertificateInvalidNotificationId = "ServerCertificateInvalidNotificationId";

[ImportingConstructor]
public HttpConfigurationListener(ILogger logger, ICertificateChainValidator chainValidator, ICertificateDtoConverter certificateDtoConverter, ISystemProxyDetector proxySettingsDetector)
public HttpConfigurationListener(ILogger logger,
ICertificateChainValidator chainValidator,
ICertificateDtoConverter certificateDtoConverter,
ISystemProxyDetector proxySettingsDetector,
INotificationService notificationService,
IBrowserService browserService,
IOutputWindowService outputWindowService)
{
this.logger = logger;
this.chainValidator = chainValidator;
this.certificateDtoConverter = certificateDtoConverter;
this.proxySettingsDetector = proxySettingsDetector;
this.notificationService = notificationService;
this.browserService = browserService;
this.outputWindowService = outputWindowService;
}

public Task<SelectProxiesResponse> SelectProxiesAsync(SelectProxiesParams parameters)
Expand Down Expand Up @@ -84,11 +98,23 @@ public Task<CheckServerTrustedResponse> CheckServerTrustedAsync(CheckServerTrust
{
logger.WriteLine(SLCoreStrings.HttpConfiguration_ServerTrustVerificationRequest);
var verificationResult = VerifyChain(parameters.chain);
if (!verificationResult)
{
notificationService.ShowNotification(GetServerCertificateInvalidNotification());
}
logger.WriteLine(SLCoreStrings.HttpConfiguration_ServerTrustVerificationResult, verificationResult);

return Task.FromResult(new CheckServerTrustedResponse(verificationResult));
}

private VisualStudio.Core.Notifications.Notification GetServerCertificateInvalidNotification() =>
new(ServerCertificateInvalidNotificationId,
SLCoreStrings.ServerCertificateInfobar_CertificateInvalidMessage,
[
new NotificationAction(SLCoreStrings.ServerCertificateInfobar_LearnMore, _ => browserService.Navigate(DocumentationLinks.SslCertificate), false),
new NotificationAction(SLCoreStrings.ServerCertificateInfobar_ShowLogs, _ => outputWindowService.Show(), false)
]);

private bool VerifyChain(List<X509CertificateDto> chain)
{
try
Expand Down
27 changes: 27 additions & 0 deletions src/SLCore/SLCoreStrings.Designer.cs

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

9 changes: 9 additions & 0 deletions src/SLCore/SLCoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,13 @@
<data name="SLCoreName" xml:space="preserve">
<value>SLCore</value>
</data>
<data name="ServerCertificateInfobar_CertificateInvalidMessage" xml:space="preserve">
<value>The server certificate can not be verified. Please see the logs for more info.</value>
</data>
<data name="ServerCertificateInfobar_LearnMore" xml:space="preserve">
<value>Learn more</value>
</data>
<data name="ServerCertificateInfobar_ShowLogs" xml:space="preserve">
<value>Show logs</value>
</data>
</root>

0 comments on commit acde5c9

Please sign in to comment.