diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9d28e613d..676c02f40 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -79,6 +79,7 @@ steps: inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' +# https://github.com/NuGet/Home/issues/8148 - task: DotNetCoreCLI@2 displayName: Push to MyGet condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests diff --git a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs index 294a1e6d0..55bf86f3f 100644 --- a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs +++ b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs @@ -105,6 +105,24 @@ public static void Run() .WithProxy(new ProxyAndRecordSettings { Url = "http://postman-echo.com/get" }) ); + server + .Given(Request.Create() + .UsingGet() + .WithHeader("postmanecho", "get2") + ) + .RespondWith(Response.Create() + .WithProxy(new ProxyAndRecordSettings + { + Url = "http://postman-echo.com/get", + WebProxySettings = new WebProxySettings + { + Address = "http://company", + UserName = "test", + Password = "pwd" + } + }) + ); + server .Given(Request.Create() .UsingGet() diff --git a/src/WireMock.Net.StandAlone/StandAloneApp.cs b/src/WireMock.Net.StandAlone/StandAloneApp.cs index a1b22056c..21c597c6f 100644 --- a/src/WireMock.Net.StandAlone/StandAloneApp.cs +++ b/src/WireMock.Net.StandAlone/StandAloneApp.cs @@ -1,6 +1,5 @@ -using System; +using JetBrains.Annotations; using System.Linq; -using JetBrains.Annotations; using WireMock.Logging; using WireMock.Server; using WireMock.Settings; @@ -87,8 +86,20 @@ public static FluentMockServer Start([NotNull] string[] args, [CanBeNull] IWireM SaveMappingForStatusCodePattern = parser.GetStringValue("SaveMappingForStatusCodePattern"), ClientX509Certificate2ThumbprintOrSubjectName = parser.GetStringValue("ClientX509Certificate2ThumbprintOrSubjectName"), BlackListedHeaders = parser.GetValues("BlackListedHeaders"), - BlackListedCookies = parser.GetValues("BlackListedCookies") + BlackListedCookies = parser.GetValues("BlackListedCookies"), + AllowAutoRedirect = parser.GetBoolValue("AllowAutoRedirect") }; + + string proxyAddress = parser.GetStringValue("WebProxyAddress"); + if (!string.IsNullOrEmpty(proxyAddress)) + { + settings.ProxyAndRecordSettings.WebProxySettings = new WebProxySettings + { + Address = proxyAddress, + UserName = parser.GetStringValue("WebProxyUserName"), + Password = parser.GetStringValue("WebProxyPassword") + }; + } } settings.Logger.Debug("WireMock.Net server arguments [{0}]", string.Join(", ", args.Select(a => $"'{a}'"))); diff --git a/src/WireMock.Net/Admin/Mappings/ResponseModel.cs b/src/WireMock.Net/Admin/Mappings/ResponseModel.cs index 158624f46..78ea85aab 100644 --- a/src/WireMock.Net/Admin/Mappings/ResponseModel.cs +++ b/src/WireMock.Net/Admin/Mappings/ResponseModel.cs @@ -96,5 +96,10 @@ public class ResponseModel /// Gets or sets the fault. /// public FaultModel Fault { get; set; } + + /// + /// Gets or sets the WebProxy settings. + /// + public WebProxyModel WebProxy { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Admin/Mappings/WebProxyModel.cs b/src/WireMock.Net/Admin/Mappings/WebProxyModel.cs new file mode 100644 index 000000000..a05591188 --- /dev/null +++ b/src/WireMock.Net/Admin/Mappings/WebProxyModel.cs @@ -0,0 +1,23 @@ +namespace WireMock.Admin.Mappings +{ + /// + /// WebProxy settings + /// + public class WebProxyModel + { + /// + /// A string instance that contains the address of the proxy server. + /// + public string Address { get; set; } + + /// + /// The user name associated with the credentials. + /// + public string UserName { get; set; } + + /// + /// The password for the user name associated with the credentials. + /// + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Compatibility/WebProxy.cs b/src/WireMock.Net/Compatibility/WebProxy.cs new file mode 100644 index 000000000..538c9c140 --- /dev/null +++ b/src/WireMock.Net/Compatibility/WebProxy.cs @@ -0,0 +1,28 @@ +#if NETSTANDARD1_3 +using System; +using System.Net; + +namespace System.Net +{ + internal class WebProxy : IWebProxy + { + private readonly string _proxy; + public ICredentials Credentials { get; set; } + + public WebProxy(string proxy) + { + _proxy = proxy; + } + + public Uri GetProxy(Uri destination) + { + return new Uri(_proxy); + } + + public bool IsBypassed(Uri host) + { + return true; + } + } +} +#endif \ No newline at end of file diff --git a/src/WireMock.Net/Http/HttpClientHelper.cs b/src/WireMock.Net/Http/HttpClientHelper.cs index 4b5b05e33..296cdabc9 100644 --- a/src/WireMock.Net/Http/HttpClientHelper.cs +++ b/src/WireMock.Net/Http/HttpClientHelper.cs @@ -1,16 +1,17 @@ -using System; +using JetBrains.Annotations; +using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using JetBrains.Annotations; using WireMock.HttpsCertificate; +using WireMock.Settings; using WireMock.Validation; namespace WireMock.Http { internal static class HttpClientHelper { - public static HttpClient CreateHttpClient(string clientX509Certificate2ThumbprintOrSubjectName = null) + public static HttpClient CreateHttpClient(IProxyAndRecordSettings settings) { #if NETSTANDARD var handler = new HttpClientHandler @@ -36,20 +37,30 @@ public static HttpClient CreateHttpClient(string clientX509Certificate2Thumbprin ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11; #endif - if (!string.IsNullOrEmpty(clientX509Certificate2ThumbprintOrSubjectName)) + if (!string.IsNullOrEmpty(settings.ClientX509Certificate2ThumbprintOrSubjectName)) { handler.ClientCertificateOptions = ClientCertificateOption.Manual; - var x509Certificate2 = ClientCertificateHelper.GetCertificate(clientX509Certificate2ThumbprintOrSubjectName); + var x509Certificate2 = ClientCertificateHelper.GetCertificate(settings.ClientX509Certificate2ThumbprintOrSubjectName); handler.ClientCertificates.Add(x509Certificate2); } - // For proxy we shouldn't follow auto redirects - handler.AllowAutoRedirect = false; + handler.AllowAutoRedirect = settings.AllowAutoRedirect == true; // If UseCookies enabled, httpClient ignores Cookie header handler.UseCookies = false; + if (settings.WebProxySettings != null) + { + handler.UseProxy = true; + + handler.Proxy = new WebProxy(settings.WebProxySettings.Address); + if (settings.WebProxySettings.UserName != null && settings.WebProxySettings.Password != null) + { + handler.Proxy.Credentials = new NetworkCredential(settings.WebProxySettings.UserName, settings.WebProxySettings.Password); + } + } + var client = new HttpClient(handler); #if NET452 || NET46 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; diff --git a/src/WireMock.Net/ResponseBuilders/Response.WithProxy.cs b/src/WireMock.Net/ResponseBuilders/Response.WithProxy.cs new file mode 100644 index 000000000..b43880cbc --- /dev/null +++ b/src/WireMock.Net/ResponseBuilders/Response.WithProxy.cs @@ -0,0 +1,48 @@ +using System.Net.Http; +using WireMock.Http; +using WireMock.Settings; +using WireMock.Validation; + +namespace WireMock.ResponseBuilders +{ + public partial class Response + { + private HttpClient _httpClientForProxy; + + /// + /// The Proxy URL to use. + /// + public string ProxyUrl { get; private set; } + + /// + /// The WebProxy settings. + /// + public IWebProxySettings WebProxySettings { get; private set; } + + /// + public IResponseBuilder WithProxy(string proxyUrl, string clientX509Certificate2ThumbprintOrSubjectName = null) + { + Check.NotNullOrEmpty(proxyUrl, nameof(proxyUrl)); + + var settings = new ProxyAndRecordSettings + { + Url = proxyUrl, + ClientX509Certificate2ThumbprintOrSubjectName = clientX509Certificate2ThumbprintOrSubjectName + }; + + return WithProxy(settings); + } + + /// + public IResponseBuilder WithProxy(IProxyAndRecordSettings settings) + { + Check.NotNull(settings, nameof(settings)); + + ProxyUrl = settings.Url; + WebProxySettings = settings.WebProxySettings; + + _httpClientForProxy = HttpClientHelper.CreateHttpClient(settings); + return this; + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 8d042251d..9d716286f 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -21,8 +21,6 @@ namespace WireMock.ResponseBuilders /// public partial class Response : IResponseBuilder { - private HttpClient _httpClientForProxy; - /// /// The delay /// @@ -38,16 +36,6 @@ public partial class Response : IResponseBuilder /// public bool UseTransformerForBodyAsFile { get; private set; } - /// - /// The Proxy URL to use. - /// - public string ProxyUrl { get; private set; } - - /// - /// The client X509Certificate2 Thumbprint or SubjectName to use. - /// - public string ClientX509Certificate2ThumbprintOrSubjectName { get; private set; } - /// /// Gets the response message. /// @@ -339,25 +327,6 @@ public IResponseBuilder WithDelay(int milliseconds) return WithDelay(TimeSpan.FromMilliseconds(milliseconds)); } - /// - public IResponseBuilder WithProxy(string proxyUrl, string clientX509Certificate2ThumbprintOrSubjectName = null) - { - Check.NotNullOrEmpty(proxyUrl, nameof(proxyUrl)); - - ProxyUrl = proxyUrl; - ClientX509Certificate2ThumbprintOrSubjectName = clientX509Certificate2ThumbprintOrSubjectName; - _httpClientForProxy = HttpClientHelper.CreateHttpClient(clientX509Certificate2ThumbprintOrSubjectName); - return this; - } - - /// - public IResponseBuilder WithProxy(IProxyAndRecordSettings settings) - { - Check.NotNull(settings, nameof(settings)); - - return WithProxy(settings.Url, settings.ClientX509Certificate2ThumbprintOrSubjectName); - } - /// public IResponseBuilder WithCallback(Func callbackHandler) { diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index 144ecc67c..383d4135f 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -4,6 +4,7 @@ using WireMock.Matchers.Request; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; +using WireMock.Settings; using WireMock.Util; using WireMock.Validation; @@ -116,12 +117,14 @@ public MappingModel ToMappingModel(IMapping mapping) mappingModel.Response.BodyEncoding = null; mappingModel.Response.ProxyUrl = response.ProxyUrl; mappingModel.Response.Fault = null; + mappingModel.Response.WebProxy = MapWebProxy(response.WebProxySettings); } else { + mappingModel.Response.WebProxy = null; mappingModel.Response.BodyDestination = response.ResponseMessage.BodyDestination; mappingModel.Response.StatusCode = response.ResponseMessage.StatusCode; - mappingModel.Response.Headers = Map(response.ResponseMessage.Headers); + mappingModel.Response.Headers = MapHeaders(response.ResponseMessage.Headers); if (response.UseTransformer) { mappingModel.Response.UseTransformer = response.UseTransformer; @@ -181,14 +184,25 @@ public MappingModel ToMappingModel(IMapping mapping) return mappingModel; } - private static IDictionary Map(IDictionary> dictionary) + private static WebProxyModel MapWebProxy(IWebProxySettings settings) { + return settings != null ? new WebProxyModel + { + Address = settings.Address, + UserName = settings.UserName, + Password = settings.Password + } : null; + } + + private static IDictionary MapHeaders(IDictionary> dictionary) + { + var newDictionary = new Dictionary(); + if (dictionary == null || dictionary.Count == 0) { - return null; + return newDictionary; } - var newDictionary = new Dictionary(); foreach (var entry in dictionary) { object value = entry.Value.Count == 1 ? (object)entry.Value.ToString() : entry.Value; diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs index 239006f39..94d94e198 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -254,7 +254,7 @@ public bool ReadStaticMappingAndAddOrUpdate([NotNull] string path) private void InitProxyAndRecord(IFluentMockServerSettings settings) { - _httpClientForProxy = HttpClientHelper.CreateHttpClient(settings.ProxyAndRecordSettings.ClientX509Certificate2ThumbprintOrSubjectName); + _httpClientForProxy = HttpClientHelper.CreateHttpClient(settings.ProxyAndRecordSettings); var respondProvider = Given(Request.Create().WithPath("/*").UsingAnyMethod()); if (settings.StartAdminInterface == true) @@ -790,12 +790,19 @@ private IResponseBuilder InitResponseBuilder(ResponseModel responseModel) if (!string.IsNullOrEmpty(responseModel.ProxyUrl)) { - if (string.IsNullOrEmpty(responseModel.X509Certificate2ThumbprintOrSubjectName)) + var proxyAndRecordSettings = new ProxyAndRecordSettings { - return responseBuilder.WithProxy(responseModel.ProxyUrl); - } + Url = responseModel.ProxyUrl, + ClientX509Certificate2ThumbprintOrSubjectName = responseModel.X509Certificate2ThumbprintOrSubjectName, + WebProxySettings = responseModel.WebProxy != null ? new WebProxySettings + { + Address = responseModel.WebProxy.Address, + UserName = responseModel.WebProxy.UserName, + Password = responseModel.WebProxy.Password + } : null + }; - return responseBuilder.WithProxy(responseModel.ProxyUrl, responseModel.X509Certificate2ThumbprintOrSubjectName); + return responseBuilder.WithProxy(proxyAndRecordSettings); } if (responseModel.StatusCode.HasValue) diff --git a/src/WireMock.Net/Settings/IProxyAndRecordSettings.cs b/src/WireMock.Net/Settings/IProxyAndRecordSettings.cs index 2e1735532..26b93b5df 100644 --- a/src/WireMock.Net/Settings/IProxyAndRecordSettings.cs +++ b/src/WireMock.Net/Settings/IProxyAndRecordSettings.cs @@ -44,5 +44,15 @@ public interface IProxyAndRecordSettings /// Defines a list of cookies which will excluded from the saved mappings. /// string[] BlackListedCookies { get; set; } + + /// + /// Defines the WebProxySettings. + /// + IWebProxySettings WebProxySettings { get; set; } + + /// + /// Proxy requests should follow redirection (30x). + /// + bool? AllowAutoRedirect { get; set; } } -} +} \ No newline at end of file diff --git a/src/WireMock.Net/Settings/IWebProxySettings.cs b/src/WireMock.Net/Settings/IWebProxySettings.cs new file mode 100644 index 000000000..3cdbe172d --- /dev/null +++ b/src/WireMock.Net/Settings/IWebProxySettings.cs @@ -0,0 +1,20 @@ +namespace WireMock.Settings +{ + public interface IWebProxySettings + { + /// + /// A string instance that contains the address of the proxy server. + /// + string Address { get; set; } + + /// + /// The user name associated with the credentials. + /// + string UserName { get; set; } + + /// + /// The password for the user name associated with the credentials. + /// + string Password { get; set; } + } +} diff --git a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs index 09c7a48e9..a4be77766 100644 --- a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs +++ b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs @@ -34,5 +34,13 @@ public class ProxyAndRecordSettings : IProxyAndRecordSettings /// [PublicAPI] public string[] BlackListedCookies { get; set; } + + /// + [PublicAPI] + public IWebProxySettings WebProxySettings { get; set; } + + /// + [PublicAPI] + public bool? AllowAutoRedirect { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WebProxySettings.cs b/src/WireMock.Net/Settings/WebProxySettings.cs new file mode 100644 index 000000000..1de8b380c --- /dev/null +++ b/src/WireMock.Net/Settings/WebProxySettings.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; + +namespace WireMock.Settings +{ + public class WebProxySettings : IWebProxySettings + { + /// + [PublicAPI] + public string Address { get; set; } + + /// + [PublicAPI] + public string UserName { get; set; } + + /// + [PublicAPI] + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithProxyTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithProxyTests.cs index 733ed7528..d25b4680b 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithProxyTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithProxyTests.cs @@ -2,6 +2,7 @@ using NFluent; using System; using System.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; using WireMock.Models; using WireMock.RequestBuilders; @@ -45,6 +46,28 @@ public async Task Response_WithProxy() Check.That(responseMessage.Headers["Content-Type"].ToString()).IsEqualTo("application/json"); } + [Fact] + public void Response_WithProxy_WebProxySettings() + { + // Assign + var settings = new ProxyAndRecordSettings + { + Url = "http://test.nl", + WebProxySettings = new WebProxySettings + { + Address = "http://company", + UserName = "x", + Password = "y" + } + }; + var response = Response.Create().WithProxy(settings); + + // Act + var request = new RequestMessage(new UrlDetails($"{_server.Urls[0]}/{_guid}"), "GET", "::1"); + + Check.ThatAsyncCode(() => response.ProvideResponseAsync(request, _settingsMock.Object)).Throws(); + } + public void Dispose() { _server?.Dispose();