diff --git a/.vscode/launch.json b/.vscode/launch.json index 07c6fa08..f649f34e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,22 +1,11 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": ".NET Core Launch (WireMock.Net.StandAlone.NETCoreApp)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build_WireMock.Net.StandAlone.NETCoreApp", - "program": "${workspaceRoot}/examples/WireMock.Net.StandAlone.NETCoreApp/bin/Debug/netcoreapp2.0/WireMock.Net.StandAlone.NETCoreApp.dll", - "args": [], - "cwd": "${workspaceRoot}", - "stopAtEntry": false, - "console": "internalConsole" - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" - } - ] +{ + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] } \ No newline at end of file diff --git a/src/WireMock.Net/Http/HttpClientHelper.cs b/src/WireMock.Net/Http/HttpClientHelper.cs index 550fa191..b606bb60 100644 --- a/src/WireMock.Net/Http/HttpClientHelper.cs +++ b/src/WireMock.Net/Http/HttpClientHelper.cs @@ -68,7 +68,7 @@ namespace WireMock.Http return client; } - public static async Task SendAsync([NotNull] HttpClient client, [NotNull] RequestMessage requestMessage, string url, bool deserializeJson) + public static async Task SendAsync([NotNull] HttpClient client, [NotNull] RequestMessage requestMessage, string url, bool deserializeJson, bool decompressGzipAndDeflate) { Check.NotNull(client, nameof(client)); Check.NotNull(requestMessage, nameof(requestMessage)); @@ -83,7 +83,7 @@ namespace WireMock.Http var httpResponseMessage = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseContentRead); // Create ResponseMessage - return await HttpResponseMessageHelper.CreateAsync(httpResponseMessage, requiredUri, originalUri, deserializeJson); + return await HttpResponseMessageHelper.CreateAsync(httpResponseMessage, requiredUri, originalUri, deserializeJson, decompressGzipAndDeflate); } } } \ No newline at end of file diff --git a/src/WireMock.Net/Http/HttpResponseMessageHelper.cs b/src/WireMock.Net/Http/HttpResponseMessageHelper.cs index b73d881f..7c73f3e5 100644 --- a/src/WireMock.Net/Http/HttpResponseMessageHelper.cs +++ b/src/WireMock.Net/Http/HttpResponseMessageHelper.cs @@ -9,7 +9,7 @@ namespace WireMock.Http { internal static class HttpResponseMessageHelper { - public static async Task CreateAsync(HttpResponseMessage httpResponseMessage, Uri requiredUri, Uri originalUri, bool deserializeJson) + public static async Task CreateAsync(HttpResponseMessage httpResponseMessage, Uri requiredUri, Uri originalUri, bool deserializeJson, bool decompressGzipAndDeflate) { var responseMessage = new ResponseMessage { StatusCode = (int)httpResponseMessage.StatusCode }; @@ -24,7 +24,21 @@ namespace WireMock.Http contentTypeHeader = headers.First(header => string.Equals(header.Key, HttpKnownHeaderNames.ContentType, StringComparison.OrdinalIgnoreCase)).Value; } - responseMessage.BodyData = await BodyParser.Parse(stream, contentTypeHeader?.FirstOrDefault(), deserializeJson); + IEnumerable contentEncodingHeader = null; + if (headers.Any(header => string.Equals(header.Key, HttpKnownHeaderNames.ContentEncoding, StringComparison.OrdinalIgnoreCase))) + { + contentEncodingHeader = headers.First(header => string.Equals(header.Key, HttpKnownHeaderNames.ContentEncoding, StringComparison.OrdinalIgnoreCase)).Value; + } + + var bodyParserSettings = new BodyParserSettings + { + Stream = stream, + ContentType = contentTypeHeader?.FirstOrDefault(), + DeserializeJson = deserializeJson, + ContentEncoding = contentEncodingHeader?.FirstOrDefault(), + DecompressGZipAndDeflate = decompressGzipAndDeflate + }; + responseMessage.BodyData = await BodyParser.Parse(bodyParserSettings); } foreach (var header in headers) diff --git a/src/WireMock.Net/Owin/IWireMockMiddlewareOptions.cs b/src/WireMock.Net/Owin/IWireMockMiddlewareOptions.cs index 65acc007..f1bd275e 100644 --- a/src/WireMock.Net/Owin/IWireMockMiddlewareOptions.cs +++ b/src/WireMock.Net/Owin/IWireMockMiddlewareOptions.cs @@ -43,5 +43,7 @@ namespace WireMock.Owin bool? AllowOnlyDefinedHttpStatusCodeInResponse { get; set; } bool? DisableJsonBodyParsing { get; set; } + + bool? DisableRequestBodyDecompressing { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs b/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs index a8e1a027..8871cde9 100644 --- a/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs +++ b/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using WireMock.Http; using WireMock.Models; using WireMock.Util; #if !USE_ASPNETCORE @@ -27,12 +28,18 @@ namespace WireMock.Owin.Mappers string method = request.Method; Dictionary headers = null; + IEnumerable contentEncodingHeader = null; if (request.Headers.Any()) { headers = new Dictionary(); foreach (var header in request.Headers) { headers.Add(header.Key, header.Value); + + if (string.Equals(header.Key, HttpKnownHeaderNames.ContentEncoding, StringComparison.OrdinalIgnoreCase)) + { + contentEncodingHeader = header.Value; + } } } @@ -49,7 +56,15 @@ namespace WireMock.Owin.Mappers BodyData body = null; if (request.Body != null && BodyParser.ShouldParseBody(method, options.AllowBodyForAllHttpMethods == true)) { - body = await BodyParser.Parse(request.Body, request.ContentType, !options.DisableJsonBodyParsing.GetValueOrDefault(false)); + var bodyParserSettings = new BodyParserSettings + { + Stream = request.Body, + ContentType = request.ContentType, + DeserializeJson = !options.DisableJsonBodyParsing.GetValueOrDefault(false), + ContentEncoding = contentEncodingHeader?.FirstOrDefault(), + DecompressGZipAndDeflate = !options.DisableRequestBodyDecompressing.GetValueOrDefault(false) + }; + body = await BodyParser.Parse(bodyParserSettings); } return new RequestMessage(urldetails, method, clientIP, body, headers, cookies) { DateTime = DateTime.UtcNow }; diff --git a/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs b/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs index d0be329f..37bbac54 100644 --- a/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs +++ b/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs @@ -46,7 +46,10 @@ namespace WireMock.Owin /// public bool? AllowOnlyDefinedHttpStatusCodeInResponse { get; set; } - /// + /// public bool? DisableJsonBodyParsing { get; set; } + + /// + public bool? DisableRequestBodyDecompressing { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/RequestMessage.cs b/src/WireMock.Net/RequestMessage.cs index cfe72710..10882ba6 100644 --- a/src/WireMock.Net/RequestMessage.cs +++ b/src/WireMock.Net/RequestMessage.cs @@ -112,6 +112,11 @@ namespace WireMock /// public string DetectedBodyTypeFromContentType { get; } + /// + /// The detected compression from the Content-Encoding header. Convenience getter for Handlebars. + /// + public string DetectedCompression { get; } + /// /// Gets the Host /// @@ -170,6 +175,7 @@ namespace WireMock BodyAsBytes = BodyData?.BodyAsBytes; DetectedBodyType = BodyData?.DetectedBodyType.ToString(); DetectedBodyTypeFromContentType = BodyData?.DetectedBodyTypeFromContentType.ToString(); + DetectedCompression = BodyData?.DetectedCompression; Headers = headers?.ToDictionary(header => header.Key, header => new WireMockList(header.Value)); Cookies = cookies; diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index dc7a0007..e3d67985 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -347,7 +347,13 @@ namespace WireMock.ResponseBuilders var proxyUri = new Uri(ProxyUrl); var proxyUriWithRequestPathAndQuery = new Uri(proxyUri, requestUri.PathAndQuery); - return await HttpClientHelper.SendAsync(_httpClientForProxy, requestMessage, proxyUriWithRequestPathAndQuery.AbsoluteUri, !settings.DisableJsonBodyParsing.GetValueOrDefault(false)); + return await HttpClientHelper.SendAsync( + _httpClientForProxy, + requestMessage, + proxyUriWithRequestPathAndQuery.AbsoluteUri, + !settings.DisableJsonBodyParsing.GetValueOrDefault(false), + !settings.DisableRequestBodyDecompressing.GetValueOrDefault(false) + ); } ResponseMessage responseMessage; diff --git a/src/WireMock.Net/Server/WireMockServer.Admin.cs b/src/WireMock.Net/Server/WireMockServer.Admin.cs index feb470a0..9dec89f0 100644 --- a/src/WireMock.Net/Server/WireMockServer.Admin.cs +++ b/src/WireMock.Net/Server/WireMockServer.Admin.cs @@ -275,7 +275,13 @@ namespace WireMock.Server var proxyUri = new Uri(settings.ProxyAndRecordSettings.Url); var proxyUriWithRequestPathAndQuery = new Uri(proxyUri, requestUri.PathAndQuery); - var responseMessage = await HttpClientHelper.SendAsync(_httpClientForProxy, requestMessage, proxyUriWithRequestPathAndQuery.AbsoluteUri, !settings.DisableJsonBodyParsing.GetValueOrDefault(false)); + var responseMessage = await HttpClientHelper.SendAsync( + _httpClientForProxy, + requestMessage, + proxyUriWithRequestPathAndQuery.AbsoluteUri, + !settings.DisableJsonBodyParsing.GetValueOrDefault(false), + !settings.DisableRequestBodyDecompressing.GetValueOrDefault(false) + ); if (HttpStatusRangeParser.IsMatch(settings.ProxyAndRecordSettings.SaveMappingForStatusCodePattern, responseMessage.StatusCode) && (settings.ProxyAndRecordSettings.SaveMapping || settings.ProxyAndRecordSettings.SaveMappingToFile)) diff --git a/src/WireMock.Net/Settings/IWireMockServerSettings.cs b/src/WireMock.Net/Settings/IWireMockServerSettings.cs index fe0e2798..843b8729 100644 --- a/src/WireMock.Net/Settings/IWireMockServerSettings.cs +++ b/src/WireMock.Net/Settings/IWireMockServerSettings.cs @@ -1,155 +1,161 @@ -using System; -using HandlebarsDotNet; -using JetBrains.Annotations; -using WireMock.Handlers; -using WireMock.Logging; - -namespace WireMock.Settings -{ - /// - /// IWireMockServerSettings - /// - public interface IWireMockServerSettings - { - /// - /// Gets or sets the port. - /// - [PublicAPI] - int? Port { get; set; } - - /// - /// Gets or sets the use SSL. - /// - // ReSharper disable once InconsistentNaming - [PublicAPI] - bool? UseSSL { get; set; } - - /// - /// Gets or sets whether to start admin interface. - /// - [PublicAPI] - bool? StartAdminInterface { get; set; } - - /// - /// Gets or sets if the static mappings should be read at startup. - /// - [PublicAPI] - bool? ReadStaticMappings { get; set; } - - /// - /// Watch the static mapping files + folder for changes when running. - /// - [PublicAPI] - bool? WatchStaticMappings { get; set; } - - /// - /// A value indicating whether subdirectories within the static mappings path should be monitored. - /// - [PublicAPI] - bool? WatchStaticMappingsInSubdirectories { get; set; } - - /// - /// Gets or sets if the proxy and record settings. - /// - [PublicAPI] - IProxyAndRecordSettings ProxyAndRecordSettings { get; set; } - - /// - /// Gets or sets the urls. - /// - [PublicAPI] - string[] Urls { get; set; } - - /// - /// StartTimeout - /// - [PublicAPI] - int StartTimeout { get; set; } - - /// - /// Allow Partial Mapping (default set to false). - /// - [PublicAPI] - bool? AllowPartialMapping { get; set; } - - /// - /// The username needed for __admin access. - /// - [PublicAPI] - string AdminUsername { get; set; } - - /// - /// The password needed for __admin access. - /// - [PublicAPI] - string AdminPassword { get; set; } - - /// - /// The RequestLog expiration in hours (optional). - /// - [PublicAPI] - int? RequestLogExpirationDuration { get; set; } - - /// - /// The MaxRequestLog count (optional). - /// - [PublicAPI] - int? MaxRequestLogCount { get; set; } - - /// - /// Action which is called (with the IAppBuilder or IApplicationBuilder) before the internal WireMockMiddleware is initialized. [Optional] - /// - [PublicAPI] - Action PreWireMockMiddlewareInit { get; set; } - - /// - /// Action which is called (with the IAppBuilder or IApplicationBuilder) after the internal WireMockMiddleware is initialized. [Optional] - /// - [PublicAPI] - Action PostWireMockMiddlewareInit { get; set; } - - /// - /// The IWireMockLogger which logs Debug, Info, Warning or Error - /// - [PublicAPI] - IWireMockLogger Logger { get; set; } - - /// - /// Handler to interact with the file system to read and write static mapping files. - /// - [PublicAPI] - IFileSystemHandler FileSystemHandler { get; set; } - - /// - /// Action which can be used to add additional Handlebars registrations. [Optional] - /// - [PublicAPI] - Action HandlebarsRegistrationCallback { get; set; } - - /// - /// Allow the usage of CSharpCodeMatcher (default is not allowed). - /// - [PublicAPI] - bool? AllowCSharpCodeMatcher { get; set; } - - /// - /// Allow a Body for all HTTP Methods. (default set to false). - /// - [PublicAPI] - bool? AllowBodyForAllHttpMethods { get; set; } - - /// - /// Allow only a HttpStatus Code in the response which is defined. (default set to false). - /// - false : also null, 0, empty or invalid HttpStatus codes are allowed. - /// - true : only codes defined in are allowed. - /// - /// [PublicAPI] - bool? AllowOnlyDefinedHttpStatusCodeInResponse { get; set; } - - /// - /// Set to true to disable Json deserialization when processing requests. (default set to false). - /// - [PublicAPI] - bool? DisableJsonBodyParsing { get; set; } - } +using System; +using HandlebarsDotNet; +using JetBrains.Annotations; +using WireMock.Handlers; +using WireMock.Logging; + +namespace WireMock.Settings +{ + /// + /// IWireMockServerSettings + /// + public interface IWireMockServerSettings + { + /// + /// Gets or sets the port. + /// + [PublicAPI] + int? Port { get; set; } + + /// + /// Gets or sets the use SSL. + /// + // ReSharper disable once InconsistentNaming + [PublicAPI] + bool? UseSSL { get; set; } + + /// + /// Gets or sets whether to start admin interface. + /// + [PublicAPI] + bool? StartAdminInterface { get; set; } + + /// + /// Gets or sets if the static mappings should be read at startup. + /// + [PublicAPI] + bool? ReadStaticMappings { get; set; } + + /// + /// Watch the static mapping files + folder for changes when running. + /// + [PublicAPI] + bool? WatchStaticMappings { get; set; } + + /// + /// A value indicating whether subdirectories within the static mappings path should be monitored. + /// + [PublicAPI] + bool? WatchStaticMappingsInSubdirectories { get; set; } + + /// + /// Gets or sets if the proxy and record settings. + /// + [PublicAPI] + IProxyAndRecordSettings ProxyAndRecordSettings { get; set; } + + /// + /// Gets or sets the urls. + /// + [PublicAPI] + string[] Urls { get; set; } + + /// + /// StartTimeout + /// + [PublicAPI] + int StartTimeout { get; set; } + + /// + /// Allow Partial Mapping (default set to false). + /// + [PublicAPI] + bool? AllowPartialMapping { get; set; } + + /// + /// The username needed for __admin access. + /// + [PublicAPI] + string AdminUsername { get; set; } + + /// + /// The password needed for __admin access. + /// + [PublicAPI] + string AdminPassword { get; set; } + + /// + /// The RequestLog expiration in hours (optional). + /// + [PublicAPI] + int? RequestLogExpirationDuration { get; set; } + + /// + /// The MaxRequestLog count (optional). + /// + [PublicAPI] + int? MaxRequestLogCount { get; set; } + + /// + /// Action which is called (with the IAppBuilder or IApplicationBuilder) before the internal WireMockMiddleware is initialized. [Optional] + /// + [PublicAPI] + Action PreWireMockMiddlewareInit { get; set; } + + /// + /// Action which is called (with the IAppBuilder or IApplicationBuilder) after the internal WireMockMiddleware is initialized. [Optional] + /// + [PublicAPI] + Action PostWireMockMiddlewareInit { get; set; } + + /// + /// The IWireMockLogger which logs Debug, Info, Warning or Error + /// + [PublicAPI] + IWireMockLogger Logger { get; set; } + + /// + /// Handler to interact with the file system to read and write static mapping files. + /// + [PublicAPI] + IFileSystemHandler FileSystemHandler { get; set; } + + /// + /// Action which can be used to add additional Handlebars registrations. [Optional] + /// + [PublicAPI] + Action HandlebarsRegistrationCallback { get; set; } + + /// + /// Allow the usage of CSharpCodeMatcher (default is not allowed). + /// + [PublicAPI] + bool? AllowCSharpCodeMatcher { get; set; } + + /// + /// Allow a Body for all HTTP Methods. (default set to false). + /// + [PublicAPI] + bool? AllowBodyForAllHttpMethods { get; set; } + + /// + /// Allow only a HttpStatus Code in the response which is defined. (default set to false). + /// - false : also null, 0, empty or invalid HttpStatus codes are allowed. + /// - true : only codes defined in are allowed. + /// + /// [PublicAPI] + bool? AllowOnlyDefinedHttpStatusCodeInResponse { get; set; } + + /// + /// Set to true to disable Json deserialization when processing requests. (default set to false). + /// + [PublicAPI] + bool? DisableJsonBodyParsing { get; set; } + + /// + /// Disable support for GZip and Deflate request body decompression. (default set to false). + /// + [PublicAPI] + bool? DisableRequestBodyDecompressing { get; set; } + } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WireMockServerSettings.cs b/src/WireMock.Net/Settings/WireMockServerSettings.cs index 5debcf65..f7c32736 100644 --- a/src/WireMock.Net/Settings/WireMockServerSettings.cs +++ b/src/WireMock.Net/Settings/WireMockServerSettings.cs @@ -103,10 +103,15 @@ namespace WireMock.Settings public bool? AllowBodyForAllHttpMethods { get; set; } /// + [PublicAPI] public bool? AllowOnlyDefinedHttpStatusCodeInResponse { get; set; } /// [PublicAPI] public bool? DisableJsonBodyParsing { get; set; } + + /// + [PublicAPI] + public bool? DisableRequestBodyDecompressing { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Util/BodyData.cs b/src/WireMock.Net/Util/BodyData.cs index 4cb6bc09..50c8d1d7 100644 --- a/src/WireMock.Net/Util/BodyData.cs +++ b/src/WireMock.Net/Util/BodyData.cs @@ -52,5 +52,10 @@ namespace WireMock.Util /// The detected body type (detection based on Content-Type). /// public BodyType DetectedBodyTypeFromContentType { get; set; } + + /// + /// The detected compression. + /// + public string DetectedCompression { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Util/BodyParser.cs b/src/WireMock.Net/Util/BodyParser.cs index 70a675e2..b890696c 100644 --- a/src/WireMock.Net/Util/BodyParser.cs +++ b/src/WireMock.Net/Util/BodyParser.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; -using Newtonsoft.Json; using WireMock.Http; using WireMock.Matchers; using WireMock.Types; @@ -108,15 +109,17 @@ namespace WireMock.Util return BodyType.Bytes; } - public static async Task Parse([NotNull] Stream stream, [CanBeNull] string contentType = null, bool deserializeJson = true) + public static async Task Parse([NotNull] BodyParserSettings settings) { - Check.NotNull(stream, nameof(stream)); + Check.NotNull(settings, nameof(settings)); + var bodyWithContentEncoding = await ReadBytesAsync(settings.Stream, settings.ContentEncoding, settings.DecompressGZipAndDeflate); var data = new BodyData { - BodyAsBytes = await ReadBytesAsync(stream), + BodyAsBytes = bodyWithContentEncoding.Value, + DetectedCompression = bodyWithContentEncoding.Key, DetectedBodyType = BodyType.Bytes, - DetectedBodyTypeFromContentType = DetectBodyTypeFromContentType(contentType) + DetectedBodyTypeFromContentType = DetectBodyTypeFromContentType(settings.ContentType) }; // 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 @@ -141,7 +144,7 @@ namespace WireMock.Util data.DetectedBodyType = BodyType.String; // If string is not null or empty, try to deserialize the string to a JObject - if (deserializeJson && !string.IsNullOrEmpty(data.BodyAsString)) + if (settings.DeserializeJson && !string.IsNullOrEmpty(data.BodyAsString)) { try { @@ -161,12 +164,21 @@ namespace WireMock.Util return data; } - private static async Task ReadBytesAsync(Stream stream) + + private static async Task> ReadBytesAsync(Stream stream, string contentEncoding = null, bool decompressGZipAndDeflate = true) { using (var memoryStream = new MemoryStream()) { await stream.CopyToAsync(memoryStream); - return memoryStream.ToArray(); + byte[] data = memoryStream.ToArray(); + + string type = contentEncoding?.ToLowerInvariant(); + if (decompressGZipAndDeflate && (type == "gzip" || type == "deflate")) + { + return new KeyValuePair(type, CompressionUtils.Decompress(type, data)); + } + + return new KeyValuePair(null, data); } } } diff --git a/src/WireMock.Net/Util/BodyParserSettings.cs b/src/WireMock.Net/Util/BodyParserSettings.cs new file mode 100644 index 00000000..3dfa820a --- /dev/null +++ b/src/WireMock.Net/Util/BodyParserSettings.cs @@ -0,0 +1,17 @@ +using System.IO; + +namespace WireMock.Util +{ + internal class BodyParserSettings + { + public Stream Stream { get; set; } + + public string ContentType { get; set; } + + public string ContentEncoding { get; set; } + + public bool DecompressGZipAndDeflate { get; set; } = true; + + public bool DeserializeJson { get; set; } = true; + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Util/CloneUtils.cs b/src/WireMock.Net/Util/CloneUtils.cs deleted file mode 100644 index 75b9d2ec..00000000 --- a/src/WireMock.Net/Util/CloneUtils.cs +++ /dev/null @@ -1,29 +0,0 @@ -using FastDeepCloner; -using System; -using System.Runtime.Serialization; - -namespace WireMock.Util -{ - internal static class CloneUtils - { - private static FastDeepClonerSettings settings = new FastDeepCloner.FastDeepClonerSettings() - { - FieldType = FastDeepCloner.FieldType.FieldInfo, - OnCreateInstance = new FastDeepCloner.Extensions.CreateInstance((Type type) => - { -#if !NETSTANDARD1_3 - return FormatterServices.GetUninitializedObject(type); -#else - -#endif - }) - }; - - public static T DeepClone(T objectToBeCloned) where T : class - { - //return CloneExtensionsEx.CloneFactory.GetClone(objectToBeCloned); - // Expression.Lambda>(Expression.New(type)).Compile() - return FastDeepCloner.DeepCloner.Clone(objectToBeCloned, settings); - } - } -} \ No newline at end of file diff --git a/src/WireMock.Net/Util/CompressionUtils.cs b/src/WireMock.Net/Util/CompressionUtils.cs new file mode 100644 index 00000000..78880ce4 --- /dev/null +++ b/src/WireMock.Net/Util/CompressionUtils.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.IO.Compression; + +namespace WireMock.Util +{ + internal static class CompressionUtils + { + public static byte[] Compress(string contentEncoding, byte[] data) + { + using (var compressedStream = new MemoryStream()) + using (var zipStream = Create(contentEncoding, compressedStream, CompressionMode.Compress)) + { + zipStream.Write(data, 0, data.Length); + +#if !NETSTANDARD1_3 + zipStream.Close(); +#endif + return compressedStream.ToArray(); + } + } + + public static byte[] Decompress(string contentEncoding, byte[] data) + { + using (var compressedStream = new MemoryStream(data)) + using (var zipStream = Create(contentEncoding, compressedStream, CompressionMode.Decompress)) + using (var resultStream = new MemoryStream()) + { + zipStream.CopyTo(resultStream); + return resultStream.ToArray(); + } + } + + private static Stream Create(string contentEncoding, Stream stream, CompressionMode mode) + { + switch (contentEncoding) + { + case "gzip": + return new GZipStream(stream, mode); + + case "deflate": + return new DeflateStream(stream, mode); + + default: + throw new NotSupportedException($"ContentEncoding '{contentEncoding}' is not supported."); + } + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Util/IndexableDictionary.cs b/src/WireMock.Net/Util/IndexableDictionary.cs deleted file mode 100644 index af6b8fb0..00000000 --- a/src/WireMock.Net/Util/IndexableDictionary.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace WireMock.Util -{ - public class IndexableDictionary : Dictionary - { - /// - /// Gets the value associated with the specified index. - /// - /// The index of the value to get. - /// The value associated with the specified index. - public TValue this[int index] - { - get - { - // get the item for that index. - if (index < 0 || index > Count) - { - throw new KeyNotFoundException(); - } - return Values.Cast().ToArray()[index]; - } - } - } -} diff --git a/src/WireMock.Net/Util/NamedReaderWriterLocker.cs b/src/WireMock.Net/Util/NamedReaderWriterLocker.cs deleted file mode 100644 index 6194cf19..00000000 --- a/src/WireMock.Net/Util/NamedReaderWriterLocker.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; - -namespace WireMock.Util -{ - /// - /// http://johnculviner.com/achieving-named-lock-locker-functionality-in-c-4-0/ - /// - internal class NamedReaderWriterLocker - { - private readonly ConcurrentDictionary _lockDict = new ConcurrentDictionary(); - - public ReaderWriterLockSlim GetLock(string name) - { - return _lockDict.GetOrAdd(name, s => new ReaderWriterLockSlim()); - } - - public TResult RunWithReadLock(string name, Func body) - { - var rwLock = GetLock(name); - try - { - rwLock.EnterReadLock(); - return body(); - } - finally - { - rwLock.ExitReadLock(); - } - } - - public void RunWithReadLock(string name, Action body) - { - var rwLock = GetLock(name); - try - { - rwLock.EnterReadLock(); - body(); - } - finally - { - rwLock.ExitReadLock(); - } - } - - public TResult RunWithWriteLock(string name, Func body) - { - var rwLock = GetLock(name); - try - { - rwLock.EnterWriteLock(); - return body(); - } - finally - { - rwLock.ExitWriteLock(); - } - } - - public void RunWithWriteLock(string name, Action body) - { - var rwLock = GetLock(name); - try - { - rwLock.EnterWriteLock(); - body(); - } - finally - { - rwLock.ExitWriteLock(); - } - } - - public void RemoveLock(string name) - { - _lockDict.TryRemove(name, out _); - } - } -} \ No newline at end of file diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index 415510e2..fb811604 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -3,9 +3,7 @@ Lightweight Http Mocking Server for .Net, inspired by WireMock from the Java landscape. WireMock.Net Stef Heyenrath - - - net451;net452;net46;net461;netstandard1.3;netstandard2.0;;netstandard2.1 + net451;net452;net46;net461;netstandard1.3;netstandard2.0;netstandard2.1 true WireMock.Net WireMock.Net @@ -29,7 +27,8 @@ --> - + + @@ -53,26 +52,22 @@ USE_ASPNETCORE;NET46 - - - - - - all - + + + + + + all runtime; build; native; contentfiles; analyzers - - - diff --git a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs index 18f0f904..6969a237 100644 --- a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs +++ b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs @@ -243,18 +243,36 @@ namespace WireMock.Net.Tests.RequestMatchers // assign BodyData bodyData; if (body is byte[] b) - bodyData = await BodyParser.Parse(new MemoryStream(b), null, true); + { + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(b), + ContentType = null, + DeserializeJson = true + }; + bodyData = await BodyParser.Parse(bodyParserSettings); + } else if (body is string s) - bodyData = await BodyParser.Parse(new MemoryStream(Encoding.UTF8.GetBytes(s)), null, true); + { + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(Encoding.UTF8.GetBytes(s)), + ContentType = null, + DeserializeJson = true + }; + bodyData = await BodyParser.Parse(bodyParserSettings); + } else + { throw new Exception(); + } var requestMessage = new RequestMessage(new UrlDetails("http://localhost"), "GET", "127.0.0.1", bodyData); // act var result = new RequestMatchResult(); var score = matcher.GetMatchingScore(requestMessage, result); - + // assert Check.That(score).IsEqualTo(shouldMatch ? 1d : 0d); } @@ -265,7 +283,7 @@ namespace WireMock.Net.Tests.RequestMatchers { var json = "{'a':'b'}"; var str = "HelloWorld"; - var bytes = new byte[] {0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00}; + var bytes = new byte[] { 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00 }; return new TheoryData { diff --git a/test/WireMock.Net.Tests/Util/BodyParserTests.cs b/test/WireMock.Net.Tests/Util/BodyParserTests.cs index 9538de94..156d49e4 100644 --- a/test/WireMock.Net.Tests/Util/BodyParserTests.cs +++ b/test/WireMock.Net.Tests/Util/BodyParserTests.cs @@ -1,7 +1,10 @@ -using NFluent; +using System; +using NFluent; using System.IO; +using System.IO.Compression; using System.Text; using System.Threading.Tasks; +using FluentAssertions; using WireMock.Types; using WireMock.Util; using Xunit; @@ -19,10 +22,15 @@ namespace WireMock.Net.Tests.Util public async Task BodyParser_Parse_ContentTypeJson(string contentType, string bodyAsJson, BodyType detectedBodyType, BodyType detectedBodyTypeFromContentType) { // Arrange - var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(bodyAsJson)); + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(Encoding.UTF8.GetBytes(bodyAsJson)), + ContentType = contentType, + DeserializeJson = true + }; // Act - var body = await BodyParser.Parse(memoryStream, contentType, true); + var body = await BodyParser.Parse(bodyParserSettings); // Assert Check.That(body.BodyAsBytes).IsNotNull(); @@ -38,10 +46,15 @@ namespace WireMock.Net.Tests.Util public async Task BodyParser_Parse_ContentTypeString(string contentType, string bodyAsString, BodyType detectedBodyType, BodyType detectedBodyTypeFromContentType) { // Arrange - var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(bodyAsString)); + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(Encoding.UTF8.GetBytes(bodyAsString)), + ContentType = contentType, + DeserializeJson = true + }; // Act - var body = await BodyParser.Parse(memoryStream, contentType, true); + var body = await BodyParser.Parse(bodyParserSettings); // Assert Check.That(body.BodyAsBytes).IsNotNull(); @@ -52,16 +65,21 @@ namespace WireMock.Net.Tests.Util } [Theory] - [InlineData(new byte[] {34, 97, 34}, BodyType.Json)] - [InlineData(new byte[] {97}, BodyType.String)] - [InlineData(new byte[] {0xFF, 0xD8, 0xFF, 0xE0}, BodyType.Bytes)] + [InlineData(new byte[] { 34, 97, 34 }, BodyType.Json)] + [InlineData(new byte[] { 97 }, BodyType.String)] + [InlineData(new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, BodyType.Bytes)] public async Task BodyParser_Parse_DetectedBodyType(byte[] content, BodyType detectedBodyType) { // arrange - var memoryStream = new MemoryStream(content); + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(content), + ContentType = null, + DeserializeJson = true + }; // act - var body = await BodyParser.Parse(memoryStream, null, true); + var body = await BodyParser.Parse(bodyParserSettings); // assert Check.That(body.DetectedBodyType).IsEqualTo(detectedBodyType); @@ -74,10 +92,15 @@ namespace WireMock.Net.Tests.Util public async Task BodyParser_Parse_DetectedBodyTypeNoJsonParsing(byte[] content, BodyType detectedBodyType) { // arrange - var memoryStream = new MemoryStream(content); + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(content), + ContentType = null, + DeserializeJson = false + }; // act - var body = await BodyParser.Parse(memoryStream, null, false); + var body = await BodyParser.Parse(bodyParserSettings); // assert Check.That(body.DetectedBodyType).IsEqualTo(detectedBodyType); @@ -108,10 +131,15 @@ Content-Type: text/html -----------------------------9051914041544843365972754266--"; - var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(body)); + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(Encoding.UTF8.GetBytes(body)), + ContentType = contentType, + DeserializeJson = true + }; // Act - var result = await BodyParser.Parse(memoryStream, contentType, true); + var result = await BodyParser.Parse(bodyParserSettings); // Assert Check.That(result.DetectedBodyType).IsEqualTo(BodyType.String); @@ -127,11 +155,15 @@ Content-Type: text/html // 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)); + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(Encoding.UTF8.GetBytes(body)), + ContentType = contentType, + DeserializeJson = true + }; // Act - var result = await BodyParser.Parse(memoryStream, contentType, true); + var result = await BodyParser.Parse(bodyParserSettings); // Assert Check.That(result.DetectedBodyType).IsEqualTo(BodyType.Bytes); @@ -142,14 +174,19 @@ Content-Type: text/html } [Theory] - [InlineData(null, "hello", BodyType.String, BodyType.Bytes)] - public async Task BodyParser_Parse_ContentTypeIsNull(string contentType, string bodyAsString, BodyType detectedBodyType, BodyType detectedBodyTypeFromContentType) + [InlineData("hello", BodyType.String, BodyType.Bytes)] + public async Task BodyParser_Parse_ContentTypeIsNull(string bodyAsString, BodyType detectedBodyType, BodyType detectedBodyTypeFromContentType) { // Arrange - var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(bodyAsString)); + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(Encoding.UTF8.GetBytes(bodyAsString)), + ContentType = null, + DeserializeJson = true + }; // Act - var body = await BodyParser.Parse(memoryStream, contentType, true); + var body = await BodyParser.Parse(bodyParserSettings); // Assert Check.That(body.BodyAsBytes).IsNotNull(); @@ -159,6 +196,60 @@ Content-Type: text/html Check.That(body.DetectedBodyTypeFromContentType).IsEqualTo(detectedBodyTypeFromContentType); } + [Theory] + [InlineData("gzip")] + [InlineData("deflate")] + public async Task BodyParser_Parse_ContentEncoding_GZip_And_DecompressGzipAndDeflate_Is_True_Should_Decompress(string compression) + { + // Arrange + var bytes = Encoding.ASCII.GetBytes("0"); + var compressed = CompressionUtils.Compress(compression, bytes); + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(compressed), + ContentType = "text/plain", + DeserializeJson = false, + ContentEncoding = compression.ToUpperInvariant(), + DecompressGZipAndDeflate = true + }; + + // Act + var result = await BodyParser.Parse(bodyParserSettings); + + // Assert + result.DetectedBodyType.Should().Be(BodyType.String); + result.DetectedBodyTypeFromContentType.Should().Be(BodyType.String); + result.BodyAsBytes.Should().BeEquivalentTo(new byte[] { 48 }); + result.BodyAsJson.Should().BeNull(); + result.BodyAsString.Should().Be("0"); + result.DetectedCompression.Should().Be(compression); + } + + [Theory] + [InlineData("gzip")] + [InlineData("deflate")] + public async Task BodyParser_Parse_ContentEncoding_GZip_And_DecompressGzipAndDeflate_Is_False_Should_Not_Decompress(string compression) + { + // Arrange + var bytes = Encoding.ASCII.GetBytes(Guid.NewGuid().ToString()); + var compressed = CompressionUtils.Compress(compression, bytes); + var bodyParserSettings = new BodyParserSettings + { + Stream = new MemoryStream(compressed), + ContentType = "text/plain", + DeserializeJson = false, + ContentEncoding = compression.ToUpperInvariant(), + DecompressGZipAndDeflate = false + }; + + // Act + var result = await BodyParser.Parse(bodyParserSettings); + + // Assert + result.BodyAsBytes.Should().BeEquivalentTo(compressed); + result.DetectedCompression.Should().BeNull(); + } + [Theory] [InlineData("HEAD", false)] [InlineData("GET", false)] diff --git a/test/WireMock.Net.Tests/WireMockServerTests.cs b/test/WireMock.Net.Tests/WireMockServerTests.cs index 86ed5f58..d72167ce 100644 --- a/test/WireMock.Net.Tests/WireMockServerTests.cs +++ b/test/WireMock.Net.Tests/WireMockServerTests.cs @@ -1,13 +1,17 @@ using System; using System.Diagnostics; +using System.IO; +using System.IO.Compression; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Threading.Tasks; using NFluent; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; +using WireMock.Util; using Xunit; namespace WireMock.Net.Tests @@ -252,5 +256,35 @@ namespace WireMock.Net.Tests Check.That(response.StatusCode).Equals(HttpStatusCode.Created); Check.That(await response.Content.ReadAsStringAsync()).Contains("Mapping added"); } + + [Theory] + [InlineData("gzip")] + [InlineData("deflate")] + public async Task WireMockServer_Should_SupportRequestGZipAndDeflate(string contentEncoding) + { + // Arrange + const string body = "hello wiremock"; + byte[] compressed = CompressionUtils.Compress(contentEncoding, Encoding.UTF8.GetBytes(body)); + + var server = WireMockServer.Start(); + server.Given( + Request.Create() + .WithPath("/foo") + .WithBody("hello wiremock") + ) + .RespondWith( + Response.Create().WithBody("OK") + ); + + var content = new StreamContent(new MemoryStream(compressed)); + content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + content.Headers.ContentEncoding.Add(contentEncoding); + + // Act + var response = await new HttpClient().PostAsync($"{server.Urls[0]}/foo", content); + + // Assert + Check.That(await response.Content.ReadAsStringAsync()).Contains("OK"); + } } } \ No newline at end of file