From 9ef8bd0b7b624317930e25e4d2ab6a395ae448e0 Mon Sep 17 00:00:00 2001 From: nudejustin <97242753+nudejustin@users.noreply.github.com> Date: Sun, 23 Apr 2023 09:31:38 +0200 Subject: [PATCH] Allow removal of prefix when proxying to another server (#630) (#924) * #630 Allow removal of prefix when proxying to another server * #630 Rename replace to replace settings and ensure properties used in place of fields * #630 Update replace settings type name to ProxyUrlReplaceSettings * #630 Add admin model and update settings parser to parse new values * Fix formatting issues * #630 Ensure json mapping between admin model and internal model takes place * #630 Refactor parsing and structure of extracting new proxy url * Reduce function complexity * #630 Fix line length issues and remove try prefix from parser methods --- .../Settings/ProxyAndRecordSettingsModel.cs | 145 +++++++------- .../Settings/ProxyUrlReplaceSettingsModel.cs | 18 ++ src/WireMock.Net/Proxy/ProxyHelper.cs | 4 +- .../Settings/ProxyAndRecordSettings.cs | 186 +++++++++--------- .../Settings/ProxyUrlReplaceSettings.cs | 17 ++ .../Settings/WireMockServerSettingsParser.cs | 88 ++++++--- src/WireMock.Net/Util/TinyMapperUtils.cs | 58 +++--- .../WireMockServer.Proxy.cs | 48 ++++- 8 files changed, 351 insertions(+), 213 deletions(-) create mode 100644 src/WireMock.Net.Abstractions/Admin/Settings/ProxyUrlReplaceSettingsModel.cs create mode 100644 src/WireMock.Net/Settings/ProxyUrlReplaceSettings.cs diff --git a/src/WireMock.Net.Abstractions/Admin/Settings/ProxyAndRecordSettingsModel.cs b/src/WireMock.Net.Abstractions/Admin/Settings/ProxyAndRecordSettingsModel.cs index 1867dd9d..5bf9e95c 100644 --- a/src/WireMock.Net.Abstractions/Admin/Settings/ProxyAndRecordSettingsModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Settings/ProxyAndRecordSettingsModel.cs @@ -1,71 +1,76 @@ -namespace WireMock.Admin.Settings; - -[FluentBuilder.AutoGenerateBuilder] -public class ProxyAndRecordSettingsModel -{ - /// - /// The clientCertificate thumbprint or subject name fragment to use. - /// Example thumbprint : "D2DBF135A8D06ACCD0E1FAD9BFB28678DF7A9818". Example subject name: "www.google.com"" - /// - public string ClientX509Certificate2ThumbprintOrSubjectName { get; set; } - - /// - /// Defines the WebProxySettings. - /// - public WebProxySettingsModel WebProxySettings { get; set; } - - /// - /// Proxy requests should follow redirection (30x). - /// - public bool? AllowAutoRedirect { get; set; } - - /// - /// The URL to proxy. - /// - public string Url { get; set; } - - /// - /// Save the mapping for each request/response to the internal Mappings. - /// - public bool SaveMapping { get; set; } - - /// - /// Save the mapping for each request/response also to a file. (Note that SaveMapping must also be set to true.) - /// - public bool SaveMappingToFile { get; set; } - - /// - /// Only save request/response to the internal Mappings if the status code is included in this pattern. (Note that SaveMapping must also be set to true.) - /// The pattern can contain a single value like "200", but also ranges like "2xx", "100,300,600" or "100-299,6xx" are supported. - /// - public string SaveMappingForStatusCodePattern { get; set; } = "*"; - - /// - /// Defines a list from headers which will be excluded from the saved mappings. - /// - public string[] ExcludedHeaders { get; set; } - - /// - /// Defines a list of cookies which will be excluded from the saved mappings. - /// - public string[] ExcludedCookies { get; set; } - - /// - /// Prefer the Proxy Mapping over the saved Mapping (in case SaveMapping is set to true). - /// - // public bool PreferProxyMapping { get; set; } - - /// - /// When SaveMapping is set to true, this setting can be used to control the behavior of the generated request matchers for the new mapping. - /// - false, the default matchers will be used. - /// - true, the defined mappings in the request wil be used for the new mapping. - /// - /// Default value is false. - /// - public bool UseDefinedRequestMatchers { get; set; } - - /// - /// Append an unique GUID to the filename from the saved mapping file. - /// - public bool AppendGuidToSavedMappingFile { get; set; } +namespace WireMock.Admin.Settings; + +[FluentBuilder.AutoGenerateBuilder] +public class ProxyAndRecordSettingsModel +{ + /// + /// The clientCertificate thumbprint or subject name fragment to use. + /// Example thumbprint : "D2DBF135A8D06ACCD0E1FAD9BFB28678DF7A9818". Example subject name: "www.google.com"" + /// + public string ClientX509Certificate2ThumbprintOrSubjectName { get; set; } + + /// + /// Defines the WebProxySettings. + /// + public WebProxySettingsModel WebProxySettings { get; set; } + + /// + /// Proxy requests should follow redirection (30x). + /// + public bool? AllowAutoRedirect { get; set; } + + /// + /// The URL to proxy. + /// + public string Url { get; set; } + + /// + /// Save the mapping for each request/response to the internal Mappings. + /// + public bool SaveMapping { get; set; } + + /// + /// Save the mapping for each request/response also to a file. (Note that SaveMapping must also be set to true.) + /// + public bool SaveMappingToFile { get; set; } + + /// + /// Only save request/response to the internal Mappings if the status code is included in this pattern. (Note that SaveMapping must also be set to true.) + /// The pattern can contain a single value like "200", but also ranges like "2xx", "100,300,600" or "100-299,6xx" are supported. + /// + public string SaveMappingForStatusCodePattern { get; set; } = "*"; + + /// + /// Defines a list from headers which will be excluded from the saved mappings. + /// + public string[] ExcludedHeaders { get; set; } + + /// + /// Defines a list of cookies which will be excluded from the saved mappings. + /// + public string[] ExcludedCookies { get; set; } + + /// + /// Prefer the Proxy Mapping over the saved Mapping (in case SaveMapping is set to true). + /// + // public bool PreferProxyMapping { get; set; } + + /// + /// When SaveMapping is set to true, this setting can be used to control the behavior of the generated request matchers for the new mapping. + /// - false, the default matchers will be used. + /// - true, the defined mappings in the request wil be used for the new mapping. + /// + /// Default value is false. + /// + public bool UseDefinedRequestMatchers { get; set; } + + /// + /// Append an unique GUID to the filename from the saved mapping file. + /// + public bool AppendGuidToSavedMappingFile { get; set; } + + /// + /// Defines the Replace Settings + /// + public ProxyUrlReplaceSettingsModel? ReplaceSettings { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Admin/Settings/ProxyUrlReplaceSettingsModel.cs b/src/WireMock.Net.Abstractions/Admin/Settings/ProxyUrlReplaceSettingsModel.cs new file mode 100644 index 00000000..2073acb5 --- /dev/null +++ b/src/WireMock.Net.Abstractions/Admin/Settings/ProxyUrlReplaceSettingsModel.cs @@ -0,0 +1,18 @@ +namespace WireMock.Admin.Settings; + +/// +/// Defines an old path param and a new path param to be replaced when proxying. +/// +[FluentBuilder.AutoGenerateBuilder] +public class ProxyUrlReplaceSettingsModel +{ + /// + /// The old path value to be replaced by the new path value + /// + public string OldValue { get; set; } = null!; + + /// + /// The new path value to replace the old value with + /// + public string NewValue { get; set; } = null!; +} \ No newline at end of file diff --git a/src/WireMock.Net/Proxy/ProxyHelper.cs b/src/WireMock.Net/Proxy/ProxyHelper.cs index 519a6056..cd0c279c 100644 --- a/src/WireMock.Net/Proxy/ProxyHelper.cs +++ b/src/WireMock.Net/Proxy/ProxyHelper.cs @@ -37,7 +37,9 @@ internal class ProxyHelper var requiredUri = new Uri(url); // Create HttpRequestMessage - var httpRequestMessage = HttpRequestMessageHelper.Create(requestMessage, url); + var replaceSettings = proxyAndRecordSettings.ReplaceSettings; + var proxyUrl = replaceSettings is not null ? url.Replace(replaceSettings.OldValue, replaceSettings.NewValue) : url; + var httpRequestMessage = HttpRequestMessageHelper.Create(requestMessage, proxyUrl); // Call the URL var httpResponseMessage = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false); diff --git a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs index 719a04fc..2ca3fda5 100644 --- a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs +++ b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs @@ -1,91 +1,97 @@ -using JetBrains.Annotations; - -namespace WireMock.Settings; - -/// -/// ProxyAndRecordSettings -/// -public class ProxyAndRecordSettings : HttpClientSettings -{ - /// - /// The URL to proxy. - /// - [PublicAPI] - public string Url { get; set; } = null!; - - /// - /// Save the mapping for each request/response to the internal Mappings. - /// - [PublicAPI] - public bool SaveMapping { get; set; } - - /// - /// Save the mapping for each request/response also to a file. (Note that SaveMapping must also be set to true.) - /// - [PublicAPI] - public bool SaveMappingToFile { get; set; } - - /// - /// Only save request/response to the internal Mappings if the status code is included in this pattern. (Note that SaveMapping must also be set to true.) - /// The pattern can contain a single value like "200", but also ranges like "2xx", "100,300,600" or "100-299,6xx" are supported. - /// - /// Deprecated : use SaveMappingSettings. - /// - [PublicAPI] - public string SaveMappingForStatusCodePattern - { - set - { - if (SaveMappingSettings is null) - { - SaveMappingSettings = new ProxySaveMappingSettings(); - } - - SaveMappingSettings.StatusCodePattern = value; - } - } - - /// - /// Additional SaveMappingSettings. - /// - [PublicAPI] - public ProxySaveMappingSettings? SaveMappingSettings { get; set; } - - /// - /// Defines a list from headers which will be excluded from the saved mappings. - /// - [PublicAPI] - public string[]? ExcludedHeaders { get; set; } - - /// - /// Defines a list of params which will be excluded from the saved mappings. - /// - [PublicAPI] - public string[]? ExcludedParams { get; set; } - - /// - /// Defines a list of cookies which will be excluded from the saved mappings. - /// - [PublicAPI] - public string[]? ExcludedCookies { get; set; } - - /// - /// Prefer the Proxy Mapping over the saved Mapping (in case SaveMapping is set to true). - /// - //[PublicAPI] - //public bool PreferProxyMapping { get; set; } - - /// - /// When SaveMapping is set to true, this setting can be used to control the behavior of the generated request matchers for the new mapping. - /// - false, the default matchers will be used. - /// - true, the defined mappings in the request wil be used for the new mapping. - /// - /// Default value is false. - /// - public bool UseDefinedRequestMatchers { get; set; } - - /// - /// Append an unique GUID to the filename from the saved mapping file. - /// - public bool AppendGuidToSavedMappingFile { get; set; } +using JetBrains.Annotations; + +namespace WireMock.Settings; + +/// +/// ProxyAndRecordSettings +/// +public class ProxyAndRecordSettings : HttpClientSettings +{ + /// + /// The URL to proxy. + /// + [PublicAPI] + public string Url { get; set; } = null!; + + /// + /// Save the mapping for each request/response to the internal Mappings. + /// + [PublicAPI] + public bool SaveMapping { get; set; } + + /// + /// Save the mapping for each request/response also to a file. (Note that SaveMapping must also be set to true.) + /// + [PublicAPI] + public bool SaveMappingToFile { get; set; } + + /// + /// Only save request/response to the internal Mappings if the status code is included in this pattern. (Note that SaveMapping must also be set to true.) + /// The pattern can contain a single value like "200", but also ranges like "2xx", "100,300,600" or "100-299,6xx" are supported. + /// + /// Deprecated : use SaveMappingSettings. + /// + [PublicAPI] + public string SaveMappingForStatusCodePattern + { + set + { + if (SaveMappingSettings is null) + { + SaveMappingSettings = new ProxySaveMappingSettings(); + } + + SaveMappingSettings.StatusCodePattern = value; + } + } + + /// + /// Additional SaveMappingSettings. + /// + [PublicAPI] + public ProxySaveMappingSettings? SaveMappingSettings { get; set; } + + /// + /// Defines a list from headers which will be excluded from the saved mappings. + /// + [PublicAPI] + public string[]? ExcludedHeaders { get; set; } + + /// + /// Defines a list of params which will be excluded from the saved mappings. + /// + [PublicAPI] + public string[]? ExcludedParams { get; set; } + + /// + /// Defines a list of cookies which will be excluded from the saved mappings. + /// + [PublicAPI] + public string[]? ExcludedCookies { get; set; } + + /// + /// Replace Settings + /// + [PublicAPI] + public ProxyUrlReplaceSettings? ReplaceSettings { get; set; } + + /// + /// Prefer the Proxy Mapping over the saved Mapping (in case SaveMapping is set to true). + /// + //[PublicAPI] + //public bool PreferProxyMapping { get; set; } + + /// + /// When SaveMapping is set to true, this setting can be used to control the behavior of the generated request matchers for the new mapping. + /// - false, the default matchers will be used. + /// - true, the defined mappings in the request wil be used for the new mapping. + /// + /// Default value is false. + /// + public bool UseDefinedRequestMatchers { get; set; } + + /// + /// Append an unique GUID to the filename from the saved mapping file. + /// + public bool AppendGuidToSavedMappingFile { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/ProxyUrlReplaceSettings.cs b/src/WireMock.Net/Settings/ProxyUrlReplaceSettings.cs new file mode 100644 index 00000000..11da3362 --- /dev/null +++ b/src/WireMock.Net/Settings/ProxyUrlReplaceSettings.cs @@ -0,0 +1,17 @@ +namespace WireMock.Settings; + +/// +/// Defines an old path param and a new path param to be replaced when proxying. +/// +public class ProxyUrlReplaceSettings +{ + /// + /// The old path value to be replaced by the new path value + /// + public string OldValue { get; set; } = null!; + + /// + /// The new path value to replace the old value with + /// + public string NewValue { get; set; } = null!; +} \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs b/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs index f4c6c5be..b8caea0e 100644 --- a/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs +++ b/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs @@ -21,7 +21,8 @@ public static class WireMockServerSettingsParser /// The logger (optional, can be null) /// The parsed settings [PublicAPI] - public static bool TryParseArguments(string[] args, [NotNullWhen(true)] out WireMockServerSettings? settings, IWireMockLogger? logger = null) + public static bool TryParseArguments(string[] args, [NotNullWhen(true)] out WireMockServerSettings? settings, + IWireMockLogger? logger = null) { Guard.HasNoNulls(args); @@ -70,6 +71,17 @@ public static class WireMockServerSettingsParser settings.AcceptAnyClientCertificate = parser.GetBoolValue(nameof(WireMockServerSettings.AcceptAnyClientCertificate)); #endif + ParseLoggerSettings(settings, logger, parser); + ParsePortSettings(settings, parser); + ParseProxyAndRecordSettings(settings, parser); + ParseCertificateSettings(settings, parser); + + return true; + } + + private static void ParseLoggerSettings(WireMockServerSettings settings, IWireMockLogger? logger, + SimpleCommandLineParser parser) + { var loggerType = parser.GetStringValue("WireMockLogger"); switch (loggerType) { @@ -86,22 +98,17 @@ public static class WireMockServerSettingsParser { settings.Logger = logger; } + break; } + } - if (parser.Contains(nameof(WireMockServerSettings.Port))) - { - settings.Port = parser.GetIntValue(nameof(WireMockServerSettings.Port)); - } - else if (settings.HostingScheme is null) - { - settings.Urls = parser.GetValues("Urls", new[] { "http://*:9091/" }); - } - + private static void ParseProxyAndRecordSettings(WireMockServerSettings settings, SimpleCommandLineParser parser) + { var proxyUrl = parser.GetStringValue("ProxyURL") ?? parser.GetStringValue("ProxyUrl"); if (!string.IsNullOrEmpty(proxyUrl)) { - settings.ProxyAndRecordSettings = new ProxyAndRecordSettings + var proxyAndRecordSettings = new ProxyAndRecordSettings { AllowAutoRedirect = parser.GetBoolValue("AllowAutoRedirect"), ClientX509Certificate2ThumbprintOrSubjectName = parser.GetStringValue("ClientX509Certificate2ThumbprintOrSubjectName"), @@ -121,18 +128,27 @@ public static class WireMockServerSettingsParser } }; - string? proxyAddress = parser.GetStringValue("WebProxyAddress"); - if (!string.IsNullOrEmpty(proxyAddress)) - { - settings.ProxyAndRecordSettings.WebProxySettings = new WebProxySettings - { - Address = proxyAddress!, - UserName = parser.GetStringValue("WebProxyUserName"), - Password = parser.GetStringValue("WebProxyPassword") - }; - } - } + ParseWebProxyAddressSettings(proxyAndRecordSettings, parser); + ParseProxyUrlReplaceSettings(proxyAndRecordSettings, parser); + settings.ProxyAndRecordSettings = proxyAndRecordSettings; + } + } + + private static void ParsePortSettings(WireMockServerSettings settings, SimpleCommandLineParser parser) + { + if (parser.Contains(nameof(WireMockServerSettings.Port))) + { + settings.Port = parser.GetIntValue(nameof(WireMockServerSettings.Port)); + } + else if (settings.HostingScheme is null) + { + settings.Urls = parser.GetValues("Urls", new[] { "http://*:9091/" }); + } + } + + private static void ParseCertificateSettings(WireMockServerSettings settings, SimpleCommandLineParser parser) + { var certificateSettings = new WireMockCertificateSettings { X509StoreName = parser.GetStringValue("X509StoreName"), @@ -145,7 +161,33 @@ public static class WireMockServerSettingsParser { settings.CertificateSettings = certificateSettings; } + } - return true; + private static void ParseWebProxyAddressSettings(ProxyAndRecordSettings settings, SimpleCommandLineParser parser) + { + string? proxyAddress = parser.GetStringValue("WebProxyAddress"); + if (!string.IsNullOrEmpty(proxyAddress)) + { + settings.WebProxySettings = new WebProxySettings + { + Address = proxyAddress!, + UserName = parser.GetStringValue("WebProxyUserName"), + Password = parser.GetStringValue("WebProxyPassword") + }; + } + } + + private static void ParseProxyUrlReplaceSettings(ProxyAndRecordSettings settings, SimpleCommandLineParser parser) + { + var proxyUrlReplaceOldValue = parser.GetStringValue("ProxyUrlReplaceOldValue"); + var proxyUrlReplaceNewValue = parser.GetStringValue("ProxyUrlReplaceNewValue"); + if (!string.IsNullOrEmpty(proxyUrlReplaceOldValue) && proxyUrlReplaceNewValue != null) + { + settings.ReplaceSettings = new ProxyUrlReplaceSettings + { + OldValue = proxyUrlReplaceOldValue!, + NewValue = proxyUrlReplaceNewValue! + }; + } } } \ No newline at end of file diff --git a/src/WireMock.Net/Util/TinyMapperUtils.cs b/src/WireMock.Net/Util/TinyMapperUtils.cs index 3bc9fb6d..06de691a 100644 --- a/src/WireMock.Net/Util/TinyMapperUtils.cs +++ b/src/WireMock.Net/Util/TinyMapperUtils.cs @@ -1,29 +1,31 @@ -using Nelibur.ObjectMapper; -using WireMock.Admin.Settings; -using WireMock.Settings; - -namespace WireMock.Util; - -internal sealed class TinyMapperUtils -{ - public static TinyMapperUtils Instance { get; } = new(); - - private TinyMapperUtils() - { - TinyMapper.Bind(); - TinyMapper.Bind(); - - TinyMapper.Bind(); - TinyMapper.Bind(); - } - - public ProxyAndRecordSettingsModel? Map(ProxyAndRecordSettings? instance) - { - return instance == null ? null : TinyMapper.Map(instance); - } - - public ProxyAndRecordSettings? Map(ProxyAndRecordSettingsModel? model) - { - return model == null ? null : TinyMapper.Map(model); - } +using Nelibur.ObjectMapper; +using WireMock.Admin.Settings; +using WireMock.Settings; + +namespace WireMock.Util; + +internal sealed class TinyMapperUtils +{ + public static TinyMapperUtils Instance { get; } = new(); + + private TinyMapperUtils() + { + TinyMapper.Bind(); + TinyMapper.Bind(); + TinyMapper.Bind(); + + TinyMapper.Bind(); + TinyMapper.Bind(); + TinyMapper.Bind(); + } + + public ProxyAndRecordSettingsModel? Map(ProxyAndRecordSettings? instance) + { + return instance == null ? null : TinyMapper.Map(instance); + } + + public ProxyAndRecordSettings? Map(ProxyAndRecordSettingsModel? model) + { + return model == null ? null : TinyMapper.Map(model); + } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMockServer.Proxy.cs b/test/WireMock.Net.Tests/WireMockServer.Proxy.cs index 25cc876c..8d4d7273 100644 --- a/test/WireMock.Net.Tests/WireMockServer.Proxy.cs +++ b/test/WireMock.Net.Tests/WireMockServer.Proxy.cs @@ -570,6 +570,52 @@ public class WireMockServerProxyTests Check.That(matchers).Contains("name"); } + [Fact] + public async Task WireMockServer_Proxy_Should_replace_old_path_value_with_new_path_value_in_replace_settings() + { + // Assign + var replaceSettings = new ProxyUrlReplaceSettings + { + OldValue = "value-to-replace", + NewValue = "new-value" + }; + string path = $"/prx_{Guid.NewGuid()}"; + var serverForProxyForwarding = WireMockServer.Start(); + serverForProxyForwarding + .Given(Request.Create().WithPath($"/{replaceSettings.NewValue}{path}")) + .RespondWith(Response.Create()); + + var settings = new WireMockServerSettings + { + ProxyAndRecordSettings = new ProxyAndRecordSettings + { + Url = serverForProxyForwarding.Urls[0], + SaveMapping = true, + SaveMappingToFile = false, + ReplaceSettings = replaceSettings + } + }; + var server = WireMockServer.Start(settings); + var defaultMapping = server.Mappings.First(); + + // Act + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri($"{server.Urls[0]}/{replaceSettings.OldValue}{path}"), + Content = new StringContent("stringContent") + }; + + var handler = new HttpClientHandler(); + await new HttpClient(handler).SendAsync(requestMessage).ConfigureAwait(false); + + // Assert + var mapping = serverForProxyForwarding.Mappings.FirstOrDefault(m => m.Guid != defaultMapping.Guid); + var score = mapping.RequestMatcher.GetMatchingScore(serverForProxyForwarding.LogEntries.First().RequestMessage, + new RequestMatchResult()); + Check.That(score).IsEqualTo(1.0); + } + [Fact] public async Task WireMockServer_Proxy_Should_preserve_content_header_in_proxied_request_with_empty_content() { @@ -701,7 +747,7 @@ public class WireMockServerProxyTests /// /// Send some binary content in a request through the proxy and check that the same content /// arrived at the target. As example a JPEG/JIFF header is used, which is not representable - /// in UTF8 and breaks if it is not treated as binary content. + /// in UTF8 and breaks if it is not treated as binary content. /// [Fact] public async Task WireMockServer_Proxy_Should_preserve_binary_request_content()