diff --git a/CHANGELOG.md b/CHANGELOG.md index d849439b3..d5356ac4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [SIL.Core.Clearshare] New methods "GetIsStringAvailableForLangId" and "GetDynamicStringOrEnglish" were added to Localizer for use in LicenseInfo's "GetBestLicenseTranslation" method, to remove LicenseInfo's L10NSharp dependency. - [SIL.Windows.Forms.Clearshare] New ILicenseWithImage interface handles "GetImage" method for Winforms-dependent licenses, implemented in CreativeCommonsLicense and CustomLicense, and formerly included in LicenseInfo. - [SIL.Core.Clearshare] New tests MetadataBareTests are based on previous MetadataTests in SIL.Windows.Forms.Clearshare. The tests were updated to use ImageSharp instead of Winforms for handling images. +- [SIL.Core.Desktop] Added a constant (kBrowserCompatibleUserAgent) to RobustNetworkOperation: a browser-like User Agent string that can be used when making HTTP requests to strict servers. ### Fixed - [SIL.DictionaryServices] Fix memory leak in LiftWriter @@ -49,6 +50,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [SIL.Windows.Forms] BREAKING CHANGE: ToolStripExtensions.InitializeWithAvailableUILocales() removed the ILocalizationManager parameter. This method no longer provides functionality to display the localization dialog box in response to the user clicking More. - [SIL.Windows.Forms] BREAKING CHANGE: Removed optional moreSelected parameter from ToolStripExtensions.InitializeWithAvailableUILocales method. This parameter was no longer being used. Clients that want to have a More menu item that performs a custom action will now need to add it themselves. - [SIL.Windows.Forms] BREAKING CHANGE: LocalizationIncompleteDlg's EmailAddressForLocalizationRequests is no longer autopopulated from LocalizationManager. A new optional constructor parameter, emailAddressForLocalizationRequests, can be used instead. If not supplied, the "More information" controls will be hidden. +- [SIL.Core.Desktop] Added an optional userAgentHeader parameter to DoHttpGetAndGetProxyInfo to allow a client to mimic a real browser if necessary. ### Removed - [SIL.Windows.Forms] In .NET 8 builds, removed Scanner and Camera options from the Image Toolbox. diff --git a/SIL.Core.Desktop.Tests/Network/RobustNetworkOperation.Tests.cs b/SIL.Core.Desktop.Tests/Network/RobustNetworkOperation.Tests.cs index 0e23ea73a..d78a542dc 100644 --- a/SIL.Core.Desktop.Tests/Network/RobustNetworkOperation.Tests.cs +++ b/SIL.Core.Desktop.Tests/Network/RobustNetworkOperation.Tests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using NUnit.Framework; using SIL.Network; @@ -19,7 +19,8 @@ public void DoHttpGetAndGetProxyInfo_404() public void DoHttpGetAndGetProxyInfo_NoProxy_ReturnsFalse() { var gotProxy = RobustNetworkOperation.DoHttpGetAndGetProxyInfo( - "https://sil.org/", out _, out _, out _, s => Debug.WriteLine(s)); + "https://sil.org/", out _, out _, out _, s => Debug.WriteLine(s), + RobustNetworkOperation.kBrowserCompatibleUserAgent); Assert.That(gotProxy, Is.False); } } diff --git a/SIL.Core.Desktop/Network/RobustNetworkOperation.cs b/SIL.Core.Desktop/Network/RobustNetworkOperation.cs index 960813224..2cc8173a2 100644 --- a/SIL.Core.Desktop/Network/RobustNetworkOperation.cs +++ b/SIL.Core.Desktop/Network/RobustNetworkOperation.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using System.Security.Cryptography; using System.Text; @@ -8,6 +8,14 @@ namespace SIL.Network public class RobustNetworkOperation { + /// + /// A browser-like User Agent string that can be used when making HTTP requests to servers + /// that reject requests lacking a typical browser User-Agent header. Used in tests and + /// available to callers that encounter 403 responses due to restrictive server filtering. + /// + public const string kBrowserCompatibleUserAgent = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) libpalaso"; + /// /// Perform a web action, trying various things to use a proxy if needed, including requesting /// (and remembering) user credentials from the user. @@ -22,7 +30,7 @@ public static IWebProxy Do(Action action, Action verboseLog) var proxy = new WebProxy(); action(proxy); - //!!!!!!!!!!!!!! in Sept 2011, hatton disabled proxy lookup. It was reportedly causing grief in Nigeria, + //!!!!!!!!!!!!!! in Sept 2011, Hatton disabled proxy lookup. It was reportedly causing grief in Nigeria, //asking for credentials over and over, and SIL PNG doesn't use a proxy anymore. So for now... @@ -109,10 +117,51 @@ public static string GetClearText(string encryptedString) } /// - /// Used by Chorus to get proxy name, user name, and password of the remote repository. + /// Used to determine whether an HTTP GET request requires a proxy and, if so, to retrieve + /// the proxy host and credentials needed to access the specified remote repository URL. /// - /// true if a proxy is needed. THROWS if it just can't get through - public static bool DoHttpGetAndGetProxyInfo(string url, out string hostAndPort, out string userName, out string password, Action verboseLog) + /// + /// The full URL to send an HTTP GET request to. Used to test connectivity and determine + /// whether proxy credentials are required. + /// + /// + /// Outputs the proxy host (including port, if applicable) if a proxy is required; + /// otherwise, an empty string. + /// + /// + /// Outputs the proxy username if authentication is required; otherwise an empty string. + /// + /// + /// Outputs the proxy password if authentication is required; otherwise an empty string. + /// + /// + /// Optional callback for logging diagnostic information about the request and proxy + /// detection process. + /// + /// + /// Optional user agent header string to send with the request. Some servers require a + /// browser-like user agent and may reject requests that appear to come from automated + /// clients. See for an example value. + /// + /// + /// True if a proxy is required and credentials were obtained; false if no proxy is needed. + /// + /// + /// Thrown if is null. + /// + /// + /// Thrown if is not a valid URI. + /// + /// + /// Thrown if the HTTP request fails due to network errors, proxy configuration + /// issues, or authentication failures. + /// + /// + /// Thrown if the URI scheme is not supported. + /// + public static bool DoHttpGetAndGetProxyInfo(string url, out string hostAndPort, + out string userName, out string password, Action verboseLog, + string userAgentHeader = null) { hostAndPort = string.Empty; userName = string.Empty; @@ -125,6 +174,8 @@ public static bool DoHttpGetAndGetProxyInfo(string url, out string hostAndPort, proxy => { client.Proxy = proxy; + if (userAgentHeader != null) + client.Headers[HttpRequestHeader.UserAgent] = userAgentHeader; client.DownloadData(url); //we don't actually care what comes back }, verboseLog @@ -150,9 +201,7 @@ public static bool DoHttpGetAndGetProxyInfo(string url, out string hostAndPort, var networkCredential = proxyInfo.Credentials.GetCredential(destination, ""); userName = networkCredential.UserName; password = networkCredential.Password; - if (verboseLog != null) - verboseLog.Invoke("DoHttpGetAndGetProxyInfo Returning with credentials. UserName is " + userName); - + verboseLog?.Invoke("DoHttpGetAndGetProxyInfo Returning with credentials. UserName is " + userName); return true; }