diff --git a/AzFappDebugger.csproj b/AzFappDebugger.csproj index bd6d8c6..c785515 100644 --- a/AzFappDebugger.csproj +++ b/AzFappDebugger.csproj @@ -1,12 +1,11 @@ - + net6.0 v4 - - + diff --git a/HtmlBrandingHelper.cs b/HtmlBrandingHelper.cs index f6a4b22..98d792c 100644 --- a/HtmlBrandingHelper.cs +++ b/HtmlBrandingHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Authentication; using System.Text; using System.Threading.Tasks; @@ -80,7 +81,47 @@ internal static string GetBootstrapWhatItMeans(string uniqueId, string text, boo $"
{text}
"; } + internal static string NormalizeLength(string value, int maxLength) + { + return value.Length <= maxLength ? value : value.Substring(0, maxLength) + "..."; + } + + internal static string NiceTlsProtocol(SslProtocols protocol) + { + switch (protocol) + { + default: + return protocol.ToString(); + break; + case SslProtocols.None: + return "<>"; + break; + + case SslProtocols.Ssl2: + return "SSL 2.0"; + break; + case SslProtocols.Ssl3: + return "SSL 3.0"; + break; + case SslProtocols.Tls: + return "TLS 1.0"; + break; + case SslProtocols.Default: + return "SSL 3.0/TLS 1.0"; + break; + case SslProtocols.Tls11: + return "TLS 1.1"; + break; + case SslProtocols.Tls12: + return "TLS 1.2"; + break; + case SslProtocols.Tls13: + return "TLS 1.3"; + break; + + } + } } } diff --git a/README.md b/README.md index 65c8968..c5d3651 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,13 @@ The tool is released under the MIT License, see LICENSE file. ## Usage -Just install this codebase as package to given Azure FunctionApp. +Just install this codebase as package to given Azure FunctionApp and visit `<>/RunTests` -To control tool set Configuration variables:
-`TEST_DNS_RESOLVE_DOMAINS` = `<>` - performs resolutions of given domains via all available DNS servers
+For tool configuration set following Configuration variables:
+- `TEST_DNS_RESOLVE_DOMAINS` = `<>` - performs resolutions of given domains via all available DNS servers
e.g. `TEST_DNS_RESOLVE_DOMAINS` = `azure.com,someonpremresource.contoso.internal,vjirovsky.cz` -`TEST_HTTPCLIENT_GET_URL` = `<>` - performs HTTP request to given URL via same outbound connectivity configuration as the real application will use
+- `TEST_HTTPCLIENT_GET_URL` = `<>` - performs HTTP request to given URL via same outbound connectivity configuration as the real application will use
e.g. `TEST_DNS_RESOLVE_DOMAINS` = `https://vjirovsky.cz` diff --git a/RunTestsFunction.cs b/RunTestsFunction.cs index 39f551f..47f9f8c 100644 --- a/RunTestsFunction.cs +++ b/RunTestsFunction.cs @@ -21,12 +21,11 @@ namespace AzFappDebugger { public static class RunTestsFunction { - private static HttpClient _httpClient = null; + private static HttpClient _defaultHttpClient = null; + private static HttpClientHandler _defaultHttpClientHandler = null; private static IDictionary _environmentVariablesDictionary = new Dictionary(); - - static RunTestsFunction() { @@ -36,12 +35,12 @@ static RunTestsFunction() } - // ignore SSL cert errors for this debugger (e.g. DPI, proxy, etc.) - var httpClientHandler = new HttpClientHandler(); + _defaultHttpClientHandler = new HttpClientHandler(); + + _defaultHttpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => {return true;}; - httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>{return true;}; - _httpClient = new HttpClient(httpClientHandler); + _defaultHttpClient = new HttpClient(_defaultHttpClientHandler); } @@ -58,7 +57,7 @@ public static async Task Run( var dnsTestsProvider = new DnsTests(_environmentVariablesDictionary); - var outboundConnectivityTestsProvider = new OutboundConnectivityTests(_environmentVariablesDictionary, _httpClient); + var outboundConnectivityTestsProvider = new OutboundConnectivityTests(_environmentVariablesDictionary, _defaultHttpClient); var overviewTestsProvider = new OverviewTests(_environmentVariablesDictionary); var contentStorageAccessTestsProvider = new ContentStorageAccessTests(_environmentVariablesDictionary); diff --git a/Tests/OutboundConnectivityTests.cs b/Tests/OutboundConnectivityTests.cs index 20e84f7..4fd407f 100644 --- a/Tests/OutboundConnectivityTests.cs +++ b/Tests/OutboundConnectivityTests.cs @@ -5,6 +5,9 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; using System.Web; @@ -13,15 +16,14 @@ namespace AzFappDebugger.Tests { internal class OutboundConnectivityTests { - private static IDictionary _environmentVariablesDictionary = null; + private IDictionary _environmentVariablesDictionary = null; + private HttpClient _defaultHttpClient; - private HttpClient _httpClient = null; - - internal OutboundConnectivityTests(IDictionary environmentVariablesDictionary, HttpClient httpClient) + internal OutboundConnectivityTests(IDictionary environmentVariablesDictionary, HttpClient defaultHttpClient) { _environmentVariablesDictionary = environmentVariablesDictionary; - _httpClient = httpClient; + _defaultHttpClient = defaultHttpClient; } @@ -44,9 +46,10 @@ internal async Task RunAllTestsAsHtmlOutputAsync() { output += HtmlBrandingHelper.GetStandardTableRow("Integrated vNET", $"{configItemValue}"); } - else { + else + { output += HtmlBrandingHelper.GetStandardTableRow("Integrated vNET", $"No vNET integration
" + - $"The application is not integrated with any vNET."); + $"The application is not integrated with any vNET."); } @@ -62,71 +65,108 @@ internal async Task RunAllTestsAsHtmlOutputAsync() output += $"

Connectivity tests

"; - if (_httpClient != null) - { + string httpClientUrlToResolve = string.Empty; - string httpClientUrlToResolve = string.Empty; - - output += $"

HttpClient test

"; - output += HtmlBrandingHelper.GetBootstrapWhatItMeans("OutboundConnectivityHttpClient", - $"

This test performs HTTP request via outbound connectivity of the application to hostname specified in {Constants.TEST_HTTPCLIENT_RESOLVE_URL_VARIABLE} configuration variable.

", false, false, "Test description"); - output += "
"; + output += $"

HttpClient test

"; + output += HtmlBrandingHelper.GetBootstrapWhatItMeans("OutboundConnectivityHttpClient", + $"

This test performs HTTP request via outbound connectivity of the application to hostname specified in {Constants.TEST_HTTPCLIENT_RESOLVE_URL_VARIABLE} configuration variable.

", false, false, "Test description"); + output += "
"; - _environmentVariablesDictionary.TryGetValue(Constants.TEST_HTTPCLIENT_RESOLVE_URL_VARIABLE, out httpClientUrlToResolve); + _environmentVariablesDictionary.TryGetValue(Constants.TEST_HTTPCLIENT_RESOLVE_URL_VARIABLE, out httpClientUrlToResolve); - if (!string.IsNullOrWhiteSpace(httpClientUrlToResolve)) + if (!string.IsNullOrWhiteSpace(httpClientUrlToResolve) && _defaultHttpClient != null) + { + output += ""; + string testResult = ""; + try { - output += "
"; - string testResult = ""; - try - { - var content = await _httpClient.GetStringAsync(httpClientUrlToResolve); - testResult = $"OK
{HttpUtility.HtmlEncode(content)}"; - } - catch (Exception e) - { - testResult = "QUERY FAILED
" + e.Message +"
" + (e.InnerException != null ? e.InnerException.Message : ""); - } - finally - { - output += HtmlBrandingHelper.GetStandardTableRow($"{httpClientUrlToResolve}", testResult); - } + var httpClientUrlToResolveUri = new Uri(httpClientUrlToResolve); + var content = await _defaultHttpClient.GetAsync(httpClientUrlToResolveUri); + content.EnsureSuccessStatusCode(); + string text = await content.Content.ReadAsStringAsync(); - output += "
"; - } + testResult = $"OK  {(int)content.StatusCode} {content.StatusCode}

"; - if (!string.IsNullOrWhiteSpace(Constants.MY_IP_ADDRESS_EXTERNAL_SERVICE_URL)) - { - output += $"

My outbound IP address

"; - output += HtmlBrandingHelper.GetBootstrapWhatItMeans("OutboundConnectivityExternalIp", - $"

This test performs a HTTP request to external service, which returns a IP address of HTTP request.

", false, false, "Test description"); - output += "
"; - output += ""; - string testResult = ""; + testResult += $"Connection details
" + + $"Protocol: HTTP {content.Version}
"; try { - var content = await _httpClient.GetStringAsync(Constants.MY_IP_ADDRESS_EXTERNAL_SERVICE_URL); - testResult = $"{HttpUtility.HtmlEncode(content)}"; - } - catch (Exception e) - { - testResult = "QUERY FAILED
" + e.InnerException.Message; + RemoteCertificateValidationCallback certCallback = (_, _, _, _) => true; + using var client = new TcpClient(httpClientUrlToResolveUri.Host, httpClientUrlToResolveUri.Port); + using var sslStream = new SslStream(client.GetStream(), true, certCallback); + await sslStream.AuthenticateAsClientAsync(httpClientUrlToResolveUri.Host); + var serverCertificate = sslStream.RemoteCertificate; + var certificate = new X509Certificate2(serverCertificate); + + if (certificate != null) + { + testResult += $"Security protocol: {HtmlBrandingHelper.NiceTlsProtocol(sslStream.SslProtocol)}
" + + $"Negotiated cipher: {sslStream.NegotiatedCipherSuite}

"; + + testResult += $"Certificate
" + + $"Subject: {certificate.Subject}
" + + $"Issuer: {certificate.Issuer}
" + + $"Valid from: {certificate.GetEffectiveDateString()}
" + + $"Expires on: {certificate.GetExpirationDateString()}
" + + $"Thumbprint: {certificate.Thumbprint}

" + ; + } } - finally + catch (Exception ee) { - output += HtmlBrandingHelper.GetStandardTableRow($"My IP address", testResult); + testResult += $"Connection is not secured

"; + } - output += "
"; + testResult += $"Body:
" + + $"{HttpUtility.HtmlEncode(HtmlBrandingHelper.NormalizeLength(text, 1000))}"; } + catch (Exception e) + { + testResult = "QUERY FAILED
" + e.Message + "
" + (e.InnerException != null ? e.InnerException.Message : ""); + } + finally + { + output += HtmlBrandingHelper.GetStandardTableRow($"{httpClientUrlToResolve}", testResult); + } + + output += ""; + } + else + { + output += $"
No valid {Constants.TEST_HTTPCLIENT_RESOLVE_URL_VARIABLE} variable defined, test has been skipped.
"; } + if (!string.IsNullOrWhiteSpace(Constants.MY_IP_ADDRESS_EXTERNAL_SERVICE_URL) && _defaultHttpClient != null) + { + output += $"

My outbound IP address

"; + output += HtmlBrandingHelper.GetBootstrapWhatItMeans("OutboundConnectivityExternalIp", + $"

This test performs a HTTP request to external service, which returns a IP address of HTTP request.

", false, false, "Test description"); + output += "
"; + output += ""; + string testResult = ""; + try + { + var content = await _defaultHttpClient.GetStringAsync(Constants.MY_IP_ADDRESS_EXTERNAL_SERVICE_URL); + testResult = $"{HttpUtility.HtmlEncode(content)}"; + } + catch (Exception e) + { + testResult = "QUERY FAILED
" + e.InnerException.Message; + } + finally + { + output += HtmlBrandingHelper.GetStandardTableRow($"My IP address", testResult); + } + + output += "
"; + } return output; }