Compare commits

...

2 Commits

Author SHA1 Message Date
Stef Heyenrath
6c32b9c31a 1.0.12.0 2019-04-05 15:15:22 +02:00
Stef Heyenrath
3d845d5be5 Proxy : also save multipart as string in mapping file (#264)
* ExactObjectMatcher

* BytesEncodingUtils

* BodyParser

* Encoding.ASCII
2019-04-05 14:51:29 +02:00
12 changed files with 383 additions and 34 deletions

View File

@@ -1,3 +1,7 @@
# 1.0.12.0 (05 April 2019)
- [#264](https://github.com/WireMock-Net/WireMock.Net/pull/264) - Proxy : also save multipart as string in mapping file contributed by [StefH](https://github.com/StefH)
- [#263](https://github.com/WireMock-Net/WireMock.Net/issues/263) - Content-Type multipart/form-data is not serialized in proxy and recording mode [bug]
# 1.0.11.0 (30 March 2019)
- [#261](https://github.com/WireMock-Net/WireMock.Net/pull/261) - Fix BodyAsJson transform bug in ResponseMessageTransformer contributed by [psypilat](https://github.com/psypilat)
- [#262](https://github.com/WireMock-Net/WireMock.Net/pull/262) - Add ProvideResponse_WithJsonBodyAndTransform test contributed by [psypilat](https://github.com/psypilat)

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>1.0.11</VersionPrefix>
<VersionPrefix>1.0.12</VersionPrefix>
</PropertyGroup>
<Choose>

View File

@@ -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 @@ namespace WireMock.Net.Console.Proxy.Net452
{
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 @@ namespace WireMock.Net.Console.Proxy.Net452
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();

View File

@@ -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
/// <seealso cref="IObjectMatcher" />
public class ExactObjectMatcher : IObjectMatcher
{
private readonly object _object;
private readonly byte[] _bytes;
public object ValueAsObject { get; }
public byte[] ValueAsBytes { get; }
/// <inheritdoc cref="IMatcher.MatchBehaviour"/>
public MatchBehaviour MatchBehaviour { get; }
@@ -33,7 +34,7 @@ namespace WireMock.Matchers
{
Check.NotNull(value, nameof(value));
_object = value;
ValueAsObject = value;
MatchBehaviour = matchBehaviour;
}
@@ -54,14 +55,14 @@ namespace WireMock.Matchers
{
Check.NotNull(value, nameof(value));
_bytes = value;
ValueAsBytes = value;
MatchBehaviour = matchBehaviour;
}
/// <inheritdoc cref="IObjectMatcher.IsMatch"/>
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));
}

View File

@@ -9,7 +9,10 @@ namespace WireMock
internal static class ResponseMessageBuilder
{
private static string ContentTypeJson = "application/json";
private static readonly IDictionary<string, WireMockList<string>> ContentTypeJsonHeaders = new Dictionary<string, WireMockList<string>> { { HttpKnownHeaderNames.ContentType, new WireMockList<string> { ContentTypeJson } } };
private static readonly IDictionary<string, WireMockList<string>> ContentTypeJsonHeaders = new Dictionary<string, WireMockList<string>>
{
{ HttpKnownHeaderNames.ContentType, new WireMockList<string> { ContentTypeJson } }
};
internal static ResponseMessage Create(string message, int statusCode = 200, Guid? guid = null)
{

View File

@@ -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 @@ namespace WireMock.Serialization
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 @@ namespace WireMock.Serialization
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<object>().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<object>().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

View File

@@ -277,13 +277,19 @@ namespace WireMock.Server
}
});
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);

View File

@@ -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.ASCII };
/*
HEAD - No defined body semantics.
@@ -91,9 +92,19 @@ namespace WireMock.Util
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;
}

View File

@@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace WireMock.Util
{
/// <summary>
/// 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
/// </summary>
public static class BytesEncodingUtils
{
/// <summary>
/// Tries the get the Encoding from an array of bytes.
/// </summary>
/// <param name="bytes">The bytes.</param>
/// <param name="encoding">The output encoding.</param>
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<byte> data, IReadOnlyCollection<byte> other)
{
byte[] arraySelf = data.Take(other.Count).ToArray();
return other.SequenceEqual(arraySelf);
}
private static bool IsUtf8(IReadOnlyList<byte> 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<byte> 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
}

View File

@@ -290,6 +290,36 @@ namespace WireMock.Net.Tests
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()
{

View File

@@ -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 @@ namespace WireMock.Net.Tests.Serialization
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()
{

View File

@@ -51,7 +51,7 @@ namespace WireMock.Net.Tests.Util
}
[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-Type: text/html
// 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);