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;
}