From d6dc8254014e9c693af062fef7cb4470ed073a40 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Mon, 1 Apr 2019 22:49:45 +0200 Subject: [PATCH 1/4] ExactObjectMatcher --- Directory.Build.props | 2 +- .../Program.cs | 16 ++++++-- .../Matchers/ExactObjectMatcher.cs | 15 +++---- src/WireMock.Net/ResponseMessageBuilder.cs | 5 ++- .../Serialization/MatcherMapper.cs | 41 +++++++++++++------ .../Server/FluentMockServer.Admin.cs | 18 +++++--- .../FluentMockServerTests.Proxy.cs | 30 ++++++++++++++ .../Serialization/MatcherModelMapperTests.cs | 21 +++++++++- 8 files changed, 116 insertions(+), 32 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index a0b6b3eaf..8031a4fb3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ - 1.0.11 + 1.0.12 diff --git a/examples/WireMock.Net.Console.Proxy.Net452/Program.cs b/examples/WireMock.Net.Console.Proxy.Net452/Program.cs index 5a0c5d526..d635c91a1 100644 --- a/examples/WireMock.Net.Console.Proxy.Net452/Program.cs +++ b/examples/WireMock.Net.Console.Proxy.Net452/Program.cs @@ -1,4 +1,6 @@ -using Newtonsoft.Json; +using System; +using System.Net.Http; +using Newtonsoft.Json; using WireMock.Server; using WireMock.Settings; @@ -8,14 +10,15 @@ class Program { static void Main(string[] args) { + string[] urls = { "http://localhost:9091/", "https://localhost:9443/" }; var server = FluentMockServer.Start(new FluentMockServerSettings { - Urls = new[] { "http://localhost:9091/", "https://localhost:9443/" }, + Urls = urls, StartAdminInterface = true, ReadStaticMappings = false, ProxyAndRecordSettings = new ProxyAndRecordSettings { - Url = "https://www.google.com", + Url = "http://postman-echo.com/post", //ClientX509Certificate2ThumbprintOrSubjectName = "www.yourclientcertname.com OR yourcertificatethumbprint (only if the service you're proxying to requires it)", SaveMapping = true, SaveMappingToFile = false, @@ -28,6 +31,13 @@ static void Main(string[] args) System.Console.WriteLine(JsonConvert.SerializeObject(eventRecordArgs.NewItems, Formatting.Indented)); }; + var uri = new Uri(urls[0]); + var form = new MultipartFormDataContent + { + { new StringContent("data"), "test", "test.txt" } + }; + new HttpClient().PostAsync(uri, form).GetAwaiter().GetResult(); + System.Console.WriteLine("Press any key to stop the server"); System.Console.ReadKey(); server.Stop(); diff --git a/src/WireMock.Net/Matchers/ExactObjectMatcher.cs b/src/WireMock.Net/Matchers/ExactObjectMatcher.cs index 4f8847c72..bfe87509a 100644 --- a/src/WireMock.Net/Matchers/ExactObjectMatcher.cs +++ b/src/WireMock.Net/Matchers/ExactObjectMatcher.cs @@ -1,5 +1,5 @@ -using System.Linq; -using JetBrains.Annotations; +using JetBrains.Annotations; +using System.Linq; using WireMock.Validation; namespace WireMock.Matchers @@ -10,8 +10,9 @@ namespace WireMock.Matchers /// public class ExactObjectMatcher : IObjectMatcher { - private readonly object _object; - private readonly byte[] _bytes; + public object ValueAsObject { get; } + + public byte[] ValueAsBytes { get; } /// public MatchBehaviour MatchBehaviour { get; } @@ -33,7 +34,7 @@ public ExactObjectMatcher(MatchBehaviour matchBehaviour, [NotNull] object value) { Check.NotNull(value, nameof(value)); - _object = value; + ValueAsObject = value; MatchBehaviour = matchBehaviour; } @@ -54,14 +55,14 @@ public ExactObjectMatcher(MatchBehaviour matchBehaviour, [NotNull] byte[] value) { Check.NotNull(value, nameof(value)); - _bytes = value; + ValueAsBytes = value; MatchBehaviour = matchBehaviour; } /// public double IsMatch(object input) { - bool equals = _object != null ? Equals(_object, input) : _bytes.SequenceEqual((byte[])input); + bool equals = ValueAsObject != null ? Equals(ValueAsObject, input) : ValueAsBytes.SequenceEqual((byte[])input); return MatchBehaviourHelper.Convert(MatchBehaviour, MatchScores.ToScore(equals)); } diff --git a/src/WireMock.Net/ResponseMessageBuilder.cs b/src/WireMock.Net/ResponseMessageBuilder.cs index b4903e896..7a14ffd90 100644 --- a/src/WireMock.Net/ResponseMessageBuilder.cs +++ b/src/WireMock.Net/ResponseMessageBuilder.cs @@ -9,7 +9,10 @@ namespace WireMock internal static class ResponseMessageBuilder { private static string ContentTypeJson = "application/json"; - private static readonly IDictionary> ContentTypeJsonHeaders = new Dictionary> { { HttpKnownHeaderNames.ContentType, new WireMockList { ContentTypeJson } } }; + private static readonly IDictionary> ContentTypeJsonHeaders = new Dictionary> + { + { HttpKnownHeaderNames.ContentType, new WireMockList { ContentTypeJson } } + }; internal static ResponseMessage Create(string message, int statusCode = 200, Guid? guid = null) { diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs index 53aabf27f..57b128e5f 100644 --- a/src/WireMock.Net/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net/Serialization/MatcherMapper.cs @@ -1,8 +1,8 @@ -using System; +using JetBrains.Annotations; +using SimMetrics.Net; +using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; -using SimMetrics.Net; using WireMock.Admin.Mappings; using WireMock.Matchers; @@ -32,6 +32,10 @@ public static IMatcher Map([CanBeNull] MatcherModel matcher) case "ExactMatcher": return new ExactMatcher(matchBehaviour, stringPatterns); + case "ExactObjectMatcher": + var bytePattern = Convert.FromBase64String(stringPatterns[0]); + return new ExactObjectMatcher(matchBehaviour, bytePattern); + case "RegexMatcher": return new RegexMatcher(matchBehaviour, stringPatterns, matcher.IgnoreCase == true); @@ -73,20 +77,33 @@ public static MatcherModel Map([CanBeNull] IMatcher matcher) return null; } - // If the matcher is a IStringMatcher, get the patterns. - // If the matcher is a IValueMatcher, get the value (can be string or object). - // Else empty array - object[] patterns = matcher is IStringMatcher stringMatcher ? - stringMatcher.GetPatterns().Cast().ToArray() : - matcher is IValueMatcher valueMatcher ? new[] { valueMatcher.Value } : - new object[0]; - bool? ignorecase = matcher is IIgnoreCaseMatcher ignoreCaseMatcher ? ignoreCaseMatcher.IgnoreCase : (bool?)null; + object[] patterns = new object[0]; // Default empty array + switch (matcher) + { + // If the matcher is a IStringMatcher, get the patterns. + case IStringMatcher stringMatcher: + patterns = stringMatcher.GetPatterns().Cast().ToArray(); + break; + + // If the matcher is a IValueMatcher, get the value (can be string or object). + case IValueMatcher valueMatcher: + patterns = new[] { valueMatcher.Value }; + break; + + // If the matcher is a ExactObjectMatcher, get the ValueAsObject or ValueAsBytes. + case ExactObjectMatcher exactObjectMatcher: + patterns = new[] { exactObjectMatcher.ValueAsObject ?? exactObjectMatcher.ValueAsBytes }; + break; + } + + bool? ignoreCase = matcher is IIgnoreCaseMatcher ignoreCaseMatcher ? ignoreCaseMatcher.IgnoreCase : (bool?)null; + bool? rejectOnMatch = matcher.MatchBehaviour == MatchBehaviour.RejectOnMatch ? true : (bool?)null; return new MatcherModel { RejectOnMatch = rejectOnMatch, - IgnoreCase = ignorecase, + IgnoreCase = ignoreCase, Name = matcher.Name, Pattern = patterns.Length == 1 ? patterns.First() : null, Patterns = patterns.Length > 1 ? patterns : null diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs index 84fa00572..4919d0317 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -277,13 +277,19 @@ private IMapping ToMapping(RequestMessage requestMessage, ResponseMessage respon } }); - if (requestMessage.BodyData?.DetectedBodyType == BodyType.Json) + switch (requestMessage.BodyData?.DetectedBodyType) { - request.WithBody(new JsonMatcher(MatchBehaviour.AcceptOnMatch, requestMessage.BodyData.BodyAsJson)); - } - else if (requestMessage.BodyData?.DetectedBodyType == BodyType.String) - { - request.WithBody(new ExactMatcher(MatchBehaviour.AcceptOnMatch, requestMessage.BodyData.BodyAsString)); + case BodyType.Json: + request.WithBody(new JsonMatcher(MatchBehaviour.AcceptOnMatch, requestMessage.BodyData.BodyAsJson)); + break; + + case BodyType.String: + request.WithBody(new ExactMatcher(MatchBehaviour.AcceptOnMatch, requestMessage.BodyData.BodyAsString)); + break; + + case BodyType.Bytes: + request.WithBody(new ExactObjectMatcher(MatchBehaviour.AcceptOnMatch, requestMessage.BodyData.BodyAsBytes)); + break; } var response = Response.Create(responseMessage); diff --git a/test/WireMock.Net.Tests/FluentMockServerTests.Proxy.cs b/test/WireMock.Net.Tests/FluentMockServerTests.Proxy.cs index 95fd452a1..bef5e04fe 100644 --- a/test/WireMock.Net.Tests/FluentMockServerTests.Proxy.cs +++ b/test/WireMock.Net.Tests/FluentMockServerTests.Proxy.cs @@ -290,6 +290,36 @@ public async Task FluentMockServer_Proxy_Should_set_BodyAsJson_in_proxied_respon Check.That(response.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/json; charset=utf-8"); } + [Fact] + public async Task FluentMockServer_Proxy_Should_set_Body_in_multipart_proxied_response() + { + // Assign + string path = $"/prx_{Guid.NewGuid().ToString()}"; + var serverForProxyForwarding = FluentMockServer.Start(); + serverForProxyForwarding + .Given(Request.Create().WithPath(path)) + .RespondWith(Response.Create() + .WithBodyAsJson(new { i = 42 }) + ); + + var server = FluentMockServer.Start(); + server + .Given(Request.Create().WithPath(path)) + .RespondWith(Response.Create().WithProxy(serverForProxyForwarding.Urls[0])); + + // Act + var uri = new Uri($"{server.Urls[0]}{path}"); + var form = new MultipartFormDataContent + { + { new StringContent("data"), "test", "test.txt" } + }; + var response = await new HttpClient().PostAsync(uri, form); + + // Assert + string content = await response.Content.ReadAsStringAsync(); + Check.That(content).IsEqualTo("{\"i\":42}"); + } + [Fact] public async Task FluentMockServer_Proxy_Should_Not_overrule_AdminMappings() { diff --git a/test/WireMock.Net.Tests/Serialization/MatcherModelMapperTests.cs b/test/WireMock.Net.Tests/Serialization/MatcherModelMapperTests.cs index 86a8a6a4f..860571d5c 100644 --- a/test/WireMock.Net.Tests/Serialization/MatcherModelMapperTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MatcherModelMapperTests.cs @@ -1,5 +1,5 @@ -using System; -using NFluent; +using NFluent; +using System; using WireMock.Admin.Mappings; using WireMock.Matchers; using WireMock.Serialization; @@ -53,6 +53,23 @@ public void MatcherModelMapper_Map_ExactMatcher_Patterns() Check.That(matcher.GetPatterns()).ContainsExactly("x", "y"); } + [Fact] + public void MatcherModelMapper_Map_ExactObjectMatcher_Pattern() + { + // Assign + var model = new MatcherModel + { + Name = "ExactObjectMatcher", + Patterns = new object[] { "c3RlZg==" } + }; + + // Act + var matcher = (ExactObjectMatcher)MatcherMapper.Map(model); + + // Assert + Check.That(matcher.ValueAsBytes).ContainsExactly(new byte[] { 115, 116, 101, 102 }); + } + [Fact] public void MatcherModelMapper_Map_RegexMatcher() { From 54f49ee18d22afd4ace8cbfcda923bcdde797112 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 2 Apr 2019 08:45:22 +0200 Subject: [PATCH 2/4] BytesEncodingUtils --- src/WireMock.Net/Util/BytesEncodingUtils.cs | 230 ++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 src/WireMock.Net/Util/BytesEncodingUtils.cs diff --git a/src/WireMock.Net/Util/BytesEncodingUtils.cs b/src/WireMock.Net/Util/BytesEncodingUtils.cs new file mode 100644 index 000000000..8635448b6 --- /dev/null +++ b/src/WireMock.Net/Util/BytesEncodingUtils.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace WireMock.Util +{ + /// + /// Based on: + /// http://utf8checker.codeplex.com + /// https://github.com/0x53A/Mvvm/blob/master/src/Mvvm/src/Utf8Checker.cs + /// + /// References: + /// http://anubis.dkuug.dk/JTC1/SC2/WG2/docs/n1335 + /// http://www.cl.cam.ac.uk/~mgk25/ucs/ISO-10646-UTF-8.html + /// http://www.unicode.org/versions/corrigendum1.html + /// http://www.ietf.org/rfc/rfc2279.txt + /// + public static class BytesEncodingUtils + { + /// + /// Tries the get the Encoding from an array of bytes. + /// + /// The bytes. + /// The output encoding. + public static bool TryGetEncoding(byte[] bytes, out Encoding encoding) + { + encoding = null; + if (bytes.All(b => b < 80)) + { + encoding = Encoding.ASCII; + return true; + } + + if (StartsWith(bytes, new byte[] { 0xff, 0xfe, 0x00, 0x00 })) + { + encoding = Encoding.UTF32; + return true; + } + + if (StartsWith(bytes, new byte[] { 0xfe, 0xff })) + { + encoding = Encoding.BigEndianUnicode; + return true; + } + + if (StartsWith(bytes, new byte[] { 0xff, 0xfe })) + { + encoding = Encoding.Unicode; + return true; + } + + if (StartsWith(bytes, new byte[] { 0xef, 0xbb, 0xbf })) + { + encoding = Encoding.UTF8; + return true; + } + + if (IsUtf8(bytes, bytes.Length)) + { + encoding = new UTF8Encoding(false); + return true; + } + + return false; + } + + private static bool StartsWith(IEnumerable data, IReadOnlyCollection other) + { + byte[] arraySelf = data.Take(other.Count).ToArray(); + return other.SequenceEqual(arraySelf); + } + + private static bool IsUtf8(IReadOnlyList buffer, int length) + { + int position = 0; + int bytes = 0; + while (position < length) + { + if (!IsValid(buffer, position, length, ref bytes)) + { + return false; + } + position += bytes; + } + return true; + } + +#pragma warning disable S3776 // Cognitive Complexity of methods should not be too high + private static bool IsValid(IReadOnlyList buffer, int position, int length, ref int bytes) + { + if (length > buffer.Count) + { + throw new ArgumentException("Invalid length"); + } + + if (position > length - 1) + { + bytes = 0; + return true; + } + + byte ch = buffer[position]; + if (ch <= 0x7F) + { + bytes = 1; + return true; + } + + if (ch >= 0xc2 && ch <= 0xdf) + { + if (position >= length - 2) + { + bytes = 0; + return false; + } + + if (buffer[position + 1] < 0x80 || buffer[position + 1] > 0xbf) + { + bytes = 0; + return false; + } + + bytes = 2; + return true; + } + + if (ch == 0xe0) + { + if (position >= length - 3) + { + bytes = 0; + return false; + } + + if (buffer[position + 1] < 0xa0 || buffer[position + 1] > 0xbf || + buffer[position + 2] < 0x80 || buffer[position + 2] > 0xbf) + { + bytes = 0; + return false; + } + + bytes = 3; + return true; + } + + if (ch >= 0xe1 && ch <= 0xef) + { + if (position >= length - 3) + { + bytes = 0; + return false; + } + + if (buffer[position + 1] < 0x80 || buffer[position + 1] > 0xbf || + buffer[position + 2] < 0x80 || buffer[position + 2] > 0xbf) + { + bytes = 0; + return false; + } + + bytes = 3; + return true; + } + + if (ch == 0xf0) + { + if (position >= length - 4) + { + bytes = 0; + return false; + } + + if (buffer[position + 1] < 0x90 || buffer[position + 1] > 0xbf || + buffer[position + 2] < 0x80 || buffer[position + 2] > 0xbf || + buffer[position + 3] < 0x80 || buffer[position + 3] > 0xbf) + { + bytes = 0; + return false; + } + + bytes = 4; + return true; + } + + if (ch == 0xf4) + { + if (position >= length - 4) + { + bytes = 0; + return false; + } + + if (buffer[position + 1] < 0x80 || buffer[position + 1] > 0x8f || + buffer[position + 2] < 0x80 || buffer[position + 2] > 0xbf || + buffer[position + 3] < 0x80 || buffer[position + 3] > 0xbf) + { + bytes = 0; + return false; + } + + bytes = 4; + return true; + } + + if (ch >= 0xf1 && ch <= 0xf3) + { + if (position >= length - 4) + { + bytes = 0; + return false; + } + + if (buffer[position + 1] < 0x80 || buffer[position + 1] > 0xbf || + buffer[position + 2] < 0x80 || buffer[position + 2] > 0xbf || + buffer[position + 3] < 0x80 || buffer[position + 3] > 0xbf) + { + bytes = 0; + return false; + } + + bytes = 4; + return true; + } + + return false; + } + } +#pragma warning restore S3776 // Cognitive Complexity of methods should not be too high +} \ No newline at end of file From 24f4d0f6346b478e0fd411048f804a110aa079fc Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 2 Apr 2019 14:11:09 +0200 Subject: [PATCH 3/4] BodyParser --- src/WireMock.Net/Util/BodyParser.cs | 13 ++++++++++- .../Util/BodyParserTests.cs | 22 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/WireMock.Net/Util/BodyParser.cs b/src/WireMock.Net/Util/BodyParser.cs index 7bd98f744..d6f4cde93 100644 --- a/src/WireMock.Net/Util/BodyParser.cs +++ b/src/WireMock.Net/Util/BodyParser.cs @@ -14,6 +14,7 @@ namespace WireMock.Util internal static class BodyParser { private static readonly Encoding DefaultEncoding = Encoding.UTF8; + private static readonly Encoding[] SupportedBodyAsStringEncodingForMultipart = { Encoding.UTF8, Encoding.UTF8 }; /* HEAD - No defined body semantics. @@ -91,9 +92,19 @@ public static async Task Parse([NotNull] Stream stream, [CanBeNull] st DetectedBodyTypeFromContentType = DetectBodyTypeFromContentType(contentType) }; - // In case of MultiPart: never try to read as String but keep as-is + // In case of MultiPart: check if the BodyAsBytes is a valid UTF8 or ASCII string, in that case read as String else keep as-is if (data.DetectedBodyTypeFromContentType == BodyType.MultiPart) { + if (BytesEncodingUtils.TryGetEncoding(data.BodyAsBytes, out Encoding encoding) && + SupportedBodyAsStringEncodingForMultipart.Select(x => x.Equals(encoding)).Any()) + { + data.BodyAsString = encoding.GetString(data.BodyAsBytes); + data.Encoding = encoding; + data.DetectedBodyType = BodyType.String; + + return data; + } + return data; } diff --git a/test/WireMock.Net.Tests/Util/BodyParserTests.cs b/test/WireMock.Net.Tests/Util/BodyParserTests.cs index a5425b2ae..62fa4ebb9 100644 --- a/test/WireMock.Net.Tests/Util/BodyParserTests.cs +++ b/test/WireMock.Net.Tests/Util/BodyParserTests.cs @@ -51,7 +51,7 @@ public async Task BodyParser_Parse_ContentTypeString(string contentType, string } [Fact] - public async Task BodyParser_Parse_ContentTypeMultipart() + public async Task BodyParser_Parse_WithUTF8EncodingAndContentTypeMultipart_DetectedBodyTypeEqualsString() { // Arrange string contentType = "multipart/form-data"; @@ -80,6 +80,26 @@ Content of a txt // Act var result = await BodyParser.Parse(memoryStream, contentType); + // Assert + Check.That(result.DetectedBodyType).IsEqualTo(BodyType.String); + Check.That(result.DetectedBodyTypeFromContentType).IsEqualTo(BodyType.MultiPart); + Check.That(result.BodyAsBytes).IsNotNull(); + Check.That(result.BodyAsJson).IsNull(); + Check.That(result.BodyAsString).IsNotNull(); + } + + [Fact] + public async Task BodyParser_Parse_WithUTF16EncodingAndContentTypeMultipart_DetectedBodyTypeEqualsString() + { + // Arrange + string contentType = "multipart/form-data"; + string body = char.ConvertFromUtf32(0x1D161); //U+1D161 = MUSICAL SYMBOL SIXTEENTH NOTE + + var memoryStream = new MemoryStream(Encoding.UTF32.GetBytes(body)); + + // Act + var result = await BodyParser.Parse(memoryStream, contentType); + // Assert Check.That(result.DetectedBodyType).IsEqualTo(BodyType.Bytes); Check.That(result.DetectedBodyTypeFromContentType).IsEqualTo(BodyType.MultiPart); From 4f4e2dd67ffe23eef4dcf5b073c0c8e4e9a168c7 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 2 Apr 2019 15:43:23 +0200 Subject: [PATCH 4/4] Encoding.ASCII --- src/WireMock.Net/Util/BodyParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WireMock.Net/Util/BodyParser.cs b/src/WireMock.Net/Util/BodyParser.cs index d6f4cde93..e29a05bcd 100644 --- a/src/WireMock.Net/Util/BodyParser.cs +++ b/src/WireMock.Net/Util/BodyParser.cs @@ -14,7 +14,7 @@ namespace WireMock.Util internal static class BodyParser { private static readonly Encoding DefaultEncoding = Encoding.UTF8; - private static readonly Encoding[] SupportedBodyAsStringEncodingForMultipart = { Encoding.UTF8, Encoding.UTF8 }; + private static readonly Encoding[] SupportedBodyAsStringEncodingForMultipart = { Encoding.UTF8, Encoding.ASCII }; /* HEAD - No defined body semantics.